對gdb調試,函數棧的形式,以及棧對齊方式的理解和實例

第一  棧

首先我們需要了解一下在函數調用時候棧的結構

 

 棧的生長方向由高地址向低地址生長,棧頂指針由sp或者esp確定,當壓棧時sp減法操作

 

每一個函數都是一個棧框架(frame stack)。

 

我們簡單來分析一下下來函數,對壓棧,以及彙編語言,調試有進一步的瞭解

Section 1

Int sum (int x , int y)

{

         Intz = x + y;

         Returnz;

}

Section 2

Main()

{

         Intx=2;

         Inty=3;

         Intt=sum(2,3);

}

 

當我們對section 1 進行編譯時

Gcc –S –O1 sum.c  //  生成 sum.s 文件, 進行優化後的文件

Cat sum.s   

 

08048394 <sum>:

 8048394:        55                          push   %ebp

 8048395:        89e5                        mov    %esp,%ebp

 8048397:        83ec 10             sub    $0x10,%esp

 804839a:        8b45 0c             mov    0xc(%ebp),%eax

 804839d:        8b55 08                       mov    0x8(%ebp),%edx

 80483a0:        8d04 02                       lea   (%edx,%eax,1),%eax

 80483a3:        8945 fc               mov    %eax,-0x4(%ebp)

 80483a6:        8b45 fc               mov    -0x4(%ebp),%eax

 80483a9:        c9                          leave 

 80483aa:        c3                          ret   

 

080483ab <main>:

 80483ab:        55                  push   %ebp

 80483ac:        89e5               mov    %esp,%ebp

 80483ae:        83ec 18            sub    $0x18,%esp // 8+16,完成預留空間和16字節對齊

 80483b1:        c745 fc 02 00 00 00        movl   $0x2,-0x4(%ebp)

 80483b8:        c745 f8 03 00 00 00        movl   $0x3,-0x8(%ebp)

 80483bf:         c744 24 04 03 00 00       movl   $0x3,0x4(%esp)

 80483c6:        00

 80483c7:        c704 24 02 00 00 00       movl   $0x2,(%esp)

 80483ce:        e8 c1 ff ff ff                call   8048394 <sum>

 80483d3:        8945 f4             mov    %eax,-0xc(%ebp)

 80483d6:        c9                          leave 

 80483d7:        c3                          ret   

 80483d8:        90                          nop

 80483d9:        90                          nop

 80483da:        90                          nop

 80483db:        90                          nop

 80483dc:        90                          nop

 80483dd:        90                          nop

 80483de:        90                          nop

 80483df:         90                          nop

第二 函數的調用鏈表

  假如函數A調用函數B,函數B調用函數C ,則函數棧框架及調用關係如下圖所示:
<!--[if !supportLineBreakNewLine]-->
<!--[endif]-->

        +-------------------------+----> 高地址
   
     | EIP (上級函數返回地址)    | 
        +-------------------------+ 
 +-->   |EBP (上級函數的EBP)      | --+ <------當前函數A的EBP (即SFP框架指針) 
 |      +-------------------------+   +-->偏移量A 
 |
      | LocalVariables         |   |
 |      |..........              | --+  <------ESP指向函數A新分配的局部變量,局部變量可以通過A的ebp-偏移量A訪問 
 | f
   +-------------------------+
 | r   | Arg n(函數B的第n個參數)   | 
 | a   +-------------------------+
 | m   | Arg .(函數B的第.個參數)   |
 | e   +-------------------------+
 |      | Arg 1(函數B的第1個參數)   |
 | o   +-------------------------+
 | f   | Arg 0(函數B的第0個參數)   | --+ <------ B函數的參數可以由B的ebp+偏移量B訪問
 |      +-------------------------+   +--> 偏移量B
 | A
   | EIP (A函數的返回地址)     |   | 
 |      +-------------------------+ --+ 
 +--- | EBP (A函數的EBP)         |<--+ <------ 當前函數B的EBP (即SFP框架指針) 
        +-------------------------+   |
   
     | LocalVariables         |   |
   
     |..........              |   | <------ ESP指向函數B新分配的局部變量
        +-------------------------+   |
   
     | Arg n(函數C的第n個參數)   |   |
   
     +-------------------------+   |
   
     | Arg .(函數C的第.個參數)   |   |
   
     +-------------------------+   +--> frame of B
   
     | Arg 1(函數C的第1個參數)   |   |
   
     +-------------------------+   |
   
     | Arg 0(函數C的第0個參數)   |   |
   
     +-------------------------+   |
   
     | EIP (B函數的返回地址)     |   |
   
     +-------------------------+   |
 +--> 
  |EBP (B函數的EBP)         | --+ <------ 當前函數C的EBP (即SFP框架指針) 
 |      +-------------------------+
 |      | LocalVariables         |
 |      |..........              | <------ ESP指向函數C新分配的局部變量
 |      +-------------------------+----> 低地址
