(C語言內存十五)用一個實例來深入剖析函數進棧出棧的過程


debug

前面我們只是講解了一個函數的活動記錄是什么樣子的,相信大家對函數的詳細調用過程的認識還不是太清晰,這節我們就以 VS2010 Debug 模式為例來深入分析一下。

請看下面的代碼:

void func(int a, int b){
    int p =12, q = 345;
}
int main(){
    func(90, 26);
    return 0;
}

函數使用默認的調用慣例 cdecl,即參數從右到左入棧,由調用方負責將參數出棧。函數的進棧出棧過程如下圖所示:

函數進棧分析

步驟①到⑥是函數進棧過程:

  1. main() 是主函數,也需要進棧,如步驟①所示。

  2. 在步驟②中,執行語句func(90, 26);,先將實參 90、26 壓入棧中,再將返回地址壓入棧中,這些工作都由 main() 函數(調用方)完成。這個時候 ebp 的值並沒有變,僅僅是改變 esp 的指向。

  3. 到了步驟③,就開始執行 func() 的函數體了。首先將原來 ebp 寄存器的值壓入棧中(也即圖中的 old ebp),並將 esp 的值賦給 ebp,這樣 ebp 就從 main() 函數的棧底指向了 func() 函數的棧底,完成了函數棧的切換。由於此時 esp 和ebp 的值相等,所以它們也就指向了同一個位置。

  4. 為局部變量、返回值等預留足夠的內存,如步驟④所示。由於棧內存在函數調用之前就已經分配好了,所以這里並不是真的分配內存,而是將 esp 的值減去一個整數,例如 esp - 0XC0,就是預留 0XC0 字節的內存。

  5. 將 ebp、esi、edi 寄存器的值依次壓入棧中。

  6. 將局部變量的值放入預留好的內存中。注意,第一個變量和 old ebp 之間有4個字節的空白,變量之間也有若干字節的空白。

為什么要留出這么多的空白

為什么要留出這么多的空白,豈不是浪費內存嗎?這是因為我們使用Debug模式生成程序,留出多余的內存,方便加入調試信息;以Release模式生成程序時,內存將會變得更加緊湊,空白也被消除。

至此,func() 函數的活動記錄就構造完成了。可以發現,在函數的實際調用過程中,形參是不存在的,不會占用內存空間,內存中只有實參,而且是在執行函數體代碼之前、由調用方壓入棧中的。

未初始化的局部變量的值為什么是垃圾值

為局部變量分配內存時,僅僅是將 esp 的值減去一個整數,預留出足夠的空白內存,不同的編譯器在不同的模式下會對這片空白內存進行不同的處理,可能會初始化為一個固定的值,也可能不進行初始化。

例如在VS2010 Debug模式下,會將預留出來的內存初始化為 0XCCCCCCCC,如果不對局部變量賦值,它們的內存就不會改變,輸出時的結果就是 0XCCCCCCCC,請看下面的代碼:

#include <stdio.h>
#include <stdlib.h>
int main(){
    int m, n;
    printf("%#X, %#X\n", m, n);
    system("pause");
    return 0;
}

運行結果:

0XCCCCCCCC, 0XCCCCCCCC

雖然編譯器對空白內存進行了初始化,但這個值對我們來說一般沒有意義,所以我們可以認為它是垃圾值、是隨機的。

函數出棧分析

步驟⑦到⑨是函數 func() 出棧過程:
7) 函數 func() 執行完成后開始出棧,首先將 edi、esi、ebx 寄存器的值出棧。

  1. 將局部變量、返回值等數據出棧時,直接將 ebp 的值賦給 esp,這樣 ebp 和 esp 就指向了同一個位置。

  2. 接下來將 old ebp 出棧,並賦值給現在的 ebp,此時 ebp 就指向了 func() 調用之前的位置,即 main() 活動記錄的 old ebp 位置,如步驟⑨所示。

這一步很關鍵,保證了還原到函數調用之前的情況,這也是每次調用函數時都必須將 old ebp 壓入棧中的原因。

最后根據返回地址找到下一條指令的位置,並將返回地址和實參都出棧,此時 esp 就指向了 main() 活動記錄的棧頂, 這意味着 func() 完全出棧了,棧被還原到了 func() 被調用之前的情況。

遺留的錯誤認知

經過上面的分析可以發現,函數出棧只是在增加 esp 寄存器的值,使它指向上一個數據,並沒有銷毀之前的數據。前面我們講局部變量在函數運行結束后立即被銷毀其實是錯誤的,這只是為了讓大家更容易理解,對局部變量的作用范圍有一個清晰的認識。

棧上的數據只有在后續函數繼續入棧時才能被覆蓋掉,這就意味着,只要時機合適,在函數外部依然能夠取得局部變量的值。請看下面的代碼:

#include <stdio.h>
int *p;
void func(int m, int n){
    int a = 18, b = 100;
    p = &a;
}
int main(){
    int n;
    func(10, 20);
    n = *p;
    printf("n = %d\n", n);
    return 0;
}

運行結果:

n = 18

在 func() 中,將局部變量 a 的地址賦給 p,在 main() 函數中調用 func(),函數剛剛調用結束,還沒有其他函數入棧,局部變量 a 所在的內存沒有被覆蓋掉,所以通過語句n = *p;能夠取得它的值。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM