函数栈帧

来源:互联网 发布:内存卡误删恢复软件 编辑:程序博客网 时间:2024/06/06 08:50


想要把函数调用时的过程搞清楚,这里是一些记录。


注:因为是linux系统下的调试,汇编代码使用的是AT&T格式。


首先,linux系统会将1GB的空间分配给内核,这1GB的内存占据4GB内存的高地址段,即0XC0000000~0XFFFFFFFF。而栈底就从0Xbfffffff开始,平时用GDB调试的时候观察一下esp和ebp,基本都是在0xbfff....这个位置。


先看一下最简单的情况

  int main(void)
   {   
      int a=4, b=5;
      return 0;

  }


编译后用objdump -d a.out来观察main函数的汇编代码

08048394 <main>:
 8048394:       55                                    push   %ebp
 8048395:       89 e5                              mov    %esp,%ebp
 8048397:       83 ec 10                         sub    $0x10,%esp
 804839a:       c7 45 fc 04 00 00 00    movl   $0x4,-0x4(%ebp)
 80483a1:       c7 45 f8 05 00 00 00    movl   $0x5,-0x8(%ebp)
 80483a8:       b8 00 00 00 00              mov    $0x0,%eax
 80483ad:       c9                                     leave  
 80483ae:       c3                                     ret    
 80483af:        90                                     nop


最开头的两句:

push %ebp

mov %esp, %ebp

作用是将ebp原来的值保存在栈上。由于是push操作,esp栈顶指针指向栈顶,此时mov %esp, %ebp,让ebp的值等于esp,作用就是重新开辟来一个新的栈,函数main的栈帧。


之后一句:

sub $0x10, %esp

字面意思是将esp的值减0x10,十进制的16。本来esp指向的是栈底,现在减了16个字节(栈的增长是由高地址向低地址减少),也就是栈顶和栈底之间有了16个字节的空间。一个int型变量占4个字节,16/4等于4,可以在这段范围内放4个int型的变量。ebp是栈底,至少在这个函数运行期间不会改变,就可以用ebp减去一个值这样的方法在栈上寻找变量。


c7 45 fc 04 00 00 00     movl  $0x4, -0x4(%ebp)

这句对应源代码里面的a=4; 

$0x4是一个立即数。

c7 45 fc 04 00 00 00是机器码,可以看到这句最后面的四位04 00 00 00就是立即数4,因为x86是小端字节序,所以04放在最前面。

c7 45 fc 就是对应的movl   $0x4,-0x4(%ebp)的指令。-0x4(%ebp)这是一个地址的表达式,表示ebp-4。这条语句的作用就是将立即数4保存到ebp-4的内存中。这里,用-4($ebp)这种方式在栈上定义、寻找变量。和我们平时理解中用push和pop入栈出栈有些不一样,直接用ebp-x在栈上进行操作。


c7 45 f8 05 00 00 00    movl   $0x5,-0x8(%ebp)

这句和上一句一个意思,只是指令有一点点区别,地址上用-0x8(%ebp)来表示,这是变量b=5。


b8 00 00 00 00       mov    $0x0,%eax

return的是0,这里用eax存放函数的返回值。


c9      leave  
c3      ret    
90      nop

这三条是函数结束时的操作。



函数(以main为例)调用另一个函数时,会有一些小的动作,如下:

1、参数入栈。

2、把当前指令的下一条指令的地址入栈。

3、跳转到函数体执行。

其中2和3由指令call一起执行


一个函数被调用,也会在函数开始的时候,做一些小动作,如下:

1、push $ebp(是上一个函数栈帧的ebp)

2、mov  $esp, $ebp(设置栈底)

3、sub 0x??, $esp (根据需要开辟??大小的空间)

4、push 寄存器 (如果有需要)


该函数返回时会有如下动作(基本于上面的顺序相反)

1、pop 寄存器 (如果有需要)

2、mov $ebp, $esp (将栈顶直接指到栈底,此时栈上的东西已不再受到控制)

3、pop  $ebp (将上一个函数的ebp弹出,回到主调函数栈帧内)

