4

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

我刚刚将解决方案从 .NET 3.5 转换为 .NET 4.0 并得到了这个 PInvokeStackImbalance “A call to PInvoke function [...] has unbalanced the stack”异常。事实证明,我正在调用 __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 在他的文章中提到的那样?这也很有趣,为什么相反类型的破坏约定(让 __stdcall 被调用者像 __cdecl 一样被 PInvoked)根本不起作用,只导致 EntryPointNotFoundException。

4

4 回答 4

7

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

使堆栈不平衡会导致非常讨厌的问题,从奇怪的数据损坏到获得 SOE 或 AVE。也很难诊断。但这也不会造成任何麻烦,当方法返回时,堆栈指针会被恢复。

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

于 2011-02-17T22:02:31.810 回答
5

经过一番调查:

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

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

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

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

于 2011-02-18T17:07:07.380 回答
4

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

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

于 2011-09-07T09:02:39.600 回答
-1

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

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

于 2011-02-17T21:23:05.977 回答