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,其他都是採用定址的方式使用的。