4、ret (从栈中取得返回地址,并跳转到该位置

2和3由指令leave一起执行。



再来一段简单的函数调用的代码


int  foo(int a, int b)

{

    int c ;

    a = 3, b = 4;

    c = a + b;

    return c;

}

int main(void)

{

    int n, m;

    n = 1, m = 2;

    foo(n, m);

    return 0;

}


08048394 <foo>:
 8048394:       55                                 push   %ebp
 8048395:       89 e5                            mov    %esp,%ebp
 8048397:       83 ec 10                       sub    $0x10,%esp
 804839a:       c7 45 fc 03 00 00 00    movl   $0x3,-0x4(%ebp)
 80483a1:       c7 45 f8 04 00 00 00    movl   $0x4,-0x8(%ebp)
 80483a8:       8b 45 f8                        mov    -0x8(%ebp),%eax
 80483ab:       8b 55 fc                        mov    -0x4(%ebp),%edx
 80483ae:       01 d0                            add    %edx,%eax
 80483b0:       89 45 f4                        mov    %eax,-0xc(%ebp)
 80483b3:       8b 45 f4                        mov    -0xc(%ebp),%eax
 80483b6:       c9                                 leave  
 80483b7:       c3                                 ret    

080483b8 <main>:
 80483b8:       55                                 push   %ebp
 80483b9:       89 e5                            mov    %esp,%ebp
 80483bb:       83 ec 18                       sub     $0x18,%esp
 80483be:       c7 45 fc 01 00 00 00    movl   $0x1,-0x4(%ebp)
 80483c5:       c7 45 f8 02 00 00 00    movl   $0x2,-0x8(%ebp)
 80483cc:       8b 45 f8                        mov    -0x8(%ebp),%eax
 80483cf:        89 44 24 04                  mov    %eax,0x4(%esp)
 80483d3:       8b 45 fc                        mov    -0x4(%ebp),%eax
 80483d6:       89 04 24                       mov    %eax,(%esp)
 80483d9:       e8 b6 ff ff ff                   call    8048394 <foo>
 80483de:       b8 00 00 00 00             mov    $0x0,%eax
 80483e3:       c9                                 leave  
 80483e4:       c3                                 ret    
 80483e5:       90                                 nop


可以看到,foo函数的最后一条指令地址是80483b7,就一个字节的指令ret(c3)。紧接着的80483b8就是main函数的第一条指令来。这个顺序和源代码中函数位置有关。


先看main函数。很标准的

push $ebp 

mov $esp, $ebp

sub $0x18, $esp  (在栈上开辟来0x18的空间,十进制的24,可以放6个4字节的变量、地址)


下面两句mvol就是给n和m赋值。这条指令执行完毕后,ebp=0xbfffeff8(方便起见用ff8表示,esp一样),esp=0xbfffefe0,栈上内容如下:

内存地址         该地址内4字节内容

0xbfffefe0:     0x4f0c0384       0x4ef11fc4        0x080483f9      0x4f0bfff4                                     
0xbfffeff0:      0x00000002      0x00000001      0x00000000

可以看到地址ff8(ebp)内保存的是0x00000000,ebp这个位置应该是放old ebp(主调函数ebp的值),也许是因为main函数的关系,这里都是0,先不管。

ff4(ebp-4)里面的内容是int型的1,就是源代码中变量n。ff0(ebp-8)里面的内容是int型的2,就是源代码中变量m。后面的fec~fe4这几个位置都是一些未初始化过的垃圾数据。

当这两句movl执行完毕之后,接下来的就是为调用foo函数作准备工作,即参数入栈,main函数中foo()语句之后的下一条指令return 0的地址入栈,再跳转到foo函数。



下面两条条指令

80483be:       8b 45 f8          mov    -0x8(%ebp),%eax (这句将ebp-8的内容,也就是变量m的值放进eax)

80483cf:        89 44 24 04    mov    %eax,0x4(%esp)   (这句将eax的内容保存到esp+4,注意,这里用的是esp,这个动作就是参数入栈。因为用的是默认的调用惯例cdecl,函数的参数从右向左入栈,foo(n, m),于是先将m入栈)。


80483d3:       8b 45 fc                        mov    -0x4(%ebp),%eax
80483d6:       89 04 24                       mov    %eax,(%esp)

这两句的作用是将参数n入栈。此时foo函数的参数m和n,与main函数中定义的m与n,在地址上是分开的。或者说,刚才那四条指令在内存中保存的数据,已经是foo函数自己内部的变量了,所以改变这两个变量,不会对main函数中的m和n有任何改变。


 80483d9:       e8 b6 ff ff ff                   call    8048394 <foo>
 80483de:       b8 00 00 00 00             mov    $0x0,%eax

这里有一句call指令。call有两个作用,先将call指令后面那条指令的地址,也就是mov $0x0, %eax这条指令的地址80483de入栈。所以在foo函数的栈帧上会看到这个数字。地址入栈后,跳转,开始执行foo函数的指令。


08048394 <foo>:
 8048394:       55                                  push   %ebp
 8048395:       89 e5                             mov    %esp,%ebp
 8048397:       83 ec 10                       sub    $0x10,%esp
 804839a:       c7 45 fc 03 00 00 00    movl   $0x3,-0x4(%ebp)
 80483a1:       c7 45 f8 04 00 00 00    movl   $0x4,-0x8(%ebp)
 80483a8:       8b 45 f8                        mov    -0x8(%ebp),%eax
 80483ab:       8b 55 fc                        mov    -0x4(%ebp),%edx
 80483ae:       01 d0                            add    %edx,%eax
 80483b0:       89 45 f4                        mov    %eax,-0xc(%ebp)
 80483b3:       8b 45 f4                        mov    -0xc(%ebp),%eax
 80483b6:       c9                                 leave  
 80483b7:       c3                                 ret   

foo函数开始也是push,mov,sub这一个套路。

sub $0x10, %esp,在栈上开了16个字节,可以放4个变量。这时候,看下寄存器的内容,可以看到esp=fc8,ebp=fd8。我们看一下这个时候栈上的内容(为了作对比,把main函数刚才的内容一起复制过来)

再强调一下,x86下,栈由高地址向低地址变化,push入栈的操作会让esp-4,pop则会让esp+4。gdb调试的时候,显示出来的内存内容则是由低向高增长,所以看的时候要从后往前看。

foo函数栈上内容

内存地址                         该地址内4字节数据

0xbfffefc8:   0x080496c4 <- foo栈顶               0x08048411      0x080483f0                         0x080482e0
0xbfffefd8:   0xbfffeff8  <-foo栈底 (old ebp)    0x080483de     0x00000001<-main栈顶   0x00000002
0xbfffefe8:   0x080483f9                                   0x4f0bfff4          0x00000002                        0x00000001
0xbfffeff8:    0x00000000<-main栈底

(old ebp就是main函数栈帧中的ebp: ff8)


main函数栈上内容


内存地址        该地址内4字节数据      

0xbfffefe0:     这个是栈顶-> 0x00000001      0x00000002      0x080483f9      0x4f0bfff4                                     
0xbfffeff0:                               0x00000002      0x00000001      0x00000000 <-此处是栈底


可以看到,main的栈顶之后,foo的栈底之前还有一个值,4个字节,地址为fdc,值为0x080483de,其实就是call指令执行时压入栈中的返回地址。main函数和foo函数的栈帧在内存上是相接的,都没有空隙。由于刚进入foo函数,所以栈上的内容都是无用的垃圾。


 804839a:       c7 45 fc 03 00 00 00    movl   $0x3,-0x4(%ebp)
 80483a1:       c7 45 f8 04 00 00 00    movl   $0x4,-0x8(%ebp)

这两条指令对应源代码中的a = 3, b = 4。进入foo函数后,esp和ebp已经通过上面那三条指令改变过,都是在foo的栈帧范围内。ebp-4的位置存放3, ebp-8的位置存放4。再看下栈上的变化


0xbfffefc8:     0x080496c4<-foo栈顶      0x08048411      0x00000004      0x00000003
0xbfffefd8:     0xbfffeff8<-foo栈底            0x080483de     0x00000001      0x00000002
0xbfffefe8:     0x080483f9                       0x4f0bfff4         0x00000002      0x00000001
0xbfffeff8:      0x00000000      

我们看到fd4(ebp-4)和fd0(ebp-8)的位置都已经初始化了。


 80483ae:       01 d0                            add    %edx,%eax
 80483b0:       89 45 f4                        mov    %eax,-0xc(%ebp)

这两句对应c=a+b。可以看到变量c的位置在ebp-0xc


80483b3:       8b 45 f4      mov    -0xc(%ebp),%eax

这里在为return作准备,将需要return的值放入eax。

之后收尾。


栈上基本的东西弄明白来,但也有几个地方的值我也没搞清楚,只能先存疑了。

原创粉丝点击