<address id="jzxfj"></address>

<noframes id="jzxfj"><form id="jzxfj"><nobr id="jzxfj"></nobr></form>
<listing id="jzxfj"><listing id="jzxfj"></listing></listing>

<form id="jzxfj"><th id="jzxfj"><th id="jzxfj"></th></th></form>

<form id="jzxfj"></form>

    [科技]一段 C 語言和匯編的對應分析

      最近網易云課堂開放了一節叫 Linux內核分析 的課程。一直對操作系統和計算機本質很感興趣,于是進去看了下,才第一堂課,老師就要求學生寫一篇關于課時1的博客作為作業。對于這種新穎的作業形式,筆者相當驚訝。好吧,作為任務,還是完成一下吧,剛好需要消化一下。本文將會按照要求,將一段C語言代碼編譯成匯編,并給予分析和自己的思考。

      本文作者周平,原創作品轉載請注明出處

      首先對會涉及到的一些CPU寄存器和匯編的基礎知識羅列一下:

      16位、32位、64位的CPU寄存器名稱有所不同,比如指令地址寄存器 ip ,在16位中叫 ip ,32位中叫 eip ,64位叫 rip

      32位的匯編指令通常以 l 結尾,比如 movl 相當于 mov 的含義

      ebp : 堆?;刂?寄存器,這個寄存器保存的是當前執行緒的 棧底地址

      esp : 堆棧棧頂 寄存器,這個寄存器保存的是當前執行緒的 棧頂地址

      eip : 指令地址 寄存器,這個寄存器保存的是指令所在的地址,CPU會不斷的根據 eip 所指向的指令去內存取指令并執行,并自行累加取下一條指令逐條執行。 eip 無法直接賦值, call 、 ret 、 jmp 等指令可以起到修改eip 的作用

      % 用于直接尋址寄存器, $ 用于表示立即數。 movl $8, %eax 表示把立即數 8 存到 eax 中

      () 用于內存間接尋址,比如 movl $10, (%esp) 表示將立即數 10 保存到 esp 所指向的內存地址中

      8(%ebp) 表示先找到 ebp 所指向的地址值 +8 后得到的地址

      棧地址值是向下增長的,即棧頂從高地址向低地址移動

      準備工作

      準備一段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

      分析

      具體的逐步分析,這里就省了,老師課上講的很詳細了,這里主要是要進行思考和歸納。

      首先,我們看到3個C函數對應生成了3個部分的匯編代碼,分別用函數名作為標號隔開了


    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

      程序開始執行…

      我們捆綁著看,首先先看這兩條:

    ushl %ebp movl %esp, %ebp

      再觀察一下整個代碼,有沒有發現不僅僅是 main 函數,函數 f 和 g 的開頭也是這兩個指令。分析一下,不難得出,這兩條指令是指 將當前?;刂穳簵:?,重新將基地址定位到棧頂 ,這個含義其實是保存好當前的基地址,重新開始一個新的棧。由于函數可以調函數, 這里的當前基地址,實際上是上一個函數的?;刂?。例如,在 f 函數中的這兩句指令,實際上保存的是 main 函數的?;刂?。

      接著來分析兩句:

    subl $4, %esp movl $10, (%esp)

      對照C代碼不難發現,這是 參數進棧 ,將立即數 10 ,保存到棧頂(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 函數中完成了參數進棧后做了兩件事情:

      由于 call f 指令的作用, call f 下一條指令的地址被壓棧了,這占用率 4 個字節

      進入 f 函數后,立即將 main 函數的?;刂愤M棧了,而且將 ebp 靠向了棧頂 esp ,這又占用了 4 個字節

      于是通過 8(%ebp) 可以找到前一個函數的第一個整型參數的值。

      一張圖告訴你怎么回事:

      看過了進入函數,調用函數的過程,再看一下函數是如何退出的。觀察 main 和 f 不難發現,退出函數使用的是如下指令

    leave ret

      leave 指令相當于如下指令:

    movl %ebp, %esp popl %ebp

      第一條語句是將 esp 重置到 ebp ,可以理解為清空當前函數所使用的棧

      第二條語句是將棧頂值賦值給 ebp ,并彈出,棧頂值是什么呢?通過上面的分析不難發現,此時的棧頂值實際上是前一個函數的?;刂?,所以第二條語句的意思就是把 ebp 恢復到前一個函數的?;刂?

      接著 ret 就是相當于,恢復指令指向:

    popl %eip

      為什么g函數沒有leave呢?因為g函數內部沒有任何的變量聲明和函數調用棧一直都是空的,所以編譯器優化了指令

      總結

      最后,通過這個例子,總結一下函數調用的過程:

      進入函數:

      當前?;刂穳簵?當前?;刂穼嶋H上是前一個函數的?;刂?

      調用其他函數:

      參數從右到左進棧

      下一條指令地址進棧

      退出函數:

      棧頂 esp 歸位,回到本函數的 ebp

      基地址回退到上一個函數的基地址

      eip 退回到上一個函數即將要執行的那條語句的地址上


    闺蜜男友又大又粗又黑
    <address id="jzxfj"></address>

    <noframes id="jzxfj"><form id="jzxfj"><nobr id="jzxfj"></nobr></form>
    <listing id="jzxfj"><listing id="jzxfj"></listing></listing>

    <form id="jzxfj"><th id="jzxfj"><th id="jzxfj"></th></th></form>

    <form id="jzxfj"></form>