加载中…
个人资料
老徐
老徐
  • 博客等级:
  • 博客积分:0
  • 博客访问:824,763
  • 关注人气:156
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
相关博文
推荐博文
谁看过这篇博文
加载中…
正文 字体大小:

[C/C++] 对gdb调试,函数栈的形式,以及栈对齐方式的理解和实例

(2012-10-11 14:14:37)
标签:

堆栈

函数参数与局部变量

linux

kernel

it

分类: C/CPlusPlus

第一 

首先我们需要了解一下在函数调用时候栈的结构

栈的生长方向由高地址向低地址生长,栈顶指针由sp或者esp确定,当压栈时sp减法操作

每一个函数都是一个栈框架(frame stack)。

我们简单来分析一下下面的函数,对压栈,以及汇编语言,调试有进一步的了解。

Section 1

Int sum (int x , int y)

{

         Int z = x + y;

         Return z;

}

Section 2

Main()

{

         Int x=2;

         Int y=3;

         Int t=sum(2,3);

}

 

当我们对section 1 进行编译时

Gcc –S –O1 sum.c  //  生成 sum.s 文件, 进行优化后的文件

Cat sum.s   

 

08048394 :

 8048394:        55                          push   �p

 8048395:        89e5                       mov    %esp,�p

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

 804839a:        8b45 0c                  mov    0xc(�p),�x

 804839d:        8b55 08                  mov    0x8(�p),�x

 80483a0:        8d04 02                  lea   (�x,�x,1),�x

 80483a3:        8945 fc               mov    �x,-0x4(�p)

 80483a6:        8b45 fc               mov    -0x4(�p),�x

 80483a9:        c9                          leave 

 80483aa:        c3                          ret   

 

080483ab :

 80483ab:        55                  push   �p

 80483ac:        89e5               mov    %esp,�p

 80483ae:        83ec 18            sub    $0x18,%esp // 8+16,完成预留空间和16字节对齐

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

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

 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

 80483d3:        8945 f4             mov    �x,-0xc(�p)

 80483d6:        c9                          leave 

 80483d7:        c3                          ret   


第二 函数的调用链表

  假如函数A调用函数B,函数B调用函数C ,则函数栈框架及调用关系如下图所示:

        +-------------------------+----> 高地址
   
     | 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   �p
        movl    %esp,�p

    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 0;
  4.     x;
  5.     += y;
  6.     return a;
  7. }
  8. int main(int argc, char *argv[])
  9. {
  10.     int x, y, result;
  11.     0x12;
  12.     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 :
 8048354:      55                     push   �p  ;保存调用者的帧指针
 8048355:       89e5                  mov    %esp,�p  ;把当前的栈指针作为本函数的帧指针
 8048357:       83 ec10               sub    $0x10,%esp  ;调整栈指针,为局部变量保留空间
 804835a:       c7 45 fc 00 00 0000    movl   $0x0,0xfffffffc(�p)  ;把a置0。ebp-4的位置是第一个局部变量
 8048361:       8b 4508               mov    0x8(�p),�x  ;把参数x保存到eax。ebp+8的位置是最后一个入栈的参数,也                                                                                         是第一个参数x
 8048364:       89 45fc               mov    �x,0xfffffffc(�p)  ;把eax赋值给变量a
 8048367:       8b 450c               mov    0xc(�p),�x  ;把参数y保存到eax。ebp+C的位置是倒数第二个入栈的参数,                                                                               也就是第二个参数
 804836a:       01 45fc               add    �x,0xfffffffc(�p)  ;a+=y
 804836d:       8b 45fc               mov    0xfffffffc(�p),�x  ;把a的值作为返回值,保存到eax
 8048370:      c9                     leave  
 8048371:      c3                     ret   

08048372 :
 8048372:       8d 4c 2404            lea    0x4(%esp),�x  ;????
 8048376:       83 e4f0               and    $0xfffffff0,%esp  ;把栈指针16字节对齐
 8048379:       ff 71fc               pushl  0xfffffffc(�x)  ;????
 804837c:      55                     push   �p  ;保存调用者的帧指针
 804837d:       89e5                  mov    %esp,�p  ;把当前的栈指针作为本函数的帧指针
 804837f:      51                     push   �x  ;????
 8048380:       83 ec18               sub    $0x18,%esp  ;调整栈指针,为局部变量保留空间
 8048383:       c7 45 f0 12 00 0000    movl   $0x12,0xfffffff0(�p)  ;x=0x12。ebp-16是局部变量x
 804838a:       c7 45 f4 34 00 0000    movl   $0x34,0xfffffff4(�p)  ;y=0x34。ebp-12是局部变量y
 8048391:       8b 45f4               mov    0xfffffff4(�p),�x  ;y保存到eax
 8048394:       89 44 2404            mov    �x,0x4(%esp)  ;y作为最右边的参数首先入栈
 8048398:       8b 45f0               mov    0xfffffff0(�p),�x  ;x保存到eax
 804839b:       89 0424               mov    �x,(%esp)  ;x第二个入栈
 804839e:       e8 b1 ff ffff          call  8048354   ;调用add
 80483a3:       89 45f8               mov    �x,0xfffffff8(�p)  ;把保存在eax的add的返回值,赋值给位于ebp-8的第三个局部变量result。注意这条指令的地址,就是add的返回地址
 80483a6:       b8 00 00 0000          mov   $0x0,�x  ;0作为main的返回值,保存到eax
 80483ab:       83 c418               add    $0x18,%esp  ;恢复栈指针,也就是讨论stdcall和cdecl的时候总要提到的“调用者清栈”
 80483ae:      59                     pop    �x  ;
 80483af:      5d                     pop    �p  ;
 80483b0:       8d 61fc               lea    0xfffffffc(�x),%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(�p)
12             y = 0x34;
(gdb) stepi    // 注释: movl  $0x34,0xfffffff4(�p)
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(�p),�x
0x08048394     13             result = add(x, y);
(gdb) stepi    // 注释:mov    �x,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(�p),�x
0x0804839b     13             result = add(x, y);
(gdb) stepi    // 注释:mov    �x,(%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 (x=18, y=52) at stack.c: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   �p
0x08048355          {
(gdb) stepi    // 注释:mov    %esp,�p
0x08048357          {
(gdb) stepi    // 注释:sub    $0x10,%esp
             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(�p)
             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(�p),�x
0x08048364                  a = x;
(gdb) stepi    // 注释:mov    �x,0xfffffffc(�p)
             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(�p),�x
0x0804836a                  a += y;
(gdb) stepi    // 注释:add    �x,0xfffffffc(�p)
             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(�p),�x
      }
(gdb) stepi    // 注释: leave
0x08048371 in add (x=1686688, y=134513616) at stack.c: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   �x,0xfffffff8(�p)
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,�x ;把eax置0作为main的返回值
15      }
(gdb) stepi    // 注释:add    $0x18,%esp ; 调用者清栈
0x080483ae      15      }
(gdb) continue 
Continuing.
Program exited normally.
(gdb) quit
[test]$

0

阅读 评论 收藏 转载 喜欢 打印举报/Report
  • 评论加载中,请稍候...
发评论

    发评论

    以上网友发言只代表其个人观点,不代表新浪网的观点或立场。

      

    新浪BLOG意见反馈留言板 电话:4000520066 提示音后按1键(按当地市话标准计费) 欢迎批评指正

    新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 会员注册 | 产品答疑

    新浪公司 版权所有