IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C语言の底层神功——图解函数栈帧的创建和销毁 -> 正文阅读

[C++知识库]C语言の底层神功——图解函数栈帧的创建和销毁

目录

一、寄存器介绍

二、栈帧

三、汇编指令

四、准备工作

五、观察函数栈帧的创建和销毁

1.转到汇编语言

2.main函数栈帧的创建?

2.调用Add函数

?六、小结


一、寄存器介绍

寄存器是是集成到CPU上的,在电脑中的存储有:硬盘、内存和寄存器,它们是各自独立的。要理解函数栈帧,必须知道ebp和esp这两个寄存器是用来存放地址的,通过ebp和esp中存放的地址维护函数栈帧。

常见的寄存器及其用途如下:

寄存器介绍
eax累加寄存器,用于乘除法、函数返回值
ebx用于存放内存数据指针
ecx计数器
edx用于乘除法、IO指针
esi源变址寄存器,存放源字符串指针
edi目的变址寄存器,存放目标字符串指针
esp存放栈顶指针
ebp存放栈底指针

二、栈帧

我们来看一下百度给出的解释:

从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。

实现上有硬件方式和软件方式(有些体系不支持硬件栈)

首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。

注意:EBP指向当前位于系统栈最上边一个栈帧的底部,而不是系统栈的底部。严格说来,“栈帧底部”和“栈底”是不同的概念;ESP所指的栈帧顶部和系统栈的顶部是同一个位置。

三、汇编指令

为了能在编译器中清楚观察到函数栈帧创建和销毁的全过程,需要通过反汇编操作转到汇编语言。为了能看懂汇编语言,我们需要了解一些常见汇编指令及其含义。

常见的汇编指令及其对应的含义如下:

汇编指令含义
push压栈
pop出栈
call函数调用
movmov A,B? 即把B的值赋给A
add加法
sub减法
rep重复
lea(load effective address)加载有效地址

四、准备工作

要先了解一点,在不同的编译器里函数栈帧的创建和销毁存在一定的差异,但总体逻辑是一致的。由于越高级的编译器越难以观察函数栈帧创建和销毁的过程,这里我们演示用的编译器vs2013。

首先我们先写一段代码,其中通过函数调用进行加法运算。代码如下:

#include <stdio.h>
int Add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}
int main()
{
	int a = 10;
	int b = 20;
	int c = 0;
	c = Add(a, b);
	printf("%d\n", c);
	return 0;
}

五、观察函数栈帧的创建和销毁

1.转到汇编语言

首先我们开始调试,之后进行的操作如下:

这样我们就可以清楚地观察函数栈帧创建和销毁的全过程啦~

2.main函数栈帧的创建?

进行上述操作后我们可以看到main函数的反汇编代码如下:

int main()
{
00DA1410  push        ebp  
00DA1411  mov         ebp,esp  
00DA1413  sub         esp,0E4h  
00DA1419  push        ebx  
00DA141A  push        esi  
00DA141B  push        edi  
00DA141C  lea         edi,[ebp-0E4h]  
00DA1422  mov         ecx,39h  
00DA1427  mov         eax,0CCCCCCCCh  
00DA142C  rep stos    dword ptr es:[edi]  
	int a = 10;
00DA142E  mov         dword ptr [a],0Ah  
	int b = 20;
00DA1435  mov         dword ptr [b],14h  
	int c = 0;
00DA143C  mov         dword ptr [c],0  
	c = Add(a, b);
00DA1443  mov         eax,dword ptr [b]  
00DA1446  push        eax  
00DA1447  mov         ecx,dword ptr [a]  
00DA144A  push        ecx  
00DA144B  call        Add (0DA1087h)  
00DA1450  add         esp,8  
00DA1453  mov         dword ptr [c],eax  
	printf("%d\n", c);
00DA1456  mov         esi,esp  
00DA1458  mov         eax,dword ptr [c]  
00DA145B  push        eax  
00DA145C  push        0DA5858h  
00DA1461  call        dword ptr ds:[0DA9114h]  
00DA1467  add         esp,8  
00DA146A  cmp         esi,esp  
00DA146C  call        __RTC_CheckEsp (0DA113Bh)  
	return 0;
00DA1471  xor         eax,eax  
}
00DA1473  pop         edi  
00DA1474  pop         esi  
00DA1475  pop         ebx  
00DA1476  add         esp,0E4h  
00DA147C  cmp         ebp,esp  
00DA147E  call        __RTC_CheckEsp (0DA113Bh)  
00DA1483  mov         esp,ebp  
00DA1485  pop         ebp  
00DA1486  ret  

