C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)

前言 读完这篇博客,你可以明白什么?
①局部变量到底是怎么在栈上创建的?
②为什么局部变量不初始化为随机值?
③函数是怎么传参的?传参的先后顺序是什么?
④形参和实参是什么关系?
⑤函数调用是怎么实现的?
⑥函数调用后是怎么返回的?
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片

在这篇博客里,我将带领大家利用反汇编从底层上理解,不用担心,都是零基础入门的。当你学完这篇博客去面试,面试官会非常高兴,觉得这小伙子真。所以学起来吧!

?作者概况:就读南京邮电大学努力学习的大一小伙
?联系方式:2879377052(QQ小号)
?资源推荐:C语言从入门到进阶
?今日书籍分享:《深入理解计算机系统》
目录
一、寄存器
二、main函数的调用
三、准备阶段
四、main栈帧的创建分析
五、add函数栈帧的创建
六、add函数栈帧的销毁
七、main函数栈帧的销毁
八、完整反汇编代码
九、后记
一、寄存器 在C语言中我们可以把寄存器当成指针来看待,他可以指向一块空间,也可以用来存储数据。现在向大家介绍以下几种基本寄存器
参考博客:函数栈帧的创建和销毁(图解)
寄存器名称 功能
①eax "累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。
②ebx "基地址"(base)寄存器, 在内存寻址时存放基地址。
③ecx 计数器(counter),计数寄存器,用于循环操作,比如重复的字符存储操作,或者数字统计。
④edx 作为EAX的溢出寄存器,总是被用来放整数除法产生的余数。
⑤esi 源变址寄存器,主要用于存放存储单元在段内的偏移量。通常在内存操作指令中作为“源地址指针”使用。
⑥edi 目的变址寄存器,主要用于存放存储单元在段内的偏移量。
⑦esp? 栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向栈最上面一个栈帧的栈顶。esp用于堆栈操作,被形象地称为栈顶指针。堆栈的顶部是地址小的区域,压入堆栈的数据越多,esp也就越来越小。在32位平台上,esp每次减少4字节。
⑧ebp? 基址指针寄存器(extended base pointer),其内存放着一个基址指针,该指针永远指向系统栈最上面一个栈帧的底部。基址指针,又被形参称作栈底指针,一般与esp配合使用,可以存取某时刻的esp,这个时刻就是进入一个函数内后,CPU会将esp的值赋给ebp,此时就可以通过ebp对栈进行操作。
二、main函数的调用 main函数其实也是被其他函数调用的,函数调用关系如下:
mainCRTStartup→__tmainCRTStartup→main
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
【C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)】(在调用堆栈中即可观察到)
三、准备阶段 ①首先我们写一段简单的代码,将代码的每个过程拆分的尽可能简单以便于我们对过程的观察
int add(int x,int y) { int z = 0; z = x + y; return z; }int main() { int a = 10; int b = 20; int c = add(a,b); return 0; }