frame of C
        

 

 

第三 棧地址的對齊方式

 

手冊上介紹:

Thestack pointer for a stack segment should be aligned on 16-bit (word) or 32-bit(double-word)
boundaries, depending on the width of the stack segment. The D flag in thesegment descriptor
for the current code segment sets the stack-segment width (refer to Chapter 3,Protected-Mode
Memory Management of the Intel Architecture Software Developer’s Manual, Volume3). The
PUSH and POP instructions use the D flag to determine how much to decrement orincrement
the stack pointer on a push or pop operation, respectively. When the stackwidth is 16 bits, the
stack pointer is incremented or decremented in 16-bit increments; when thewidth is 32 bits, the
stack pointer is incremented or decremented in 32-bit increments.
The processor does not check stack pointer alignment. It is the responsibilityof the programs,
tasks, and system procedures running on the processor to maintain properalignment of stack
pointers. Misaligning a stack pointer can cause serious performance degradationand in some
instances program failures.

在實際測試時,需要視編譯器而定,即便是相同的編譯器,不同的優化策略得出的結果也有可能不同。

      #   vi   test4.c 
       int   main() 
       { 
               char   str1[50]; 
               char   str2[100]; 
               return   0; 
       } 

這裏分爲三個部分: 
1:   0x8 
這裏假設在進入main函數之前,棧是16字節對齊的話. 
call   main函數的時候,需要把eip和ebp壓入堆棧,此時棧地址(esp)最末4位二進制位必定是1000,esp-8則恰好使後4位地址二進制位爲0000。所以這裏爲分配了0x8個字節,是爲保證棧16字節對齊的。 

2:   0x40 
由於你定義了一個char   str1[50];如果直接分配50個字節,那麼將破壞棧16字節對齊規則,所以我們得分配一個同時滿足空間需要而且保持16字節棧對齊的,最接近的就是0x40(0x30 <50<0x40)。 

3:   0x70 
如2所說的,最接近的就是0x70(0x60 <100<0x70)。 

爲什麼說:“此時棧地址(esp)最末4位二進制位必定是1000,”
當然這裏只是針對:gcc默認的編譯是要16字節棧對齊的而言的

 

這裏假設在進入main函數之前,棧是16字節對齊的話. 假設地址是100000(0x20)
| | <- 100000
| |
| |
| |
| |
| |
| |

在調用main函數的時候,要把main函數的返回地址壓入stack中,還要把EBP壓入進入,而這兩個都是指針,在32位機上都是4個字節(1000),即push這兩個值之後,ESP的值就是11000(0x20 - 0x08 =0x18),這時候棧地址(ESP)最末4位二進制位是不是就是1000了?所以就再sub 8個字節,保證之後使用的ESP還是16字節對齊的。
 __________100000 (ESP)
| EIP |   
| EBP|__________011000
| |
| |__________010000
| |
| |
| |

 

 