我们在main函数里一直往下调试直到main函数结束后发现堆栈里发生了变化,如图所示

于是我们知道,在vs2013中main函数是被其它函数调用的。

?在结束之后的页面我们可以观察到函数的调用关系如下:

在栈区分配的空间大致如下 :

因此我们可以知道,在mian函数被调用之前,已经为__tmainCRTStartup开辟好了栈帧

?我们回到最初main函数开始被调用的断点,逐条分析反汇编指令

??a.首先 进行push操作,即把ebp压栈

?

压栈放的元素是ebp的地址,进行压栈后,esp指向的位置也会上移,如图所示(部分不必要的细节图中有所省略)

b.接着进行mov指令:

?

?即把esp的值赋给ebp,此时esp和ebp指向的是同一个地址

?

未mov时我们可以看到esp和ebp指向的地址如下:

?

执行mov执行后:

??

可以看出esp和ebp指向的地址完全一致

?c.下一条指令是sub,即把esp指向的地址减去0E4h(0E4h是八进制数字)

?

?

?减去之后esp就指向了大概这个位置 ,图中紫色部分就是为main函数申请的栈帧

可以看到esp指向的地址确实减小?

d.下一步执行的是压栈操作,esp也会随之上移

?

e.再下一步仍是压栈操作,即把esi压入栈里

?

?

f.再接下来还是压栈,压进edi

?

?

g.下面执行的操作如下图,即把后边有效的地址放入前面

?

?h.再接下来,把39h放入ecx中

?i.然后执行的指令是把0CCCCCCCCh放入eax里

?j.接下来的指令就很有趣了

?rep stops意思是把从edi下边的39h次(ecx中的值)dword(double word)(一个word两个字节,double word即为双字,四个字节)全部改成0CCCCCCCCh

?执行完这步指令我们去看看内存的情况

此时esp的地址为0x0115FC84,从0x0115FC90往下直到ebp全部初始化为CCCCCCCC

?此时为main函数开辟的栈帧已经OK

k.接下来的指令是把0Ah(0Ah为十六进制数字,换算为十进制是10)放入ebp-8里(为了观察得更清楚,我们显示符号名)

?

?

?所以创建的随机变量如果没有初始化,打印出来的结果是烫烫烫…就是系统初始化结果CCCCCCCC的缘故

?l.再接下来为局部变量b指定房间

即把14h(十进制对应数字为10)赋给ebp-14h的空间?

?

?m.再接下来,把0赋给ebp-20h的空间

?

所以我们看出,局部变量是在函数栈帧里找一些空间放进去

我们可以看到内存情况如下

?

局部变量之间的距离大小取决于编译器

?n.接下来的操作是把ebp-14h的值氟给传给eax

o.再接下来的操作是压栈,把eax压入栈里,其实里边放的是b的值,这就是形参y

?

?

p.再接下来,把ebp-8h的值赋给ecx

q.然后把ecx也压入栈里,里边放的是a的值,这就是形参x

?

?

上边的动作就是在执行传参

?从上边我们可以看出,传参是先行的,并且传参顺序是从右向左。形参是实参的一份临时拷贝。

r.到了下边,就要调用函数Add了(call指令是调用函数)

但此时我们发现call指令下一条指令的地址也被压入栈里

?接下来我们按F11进入 Add函数内部

2.Add函数栈帧的创建和销毁

进入Add函数后我们可以看到Add函数的汇编代码如下:

