函數調用的棧幀分析

張建幫 原創作品轉載請註明出處 《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000

最近開始學習孟寧老師在網易雲課堂上開設的《Linux內核分析》課程,雖然才只上了一堂課,但是感覺收穫頗豐,特撰文以記。

第一次課以C語言爲例講述了具體的函數調用過程,雖然只有短短的不到30行代碼,但卻包含了函數調用的精髓,讓我對整個計算機系統有了更深入的理解。

仿照孟寧老師的代碼,我的代碼如下:

int g(int x, int y)
{
      return x + y;
}

int f(int x)
{
      return g(x, 1);
}

int main(void)
{
      return f(8) + 1;
}

唯一的區別就在於,函數g由以前的1個參數變爲了2個參數,目的是爲了觀察參數傳遞中的入棧順序。事實證明,在參數傳遞過程中,是逆序壓棧的,即最後一個參數第一個壓棧,倒數第二個參數第二個壓棧…第一個參數最後壓棧。

使用 gcc –S –o main.s main.c -m32 命令將代碼編譯成彙編碼後,再將所有以“.”開頭的行去掉,並添加上我的個人註解,最後得到的代碼如下:

g:
    pushl   %ebp          ;保存函數f的棧底
    movl    %esp, %ebp    ;設置當前函數g的棧底

    ;開始運算,運算的結果存放在寄存器%eax中
    movl    12(%ebp), %eax  
    movl    8(%ebp), %edx   
    addl    %edx, %eax      

    popl    %ebp      ;恢復函數f的棧底
    ret    ;相當於popl %eip,即開始執行f中的leave指令

f:
    pushl   %ebp    ;保存函數main的棧底
    movl    %esp, %ebp    ;設置當前函數f的棧底

    ;由於要調用函數g,因此要先爲參數分配空間
    ;先將最後一個參數入棧,注意!!是最後
    ;一個參數值先入棧,然後是倒數第二個,
    ;然後是倒數第三個...一直到第一個
    subl    $8, %esp    
    movl    $1, 4(%esp)
    movl    8(%ebp), %eax
    movl    %eax, (%esp)

    ;參數準備好之後,開始調用函數g
    call    g    ;相當於push %eip,即將下一條指令
                 ;leave的地址入棧

    leave  ;執行此條指令之前,esp指向本函數中的
           ;最後一個參數,此指令相當於順序執行下面
           ;2條指令:movl %ebp,%esp + popl %ebp
    ret    ;相當於popl %eip,開始執行main中的 addl指令

main:
    pushl   %ebp    ;保存main函數調用者的棧底
    movl    %esp, %ebp    ;設置main函數的棧底

    ;由於要調用函數f,因此要先爲參數分配空間
    ;先將最後一個參數入棧,注意!!是最後
    ;一個參數值先入棧,然後是倒數第二個,
    ;然後是倒數第三個...一直到第一個
    subl    $4, %esp    
    movl    $8, (%esp)

    call    f    ;相當於push %eip,即將下一條指令
                 ;addl的地址入棧

    addl    $1, %eax    ;f函數的運算結果保存在%eax中,然後
                        ;將其加1,保存在%eax中,得到main函數的
                        ;的運算結果

    leave  ;執行此條指令之前,esp指向本函數中的
           ;最後一個參數,此指令相當於順序執行下面
           ;2條指令:movl %ebp,%esp + popl %ebp

    ret    ;相當於popl %eip,開始執行main調用者的下一條指令

看不懂嗎,沒關係!如果是基本的彙編指令不懂,可以單擊本文標題下面的鏈接,由網易雲課堂的孟寧老師爲你傾情奉上詳解;如果是邏輯關係過於繁瑣,那也沒關係,可以先看看下面這幅圖片:

函數棧幀結構

這幅圖片給出了基本的函數棧幀的結構,每個函數的棧幀都是由圖中這樣的結構組成:

  1. 調用該函數的函數的棧底地址,函數棧的開始,該函數的棧底指向這塊內存
  2. 該函數的局部變量
  3. 該函數調用其他函數時需要傳遞的參數,如上圖所示,爲逆序入棧
  4. 該函數調用完其他函數後,下一條要執行的指令的地址,即被調用函數的返回地址

接下來就進入被調用函數的函數棧幀了,其結構與上面相同。而該函數在調用完成後,會釋放掉它的棧幀空間,至於具體的分配與釋放的細節,都在上面彙編代碼的註解中,在孟寧老師的網易雲課堂中《linux內核分析》也有詳細的講解。計算機中的這些函數調用來調用去,內存中的棧幀精準不停分配地釋放,最後便達到了正確運行的目的。

明白了這整個結構後,再來看上面的彙編代碼,相信大家應該沒什麼問題了。(友情提示:讀彙編代碼時最好是從main開始讀,筆者一開始是其他地方開始讀的,結果是丈二和尚摸不着頭腦…)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章