<sub id="xt3p5"><listing id="xt3p5"><thead id="xt3p5"></thead></listing></sub>

    <track id="xt3p5"><big id="xt3p5"><em id="xt3p5"></em></big></track>

    <menuitem id="xt3p5"><dfn id="xt3p5"></dfn></menuitem>
      <track id="xt3p5"></track>
      <pre id="xt3p5"></pre>

          <form id="xt3p5"></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 退回到上一個函數即將要執行的那條語句的地址上


          亚洲Av日韩AV激情亚洲

            <sub id="xt3p5"><listing id="xt3p5"><thead id="xt3p5"></thead></listing></sub>

            <track id="xt3p5"><big id="xt3p5"><em id="xt3p5"></em></big></track>

            <menuitem id="xt3p5"><dfn id="xt3p5"></dfn></menuitem>
              <track id="xt3p5"></track>
              <pre id="xt3p5"></pre>

                  <form id="xt3p5"></form>