第四 IA32 基本進出棧的方式

  如我們所知:
    1)IA32的棧是用來存放臨時數據,而且是LIFO,即後進先出的。棧的增長方向是從高地址向低地址增長,按字節爲單位編址。
    2) EBP是棧基址的指針,永遠指向棧底(高地址),ESP是棧指針,永遠指向棧頂(低地址)。
    3) PUSH一個long型數據時,以字節爲單位將數據壓入棧,從高到低按字節依次將數據存入ESP-1、ESP-2、ESP-3、ESP-4的地址單元。
    4) POP一個long型數據,過程與PUSH相反,依次將ESP-4、ESP-3、ESP-2、ESP-1從棧內彈出,放入一個32位寄存器。
    5) CALL指令用來調用一個函數或過程,此時,下一條指令地址會被壓入堆棧,以備返回時能恢復執行下條指令。
    6) RET指令用來從一個函數或過程返回,之前CALL保存的下條指令地址會從棧內彈出到EIP寄存器中,程序轉到CALL之前下條指令處執行
    7) ENTER是建立當前函數的棧框架,即相當於以下兩條指令:
        pushl   %ebp
        movl    %esp,%ebp

    8) LEAVE是釋放當前函數或者過程的棧框架,即相當於以下兩條指令:
        movl ebp esp
        popl  ebp

第五(轉)詳細介紹函數調用的那些事

“參數從右到左入棧”,“局部變量在棧上分配空間”,聽的耳朵都起繭子了。
最近做項目涉及C和彙編互相調用,寫代碼的時候才發現沒真正弄明白。
自己寫了個最簡單的函數,用gdb跟蹤了調用過程,纔多少懂了一點。

參考資料:(感謝liigo和eno_rez兩位作者)

http://blog.csdn.net/liigo/archive/2006/12/23/1456938.aspx

http://blog.csdn.net/eno_rez/archive/2008/03/08/2158682.aspx

  1. int add(int x, int y)
  2. {
  3.     int a = 0;
  4.     a = x;
  5.     a += y;
  6.     return a;
  7. }
  8. int main(int argc, char *argv[])
  9. {
  10.     int x, y, result;
  11.     x = 0x12;
  12.     y = 0x34;
  13.     result = add(x, y);
  14.     return 0;
  15. }

編譯:(Fedora6,gcc 4.1.2)
[test]$ gcc -g -Wall -o stack stack.c

反彙編:
這裏的彙編的格式是AT&T彙編,它的格式和我們熟悉的彙編格式不太一樣,尤其要注意源操作數和目的操作數的順序是反過來的
[test]$ objdump -d stack > stack.dump
[test]$ cat stack.dump

......
08048354 <add>:
 8048354:      55                     push   %ebp  ;保存調用者的幀指針
 8048355:       89e5                  mov    %esp,%ebp  ;把當前的棧指針作爲本函數的幀指針
 8048357:       83 ec10               sub    $0x10,%esp  ;調整棧指針,爲局部變量保留空間
 804835a:       c7 45 fc 00 00 0000    movl   $0x0,0xfffffffc(%ebp)  ;把a置0。ebp-4的位置是第一個局部變量
 8048361:       8b 4508               mov    0x8(%ebp),%eax  ;把參數x保存到eax。ebp+8的位置是最後一個入棧的參數,也就是第一個參數
 8048364:       89 45fc               mov    %eax,0xfffffffc(%ebp)  ;把eax賦值給變量a
 8048367:       8b 450c               mov    0xc(%ebp),%eax  ;把參數y保存到eax。ebp+C的位置是倒數第二個入棧的參數,也就是第二個參數
 804836a:       01 45fc               add    %eax,0xfffffffc(%ebp)  ;a+=y
 804836d:       8b 45fc               mov    0xfffffffc(%ebp),%eax  ;把a的值作爲返回值,保存到eax
 8048370:      c9                     leave  
 8048371:      c3                     ret   

