最近網易雲課堂開放了一節叫 Linux內核分析 的課程。一直對操做系統和計算機本質很感興趣,因而進去看了下,才第一堂課,老師就要求學生寫一篇關於課時1的博客做爲做業。對於這種新穎的做業形式,筆 者至關驚訝。好吧,做爲任務,仍是完成一下吧,恰好須要消化一下。本文將會按照要求,將一段C語言代碼編譯成彙編,並給予分析和本身的思考。html
首先對會涉及到的一些CPU寄存器和彙編的基礎知識羅列一下:linux
準備一段C代碼:函數
int g(int x) { return x+5; } int f(int x) { return g(x); } int main(void) { return f(10)+1; }
使用 實驗樓 環境學習
使用以下命令編譯上面的c代碼測試
gcc -S -o main.s main.c -m32
去掉不重要的部分後,獲得:優化
彙編代碼結果爲:操作系統
g: pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax addl $5, %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 $10, (%esp) call f addl $1, %eax leave ret
具體的逐步分析,這裏就省了,老師課上講的很詳細了,這裏主要是要進行思考和概括。htm
首先,咱們看到3個C函數對應生成了3個部分的彙編代碼,分別用函數名做爲標號隔開了ip
int g(int x) -> g: int f(int x) -> f: int main(void) -> main:
咱們知道程序是從 main 函數開始執行的,那麼當程序被加載並運行時,上面的彙編代碼會被加載到內存的某一個區域。並且,CPU中的不少寄存器都會初始化,固然其中最重要的是 eip ,由於 eip 是指向下一條將要執行的命令所在的內存地址,因此此時的 eip 應該指向 main 標號下的 pushl %ebp :內存
main: eip -> pushl %ebp
程序開始執行...
咱們捆綁着看,首先先看這兩條:
pushl %ebp movl %esp, %ebp
再觀察一下整個代碼,有沒有發現不只僅是 main 函數,函數 f 和 g 的開頭也是這兩個指令。分析一下,不可貴出,這兩條指令是指 將當前棧基地址壓棧後,從新將基地址定位到棧頂 ,這個含義實際上是保存好當前的基地址,從新開始一個新的棧。因爲函數能夠調函數, 這裏的當前基地址,其實是上一個函數的棧基地址 。例如,在 f 函數中的這兩句指令,實際上保存的是 main 函數的棧基地址。
接着來分析兩句:
subl $4, %esp movl $10, (%esp)
對照C代碼不難發現,這是 參數進棧 ,將當即數,保存到棧頂(esp所指向的內存地址是棧頂)。而在 f 函數中也能夠發現相似的語句:
subl $4, %esp movl 8(%ebp), %eax movl %eax, (%esp)
因此,咱們能夠得出結論是,在調用函數前須要把參數逐個壓棧,而壓棧的順序根據筆者的測試是從右向左的。
接着調用 call 指令,跳轉到 f 函數,咱們知道 call 指令等同於下面的僞代碼:
pushl %eip+1 movl %eip f
即把 call 指令的後一條指令進棧後,將 eip 賦值爲目標函數的第一個指令地址。這樣作顯而易見:當所調用的函數結束後,須要返回當前函數繼續執行,因此必需要保存下一條指令,不然回來的時候就找不到了。
來到 f 函數,首先是保存main函數的棧基地址,而後須要調用 g 函數,因而須要參數先進棧:
subl $4, %esp movl 8(%ebp), %eax movl %eax, (%esp)
這裏重點思考一下, f 函數是如何得到main函數傳遞過來的參數的,咱們看到
movl 8(%ebp), %eax
爲何參數是從 8(%ebp) 中得到的呢?咱們知道 8(%ebp) 表示的是以ebp爲基準向棧底回溯8個字節獲得,爲何是8個字節呢?
回想一下,在 main 函數中完成了參數進棧後作了兩件事情:
因而經過 8(%ebp) 能夠找到前一個函數的第一個整型參數的值。
一張圖告訴你怎麼回事:
看過了進入函數,調用函數的過程,再看一下函數是如何退出的。觀察 main 和 f 不難發現,退出函數使用的是以下指令
leave ret
leave 指令至關於以下指令:
movl %ebp, %esp popl %ebp
接着 ret 就是至關於,恢復指令指向:
popl %eip
爲何g函數沒有leave呢?由於g函數內部沒有任何的變量聲明和函數調用棧一直都是空的,因此編譯器優化了指令
最後,經過這個例子,總結一下函數調用的過程:
進入函數:
調用其餘函數:
退出函數: