Dot-Net

為什麼 .NET 程序可以在損壞的堆棧中倖存下來?(使用錯誤的呼叫約定時)

  • March 18, 2011

在 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 屬性呢?

這是因為方法退出時恢復堆棧指針的方式。一種方法的標準序言,針對 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 來幫助您,則很難進行故障排除。

引用自:https://stackoverflow.com/questions/5358107