08048372 <main>:
 8048372:       8d 4c 2404            lea    0x4(%esp),%ecx  ;????
 8048376:       83 e4f0               and    $0xfffffff0,%esp  ;把棧指針16字節對齊
 8048379:       ff 71fc               pushl  0xfffffffc(%ecx)  ;????
 804837c:      55                     push   %ebp  ;保存調用者的幀指針
 804837d:       89e5                  mov    %esp,%ebp  ;把當前的棧指針作爲本函數的幀指針
 804837f:      51                     push   %ecx  ;????
 8048380:       83 ec18               sub    $0x18,%esp  ;調整棧指針,爲局部變量保留空間
 8048383:       c7 45 f0 12 00 0000    movl   $0x12,0xfffffff0(%ebp)  ;x=0x12。ebp-16是局部變量x
 804838a:       c7 45 f4 34 00 0000    movl   $0x34,0xfffffff4(%ebp)  ;y=0x34。ebp-12是局部變量y
 8048391:       8b 45f4               mov    0xfffffff4(%ebp),%eax  ;y保存到eax
 8048394:       89 44 2404            mov    %eax,0x4(%esp)  ;y作爲最右邊的參數首先入棧
 8048398:       8b 45f0               mov    0xfffffff0(%ebp),%eax  ;x保存到eax
 804839b:       89 0424               mov    %eax,(%esp)  ;x第二個入棧
 804839e:       e8 b1 ff ffff          call  8048354 <add>  ;調用add
 80483a3:       89 45f8               mov    %eax,0xfffffff8(%ebp)  ;把保存在eax的add的返回值,賦值給位於ebp-8的第三個局部變量result。注意這條指令的地址,就是add的返回地址
 80483a6:       b8 00 00 0000          mov   $0x0,%eax  ;0作爲main的返回值,保存到eax
 80483ab:       83 c418               add    $0x18,%esp  ;恢復棧指針,也就是討論stdcall和cdecl的時候總要提到的“調用者清棧”
 80483ae:      59                     pop    %ecx  ;
 80483af:      5d                     pop    %ebp  ;
 80483b0:       8d 61fc               lea    0xfffffffc(%ecx),%esp  ;
 80483b3:      c3                     ret    
 80483b4:      90                     nop    
......

有一點值得注意的是main在調用add之前把參數壓棧的過程。
它用的不是push指令,而是另一種方法。
在main入口調整棧指針的時候,也就是位於8048380的這條指令 sub $0x18,%esp
不但象通常函數都要做的那樣給局部變量預留了空間,還順便把調用add的兩個參數的空間也預留出來了。
然後把參數壓棧的時候,用的是mov指令。
我不太明白這種方法有什麼好處。
另外一個不明白的就是main入口的四條指令8048372、8048376、8048379、804837f,還有與之對應的main返回之前的指令。
貌似main對esp要求16字節對齊,所以先把原來的esp壓棧,然後強行把esp的低4位清0。等到返回之前再從棧裏恢復原來的esp

準備工作都做好了,現在開始gdb
對gdb不太熟悉的同學要注意一點,stepi命令執行之後顯示出來的源代碼行或者指令地址,都是即將執行的指令,而不是剛剛執行完的指令。
我在每個stepi後面都加了註釋,就是剛執行過的指令。

[test]$ gdb -q stack
(gdb) break main
Breakpoint 1 at 0x8048383: file stack.c, line 11.
gdb並沒有把斷點設置在main的第一條指令,而是設置在了調整棧指針爲局部變量保留空間之後

(gdb) run
Starting program: /home/brookmill/test/stack 
Breakpoint 1, main () at stack.c:11
11             x = 0x12;
(gdb) stepi    // 註釋: movl  $0x12,0xfffffff0(%ebp)
12             y = 0x34;
(gdb) stepi    // 註釋: movl  $0x34,0xfffffff4(%ebp)
13             result = add(x, y);
(gdb) info registers esp
esp           0xbf8df8ac       0xbf8df8ac
(gdb) info registers ebp
ebp           0xbf8df8c8       0xbf8df8c8
(gdb) x/12 0xbf8df8a0
0xbf8df8a0:     0x002daff4     0x002d9220     0xbf8df8d8      0x080483e9
0xbf8df8b0:     0x001ca8d5     0xbf8df96c     0x00000012      0x00000034
0xbf8df8c0:     0x001903d0     0xbf8df8e0     0xbf8df938      0x001b4dec

這就是傳說中的棧。在main準備調用add之前,先看看這裏有些什麼東東
0xbf8df8c8(ebp)保存的是上一層函數的幀指針:0xbf8df938,距離這裏有112字節
0xbf8df8cc(ebp+4)保存的是main的返回地址0x001b4dec
0xbf8df8b8(ebp-16)是局部變量x,已經賦值0x12;
0xbf8df8bc(ebp-12)是局部變量y,已經賦值0x34;
0xbf8df8c0(ebp-8)是局部變量result。值得注意的是,因爲我們沒有給result賦值,這裏是一個不確定的值。局部變量如果不顯式的初始化,初始值不一定是0。

