为什么.NET程序能够在损坏的堆栈中存活? (当使用错误的调用约定时)

时间:2011-03-18 21:39:07

标签: .net pinvoke dllimport

在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属性?

2 个答案:

答案 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,它应该能够毫无问题地恢复,因为堆栈上会有额外的东西可以忽略。

至于为什么要打扰?正如您所说,StdCallCdecl之间的差异实际上只是谁负责堆栈清理。此外,StdCall与变量参数列表(即C中的printf)不兼容,尽管我不知道是否甚至可以从.NET调用这样的方法(以前从未有过)需要)。在任何情况下,尽管使用Cdecl调用StdCall方法似乎没有特别的问题,但我有点像知道存在潜在错误。对我来说,这就像编写时编译器提供的错误消息:

uint x = 3;
int y = x;  // error!

我知道分配是可以的,但编译器不允许它,因为它是潜在的bug来源。在我看来,不平衡的堆栈是潜在的错误来源。不,它一个可能导致一些非常糟糕的事情发生的错误。我宁愿运行时告诉我它,以便我可以解决问题。