2011-02-17 17 views
4

我的解决方案有一个非托管的C++ DLL,它导出一个函数,以及一个PInvokes这个函数的托管应用程序。为什么在违反调用约定(.NET 3.5)的情况下PInvoke不会崩溃?

我刚刚将.NET 3.5的解决方案转换为.NET 4.0,并得到了这个PInvokeStackImpalance “调用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调用一样)完全不起作用,只会导致EntryPointNotFoundException。

回答

5

经过一番调查:

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

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

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

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

-1

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

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

+0

是的,我应该更具体一点,我的应用程序运行在Windows上,其中WinApi意味着StdCall。 – 2011-02-18 17:22:08

+1

正如我在我的回答中写道的,它似乎像PInvoking CDecl函数一样,StdCall是'安全的'。这是额外的MDA检查,它们在.NET 4.0中添加,导致应用程序失败。 – 2011-02-18 17:24:56

7

PInvokeStackImbalance不是例外。这是一个MDA警告,由Managed Debugging Assistant执行。使MDA处于活动状态是可选的,您可以从“调试+例外”对话框对其进行配置。没有调试器的情况下,它永远不会被激活。

让栈不平衡可能会导致相当不好的问题,从奇怪的数据损坏到获取SOE或AVE。很难诊断。但是它也不会造成任何麻烦,当方法返回时堆栈指针会得到恢复。

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

+0

感谢您对MDA的澄清。 – 2011-02-18 17:09:32

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)重新启用旧行为。

相关问题