1. 程式人生 > >C語言中的函式呼叫,棧的使用

C語言中的函式呼叫,棧的使用

本文共包含一下四個部分。

  • C原始碼
  • 註釋
  • 對應彙編程式碼:此彙編使用”gcc -S hello.c”命令編譯生成,部分刪減
  • 棧空間的使用過程:包括5個部分,五張圖
  • C原始碼
int sayhello(int a,int b,int c){
    int aa=100;
    int bb=200;
    bb=a;
    bb=b;
    bb=c;
    return aa;
}
main()
{
    int a=100;
    int b=10;
    int c=102;
    c=sayhello(a,b,c);
    c=200;
}
  • 彙編程式碼

080483ec <sayhello>:
 80483ec:   push   %ebp         //把main函式的ebp放入棧頂
 80483ed:   mov    %esp,%ebp    //ebp指向新的棧幀
 80483ef:   sub     $0x10,%esp //為esp分配了16個位元組空間
 80483f2:   movl   $0x64,-0x8(%ebp)    //棧內變數aa=200
 80483f9:   movl   $0xc8,-0x4(%ebp)    //棧內變數bb=200
 8048400:   mov    0x8(%ebp),%eax   //ebp向上取出a=100
 8048403
: mov %eax,-0x4(%ebp) //賦值給bb 8048406: mov 0xc(%ebp),%eax //ebp向上取出b=10 8048409: mov %eax,-0x4(%ebp) //賦值給bb 804840c: mov 0x10(%ebp),%eax //ebp向上取值c=102 804840f: mov %eax,-0x4(%ebp) //賦值給bb 8048412: mov -0x8(%ebp),%eax //取出aa的值,存入eax 8048415: leave 8048416: ret //返回現場 08048417
<main>: 804841a: sub $0x1c,%esp //為main函式的棧分配28位元組空間 804841d: movl $0x64,-0xc(%ebp) //變數a=100 8048424: movl $0xa,-0x8(%ebp) //變數b=10 804842b: movl $0x66,-0x4(%ebp) //變數c=102 8048432: mov -0x4(%ebp),%eax //把引數c放入棧頂 8048435: mov %eax,0x8(%esp) 8048439: mov -0x8(%ebp),%eax //把引數b放入棧頂 804843c: mov %eax,0x4(%esp) 8048440: mov -0xc(%ebp),%eax //把引數a放入棧頂 8048443: mov %eax,(%esp) 8048446: call 80483ec <sayhello>//呼叫sayhello,隱含把EIP放入棧頂 804844b: mov %eax,-0x4(%ebp) //返回值賦給c 804844e: movl $0xc8,-0x4(%ebp) //c重新賦值 8048455: leave 8048456: ret
  • 棧呼叫過程

1.

main函式彙編程式碼頭部分我刪去了幾行,先不要理會,我們需要的是一個確定的乾淨的簡單的起點,那就從三個變數的定義和初始化開始吧,從程式碼中可以看出,三個變數的定義和建立使用了ebp的相對位置並且是逆序存放;

此時esp指向的位置比較有意思,從ebp到esp這段空間就是gcc為main函式分配的棧幀,也就是說main函式的執行以後只會使用這一段空間了;這一段空間的大小由兩部分組成,如圖解釋。main函式中定義了三個區域性變數12個位元組,gcc為區域性變數分配了16個位元組;main函式呼叫的函式最多入參個數是3,gcc再分配12個位元組,所以main函式的棧幀總共有28個位元組,7個字。

2.

這裡寫圖片描述
main函式準備呼叫sayhello了!
1、先參考esp的位置把入參mov棧中,沒有使用PUSH,沒有使用PUSH,沒有使用PUSH
2、呼叫call指令,call指令其實隱含了一個PUSH指令,一個MOV指令。他先把當前main的EIP壓入棧中,然後把sayhello的程式碼位置MOV給EIP。程式下一步執行就到了sayhello位置了。注意,保護現場工作尚未完成!

3.

這裡寫圖片描述
進入sayhello函式後繼續進行現場保護工作,儲存main函式的ebp。之後ebp可以指向為sayhello分配的棧幀了,再為sayhello分配區域性變數空間16個位元組,建立兩個變數。接下來就要使用入參了,入參的訪問使用了當前ebp的相對定址,不過不是向sayhello的棧幀,而是向上取main函式棧幀底部的三個引數!ok,更新完畢,準備返回。

4.

這裡寫圖片描述
準備返回:1、返回的引數放入eax暫存器。2、執行ret。ret負責恢復現場,和call一樣隱含了好幾個POP和MOV。先讓esp指向當前棧幀的底部ebp,然後pop出ebp回覆main的棧幀,pop出EIP回覆main的程式碼指標,此時雖然esp上面還存著sayhello的入參,但已經沒用了。

5.

這裡寫圖片描述
函式繼續執行,EIP已經指向main的程式碼了,main開始對c進行賦值,先把eax裡的資料拿出來,然後賦值給c。結束。

註釋

  • esp:當前棧的棧頂指標
  • ebp:當前函式的棧幀基地址
  • ess:棧空間的基地址
  • eax:臨時變數暫存器
  • EIP:當前函式的程式碼指標

棧有很多棧幀組成,隨著程式的執行,每進入新的函式都需要建立一個棧幀,函式只使用自己的棧幀,函式執行結束夠要彈出棧幀。
剛進入函式時,esp會先跳一段空間,用來儲存區域性變數。
可以看出,棧只有在保護現場時使用push,pop,其他都是採用定址的方式使用的。