張建幫 原創作品轉載請註明出處 《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調用者的下一條指令
看不懂嗎,沒關係!如果是基本的彙編指令不懂,可以單擊本文標題下面的鏈接,由網易雲課堂的孟寧老師爲你傾情奉上詳解;如果是邏輯關係過於繁瑣,那也沒關係,可以先看看下面這幅圖片:
這幅圖片給出了基本的函數棧幀的結構,每個函數的棧幀都是由圖中這樣的結構組成:
- 調用該函數的函數的棧底地址,函數棧的開始,該函數的棧底指向這塊內存
- 該函數的局部變量
- 該函數調用其他函數時需要傳遞的參數,如上圖所示,爲逆序入棧
- 該函數調用完其他函數後,下一條要執行的指令的地址,即被調用函數的返回地址
接下來就進入被調用函數的函數棧幀了,其結構與上面相同。而該函數在調用完成後,會釋放掉它的棧幀空間,至於具體的分配與釋放的細節,都在上面彙編代碼的註解中,在孟寧老師的網易雲課堂中《linux內核分析》也有詳細的講解。計算機中的這些函數調用來調用去,內存中的棧幀精準不停分配地釋放,最後便達到了正確運行的目的。
明白了這整個結構後,再來看上面的彙編代碼,相信大家應該沒什麼問題了。(友情提示:讀彙編代碼時最好是從main開始讀,筆者一開始是其他地方開始讀的,結果是丈二和尚摸不着頭腦…)