現在開始調用add
(gdb) stepi    // 註釋:mov    0xfffffff4(%ebp),%eax
0x08048394     13             result = add(x, y);
(gdb) stepi    // 註釋:mov    %eax,0x4(%esp)
0x08048398      13             result = add(x, y);
(gdb) x/12 0xbf8df8a0
0xbf8df8a0:     0x002daff4     0x002d9220     0xbf8df8d8      0x080483e9
0xbf8df8b0:     0x00000034     0xbf8df96c     0x00000012      0x00000034
0xbf8df8c0:     0x001903d0     0xbf8df8e0      0xbf8df938     0x001b4dec
y首先被壓棧,在0xbf8df8b0

(gdb)stepi    // 註釋: mov   0xfffffff0(%ebp),%eax
0x0804839b     13             result = add(x, y);
(gdb) stepi    // 註釋:mov    %eax,(%esp)
0x0804839e     13             result = add(x, y);
(gdb) x/12 0xbf8df8a0
0xbf8df8a0:     0x002daff4     0x002d9220     0xbf8df8d8      0x00000012
0xbf8df8b0:     0x00000034     0xbf8df96c     0x00000012      0x00000034
0xbf8df8c0:     0x001903d0     0xbf8df8e0     0xbf8df938      0x001b4dec
x第二個進棧,在0xbf8df8ac