int Add(int x, int y)
{
00DA13C0  push        ebp  
00DA13C1  mov         ebp,esp  
00DA13C3  sub         esp,0CCh  
00DA13C9  push        ebx  
00DA13CA  push        esi  
00DA13CB  push        edi  
00DA13CC  lea         edi,[ebp+FFFFFF34h]  
00DA13D2  mov         ecx,33h  
00DA13D7  mov         eax,0CCCCCCCCh  
00DA13DC  rep stos    dword ptr es:[edi]  
	int z = 0;
00DA13DE  mov         dword ptr [ebp-8],0  
	z = x + y;
00DA13E5  mov         eax,dword ptr [ebp+8]  
00DA13E8  add         eax,dword ptr [ebp+0Ch]  
00DA13EB  mov         dword ptr [ebp-8],eax  
	return z;
00DA13EE  mov         eax,dword ptr [ebp-8]  
}
00DA13F1  pop         edi  
00DA13F2  pop         esi  
00DA13F3  pop         ebx  
00DA13F4  mov         esp,ebp  
00DA13F6  pop         ebp  
00DA13F7  ret  

a.首先,仍是执行压栈指令

?

?b.接下来的指令和之前遇到的情况一致,把esp的值赋给ebp

?

?这样ebp和esp指向的又是同一块空间

c.接下来的指令是为Add函数的栈帧申请一块空间

?

黄色空间就是给Add函数开辟的栈帧

d.接下来的指令仍是压栈

?

e.再接下来的指令仍是压栈

?

f.还是压栈,这三次压栈和之前调用main函数的情况完全一致

?

g.然后把ebp+FFFFFF34h的地址给ebi

?h.下一步指令是把33h赋给ecx

i.把0CCCCCCCCh放入eax中

?j.再下一步指令和之前遇到的情况类似,也是把从edi往下直到ebp的空间初始化?

?

k.下面的操作是为局部变量z指定一块空间,即把0赋给ebp-8的空间里

?

l.接下来就要执行计算任务了,首先把ebp+8的值(其实是形参x)赋给eax

?m.然后又把ebp+0Ch的值(其实是形参y)加到eax里

?未执行该指令是寄存器eax的情况:

执行该指令后:

?

eax中的值就变为30

n.下边的指令就是把eax中的值赋给ebp-8(即局部变量z)

?

o. 接下来的指令是把z的值放到寄存器eax存起来

?p.之后的指令就是出栈操作了,首先出栈的是最顶部的edi

?

每次出栈后esp都会下移

q.下边出栈的是esi

?

?r.然后出栈的是ebx

?

s.接下来的指令是把ebp的值赋给esp,至此黄色空间就释放了,即还给内存

?

t.接下来出栈的是ebp,由于这里存的是之前压在main函数底部的ebp的地址,所以出栈之后ebp直接指向了main栈帧底部

?

?u.接下来Add函数就返回了,当ret返回的时候,就是从栈顶,弹出了call指令下一条指令的地址,就来到了call指令下一条指令

不仅要走出去,我还要回得来


v.执行完这条指令,形参空间也还给了操作系统

?

w.然后就把eax中的值(30)赋给ebp-20h(局部变量c)中?x.之后的指令就显而易见了,main函数的出栈逻辑和Add函数一致

?六、小结

?至此,我们就搞懂了函数栈帧创建和销毁的整体逻辑。最后我们通过问答形式来梳理一下。

Q:局部变量是如何创建的?

A:首先为函数分配好栈帧空间,栈帧空间初始化好一部分空间之后,为局部变量在栈帧里面分配一些空间。

Q:为什么局部变量的值是随机值?

A:局部变量的值不初始化是随机值,这是因为随机值是系统放进去的,人为进行初始化那么随机值就被覆盖了。

Q:函数是怎么传参的?传参的顺序是怎样的?

A:当要去调用函数的时候,先从右向左开始压栈为形参创建空间,当真的进入函数Add里,通过指针的偏移量找回了形参。

Q:形参和实参的关系?

A:形参是压栈时开辟的一块空间,形参是实参的一份临时拷贝,值是相同的,空间是独立的,改变形参不会改变实参。

Q:函数调用后是怎么返回的?

A:函数在调用之前就已经把call指令下一条指令的地址压进去了,把调用的上一个函数的ebp地址就已经存进去了,当函数调用完要返回去的时候,弹出ebp就能够找到上一个函数的ebp,我们记住了call指令下一条指令的地址,当往回返的时候就跳入了call指令下一条指令的地址。返回值是通过寄存器带回来的。

?

?

?

?

?

?

?

?

?

?

?

?

?

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-08-28 08:51:02  更:2021-08-28 08:52:42 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/27 20:52:53-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计