②在调试状态下转入反汇编模式
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
(完整连续反汇编代码请看文章结尾。这里分块讲解)
看不懂没关系,现在逐一解释
我们知道,函数的调用都需要在栈区上开辟空间,那么我们先来解答几个问题:
1.什么是栈?
【答】栈是一种数据结构,它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。栈区内存空间的使用是从高地址向低地址处使用的。
2.什么是压栈?什么是出栈?
【答】一个形象的比喻就是机枪弹夹。压栈的过程就是压入一个元素,相当于向机枪弹夹压入子弹;出栈的过程就是弹出一个元素,相当于子弹弹出来的过程。这正好对应了栈的结构特点——先进入的数据被压在栈底,后进入的数据在栈顶。
3.为什么一个数据在内存中是“倒着”存放的(注意显示的是16进制)C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
【答】数据的存储涉及大小端问题:
①什么是大小端
>大端(存储)模式是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中。
>小端(存储)模式是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
②为什么有大小端
存放的内容大于1个字节,必然存在着如何将多字节安排的问题。因此就出现了大小端。具体大小端取决于编译器,一般为小端存储。
四、main栈帧的创建分析 1.我们之前提到,main函数是由__tmianCRTStartup函数调用的,所以在创建main函数栈帧前,ebp和esp寄存器维护--tmainCRTStartup的栈区,分别存放指向栈帧的栈顶和栈底。
2.同时注意,栈区上内存的使用是从高地址向低地址处使用的
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
【过程一:pushebp 】
【解释】push指令的作用:它首先减小esp的值,再将源操作数复制到栈地址,每次esp地址减去四字节。最终效果就是在栈顶压入一个元素,元素的值为ebp的地址。(4个字节)
【补充】内容不随指令执行而变化的操作数为源操作数,内容随执行指令而改变的操作数为目标操作数。
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
【过程二:movebp,esp 】
【解释】mov指令作用:将一个数据从源地址传送到目标地址,源操作地址的内容不变。最终效果是将esp的地址赋值给ebp。寄存器指向的空间发生改变。
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
【过程三:subesp,0E4h】
【解释】sub指令的作用:减操作指令,地址减去相应的数值。最终效果就是esp的地址减去0E4h。
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
【过程四:push ebx,esi,edi】
【解释】前面我们提到过push的过程就压入元素的过程。那压入这三个元素有什么用呢?等会就明白。
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
? 【过程五:leaedi,[ebp- 0E4hh]/movecx,9/moveax, 0CCCCCCCCh】
【解释】Load Effective Address,即装入有效地址的意思,它的操作数就是地址。在这里的效果就是将ebp+FFFFFF1Ch的值赋给edi。勾选“显示符号名后可以发现:
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
ebp - 0E4h不正是当初esp - 0E4h时的地址吗?
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
执行mov指令后,我们可以发现寄存器eax,ecx的值发生变化。
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
【过程六: rep stosdword ptr es:[edi] 】
【解释】rep指令的作用是:重复后面的指令。stos指令的作用是将eax中的值拷贝到es:edi指向的地址。ecx表示重复操作的次数。dword表示4个字节。所以整句指令的作用是:从edi开始,向高地址方向,将ecx个4字节内存全部修改为eax的值。
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
(可以看到,执行操作后edi上相应数量的四字节内存被赋值为cc cc cc cc。这也解释了为什么我们不初始化,变量默认的初始值为cc cc cc cc)
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
(完整连续反汇编代码请看文章结尾。这里分块讲解)
【过程七:movdwordptr[ebp - 14h] ,14h】
【解析】将ebp - 14地址处的四字节内容修改为14h(10进制中的20),也就是完成了给b赋值为20的动作。下一条语句同理。
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
至此main函数的栈帧创建的准备阶段完成。我们准备进入add函数
【过程八:moveax,dword ptr [ebp - 8h]/pusheax】
【解析】将ebp - 14地址处的数值储存到eax处;压栈,将一个数值与eax相等的元素压入栈中。ebp - 14这个地址好像似曾相识,没错,这正是b的地址,所以这条语句的作用实际上即使将b的值传到eax中。同理,a的值被存储到ecx中去。
(实际上,上述过程解释了函数究竟是如何传参的。传参的顺序和变量创建的顺序恰好相反,先传b再传a。同时注意函数传参并不是在add函数栈帧内完成的,而是在main函数的栈帧内完成,通过寄存器eax和ecx实现变量的传递)
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
【过程九:call00AD1023】
【解析】call指令的作用是:将下一条的指令的ip压入栈中,并转移到即将被调用的子程序。相当于push ip +jmp near ptr 标号。我们现在来观察栈顶的变化。
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
? 在call指令一行们按F11观察call的作用
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
我们惊奇的发现栈顶自动压入了一个元素,元素的值为——call指令的下一条指令的地址 。那压入这个元素有什么用呢?试想,当call指令调用add函数后,我们跳转到add函数内部,那函数结束后如何保证我们从add函数后面的语句继续执行呢?靠的就是栈顶压入的地址,根据这个地址我们可以回到call指令的下一条语句。
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
再次按下F11后我们就跳转到add函数内部。
五、add函数栈帧的创建 C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
(完整连续反汇编代码请看文章结尾。这里分块讲解)
这一部分的操作和main函数内完全一致,都是为栈帧的创建做准备。画一个动图,不再赘述。
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
我们现在研究接下来指令。
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
(完整连续反汇编代码请看文章结尾。这里分块讲解)
【过程一:movdword ptr [ebp-8],0】
【解析】将ebp - 8 的地址赋值为0。也就代表着将Z初始化为0
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
【过程二:moveax, dword ptr [ebp+8]】
【解析】将 ebp + 8 (从上图观察)处的值赋值给eax。此时eax储存着形参a的值
【过程三:addeax, dword ptr [ebp+0Ch]】
【解析】将eax加上 ebp + 12 处的值。此时eax表示这a+b的值
【过程四:movdword ptr [ebp-8], eax】
【解析】将eax储存的a+b的值传送到ebp - 8的地址处,也就是赋值给变量Z
【总结】从上面我们也可以加深这样的认识:形参只是实参的一个临时拷贝,所以修改形参当然不会影响实参。
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
【过程五:moveax, dword ptr [ebp-8]】
【解析】我们知道函数内创建的临时变量出函数后被销毁,那返回值是如何被带回主函数的呢?靠的就是寄存器eax。这一步的操作就是 return返回 值的过程。
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
(完整连续反汇编代码请看文章结尾。这里分块讲解)
六、add函数栈帧的销毁【过程一:popedi / esi / ebx】
【解析】pop的作用是将栈顶的数据弹出,弹出数据储存到相应寄存器中。每次pop过程中esp的地址自动加4字节。
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
【过程二:movesp, ebp 】
【解析】一句话就回收了为add函数开辟的内存
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
【过程三:pop ebp】
【解析】弹出栈顶的元素,并将弹出的数据储存到ebp寄存器中。由于此时的栈顶元素事先存入main函数中ebp的地址,所以pop ebp时,将main函数中的ebp元素的地址存入ebp寄存器中
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
【过程四:ret】
【解析】ret指令的作用实际相当于 pop IP。在这里实际就是弹出了栈顶事先存储的add下一条指令的地址,并跳转到该地址处。
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
(完整连续反汇编代码请看文章结尾。这里分块讲解)
【过程五:movdword ptr [ebp-20h],eax】
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
【解析】监视我们可以发现ebp - 20所指向的对象就是c,所以这条语句的作用就是将返回值存储到c中
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
七、main函数栈帧的销毁 C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?
(完整连续反汇编代码请看文章结尾。这里分块讲解)
如何理解之后的语句呢,其实和add函数栈帧的销毁基本是一致的,因为我们前面提到,main函数也是被其他函数调用的。以此类推。
八、完整反汇编代码
int main() { 00031410pushebp 00031411movebp,esp 00031413subesp,0E4h 00031419pushebx 0003141Apushesi 0003141Bpushedi 0003141Cleaedi,[ebp-0E4h] 00031422movecx,39h 00031427moveax,0CCCCCCCCh 0003142Crep stosdword ptr es:[edi] int a = 10; 0003142Emovdword ptr [a],0Ah int b = 20; 00031435movdword ptr [b],14h int c = add(a,b); 0003143Cmoveax,dword ptr [b] 0003143Fpusheax 00031440movecx,dword ptr [a] 00031443pushecx 00031444call_add (0310E6h) 00031449addesp,8 0003144Cmovdword ptr [c],eax return 0; 0003144Fxoreax,eax } 00031451popedi 00031452popesi 00031453popebx 00031454addesp,0E4h 0003145Acmpebp,esp 0003145Ccall__RTC_CheckEsp (03113Bh) 00031461movesp,ebp 00031463popebp 00031464ret

int add(int x, int y) { 000313C0pushebp 000313C1movebp,esp 000313C3subesp,0CCh 000313C9pushebx 000313CApushesi 000313CBpushedi 000313CCleaedi,[ebp-0CCh] 000313D2movecx,33h 000313D7moveax,0CCCCCCCCh 000313DCrep stosdword ptr es:[edi] int z = 0; 000313DEmovdword ptr [z],0 z = x + y; 000313E5moveax,dword ptr [x] 000313E8addeax,dword ptr [y] 000313EBmovdword ptr [z],eax return z; 000313EEmoveax,dword ptr [z] } 000313F1popedi 000313F2popesi 000313F3popebx 000313F4movesp,ebp 000313F6popebp 000313F7ret

【注】这是后面补充的便于大家对知识的掌握有一个连贯性,所以在地址上有些不同。关注重要的指令即可。
九、后记 恭喜你学完了函数栈帧的创建和销毁!觉得不错可以点个赞。大家最近是不是有一大波考试呢?祝大家考的都会,蒙的全对。
C语言系统学习学习手册|【C语言知识精讲③】函数栈帧的创建和销毁(全程图解)
文章图片
?

    推荐阅读