(gdb) stepi   // 註釋:call   8048354 <add>
add (x=18, y=52) at stack.c:2
2       {

剛剛執行了call指令,現在我們進入了add函數
(gdb) info registers esp
esp           0xbf8df8a8       0xbf8df8a8
(gdb) info registers ebp
ebp           0xbf8df8c8       0xbf8df8c8
(gdb) x/12 0xbf8df8a0
0xbf8df8a0:     0x002daff4     0x002d9220     0x080483a3      0x00000012
0xbf8df8b0:     0x00000034     0xbf8df96c     0x00000012      0x00000034
0xbf8df8c0:     0x001903d0     0xbf8df8e0     0xbf8df938      0x001b4dec
現在esp指向0xbf8df8a8,這裏保存的是add函數的返回地址,它是由call指令壓棧的。

(gdb) stepi    // 註釋: push   %ebp
0x08048355      2      {
(gdb) stepi    // 註釋:mov    %esp,%ebp
0x08048357      2      {
(gdb) stepi    // 註釋:sub    $0x10,%esp
3              int a = 0;
(gdb) info registers esp
esp            0xbf8df894      0xbf8df894
(gdb) info registers ebp
ebp           0xbf8df8a4       0xbf8df8a4
(gdb) x/16 0xbf8df890
0xbf8df890:     0x00000000     0x08049574     0xbf8df8a8      0x08048245
0xbf8df8a0:     0x002daff4     0xbf8df8c8     0x080483a3      0x00000012
0xbf8df8b0:     0x00000034     0xbf8df96c     0x00000012      0x00000034
0xbf8df8c0:     0x001903d0     0xbf8df8e0     0xbf8df938      0x001b4dec
剛剛執行完的3條指令是函數入口的定式。
現在我們可以看到,main的棧還是原樣,向下增長之後就是add的棧。
0xbf8df8a4(ebp)保存的是上層函數main的幀指針
0xbf8df8a8(ebp+4)保存的是返回地址
0xbf8df8ac(ebp+8)保存的是最後一個入棧的參數x
0xbf8df8b0(ebp+C)保存的是倒數第二個入棧的參數y
0xbf8df8a0(ebp-4)保存的是局部變量a,現在是一個不確定值

接下來add函數就真正開始幹活了
(gdb) stepi    // 註釋: movl  $0x0,0xfffffffc(%ebp)
4              a = x;
(gdb) x/16 0xbf8df890
0xbf8df890:     0x00000000     0x08049574     0xbf8df8a8      0x08048245
0xbf8df8a0:     0x00000000     0xbf8df8c8     0x080483a3      0x00000012
0xbf8df8b0:     0x00000034     0xbf8df96c     0x00000012      0x00000034
0xbf8df8c0:     0x001903d0     0xbf8df8e0      0xbf8df938     0x001b4dec
可以看到a被置0了

(gdb) stepi    // 註釋: mov    0x8(%ebp),%eax
0x08048364     4              a = x;
(gdb) stepi    // 註釋:mov    %eax,0xfffffffc(%ebp)
5              a += y;
(gdb) x/16 0xbf8df890
0xbf8df890:     0x00000000     0x08049574      0xbf8df8a8     0x08048245
0xbf8df8a0:     0x00000012     0xbf8df8c8     0x080483a3      0x00000012
0xbf8df8b0:     0x00000034     0xbf8df96c     0x00000012      0x00000034
0xbf8df8c0:     0x001903d0     0xbf8df8e0     0xbf8df938      0x001b4dec
參數x(ebp+8)的值通過eax賦值給了局部變量a(ebp-4)

(gdb) stepi    // 註釋: mov    0xc(%ebp),%eax
0x0804836a     5              a += y;
(gdb) stepi    // 註釋:add    %eax,0xfffffffc(%ebp)
6              return a;
(gdb) x/16 0xbf8df890
0xbf8df890:     0x00000000     0x08049574     0xbf8df8a8      0x08048245
0xbf8df8a0:     0x00000046     0xbf8df8c8     0x080483a3      0x00000012
0xbf8df8b0:     0x00000034     0xbf8df96c     0x00000012      0x00000034
0xbf8df8c0:     0x001903d0     0xbf8df8e0     0xbf8df938      0x001b4dec
參數y(ebp+C)的值通過eax加到了局部變量a(ebp-4)

現在要從add返回了。返回之前把局部變量a(ebp-4)保存到eax用作返回值
(gdb) stepi    // 註釋:mov    0xfffffffc(%ebp),%eax
7       }
(gdb) stepi    // 註釋: leave
0x08048371 in add (x=1686688, y=134513616) at stack.c:7
7       }
(gdb) stepi    // 註釋: ret
0x080483a3 in main () at stack.c:13
13             result = add(x, y);

現在我們回到了main,棧現在是這樣的
(gdb) info registers esp
esp           0xbf8df8ac       0xbf8df8ac
(gdb) info registers ebp
ebp           0xbf8df8c8       0xbf8df8c8
(gdb) x/16 0xbf8df890
0xbf8df890:     0x00000000     0x08049574     0xbf8df8a8      0x08048245
0xbf8df8a0:     0x00000046     0xbf8df8c8     0x080483a3      0x00000012
0xbf8df8b0:     0x00000034     0xbf8df96c     0x00000012      0x00000034
0xbf8df8c0:     0x001903d0     0xbf8df8e0     0xbf8df938      0x001b4dec
可以看到,esp和ebp都已經恢復到了調用add之前的值。
但是,調用add的兩個參數還在棧裏(0xbf8df8ac、0xbf8df8b0,都在esp以上)。
也就是說,被調用的函數add沒有把它們從棧上清出去,需要調用方main來清理。這就是著名的“調用者清棧”,cdecl調用方式的特點之一。

(gdb) stepi    // 註釋: mov   %eax,0xfffffff8(%ebp)
14             return 0;
(gdb) x/16 0xbf8df890
0xbf8df890:     0x00000000     0x08049574     0xbf8df8a8      0x08048245
0xbf8df8a0:     0x00000046     0xbf8df8c8     0x080483a3      0x00000012
0xbf8df8b0:     0x00000034     0xbf8df96c     0x00000012      0x00000034
0xbf8df8c0:     0x00000046     0xbf8df8e0     0xbf8df938      0x001b4dec
從eax得到函數add的返回值,賦值給了局部變量result(ebp-8)

(gdb) stepi    // 註釋: mov    $0x0,%eax ;把eax置0作爲main的返回值
15      }
(gdb) stepi    // 註釋:add    $0x18,%esp ; 調用者清棧
0x080483ae      15      }
(gdb) continue 
Continuing.
Program exited normally.
(gdb) quit
[test]$ 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章