函数栈帧

 每一次函数调用都是一个过程,为函数开辟栈空间,用于本次函数调用中临时变量的保存、现场保护。这块栈空间我们称为函数栈帧。

​ 我们从汇编角度来分析函数调用过程中底层寄存器的使用,通过ebp(栈底指针)和esp(栈顶指针)控制的空间来体现栈帧的大小,主要问题有三个:现场保护信息、形参实参传递、函数指针回落。

文章比较长,如果想直接了解过程,可以只查看函数栈帧图。

准备工作

编译器:Visual Studio 2017

​ 分析的例子如下:

​ 计算两个数的加和,定义了一个子函数ADD。

    #include <stdio.h>
    #include <stdlib.h>
    
    int ADD(int, int);
    int ADD(int x, int y) {
    	int c = 0;
    	c = x + y;
    
    	return c;
    }
    int main(void) {
    	int x = 10;
    	int y = 20;
    	int z = 0;
    
    	z = ADD(x, y);
    	printf("%d", z);
    	system("pause");
    	return 0;
    
    }

在逐语句调试状态下,查看其反汇编代码,以及内存和局部变量,具体过程如下:

① 按F11,逐语句调试程序。

② 转到反汇编:

③ 

 

 

主函数的反汇编代码如下:

 int main(void) {
  00F73B80  push        ebp  
  00F73B81  mov         ebp,esp  
  00F73B83  sub         esp,0E4h  
  00F73B89  push        ebx  
  00F73B8A  push        esi  
  00F73B8B  push        edi  
  00F73B8C  lea         edi,[ebp+FFFFFF1Ch]  
  00F73B92  mov         ecx,39h  
  00F73B97  mov         eax,0CCCCCCCCh  
  00F73B9C  rep stos    dword ptr es:[edi]  
      int x = 10;
  00F73B9E  mov         dword ptr [ebp-8],0Ah  
      int y = 20;
  00F73BA5  mov         dword ptr [ebp-14h],14h  
      int z = 0;
  00F73BAC  mov         dword ptr [ebp-20h],0  
  ​
      z = ADD(x, y);
  00F73BB3  mov         eax,dword ptr [ebp-14h]  
  00F73BB6  push        eax  
  00F73BB7  mov         ecx,dword ptr [ebp-8]  
  00F73BBA  push        ecx  
  00F73BBB  call        00F71078  
  00F73BC0  add         esp,8  
  00F73BC3  mov         dword ptr [ebp-20h],eax  
      printf("%d", z);
  00F73BC6  mov         eax,dword ptr [ebp-20h]  
  00F73BC9  push        eax  
  00F73BCA  push        0F76BCCh  
  00F73BCF  call        00F7137A  
  00F73BD4  add         esp,8  
      system("pause");
  00F73BD7  mov         esi,esp  
  00F73BD9  push        0F76BD0h  
  00F73BDE  call        dword ptr ds:[00F7A16Ch]  
  00F73BE4  add         esp,4  
  00F73BE7  cmp         esi,esp  
      system("pause");
  00F73BE9  call        00F71122  
      return 0;
  00F73BEE  xor         eax,eax  
  ​
  }
  00F73BF0  pop         edi  
  00F73BF1  pop         esi  
  00F73BF2  pop         ebx  
  00F73BF3  add         esp,0E4h  
  00F73BF9  cmp         ebp,esp  
  00F73BFB  call        00F71122  
  00F73C00  mov         esp,ebp  
  00F73C02  pop         ebp  
  00F73C03  ret  

主函数的栈帧 

首先分析主函数的栈帧:

① mainCRTstartup调用主函数,为main函数建立栈帧:

先查看 ebp 和 esp 地址:

所以在程序未开始之前,函数栈帧空间如下:

② 开始 按 F11 单步执行主程序:

 push        ebp          ;    ebp压栈,其作用最后为保护现场信息,esp 的值 会 减4
 mov         ebp,esp      ;    esp 赋值给 ebp 即栈顶栈底指向同一内存空间,形成空栈。             

因为 为 esp 和 ebp ,所以每次操作,字节变化为双字,即4个字节。

执行完第一条语句,我们发现 esp 上升了 四个字节的单位,eip 的值 也发生了变化,但在这里并不是重点,暂不讨论。

继续执行 mov ebp, esp; esp中的值为esp指向空间的地址,赋值给ebp后,esp和ebp指向同一空间。如下图:

        

此时的函数栈帧如图:

 

③ 执行第三条往下的语句:

