天天看點

gdb反彙編詳解C函數底層實作筆記(程式堆棧、記憶體配置設定)

以下是在讀《深入了解計算機系統》前面的章節“程式的機器級表示”時,自己動手在linux上使用了gdb對一個簡單的C程式進行反彙編,通過不懈的努力終于查清楚弄明白了絕大多數的語句。且均以注釋的形式列在彙編語句後面。

      所有這些注釋大概花了整整一天時間,不過還好,感覺對于C程式的機器級實作終于算是有了一個比較透徹的了解,對于以前編譯出現的有些bug的原因有了一種原來如此的感慨。感覺這段代碼及注釋對自己或者大家都可能有用,是以稍作整理放于此:

說明:

反彙編指令使用的是:gdb disas [function-name]。另外也可以使用:objdump -d/-D obj-name對整個程式進行反彙編(通過這個反彙編,你可以發現main函數并非一個程式的入口而是__start函數!)。

程式很簡單,就兩個函數:sum和main,源碼如下:

#include<stdio.h>

int sum(int x, int y)

{

 int accum = 0; 

 int t;

 t = x + y;

 accum += t;

 return accum;

}

int main( int argc, char **argv)

   int x = 1, y = 2;

   int result = sum( x, y );

   printf("\n\n     result = %d \n\n", result);

   return 0;

編譯源程式,不使用優化選項并反彙編分析:

gcc -o test test.c得到test

gdb test

(gdb) disas sum

(gdb) disas main

檢視反彙編後的代碼

Dump of assembler code for function sum:

0x08048354 <sum+0>:     push   %ebp  //esp <- esp-4

0x08048355 <sum+1>:     mov    %esp,%ebp

0x08048357 <sum+3>:     sub    $0x10,%esp

0x0804835a <sum+6>:     movl   $0x0,0xfffffff8(%ebp)   //accum = 0

0x08048361 <sum+13>:    mov    0xc(%ebp),%eax    //gotcha! ^_^

0x08048364 <sum+16>:    add    0x8(%ebp),%eax    // x + y

0x08048367 <sum+19>:    mov    %eax,0xfffffffc(%ebp)  // t = x + y

0x0804836a <sum+22>:    mov    0xfffffffc(%ebp),%eax 

0x0804836d <sum+25>:    add    %eax,0xfffffff8(%ebp)  // accum = accum + t

0x08048370 <sum+28>:    mov    0xfffffff8(%ebp),%eax // 傳回值是放在eax中的

0x08048373 <sum+31>:    leave  //恢複父函數的堆棧框指針:esp<-ebp, pop ebp,在位址上的表現就是堆棧頂底均回到之前的地方!

0x08048374 <sum+32>:    ret     //函數傳回,回到上級調用:pop eip?

main函數反彙編結果:

0x08048375 <main+0>:    lea    0x4(%esp),%ecx

0x08048379 <main+4>:    and    $0xfffffff0,%esp //使棧位址16位元組對齊!這裡也說明了棧向下(低位址)生長的優點了:位址對齊操作總是是esp指向目前位置的下方!(lihux自己悟出的^_^)

0x0804837c <main+7>:    pushl  0xfffffffc(%ecx)

0x0804837f <main+10>:   push   %ebp   //儲存main之前函數的棧基址

//pushl push都使用了的原因:當要壓棧的對象已經确定(也就是說已經知道是位元組、字或者雙字),那麼使用push就不會産生歧義,也就是說彙編器可以自己判斷自己要操作的是什麼長度的操作對象;但是當彙編器不能自己判斷操作對象長度時,就需要使用pushl之類的指令來指明操作對象長度(類似的還有mov,movl;lea,leal等);

上面的%ebp指的是esp寄存器吧,這是一個32位的寄存器,彙編器是知道這個寄存器存放的是dworld,不需要顯式的指明也沒有歧義;而 0xfffffffc(%ecx)是使用基址尋址方式指向的一個記憶體空間,記憶體是連續的,彙編器不能僅僅根據一個記憶體位址就判斷出那裡存放的資料的長度,是以直接這樣寫就會産生歧義,是以要使用pushl之類的指令來指明要操作的資料的長度;

0x08048380 <main+11>:   mov    %esp,%ebp  //在main之前函數的棧下方建main的棧,基址給ebp

0x08048382 <main+13>:   push   %ecx

0x08048383 <main+14>:   sub    $0x24,%esp  //棧頂指針下移24,以留作main函數用?是的!^_^。關于棧對齊問題:可以看到,在<main+4>執行時完畢之後棧頂指針esp已經16位元組對齊,而在執行到目前<main+14>指令前,又進行了三次壓棧操作,每次四位元組,共12位元組,是以這裡用0x24,這樣,又重新使esp16位元組對齊了(同時還必須保證配置設定的空間不小于main函數需要存放局部變量的空間^_^)。

0x08048386 <main+17>:   movl   $0x1,0xfffffff0(%ebp)  //主函數實參存放于記憶體的最高處,x = 1

