在VS2010中,如果使用错误的调用约定调用函数,托管调试助手将为您提供pInvokeStackImbalance异常(pInvokeStackImbalance MDA),这通常是因为您在调用C库时未指定CallingConvention = Cdecl。例如。你写了
[DllImport("some_c_lib.dll")]
static extern void my_c_function(int arg1, int arg2);
而不是
[DllImport("some_c_lib.dll", CallingConvention=CallingConvention.Cdecl)]
static extern void my_c_function(int arg1, int arg2);
因此获得了StdCall调用约定而不是Cdelc。
如果你回答这个问题,你已经知道了这个区别,但是对于这个线程的其他访问者:StdCall意味着被调用者清除堆栈中的参数,而Cdecl意味着调用者清理堆栈。
因此,如果你的C代码中的调用约定错误,你的堆栈就不会被清除,程序也会崩溃。
但是,.NET程序似乎没有崩溃,即使他们使用StdCall进行Cdecl功能。默认情况下,VS2008上没有启用堆栈不平衡检查,因此一些VS2008项目使用了错误呼吁他们的作者不知道公约。我刚试过GnuMpDotNet,即使缺少Cdelc声明,样本也运行得很好。 X-MPIR也是如此。
它们都在调试模式下抛出pInvokeStackImbalance MDA异常,但在发布模式下不会崩溃。为什么是这样? .NET VM是否将所有对本机代码的调用包装起来并在之后恢复堆栈本身?如果是这样,为什么还要使用CallingConvention属性?
答案 0 :(得分:7)
这是因为方法退出时堆栈指针的恢复方式。一个方法的标准序言,显示为x86抖动;
00000000 push ebp ; save old base pointer
00000001 mov ebp,esp ; setup base pointer to point to activation frame
00000003 sub esp,34h ; reserve space for local variables
结束的方式:
0000014a mov esp,ebp ; restore stack pointer
0000014c pop ebp ; restore base pointer
0000014d ret
获取esp值不平衡在这里不是问题,它从ebp寄存器值恢复。但是,当抖动优化器可以在cpu寄存器中存储局部变量时,它不会很少优化它。当RET指令从堆栈中检索错误的返回地址时,您将崩溃并刻录。无论如何,当它恰巧落在一大堆机器代码上时,真的很讨厌。
如果您在没有调试器的情况下运行发布版本,则可能会发生这种情况,如果您没有MDA来帮助您,则很难进行故障排除。
答案 1 :(得分:3)
运行时可以检测堆栈不平衡,因为堆栈指针不在预期的位置。也就是说,在StdCall
的情况下,期望被调用的函数清理堆栈,那么运行时可以这样做:
SavedSP = SP; // save the stack pointer
// now push parameters
// call the external function.
if (SP != SavedSP)
{
// error!
}
现在,如果SP
的值小于SavedSP
,那么堆栈上会有额外的东西 - 意味着运行时可以继续并恢复已保存的堆栈指针。
运行时应始终能够检测到堆栈不平衡。我不知道它是否能够永远恢复。但是如果无意中将Cdecl
方法调用为StdCall
,它应该能够毫无问题地恢复,因为堆栈上会有额外的东西可以忽略。
至于为什么要打扰?正如您所说,StdCall
和Cdecl
之间的差异实际上只是谁负责堆栈清理。此外,StdCall
与变量参数列表(即C中的printf
)不兼容,尽管我不知道是否甚至可以从.NET调用这样的方法(以前从未有过)需要)。在任何情况下,尽管使用Cdecl
调用StdCall
方法似乎没有特别的问题,但我有点像知道存在潜在错误。对我来说,这就像编写时编译器提供的错误消息:
uint x = 3;
int y = x; // error!
我知道分配是可以的,但编译器不允许它,因为它是潜在的bug来源。在我看来,不平衡的堆栈是潜在的错误来源。不,它是一个可能导致一些非常糟糕的事情发生的错误。我宁愿运行时告诉我它,以便我可以解决问题。