今天突然想分析一下函數在相互調用過程中棧幀的變化,還是想盡量以比較清晰的思路把這一過程描述出來,關於c函數調用原理的理解是很重要的。
1.關於棧
首先必須明確一點也是非常重要的一點,棧是向下生長的,所謂向下生長是指從內存高地址->地地址的路徑延伸,那麼就很明顯了,棧有棧底和棧頂,那麼棧頂的地址要比棧底低。對x86體系的CPU而言,其中
---> 寄存器ebp(base pointer )可稱為“幀指針”或“基址指針”,其實語意是相同的。
---> 寄存器esp(stack pointer)可稱為“ 棧指針”。
要知道的是:
---> ebp 在未受改變之前始終指向棧幀的開始,也就是棧底,所以ebp的用途是在堆棧中尋址用的。
---> esp是會隨著數據的入棧和出棧移動的,也就是說,esp始終指向棧頂。
見下圖,假設函數A調用函數B,我們稱A函數為"調用者",B函數為“被調用者”則函數調用過程可以這麼描述:
(1)先將調用者(A)的堆棧的基址(ebp)入棧,以保存之前任務的信息。
(2)然後將調用者(A)的棧頂指針(esp)的值賦給ebp,作為新的基址(即被調用者B的棧底)。
(3)然後在這個基址(被調用者B的棧底)上開辟(一般用sub指令)相應的空間用作被調用者B的棧空間。
(4)函數B返回後,從當前棧幀的ebp即恢復為調用者A的棧頂(esp),使棧頂恢復函數B被調用前的位置;然後調用者A再從恢復後的棧頂可彈出之前的ebp值(可以這麼做是因為這個值在函數調用前一步被壓入堆棧)。這樣,ebp和esp就都恢復了調用函數B前的位置,也就是棧恢復函數B調用前的狀態。
這個過程在AT&T匯編中通過兩條指令完成,即:
leave
ret
這兩條指令更直白點就相當於:
mov %ebp , %esp
pop %ebp
2.舉個簡單的實例,從匯編的視角看函數調用
2.1建立一個簡單的程序,程序文件名為 main.c
開發測試環境:
Ubuntu 12.04
gcc版本:4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5) (是Ubuntu自帶的)
<SPAN style="FONT-SIZE: 18px">/*main.c代碼:*/ void swap(int *a,int *b) { int c; c = *a; *a = *b; *b = c; } int main(void) { int a ; int b ; int ret; a =16; b = 64; ret = 0; swap(&a,&b); ret = a - b; return ret; }</SPAN>
2.2編譯
#gcc -g -o main main.c
#objdump -d main > main.dump
#gcc -Wall -S -o main.s main.c
這樣大家可以看main.s也可以看main.dump,這裡我們選擇使用main.dump。
截取關鍵的部分,即_start, swap , main,為什麼會有_start呢,因為ELF格式的入口其實是_start而不是main()。下面的圖展示了main()函數調用swap()前後的棧空間的結構。右邊的數字代表相對幀指針的偏移字節數。後面我們使用GDB調試就會發現棧的變化跟下圖是一致的。
(!!!請注意,由於棧對齊的緣故,編譯器分配棧空間時可能會有沒用到的內存地址,而這些沒使用到的內存地址就沒在下圖表示出來,所以下圖只能當作示意圖來了解函數棧幀結構!!具體的棧內存內容以下文的GDB調試的信息為准!!!)