为什么在违反调用约定的情况下PInvoke不会崩溃(在.NET 3.5中)?

时间:2011-02-17 21:01:04

标签: .net pinvoke calling-convention stdcall cdecl

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

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

C ++ part(callee):

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

C#part(来电者):

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

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

4 个答案:

答案 0 :(得分:7)

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

使堆栈失衡可能会导致非常令人讨厌的问题,从奇怪的数据损坏到获得SOE或AVE。很难诊断。但它也可以毫无问题,当方法返回时,堆栈指针会恢复。

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

答案 1 :(得分:5)

经过一番调查:

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

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

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

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

答案 2 :(得分:4)

值得注意的是,这在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)重新启用旧行为。

答案 3 :(得分:-1)

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

我真的不认为是这种情况,因为我记得在使用P / Invoke时总是必须指定__stdcall(或者更确切地说,WINAPI)。我不确定为什么它在.Net 3.5中有效。 (也许DllImport当时很懒,只是“忽略了”召唤大会 - 这很奇怪)