C程序運行堆棧分析

最近在上孟寧老師的《Linux內核分析》,本文是該課程的實驗做業,經過分析彙編代碼來理解C程序在計算機中是如何工做的。分析的實驗代碼以下:
code程序員

右邊爲經過gcc -S main.c -o main.s -m32命令轉成的x86彙編代碼,下文分析以右邊代碼爲準
C代碼數據結構

int g(int x) {
        return x + 31;
    }
    
    int f(int x) {
        return g(x);
    }

    int main(void) {
        return f(52) + 33;
    }

x86彙編代碼函數

g:
        pushl %ebp
        movl  %esp, %ebp
        movl  8(%ebp), %eax
        addl  $31, %eax
        popl  %ebp
        ret
    f:
        pushl %ebp
        movl  %esp, %ebp
        subl  $4, %esp
        movl  8(%ebp), %eax
        movl  %eax, (%esp)
        call  g
        leave
        ret
    main:
        pushl %ebp
        movl  %esp, %ebp
        subl  $4, %esp
        movl  $52, (%esp)
        call  f
        addl  $33, %eax
        leave
        ret

因爲C程序的入口爲main函數,因此這段代碼的起始點爲第17行,eip(Extended Instruction Pointer, 指令寄存器)指向第18行(eip指向下一條指令)。程序在啓動時,系統會爲程序分配一個堆棧空間,此時程序的堆棧爲空,ebp(Extended Base Pointer, 棧基指針寄存器)和esp(Extended Stack Pointer, 棧指針寄存器)都指向棧底。這裏使用的內存堆棧模型爲更常見的由下至上,而非課程視頻中由上之下的結構。同時須要注意的是,右邊的數字並不是內存的實際地址,這裏只是將內存作了簡單的編號。
empty.pngspa

18 pushl %ebp,將ebp寄存器中的值壓棧,同時esp向上移一個單位
18.png指針

19 movl %esp, %ebp 將esp中的至賦值給ebp。此時,esp和ebp都指向1
19.pngcode

20 subl $4, %esp 這條指令的直接做用是將esp中的值減去4,而後把結果存回esp中。這裏須要說明兩點:視頻

  1. 這裏的4指的是4個字節,也就是內存中真實地址移動4個單位(至關於本文模型中的1個單位)ip

  2. 由於棧是向低地址擴展的數據結構。對應本文內存模型就是,1的地址比0要小4個單位,2的地址比1要小4個單位,以此類推。這也是爲何這裏用了減法指令內存

因此這條指令的執行結果就是將esp指向2
20.pngget

21 movl $52, (%esp) 這條指令的含義是將52這個數傳入esp指向的內存地址中,也就是內存2
21.png

22 call f call是一個宏指令,其對應的兩個指令爲pushl %eipmovl f, %eip。上面說過eip的指表明下一條指令的位置,這裏也就是第23行代碼(記做EIP23)。pushl %eip就是將EIP23壓棧,而後經過movl f, %eip將f函數的地址(EIP8)傳入eip,使得下一條指令從f函數開始,從而實現C函數的調用。
22.png

9 pushl %ebp 將ebp的值入棧,也就是將EBP 1放入內存4中,同時esp上移一個單位
9.png

10 movl %esp, %ebp 將esp的值傳入ebp中,此時esp 和 ebp同時指向內存4
10.png

11 subl $4, %esp 將esp上移一個單位,指向內存5
11.png

12 movl 8(%ebp), %eax 8(%ebp) = (8 + %ebp) 也就是ebp指針下移兩個單位,指向內存2,而後將內存2中的值(也就是52)傳入eax(Extended Accumulator X,累加寄存器)。這條指令執行完後堆棧中並沒有變化,只是將52這個數傳給了eax

13 movl %eax, (%esp) 將%eax中的數值傳入%esp指向的內存位置(內存5)
13.png

14 call g 一樣的,call至關於pushl %eipmovl g, %eip,此時eip指向第15條指令(記做EIP15)
14.png

2 pushl %ebp 將ebp的值入棧
2.png

3 movl %esp, %ebp 將esp的值傳入ebp,執行後ebp和esp都指向內存7
3.png

4 movl 8(%ebp), %eax 將內存5中的數據(也就是52)傳入eax。此時堆棧不變化
5 addl $31, %eax 將eax中的數據加上31,並把結果存入eax,因此此時eax中的值爲83(52+31)
6 popl %ebp 將棧頂的數據彈出,並傳入ebp,因此執行後ebp指向內存4。同時esp下移一個單位,指向內存6
6.png
7 ret ret也是一個宏指令,實際執行的效果爲popl %eip,就是將棧頂的數據傳入eip,同時esp下移一個單位,此時eip指向第15行指令
7.png
15 leave leave指令對應movl %ebp, %esppopl %ebp,先將ebp的值傳入esp,執行後ebp和esp都指向內存4,而後將內存4的數據彈出並傳入ebp中。因此執行leave執行後ebp指向內存1,esp指向內存3
15.png
16 ret 也就是popl %eip,執行後eip指向第23行代碼,esp指向內存2
16.png
23 addl $33, %eax 將33累加到eax中,結果爲116(83+33)
24 leavemovl %ebp, %esppopl %ebp,執行後ebp和esp均指向內存0。至此,改程序的堆棧又從新變爲空棧
empty
25 ret 該程序執行結束,經過popl %eip將eip指向上個程序的指令

總結:經過分析能夠看出,C語言實際上是對彙編語言作了一層抽象,以方便程序員編寫和閱讀代碼。計算機在執行程序時,也只能循序漸進的逐條執行,這中間其實多了不少看似繁瑣的過程。好比每次進入一個函數,都要先保存ebp指針。同時系統分配給每一個程序的棧空間是有限的,若是調用的函數過多,則會致使棧溢出,引起程序異常。

相關文章
相關標籤/搜索