0x0804838d <main+24>:   movl   $0x2,0xfffffff4(%ebp) //y = 2

0x08048394 <main+31>:   mov    0xfffffff4(%ebp),%eax

0x08048397 <main+34>:   mov    %eax,0x4(%esp)           //x的形參存入Mem本地空間,供子函數調用

0x0804839b <main+38>:   mov    0xfffffff0(%ebp),%eax

0x0804839e <main+41>:   mov    %eax,(%esp)           //由此可以看出:調用函數sum之前,需先将形參放到棧頂!

0x080483a1 <main+44>:   call   0x8048354 <sum>  //call調用,先将下一條指令的位址壓入堆棧,是以esp<- esp-4

0x080483a6 <main+49>:   mov    %eax,0xfffffff8(%ebp) //result = sum(x,y),傳回值在eax中

0x080483a9 <main+52>:   mov    0xfffffff8(%ebp),%eax

0x080483ac <main+55>:   mov    %eax,0x4(%esp)

0x080483b0 <main+59>:   movl   $0x80484a0,(%esp)  //字元串位址

0x080483b7 <main+66>:   call   0x8048298 <>

0x080483bc <main+71>:   mov    $0x0,%eax

0x080483c1 <main+76>:   add    $0x24,%esp   //?何為

0x080483c4 <main+79>:   pop    %ecx

0x080483c5 <main+80>:   pop    %ebp

0x080483c6 <main+81>:   lea    0xfffffffc(%ecx),%esp

0x080483c9 <main+84>:   ret   

0x080483ca <main+85>:   nop   

0x080483cb <main+86>:   nop   

0x080483cc <main+87>:   nop   

0x080483cd <main+88>:   nop   

0x080483ce <main+89>:   nop   

0x080483cf <main+90>:   nop  

編譯源程式,使用 -O1 優化選項并反彙編分析:

使用 -O1優化後的程式反彙編後的結果:可見優化主要在減少了存取記憶體的次數,節省了記憶體空間

0x08048354 <sum+0>:     push   %ebp

0x08048357 <sum+3>:     mov    0xc(%ebp),%eax

0x0804835a <sum+6>:     add    0x8(%ebp),%eax

0x0804835d <sum+9>:     pop    %ebp

0x0804835e <sum+10>:    ret   

End of assembler dump.

Dump of assembler code for function main:

0x0804835f <main+0>:    lea    0x4(%esp),%ecx

0x08048363 <main+4>:    and    $0xfffffff0,%esp

0x08048366 <main+7>:    pushl  0xfffffffc(%ecx)

0x08048369 <main+10>:   push   %ebp

0x0804836a <main+11>:   mov    %esp,%ebp

0x0804836c <main+13>:   push   %ecx

0x0804836d <main+14>:   sub    $0x14,%esp   //可見優化後的程式自身預留白間也減少了。

0x08048370 <main+17>:   movl   $0x2,0x4(%esp)

0x08048378 <main+25>:   movl   $0x1,(%esp)

0x0804837f <main+32>:   call   0x8048354 <sum>

0x08048384 <main+37>:   mov    %eax,0x4(%esp)

0x08048388 <main+41>:   movl   $0x8048480,(%esp)

0x0804838f <main+48>:   call   0x8048298 <>

0x08048394 <main+53>:   mov    $0x0,%eax

0x08048399 <main+58>:   add    $0x14,%esp

0x0804839c <main+61>:   pop    %ecx

0x0804839d <main+62>:   pop    %ebp

0x0804839e <main+63>:   lea    0xfffffffc(%ecx),%esp

0x080483a1 <main+66>:   ret   

0x080483a2 <main+67>:   nop   

0x080483a3 <main+68>:   nop   

0x080483a4 <main+69>:   nop

編譯源程式,使用優化選項 -O2并反彙編分析:

使用 -O2優化後的程式反彙編後的結果:可見較-O1級優化又有大幅的縮短(代碼長度)

   0x08048410 <+0>: push   %ebp

   0x08048411 <+1>: mov    %esp,%ebp

   0x08048413 <+3>: mov    0xc(%ebp),%eax

   0x08048416 <+6>: add    0x8(%ebp),%eax

   0x08048419 <+9>: pop    %ebp

   0x0804841a <+10>: ret   

   0x08048420 <+0>: push   %ebp

   0x08048421 <+1>: mov    %esp,%ebp

   0x08048423 <+3>: and    $0xfffffff0,%esp

   0x08048426 <+6>: sub    $0x10,%esp

   0x08048429 <+9>: movl   $0x3,0x8(%esp)   //我暈:這優化的也太厲害了吧:直接計算出了1+2=3!

   0x08048431 <+17>: movl   $0x8048510,0x4(%esp)

   0x08048439 <+25>: movl   $0x1,(%esp)

   0x08048440 <+32>: call   0x804832c <>

   0x08048445 <+37>: xor    %eax,%eax   //因為IA32中xor 比 mov 速度快!

   0x08048447 <+39>: leave 

   0x08048448 <+40>: ret   

繼續閱讀