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

我的解决方案有一个非托管C ++ DLL,它导出一个函数,一个托管应用程序PInvokes这个函数。 我刚刚将解决方案从.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);
所以,我已经修复了这个bug,但现在我对.NET 3.5中的工作原理感兴趣吗?当没有人(既不是被叫者也不是调用者)清理堆栈时,为什么(多次重复)情况没有引起堆栈溢出或其他一些不当行为,但只是工作正常? Pnvoke中是否有某种检查,就像Raymond Chen在他的文章中提到的那样? 这也很有趣,为什么相反类型的破坏约定(让__stdcall被调用者像被__cdecl一样被PInvoked)根本不起作用,导致只有EntryPointNotFoundException。     
已邀请:
PInvokeStackImbalance也不例外。它是一个MDA警告,由托管调试助手实现。让MDA处于活动状态是可选的,您可以从Debug + Exceptions对话框配置它。在没有调试器的情况下运行它永远不会处于活动状态。 使堆栈不平衡可能会导致非常令人讨厌的问题,从奇怪的数据损坏到获取SOE或AVE。很难诊断。但它也可以完全没有问题,当方法返回时,堆栈指针会恢复。 编译为64位的代码往往具有弹性,更多的函数参数通过寄存器而不是堆栈传递。当被迫在x86(VS2010的新默认值)上运行时,它将失败。     
经过一番调查: 保存情况不会崩溃的助手是另一个寄存器 - EBP,指向堆栈帧开头的基指针。所有对函数局部变量的访问都是通过该指针完成的(优化代码除外,请参见下面的编辑)。在函数返回之前,堆栈指针被重置为基指针的值。 在函数(比如PInvoke)调用另一个函数(导入的DLL的函数)之前,堆栈指针指向调用函数的局部变量的末尾。然后调用者将参数推送到堆栈并调用其他函数。 在所描述的情况下,当一个函数将另一个函数调用为__stdcall时,它实际上是__cdecl,没有人从这些参数中清除堆栈。因此,从被调用者返回后,堆栈指针指向推送参数块的末尾。它就像调用函数(PInvoke)只获得了几个局部变量。 由于通过基指针访问调用者的局部变量,因此不会破坏任何内容。唯一可能发生的坏事是,如果一次调用被调用函数很多次。在这种情况下,堆栈将增长并可能溢出。但是由于PInvoke只调用DLL的函数一次,然后返回,所以堆栈指针只是重置为基指针,一切都很好。 编辑:如此处所述,代码也可以进行优化,仅将局部变量存储在CPU寄存器中。在这种情况下,不使用EBP,因此无效的ESP可能导致返回无效地址。     
值得注意的是,这在3.5和4之间变化的原因是PInvoke的默认行为发生了变化。在3.5及更早版本中,它检查了Alex所描述的内容并修复了它们。这会导致一些开销,因为需要在每个PInvoke调用上执行检查。在.NET 4中,行为更改为不执行此检查以删除正确调用的性能命中。而是添加了MDA警告。 可以使用NetFx40_PInvokeStackResilience app.config设置(http://msdn.microsoft.com/en-us/library/ff361650.aspx)重新启用旧行为。     
使用
DllImport
时,默认值为实际
WinApi
,而不是
StdCall
。 WinApi实际上不是约定,但代表系统的默认约定。也许它可能在.Net 3.5 WinApi中代表_cdecl,而现在它代表__stdcall 我真的不认为是这种情况,因为我记得在使用P / Invoke时总是必须指定__stdcall(或者更确切地说,WINAPI)。我不确定为什么它在.Net 3.5中有效。 (也许DllImport当时很懒,只是“忽略了”召唤大会 - 这很奇怪)     

要回复问题请先登录注册