sub         esp,0E4h  ;sub 为 减操作
push        ebx  
push        esi  
push        edi  

执行 sub esp, 0E4h ; esp 指针上移,实际上为main函数开辟了 一段空间。

再将 ebx, esi, edi 压入栈内,esp 的指针再 上升 12 个字节。

此时函数栈帧如下:

④ 执行接下来的语句:

lea         edi,[ebp-0E4h]  ;将[ebp-0E4h]的有效地址赋值给edi,即未push edi 之前的         
                            ;esp 值 赋值给 edi
mov         ecx,39h         ;设置循环次数为 39H
mov         eax,0CCCCCCCCh  ;将eax的值设为 0CCCCCCCCH
rep stos    dword ptr es:[edi]  
    ;串存储指令,英文缩写 store string , 将 eax 中的 数据传送到目的地址.
    ;rep 为重复操作,重复次数是 ecx 的值。
    ;目的地址 设置了段超越 即存储到附加段 ES 中,
    ;dword ptr   将每次操作的对象 设置为双字。

这段代码 将为main函数开辟的空间,赋值为CC。

我们查看执行后内存的内容:

红色部分的值也是CC CC CC CC,截这张图的时候,已经执行了mov         dword ptr [x],0Ah 这条代码,因为Visual Studio 2017 每次调试,内存的地址都不相同,所以就这样贴上了。

此时的函数栈帧为:

⑤ 开始为临时变量 x, y, z 初始化空间:

mov         dword ptr [x],0Ah  
mov         dword ptr [y],14h  
mov         dword ptr [z],0  

此时的函数栈帧如下:

上图这里没有体现出实际空间大小比例,以下是真正的内存空间:

子函数ADD的调用及其栈帧

① 子函数形参的传递:

mov         eax,dword ptr [y]  ;将 y 的值 赋给 eax
push        eax                ;将 eax 压栈
mov         ecx,dword ptr [x]  ;将 x 的值 赋给 ecx
push        ecx                ;将 ecx 压栈

执行结束后,寄存器的值如下:

函数栈帧如下:

② 子函数ADD 的调用:

call        ADD (013E1078h)    ;调用子函数,将 eip 指向 ADD 函数的 第一条代码 
add         esp,8              ;esp 上升 八个字节,用来存放call指令的下一条指令eip

单步执行后,eip 指向ADD子函数的内部:

 应当注意的是, 这里保存eip的值,是为了执行完子函数后机器能正确的执行接下来的指令。

③ 子函数内部汇编代码:

013E16D0  push        ebp    ;013E16D0 为 eip 的值
013E16D1  mov         ebp,esp  
013E16D3  sub         esp,0CCh  
013E16D9  push        ebx  
013E16DA  push        esi  
013E16DB  push        edi  
013E16DC  lea         edi,[ebp-0CCh]  
013E16E2  mov         ecx,33h  
013E16E7  mov         eax,0CCCCCCCCh  
013E16EC  rep stos    dword ptr es:[edi]  

上述过程,和主函数栈帧的建立类似,不再赘述,不过此时push ebp,保护的是main函数 栈帧的 ebp。

④ 继续执行:

mov         dword ptr [ebp-8],0

此时的函数栈帧如下:

接下来的代码:

mov         eax,dword ptr [ebp+8]  ;[ebp+8]的值为 y = 20, 赋值给 eax
add         eax,dword ptr [ebp+0Ch];[ebp+0Ch]的值为 x = 10, 相加之后的结果在eax中
mov         dword ptr [ebp-8],eax  ;[ebp-8]为c, 将结果赋值给 c 
mov         eax,dword ptr [ebp-8]  ;将 c 的值 重新赋值给 eax
                                   ;这句话是return c;的反汇编,通过eax将结果传递给主函数

结果如下:

指针回落

pop         edi  
pop         esi  
pop         ebx      ;依次弹出值给edi, esi, ebx
mov         esp,ebp  ;esp 下落,即回收了ADD的空间
pop         ebp      ;ebp 指向main函数的栈底指针
ret                  ;ret 会将eip 的值修改为 主函数中 call 指令的 下一条指令的地址

此时的函数栈帧图如下:

这里的空间回收:表现为 esp 和 ebp 的指向变化,其空间里的值还在,只不过被认为垃圾值,其使用权归还给系统。

回到主函数

 mov         dword ptr [ebp-20h],eax   ;将子函数中计算得到的结果赋给 z

接下来的代码是调用printf 函数,和system(“pause”)的汇编代码,其创建堆栈的过程一样。

最后是 return 0;从主函数返回到系统。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值