函数栈帧的创建和销毁(详细)

8 篇文章 1 订阅
本文详细分析了C/C++函数调用过程中的内存布局,包括局部变量、栈帧的创建与销毁。在VS2017和VS2013环境下,探讨了C++ main函数与C语言main函数的调用差异,涉及寄存器使用、指令操作,并展示了汇编代码实例。同时,讨论了不同编译器在Debug和Release版本中对栈内存的处理方式,以及函数调用期间的堆栈平衡检查。
摘要由CSDN通过智能技术生成

内存布局概览(我的问题:局部变量??临时变量??,C/C++区别??

在这里插入图片描述

main函数的调用:

在VS2017下,C++main函数调用过程

F11逐语句进入调试

  1. 点击调用堆栈在这里插入图片描述
  2. 如下图勾选上显示外部代码
    在这里插入图片描述
  3. 先是mainCRTStartup()调用__scrt_common_main()
    在这里插入图片描述
  4. __scrt_common_main()再调用__scrt_common_main_seh()
    在这里插入图片描述
  5. __scrt_common_main_seh()再调用invoke_main()
    在这里插入图片描述
  6. 最后invoke_main()调用main(__argc, __argv, _get_initial_narrow_environment())函数
    在这里插入图片描述

在VS2013下,C语言调用过程:

mainCRTStartup() 调用 _tmainCRTStartup() 再调用 main()


具体过程

①过程中用到的寄存器和操作

寄存器:

  1. ax(累积寄存器),bx(基底寄存器),cx(计数寄存器),dx(资料寄存器)
    eax,ebx,ecx,edx(分别为ax,bx,cx,dx的延伸,各为32位)
  2. si(来源索引寄存器),di(目的索引寄存器)
    esi,edi(分别为si,di的延伸,32位)
  3. sp(堆叠指标寄存器),bp(基底指标寄存器)
    ebp(栈底指针),esp(栈顶指针)sp 是esp的低16位,esp是rsp的低32位,ss是16位堆栈段寄存器,ebp同esp
  4. cs(代码段寄存器,用来存放当前程序代码段的地址),ip(指令指针寄存器,用来存储将要执行的下一条指令的偏移量) 在8086机中,任意时刻,CPU将CS:IP指向的内容当作指令来执行

了解更多:x86汇编学习历程3----计算机寄存器分类简介及指令(转载)
指令操作

  1. push 压栈
  2. move a,b(把a移到b里)
  3. sub 减
  4. lea是“load effective address”的缩写,简单的说,lea指令可以用来将一个内存地址直接赋给目的操作数
  5. rep指令的目的是重复其上面的指令
  6. STOS指令将al/ax/eax的值存储到[edi]指定的内存单元中
  7. ptr是规定的字(既保留字),是用来临时指定类型的。可以理解为,ptr是临时的类型转换,相当于C语言中的强制类型转换
  8. call:第一步将当前的 IP 或 CS和IP 压入栈中;第二步转移到紧跟的标号行地址执行程序。
  9. jump 跳转
  10. ret指令用栈中的数据,修改IP的内容,从而实现近转移

②分析

以C为例 代码如下

#include <stdio.h>
int Add(int x, int y)
{
	int z = x + y;
	return z;
}
int main()
{
	int a = 8;
	int b = 10;
	int c = 0;
	c= Add(a, b);
	return 0;
}

main函数

反汇编出的汇编代码如下
int main()
{

00F84610 push ebp
在这里插入图片描述

00F84611 mov ebp,esp
在这里插入图片描述

00F84613 sub esp,0E4h (注意:esp是向上走)
0E4h ,10进制的228,esp和ebp间为main函数开辟的空间在这里插入图片描述

00F84619 push ebx
00F8461A push esi
00F8461B push edi
这里将三个寄存器压栈的原因就是相关调用约定(操作系统中ABI的约定)将这三个寄存器规定为非易失寄存器。
00F8461C lea edi,[ebp+0E4h]
将ebp向上走0E4h的地址加载给edi(目的索引寄存器)
00F84622 mov ecx,39h
ecx(计数寄存器)记录 39h
00F84627 mov eax,0CCCCCCCCh
eax(累积寄存器)记录 ‘0CCCCCCCCh’
00F8462C rep stos dword ptr es:[edi]
rep指令的目的是重复其上面的指令,由于ecx是计数寄存器,所以此条指令是从es:[edi] (也就是下图蓝色部分顶部)按双字(dword)向下填充eax里的值(0CCCCCCCCh),repeat十六进制的39次
在这里插入图片描述
关于0CCCCCCCCh的解释

  1. 写入0CCCCCCCCh由于按双字(dword)向下填充,所以空间内的每个字节都是CCh
  2. 而之所以要写入CCh(也就是机器码的软件中断指令 INT 3),是因为如果程序没有按照正常的轨道运行的话,就会去执行INT 3的内容,从而报错,也就是为了便于检错与调试
  3. 这就是为什么我们看到我们的运行黑框中‘烫烫烫…’的原因,0CCCCCCCCh字符串就是‘烫烫烫’
拓展1
  1. 微软编译器会把未初始化的堆内存上的指针全部填成 0xCDCDCDCDh,字符串就是 ‘屯屯屯屯’(自动初始化的目的是为了方便我们确定错误也就是野指针问题
  2. 0xFEEEFEEEh用来标记堆上已释放的内存如果调试中有值为0xfeeefeee的指针,说明对应的内存已被释放
  3. 0xabababab:被微软的 HeapAlloc() 用于在分配堆内存后标记“无人区”保护字节
  4. 0xabadcafe :启动到此值以初始化所有空闲内存以捕获错误指针
  5. 0xbaadf00d :由 Microsoft 的 LocalAlloc(LMEM_FIXED) 用于标记未初始化分配的堆内存
  6. 0xbadcab1e:当与调试器的连接断开时,错误代码返回给 Microsoft eVC 调试器
  7. 0xbeefcace : Microsoft .NET 用作资源文件中的幻数
拓展2
  1. Debug版本是将每个字节位都赋成0xcc,而Release版本的赋值近似于随机
  2. 所以有时在debug中没错误到了release就会出现问题,所以我们最好在声明变量后马上对其初始化一个默认值

00F8462E mov ecx,0F8C003h
00F84633 call 00F81208
找到00F81208地址,此地址的指令是跳转到call指令的下一条指令
在这里插入图片描述
在这里插入图片描述
这里的__CheckForDebuggerJustMyCode应该是类似下面出栈时__RTC_CheckEsp的检查操作

int a = 8;
00F84638 mov dword ptr [ebp-8],8 把8也就是a放到ebp往上8个字节的位置一行是4个字节,一个双字
int b = 10;
00F8463F mov dword ptr [ebp-14h],0Ah把10也就是b放到ebp往上20个字节的位置,(14h就是十进制20)
int c = 0;
00F84646 mov dword ptr [ebp-20h],0 把0也就是c放到ebp往上32个字节的位置,(20h就是十进制32)
在这里插入图片描述
画图时空间有限所以变量之间只隔了一行(四个字节),实际上按照汇编代码应该隔8字节

  1. 变量内存地址是根据当前的内存管理情况分配的,不能预先确定
  2. 数组是有规律的,地址连续
  3. 变量和数组在空间中的位置可能出现这种情况C语言题目—程序死循环解释

c= Add(a, b); 形参是从右向左压栈
00F8464D mov eax,dword ptr [ebp-14h]
00F84650 push eax 把[ebp-14h]地址的值也就是b放入eax寄存器,eax入栈
00F84651 mov ecx,dword ptr [ebp-8]
00F84654 push ecx 把[ebp-8]地址的值也就是a放入eax寄存器,ecx入栈
在这里插入图片描述
这里充分体现了形参只是实参的一份零时拷贝,改变形参,不会影响实参

00F84655 call 00F813E3
00F813E3地址处的指令如下图,而00F82540正是Add函数第一条指令的地址
在这里插入图片描述
将当前的cs和ip(cs当前地址加ip下一条指令的偏移量)也就是下一条指令的地址 压入栈中相当于记录此位置,从Add函数回来后紧跟着执行
在这里插入图片描述


在这里跳转到Add函数
Add函数

int Add(int x, int y)
{
00F82540 push ebp
为了在返回时恢复 ebp 的值,我们需要一句 push ebp 来先保存 ebp 的值
00F82541 mov ebp,esp
00F82543 sub esp,0CCh (注意:esp是向上走)
0CCh ,10进制的204,esp和ebp间为Add函数开辟的空间

00F82549 push ebx
00F8254A push esi
00F8254B push edi
00F8254C lea edi,[ebp+FFFFFF34h]
00F82552 mov ecx,33h
00F82557 mov eax,0CCCCCCCCh
00F8255C rep stos dword ptr es:[edi]
与上面main函数填充0CCCCCCCCh一样这里省略
在这里插入图片描述

00F8255E mov ecx,0F8C003h
00F82563 call 00F81208
同上__CheckForDebuggerJustMyCode

int z = x + y;
00F82568 mov eax,dword ptr [ebp+8]
[ebp+8]刚好是图上蓝色的之前的ecx里存储的a的值8,再放到eax寄存器里
00F8256B add eax,dword ptr [ebp+0Ch]
[ebp+0Ch]是图上的蓝色之前的eax里存储的b的值10,和现在eax里的值8相加再放会eax里
00F8256E mov dword ptr [ebp-8],eax
最后再把eax的值放到ebp向上8给字节的位置
在这里插入图片描述

return z;
00F82571 mov eax,dword ptr [ebp-8]
到了返回的时候,先把z的值放到eax寄存器里,函数调用完会销毁,但eax并不会被销毁
}


Add函数的销毁

00F82574 pop edi
00F82575 pop esi
00F82576 pop ebx
弹出三个寄存器
在这里插入图片描述

00F82577 add esp,0CCh
esp向下走0CCh,add是向下 sub是向上

00F8257D cmp ebp,esp
00F8257F call 00F81212
在这里插入图片描述
c/c++ 生成debug函数,使用API会检查堆栈平衡

拓展3:

1.VC6会自动在每个函数的末尾插入指令来调用一个名为_chkesp的函数,_chkesp是C运行库(CRT)中的一个函数,用来检查栈指针的完好性,检查方法是比较ESP和EBP寄存器的值,看其是否相等,如果相等则通过,否则就准备参数调用_CrtDbgReport函数报告错误。编译器的编译选项/GZ用来控制是否插入栈指针检查函数。
2. VC8的_RTC_CheckEsp函数与_chkesp原理和工作方法是一样的,只是改变了函数名称和报错的方式
3. _RTC_CheckEsp_这个函数用于检查缓冲区溢出

00F82584 mov esp,ebp
esp(栈顶) 指向 ebp(栈底)
00F82586 pop ebp
弹出这里的ebp,ebp回到最初开辟main函数空间时的ebp的位置esp指向00F84655此时esp ebp又恢复到共同维护main函数空间的状态
在这里插入图片描述

00F82587 ret
CPU执行ret指令时,进行下面2步操作(相当于pop ip)

  1. (IP)=((ss)∗16+(sp))
  2. (sp)=(sp)+2

回到main函数

00F8465A add esp,8
esp向下走8个字节,相当于把10和8两个形参空间销毁
00F8465D mov dword ptr [ebp-20h],eax
由于之前Add函数销毁前把返回的值放入了eax寄存器,此时是吧eax的值给[ebp-20h]地址也就是图中的c处
在这里插入图片描述

return 0;
00F84660 xor eax,eax
将eax寄存器清零,此指令效率比mov eax,0高,所以一般用它
}

00F84662 pop edi
00F84663 pop esi
00F84664 pop ebx
00F84665 add esp,0E4h
00F8466B cmp ebp,esp
00F8466D call 00F81212
00F84672 mov esp,ebp
00F84674 pop ebp
00F84675 ret
main函数的销毁,和Add函数的销毁一样,这里省略,ret回到_tmainCRTStartup()中
在这里插入图片描述


以上是完整的汇编代码分析过程

  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值