【问题标题】:Array of pointers in C++/CLI MSIL assemblyC++/CLI MSIL 程序集中的指针数组
【发布时间】:2018-12-13 08:06:30
【问题描述】:

我正在尝试封装一些旧的 C 代码,以便与在 .NET Core 上运行的 C# 一起使用。我正在使用the approach given here 创建一个编译为纯 MSIL 的 C++ 包装器。它适用于简单的函数,但我发现如果我的代码曾经使用指针指向指针或指针数组,它将因内存冲突而崩溃。它经常使 Visual Studio 崩溃,我必须重新启动所有内容,这很乏味。

例如,以下代码会导致崩溃:

public ref class example
    {
    public:

        static void test() {
            Console::WriteLine("\nTesting pointers.");

            double a[5] = {5,6,7,8,9}; //Array.
            double *b = a; //Pointer to first element in array.

            Console::WriteLine("\nTesting bare pointers.");
            Console::WriteLine(a[0]); //Prints 5.
            Console::WriteLine(b[0]); //Prints 5.

            Console::WriteLine("\nTesting pointer-to-pointer.");
            double **c = &b;
            Console::WriteLine(c == &b); //Prints true.
            Console::WriteLine(b[0]); //Works, prints 5.
            Console::WriteLine(**c); //Crashes with memory access violation.

            Console::WriteLine("\nTesting array of pointers.");
            double* d[1];
            d[0] = b;
            Console::WriteLine(d[0] == b); //Prints false???
            Console::WriteLine(b[0]); //Works, prints 5.
            Console::WriteLine(d[0][0]); //Crashes with memory access violation.

            Console::WriteLine("\nTesting CLI array of pointers.");
            cli::array<double*> ^e = gcnew cli::array<double*> (5);
            e[0] = b;
            Console::WriteLine(e[0] == b); //Prints false???
            Console::WriteLine(b[0]); //Works, prints 5.
            Console::WriteLine(e[0][0]); //Crashes with memory access violation.
        }
}

请注意,简单地使用指针不会导致任何问题。只有当有额外的间接级别时。

如果我将代码放到 CLR C++ 控制台应用程序中,它会完全按预期工作并且不会崩溃。只有在使用 clr:pure 将代码编译为 MSIL 程序集并从 .NET 核心应用程序运行时才会发生崩溃。

会发生什么?

更新 1: 以下是 Visual Studio 文件:https://app.box.com/s/xejfm4s46r9hs0inted2kzhkh9qzmjpb 这是两个项目。 MSIL 程序集称为libraryCoreApp 是一个将调用库的 C# 控制台应用程序。警告,运行 Visual Studio 时可能会崩溃。

更新 2:我也注意到了这一点:

        double a[5] = { 5,6,7,8,9 };
        double* d[1];
        d[0] = a;
        Console::WriteLine(d[0] == a); //Prints true.
        Console::WriteLine(IntPtr(a)); //Prints a number.
        Console::WriteLine(IntPtr(d[0])); //Prints a completely different number.

【问题讨论】:

  • PITA 进行构建,但工作正常,正如预期的那样。这一点都不特别。
  • @HansPassant 您是否将其构建为纯 MSIL 程序集?然后从 .NET Core 应用程序调用?这些是它崩溃的唯一条件。如果构建为桌面 (.NET Framework) 应用程序或从桌面 (.NET Framework) 应用程序运行,它不会崩溃。
  • 当然。如果您希望有人查看您的解决方案,那么您必须将其发布到某个地方。
  • @HansPassant 这是代码。它有两个项目。 “库”是 C++ CLI 中的上述代码,配置为编译为纯 MSIL。 CoreApp 是一个 C# 控制台应用程序,它将调用该库。我已经在几台电脑上试过了。它崩溃了,通常需要 Visual Studio。 app.box.com/s/xejfm4s46r9hs0inted2kzhkh9qzmjpb感谢收看!

标签: c# .net c++-cli clr cil


【解决方案1】:

这看起来像是为 test 方法生成的 IL 中的一个问题。在崩溃点,我们正在阅读 **c,而 c 是本地编号 5。

IL_00a5  11 05             ldloc.s      0x5
IL_00a7  4a                ldind.i4    
IL_00a8  4f                ldind.r8    
IL_00a9  28 11 00 00 0a    call         0xA000011

所以这里我们看到 IL 说要加载 c 的值,然后加载一个 4 字节有符号整数,然后将该整数视为指针并加载一个 8 字节实数类型(双精度)。

在 64 位平台上,指针应为大小中性或 64 位。所以ldind.i4 是有问题的,因为底层地址是 8 个字节。由于 IL 规定只读取 4 个字节,jit 必须扩展结果以获得 8 个字节的值。这里它选择签署extend。

library.h @ 27:
00007ffd`b0cf2119 488b45a8        mov     rax,qword ptr [rbp-58h]
00007ffd`b0cf211d 8b00            mov     eax,dword ptr [rax]
00007ffd`b0cf211f 4863c0          movsxd  rax,eax   // **** sign extend ****
>>> 00007ffd`b0cf2122 c4e17b1000      vmovsd  xmm0,qword ptr [rax]
00007ffd`b0cf2127 e854f6ffff      call    System.Console.WriteLine(Double) (00007ffd`b0cf1780)

在完整框架上运行时显然你很幸运,因为数组地址很小并且适合 31 位或更少,因此读取 4 个字节然后将符号扩展为 8 个字节仍然会给出正确的地址。但在 Core 上却没有,这就是应用程序崩溃的原因。

您似乎使用 Win32 目标生成了您的库。如果您使用 x64 目标重新构建它,IL 将为 *c 使用 64 位加载:

IL_00ab:  ldloc.s    V_5
IL_00ad:  ldind.i8
IL_00ae:  ldind.r8
IL_00af:  call       void [mscorlib]System.Console::WriteLine(float64)

应用运行良好。

这似乎是 C++/CLI 中的一个特性——即使在纯模式下,它生成的二进制文件也隐含地依赖于架构。只有/clr:safe 可以生成与体系结构无关的程序集,并且您不能在此代码中使用它,因为它包含无法验证的结构,例如指针。

另外请注意,.Net Core 2.x 并不支持 C++/CLI 的所有功能。这个特定示例避免了不受支持的位,但更复杂的位可能不会。

【讨论】:

  • 感谢您的回答。也许你可以为我澄清一点。?单个裸指针工作正常。只有当涉及到第二级间接时,事情才会变糟。这是为什么呢?
  • 因为b 是一个本地的,IL 使用ldloc 来获取它的值,并且由于b 被键入为float*,这总是给出正确大小的指针。同样,当加载c 时,IL 会获得正确大小的指针,但加载 *c 时 IL 必须告诉 JIT 使用什么大小,这里它总是为 x86 构建指定 4 个字节,为 x64 构建指定 8 个字节.换句话说,有时要加载的值的大小是隐式的(由操作码指定,ldloca 说,或通过签名),有时它是显式的。在明确的情况下,IL 生产者必须获得正确的大小。
猜你喜欢
  • 2011-10-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-03
  • 1970-01-01
  • 1970-01-01
  • 2021-03-17
  • 1970-01-01
相关资源
最近更新 更多