为什么 .NET 程序能够在损坏的堆栈中幸存下来? (当使用错误的调用约定时)
在 VS2010 中,托管调试助手会给您一个 pInvokeStackImbalance 异常 (pInvokeStackImbalance MDA)如果您使用错误的调用约定调用函数,通常是因为您没有指定 CallingConvention =调用 C 库时使用 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 属性呢?
In VS2010, the managed debugging assistant will give you a pInvokeStackImbalance exception (pInvokeStackImbalance MDA) if you call a function using the wrong calling convention, commonly because you didn't specify CallingConvention = Cdecl when calling a C library. E.g. you wrote
[DllImport("some_c_lib.dll")]
static extern void my_c_function(int arg1, int arg2);
instead of
[DllImport("some_c_lib.dll", CallingConvention=CallingConvention.Cdecl)]
static extern void my_c_function(int arg1, int arg2);
and thus got the StdCall calling convention instead of Cdelc.
If you answer this, you already know the difference, but for other visitors to this thread: StdCall means that the callee cleans the arguments from the stack, whereas Cdecl means that the caller cleans up the stack.
So, if you get the calling convention wrong in your C code, your stack doesn't get cleaned up and your program crashes.
However, .NET programs don't seem to crash even though they use StdCall for Cdecl functions. The stack imbalance check wasn't enabled by default on VS2008, so some VS2008 projects use the wrong calling convention unbeknownst to their authors. I just tried GnuMpDotNet and the sample runs just fine even though the Cdelc declaration is missing. The same holds true for X-MPIR.
They both throw the pInvokeStackImbalance MDA exception in debug mode, but don't crash in release mode. Why is this? Does the .NET VM wrap all calls to native code and restore the stack itself afterwards? If so, why bother with the CallingConvention property at all?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
这是因为方法退出时堆栈指针的恢复方式。方法的标准序言,针对 x86 抖动显示;
其结束方式:
使 esp 值不平衡在这里不是问题,它会从 ebp 寄存器值恢复。然而,当抖动优化器可以将局部变量存储在 CPU 寄存器中时,它经常会优化这一点。当 RET 指令从堆栈中检索到错误的返回地址时,您将崩溃并烧毁。无论如何,希望当它偶然落在一大块机器代码上时真的很糟糕。
当您在没有调试器的情况下运行发布版本时,很可能会发生这种情况,如果没有 MDA 来帮助您,则很难排除故障。
It is because of the way the stack pointer is restored when the method exits. The standard prologue of a method, shown for the x86 jitter;
And the way it ends:
Getting the esp value unbalanced is not a problem here, it gets restored from the ebp register value. However, the jitter optimizer not infrequently optimizes this away when it can store local variables in cpu registers. You'll crash and burn when the RET instruction retrieves the wrong return address from the stack. Hopefully anyway, really nasty when it happens to land on a chunk of machine code by accident.
This is liable to happen when you run the release build without a debugger, tough to troubleshoot if you didn't have the MDA to help you.
运行时可以检测堆栈不平衡,因为堆栈指针不在预期的位置。也就是说,在
StdCall
的情况下,被调用的函数预计会清理堆栈,那么运行时可以执行以下操作:现在,如果
SP
的值为小于SavedSP
,那么堆栈上有额外的东西——这意味着运行时可以继续并恢复保存的堆栈指针。运行时应该始终能够检测到堆栈不平衡。我不知道它是否总能恢复。但是,如果无意中将
Cdecl
方法作为StdCall
调用,它应该能够毫无问题地恢复,因为堆栈上会有额外的内容可以忽略。至于为什么要麻烦呢?正如您所说,StdCall 和 Cdecl 之间的区别实际上只是谁负责堆栈清理。另外,
StdCall
与变量参数列表(即 C 中的printf
)不兼容,尽管我不知道是否可以从 .NET 调用这样的方法(从来没有需要)。无论如何,尽管使用StdCall
调用Cdecl
方法似乎没有什么特殊问题,但我有点想知道存在潜在的错误。对我来说,这就像你编写时编译器给出的错误消息:我知道赋值是可以的,但编译器不允许它,因为它是错误的潜在来源。在我看来,不平衡的堆栈是错误的潜在来源。不,它是一个错误,可能会导致一些非常糟糕的事情发生。我宁愿运行时告诉我这一点,以便我可以解决问题。
The runtime can detect the stack imbalance because the stack pointer isn't where it's expected. That is, in the case of
StdCall
, where the called function is expected to clean up the stack, then the runtime could do this:Now, if the value of
SP
is less thanSavedSP
, then there's extra stuff on the stack--meaning that the runtime can just go ahead and restore the saved stack pointer.The runtime should always be able to detect a stack imbalance. Whether or not it can always recover is unknown to me. But in the case of inadvertently calling a
Cdecl
method asStdCall
, it should be able to recover without trouble, since there will be extra stuff on the stack that it can ignore.As to why bother? As you say, the difference between
StdCall
andCdecl
is really only who's responsible for stack cleanup. Also,StdCall
is not compatible with variable argument lists (i.e.printf
in C), although I don't know if it's even possible to call such a method from .NET (haven't ever had a need to). In any case, although there doesn't appear to be a particular problem with calling aCdecl
method withStdCall
, I kind of like knowing that there's a potential error. To me, it's like the error message that the compiler gives when you write:I know that the assignment is okay, but the compiler disallows it because it's a potential source of bugs. In my mind, an unbalanced stack is a potential source of bugs. No, it is a bug that can cause some very bad things to happen. I'd rather the runtime told me about it so that I can fix the problem.