【问题标题】:Why isn't PInvoke crashing in case of violated calling convention (in .NET 3.5)?如果违反调用约定(在 .NET 3.5 中),为什么 PInvoke 不会崩溃?
【发布时间】:2011-06-29 09:47:43
【问题描述】:

我的解决方案有一个导出函数的非托管 C++ DLL,以及一个 PInvoke 此函数的托管应用程序。

我刚刚将解决方案从 .NET 3.5 转换为 .NET 4.0 并得到此 PInvokeStackImbalance “对 PInvoke 函数的调用 [...] 使堆栈不平衡” 异常。事实证明,我正在调用 __cdecl'ed 函数,因为它是 __stdcall:

C++ 部分(被调用者):

__declspec(dllexport) double TestFunction(int param1, int param2); // by default is __cdecl

C#部分(调用者):

[DllImport("TestLib.dll")] // by default is CallingConvention.StdCall
private static extern double TestFunction(int param1, int param2);

所以,我已经修复了这个错误,但现在我感兴趣的是它在 .NET 3.5 中是如何工作的?为什么(多次重复)没有人(被调用者或调用者)清理堆栈,没有导致堆栈溢出或其他一些不当行为,而是正常工作的情况? PInvoke 中是否有某种检查,就像 Raymond Chen 在他的article 中提到的那样? 这也很有趣,为什么相反类型的破坏约定(让 __stdcall 被调用者像 __cdecl 一样被 PInvoked)根本不起作用,只导致 EntryPointNotFoundException。

【问题讨论】:

    标签: .net pinvoke calling-convention stdcall cdecl


    【解决方案1】:

    PInvokeStackImbalance 也不例外。这是一个 MDA 警告,由托管调试助手实现。激活 MDA 是可选的,您可以从 Debug + Exceptions 对话框进行配置。当您在没有调试器的情况下运行时,它永远不会处于活动状态。

    使堆栈不平衡会导致非常严重的问题,从奇怪的数据损坏到获得 SOE 或 AVE。也很难诊断。但它也完全没有问题,当方法返回时,堆栈指针会被恢复。

    编译为 64 位的代码往往具有弹性,更多的函数参数通过寄存器而不是堆栈传递。强制在 x86(VS2010 的新默认设置)上运行时会失败。

    【讨论】:

    • 感谢您对 MDA 的澄清。
    【解决方案2】:

    经过一番调查:

    帮助程序避免崩溃,是另一个寄存器 - EBP,指向堆栈帧开头的基指针。所有对函数局部变量的访问都是通过这个指针完成的(优化代码除外,请参见下面的编辑)。在函数返回之前,堆栈指针被重置为基指针的值。

    在一个函数(比如 PInvoke)调用另一个函数(导入的 DLL 的函数)之前,堆栈指针指向调用者函数的局部变量的末尾。然后调用者将参数推送到堆栈并调用其他函数。

    在所描述的情况下,当一个函数调用另一个函数作为 __stdcall 时,虽然它实际上是 __cdecl,但没有人从这些参数中清除堆栈。因此,从被调用者返回后,堆栈指针指向被推送参数块的末尾。就像调用者函数 (PInvoke) 刚刚多了几个局部变量。

    由于访问调用者的局部变量是通过基指针完成的,它不会破坏任何东西。唯一可能发生的坏事是被调用函数是否会一次被调用多次。在这种情况下,堆栈将增长并可能溢出。但是由于 PInvoke 只调用了 DLL 的函数一次,然后返回,堆栈指针只是重置为基指针,一切都很好。 编辑:here 所述,代码还可以优化为仅将局部变量存储在 CPU 寄存器中。在这种情况下不使用 EBP,因此无效的 ESP 可能会导致返回无效地址。

    【讨论】:

    • 从今天的角度来看,“唯一可能发生的坏事”的表述相当乐观。另一种糟糕的情况见this question(寄存器中的所有局部变量,EBP 完全离开现场)。 .NET 3.5 仍会为您纠正此问题(以牺牲性能为代价),但默认情况下 .NET 4 不会,您会崩溃。
    【解决方案3】:

    值得注意的是,在 3.5 和 4 之间发生变化的原因是 PInvoke 的默认行为发生了变化。在 3.5 和更早的版本中,它检查了 Alex 描述的内容并修复了它们。这会导致一些开销,因为需要对每个 PInvoke 调用执行检查。在 .NET 4 中,行为更改为 not 执行此检查以消除对正确调用的性能影响。相反,添加了 MDA 警告。

    可以使用 NetFx40_PInvokeStackResilience app.config 设置 (http://msdn.microsoft.com/en-us/library/ff361650.aspx) 重新启用旧行为。

    【讨论】:

      【解决方案4】:

      使用DllImport 时,默认为实际WinApi,而不是StdCall。 WinApi 实际上并不是一个约定,而是代表系统的默认约定。也许在.Net 3.5中WinApi代表_cdecl,而现在它代表__stdcall

      但我真的不这么认为,因为我记得在使用 P/Invoke 时总是必须指定 __stdcall(或者更确切地说是 WINAPI)。我不太确定为什么它在 .Net 3.5 中有效。 (也许 DllImport 那时很懒惰,只是“忽略”了调用约定——那会很奇怪)

      【讨论】:

      • 是的,我应该更具体一点,我的应用程序在 Windows 上运行,其中 WinApi 表示 StdCall。
      • 正如我在回答中所写的,看起来 PInvoking CDecl 函数作为 StdCall 是“安全的”。这是他们在 .NET 4.0 中添加的额外 MDA 检查,导致应用程序失败。
      猜你喜欢
      • 1970-01-01
      • 2015-12-04
      • 1970-01-01
      • 1970-01-01
      • 2011-06-11
      • 1970-01-01
      • 1970-01-01
      • 2012-04-20
      • 1970-01-01
      相关资源
      最近更新 更多