日期:2014-05-16  浏览次数:20617 次

《coredump问题原理探究》Linux x86版3.2节栈布局之函数桢

看一个例子:

void FuncA()
{
}

void FuncB()
{
    FuncA();
}

int main()
{
    FuncB();
    return 0;
}


用下面命令编译出它发布版本:

[buckxu@xuzhina 1]$ g++  -o xuzhina_dump_c3_s1_relxuzhina_dump_c3_s1.cpp

在讨论它们的栈之前,先分析一下main,FuncB,FuncA这三个函数的汇编:

(gdb) disassemble FuncA
Dump of assembler code for function_Z5FuncAv:
  0x08048470 <+0>:    push   %ebp
  0x08048471 <+1>:    mov    %esp,%ebp
  0x08048473 <+3>:    pop    %ebp
  0x08048474 <+4>:    ret   
End of assembler dump.

(gdb) disassemble FuncB
Dump of assembler code for function_Z5FuncBv:
  0x08048475 <+0>:    push   %ebp
  0x08048476 <+1>:    mov    %esp,%ebp
  0x08048478 <+3>:    call   0x8048470 <_Z5FuncAv>
  0x0804847d <+8>:    pop    %ebp
  0x0804847e <+9>:    ret   
End of assembler dump.

(gdb) disassemble main
Dump of assembler code for function main:
  0x0804847f <+0>:    push   %ebp
  0x08048480 <+1>:    mov    %esp,%ebp
  0x08048482 <+3>:    call   0x8048475 <_Z5FuncBv>
  0x08048487 <+8>:    mov    $0x0,%eax
  0x0804848c <+13>:   pop    %ebp
  0x0804848d <+14>:   ret   
End of assembler dump.


从它们的汇编,都可以看到在这三个函数的开头,都有这样的指令:

push  %ebp
mov   %esp,%ebp

而在它们的结尾则有这样的指令:

pop   %ebp
ret   

在没有使用gcc的-fomit-frame-pointer选项来编译的函数一般都会有这样的开头和结尾。这几行指令可以看作是函数的开头和结尾的特征。像FuncA这样空叶子函数,一般就是由这两个特征拼起来的。

在x86里,ebp存放着函数桢指针,而esp则指向当前栈顶位置,而eip则是要执行的下一条指令地址。

所以,函数开头的两条指令的含义如下

push  %ebp             // esp = esp – 4,把ebp的值放入到esp指向的地址
mov   %esp,%ebp        // 把esp的值放到ebp里。即ebp = esp

函数结尾两条指令的含义如下

pop   %ebp                      // 把esp指向地址的内容放到ebp, esp = esp+4
ret                             //把ebp指向地址下一个单元的内容放到eip,esp = esp + 4


下面验证一下上面的内容。在main函数的开头指令地址0x0804847f打断点

(gdb) tbreak *0x0804847f
Temporary breakpoint 1 at 0x804847f

逐步地看一下是不是

(gdb) r
Starting program:/home/buckxu/work/3/1/xuzhina_dump_c3_s1_rel
 
Temporary breakpoint 1, 0x0804847f in main()
(gdb) i r ebp esp
ebp            0x0      0x0
esp            0xbffff4dc       0xbffff4dc
(gdb) x /4x $esp
0xbffff4dc:     0x4a8bf635      0x00000001      0xbffff574      0xbffff57c
(gdb) ni
0x08048480 in main ()
(gdb) i r ebp esp
ebp            0x0      0x0
esp            0xbffff4d8       0xbffff4d8
(gdb) x /4x $esp
0xbffff4d8:     0x00000000      0x4a8bf635      0x00000001      0xbffff574

果然,在运行了

push  %ebp

之后,esp的值由0xbffff4dc变为0xbffff4d8,而它所指向的单元刚好是ebp的值0。这一操作实质是把旧的函数桢指针保存到栈里。

再继续执行下去

(gdb) ni
0x08048482 in main ()
(gdb) i r ebp esp
ebp            0xbffff4d8       0xbffff4d8
esp            0xbffff4d8       0xbffff4d8
(gdb) x /4x $esp
0xbffff4d8:     0x00000000      0x4a8bf635      0x00000001      0xbffff574

可见

mov   %esp,%ebp

确实是把esp的值赋给了ebp,这一操作实质是设置函数桢指针,新的函数桢指针所指向的地址刚好放着旧的函数桢指针。

那为什么要设置函数桢呢?只是考察了main函数,并不一定能够找到规律,继续执行FuncB,FuncA,看一下能不能找到规律。

(gdb) si
0x08048475 in FuncB() ()
(gdb) i r esp ebp
esp            0xbffff4d4       0xbffff4d4
ebp            0xbffff4d8       0xbffff4d8
(gdb) x /4x $esp
0xbffff4d4: