我是靠谱客的博主 怡然蛋挞,最近开发中收集的这篇文章主要介绍CSAPP之栈帧结构理解,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

C语言需要经过编译成为机器代码(即二进制代码)才能在机器上执行,而这个过程实际上是这样子的,C语言源程序(.c)首先通过预处理器拓展得到(.i),编译器处理得到汇编(.s),汇编器处理得到目标文件(.o),此时.o文件中已经是二进制代码了,但还要最终经过链接器处理才能得到最终的可执行机器代码(.exe)。

假设我们有一个swap.c文件,那么在编译时将程序的后缀名依次转换为:

Created with Raphaël 2.1.0 hello.c hello.i hello.s hello.o hello.exe

下面我们给出swap.c文件源码,这个c文件非常简单,声明了swap函数,参数为两个整形指针, swap函数交换了两个指针指向的值,并返回之和。我们将通过观察在main函数中调用swap函数这一个过程来了解栈帧结构:

int swap(int *xp, int *yp){
    int x = *xp;
    int y = *yp;

    *yp = x;
    *xp = y;
    return x + y;
}

int main(void){
    int x = 1;
    int y = 2;
    int sum = swap(&x, &y);
    return 0;
}

我所使用的gcc版本为4.7.1(可通过gcc –version命令查看),生成ATT格式的汇编时正常编译没有进行优化(即-O0,一般来说gcc编译器会进行二级优化),接下来我们通过objdump反汇编器得到目标文件的汇编。
命令如下:

gcc -O0 -S swap.c //得到汇编文件.s
gcc -O0 -c swap.c //得到目标文件.o 
objdump -d swap.o //反汇编swap.o文件

接下来我们看反汇编得到的汇编代码,其中序列号会讲解要注意的地方:

swap.o:     file format pe-i386


Disassembly of section .text:

00000000 <_swap>:
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp  //保存ebp的值并设置帧指针
   3:   83 ec 10                sub    $0x10,%esp //栈分配了10个字节空间
   6:   8b 45 08                mov    0x8(%ebp),%eax //在当前的帧指针向上8个字节,也就是1的地址,即第一个参数 
   9:   8b 00                   mov    (%eax),%eax //将1的值存储在eax寄存器中
   b:   89 45 fc                mov    %eax,-0x4(%ebp) //1的值储存在帧指针向下的4个字节,帧指针的基地址还存储着旧的ebp值
   e:   8b 45 0c                mov    0xc(%ebp),%eax //帧指针向上12个字的地址,即第二个参数,也就是2的地址
  11:   8b 00                   mov    (%eax),%eax   //将2的值存储在eax寄存器中
  13:   89 45 f8                mov    %eax,-0x8(%ebp) //2的值存储在帧指针向下的8个字节
  16:   8b 45 0c                mov    0xc(%ebp),%eax //2的地址值存储在eax寄存器中
  19:   8b 55 fc                mov    -0x4(%ebp),%edx //1的值存储在edx寄存器
  1c:   89 10                   mov    %edx,(%eax) //将1的值覆盖eax寄存器中2的地址所指向的值,注意这里是存储器取值,所以改写的是值
  1e:   8b 45 08                mov    0x8(%ebp),%eax //1的地址值存储在eax寄存器中
  21:   8b 55 f8                mov    -0x8(%ebp),%edx //2的值储存在edu寄存器中
  24:   89 10                   mov    %edx,(%eax) //将2的值覆盖eax寄存器中1的地址所指向的值
  26:   8b 45 f8                mov    -0x8(%ebp),%eax //2的值转移到eax寄存器中
  29:   8b 55 fc                mov    -0x4(%ebp),%edx //1的值转移到edx寄存器中
  2c:   01 d0                   add    %edx,%eax
//注解④
  2e:   c9                      leave  
  2f:   c3                      ret    

00000030 <_main>:
  30:   55                      push   %ebp //帧指针入栈,保存先前的%ebp的值
  31:   89 e5                   mov    %esp,%ebp //栈指针存放的地址值设置为帧指针存放的地址值,注解①
  33:   83 e4 f0                and    $0xfffffff0,%esp  //栈指针的地址值的最后一位设置为0
  36:   83 ec 20                sub    $0x20,%esp //栈分配20字节的空间,注解②
  39:   e8 00 00 00 00          call   3e <_main+0xe> //跳转到地址为3e的指令,也就是下一条指令
  3e:   c7 44 24 18 01 00 00    movl   $0x1,0x18(%esp) //从栈指针的地址向上18个字节开始,存储4个字节的int值, 即18 ~ 22为1的存储地址,这里超过了20,但是因为之前有一条call指令push了返回地址所以实际是分配了24个字节
  45:   00 
  46:   c7 44 24 14 02 00 00    movl   $0x2,0x14(%esp) //同上, 14 ~ 18 是2的存储地址
  4d:   00 
  4e:   8d 44 24 14             lea    0x14(%esp),%eax //将2的地址赋给%eax寄存器
  52:   89 44 24 04             mov    %eax,0x4(%esp)  //2的地址存储在栈指针向上的4个字节开始,指针为4个字节
  56:   8d 44 24 18             lea    0x18(%esp),%eax //同上,1的地址赋给%eax寄存器
  5a:   89 04 24                mov    %eax,(%esp) //1的地址直接存储在栈指针上面,注解③
  5d:   e8 9e ff ff ff          call   0 <_swap> //调用swap函数
  62:   89 44 24 1c             mov    %eax,0x1c(%esp)
  66:   b8 00 00 00 00          mov    $0x0,%eax
  6b:   c9                      leave  
  6c:   c3                      ret    
  6d:   90                      nop
  6e:   90                      nop
  6f:   90                      nop

注解:
①%ebp寄存器通常用来储存帧指针的地址值,而%esp寄存器是用来储存栈指针的地址值,函数在调用的前两行一般都是保存旧的%ebp的值,并将帧指针的值设置为此时栈指针的值。push命令是先将栈指针减去4,再转移数据,即栈指针的地址值是栈顶数据的开始地址,因此,帧指针储存的是旧的%ebp的值。看图:
这里写图片描述

②由上图可知,越往栈底,地址的值越大,越往栈底,地址的值越小,所以栈是通过减少地址的值来分配地址空间的。当一条指令指定一个地址时,是指这个地址向上分配多少个字节,不是向下。

③在调用swap函数之前,我们看到首先是2的地址入栈,再是1的地址入栈,而它们分别是swap函数的参数,也就是说,函数的参数是逆着顺序入栈的,第一参数最后一个入栈,而call指令除了会跳转到调用的函数的第一条指令的地址上,还会将当前call指令的下一条的指令作为返回地址入栈。

④32位系统,在一组8个的寄存器中,%eax寄存器通常作为返回值的寄存器,除了%esp以及%ebp保存栈指针,帧指针以外,其他寄存器还分为调用者保存以及被调用者保存。%eax,%edx,% ecx作为调用者保存,可以被函数覆盖原来的数据,而%ebx,%esi,%edi在被覆盖前必须先入栈保存。当然对于x86-x64来说,共有16个寄存器,情况自然不一样。

以上纯属个人理解,若有什么不对欢迎指教。

最后

以上就是怡然蛋挞为你收集整理的CSAPP之栈帧结构理解的全部内容,希望文章能够帮你解决CSAPP之栈帧结构理解所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(61)

评论列表共有 0 条评论

立即
投稿
返回
顶部