💓博主CSDN主页:杭电码农-NEO💓
⏩专栏分类:C语言学习分享⏪
🚚代码仓库:NEO的学习日记🚚
🌹关注我🫵带你学习更多C语言知识
🔝🔝
这里写目录标题
- 1. 前言🚩
- 2. 前期铺垫🚩
- 2.1 main函数的调用🏁
- 2.2 调用main函数的函数🏁
- 3. 利用反汇编深入了解栈帧建立🚩
- 3.1 反汇编过程(创建栈帧)🏁
- 3.11 创建栈帧第一步🏳️🌈
- 3.12 创建栈帧第二步🏳️🌈
- 3.13 创建栈帧第三步🏳️🌈
- 3.14 创建栈帧第四步🏳️🌈
- 3.15 创建栈帧第五步🏳️🌈
- 3.16 创建栈帧第六步(随机值的产生)🏳️🌈
- 3.2 反汇编(执行代码过程)🏁
- 3.21 执行代码第一步(随机值的展现)🏳️🌈
- 3.22 执行代码第二步(调用Add函数)🏳️🌈
- 3.23 Add函数内🏳️🌈
- 3.24 Call调用函数🏳️🌈
- 3.3 Add函数内部🏁
- 3.31 Add函数汇编第一步🏳️🌈
- 3.32 Add函数汇编第二步🏳️🌈
- 4. 利用反汇编深入了解函数栈帧的销毁🚩
- 4.1 保存返回值🏁
- 4.2 销毁栈帧第二步🏁
- 4.3 销毁栈帧第三步🏁
- 4.4 返回main函数🏁
- 5. 总结🚩
1. 前言🚩
在我们前期学习C语言时,可能会有很多疑问 ? 比如:
- 局部变量是怎么创建的?
- 为什么未初始化的局部变量的值是随机值?
- 函数是怎样传参的?传参的顺序是怎样的?
- 形参和实参是什么关系?
- 函数调用是怎样做的?
- 函数调用后是怎样返回的?
我们本章就来研讨这个问题,掌握了函数栈帧的创建和销毁更有利于后期的学习这里建议大家要从头往后一个内容一个内容看,因为这里每一个部分关联性很强!
进入正题,我们使用的编译环境是vs2013.不要使用太高级别的编译器,越高级的编译器,越不容易学习和观察.同时在不同编译器下,函数调用过程中栈帧的创建是略微有点差别的,具体实现取决于编译器的实现
2. 前期铺垫🚩
我们之前应该听说过寄存器,寄存器中有eax,ebx,ecx,edx等寄存器,今天都能看见它们的身影,而今天的重点是这两个寄存器:ebp 和 esp,要理解函数栈帧就必须理解这两个寄存器.
ebp 和 esp 这两个寄存器是用来维护函数栈帧的.我们之前说过,内存存储一般分为几个区域,而每一次函数调用都是在栈区上创建一块儿空间,所以这里我们只讨论栈区.
我们这里先创建一个项目,简单的调用一个加法函数:
#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 ",c);
return 0;
}
2.1 main函数的调用🏁
我们首先调用main函数就会为main函数在栈区开辟一块空间,这片空间被称为main函数的函数栈帧:假设绿色方格为栈区.(栈区习惯先使用高地址后使用低地址)
这个时候我们刚刚介绍的寄存器 ebp 和 esp就要上场"表演"了,ebp是指向一个地址的寄存器,它将指向当前调用函数的高地址位置,而 esp也是指向地址的寄存器,它将指向当前调用的函数的地址值位置.这两个寄存器来维护现在正在调用的函数:
当前我们调用的是main所以ebp和esp用来维护为main函数开辟的栈帧.而当我们调用Add函数后,ebp和esp就将指向main函数栈帧的区域变为指向Add函数栈帧的区域用来维护Add函数
这是首先我们需要了解的点,我们还通常将esp称为栈顶指针,将ebp称为栈低指针
2.2 调用main函数的函数🏁
其实在我们编译代码时,还有其他函数去调用了我们的main函数,这个具体为什么调用main函数这里我们不做深入探究,
这里我们只需要知道main函数也是别别人调用的这个调用函数的函数叫做: __tmainCRTStartup( )
这里还有个套娃结构,就是__tmainCRTStartup( )函数还被一个叫mainCRTStartup( )的函数调用
听到这里你可能有一点云里雾里摸不着头脑,但是多的我们不去探究,这里我们只需要知道一个叫 mainCRTStartup( ) 的函数调用了一个叫 __tmainCRTStartup( ) 的函数,并且 __tmainCRTStartup( ) 函数调用了我们的main函数就够了!
在我们的调试界面的调用堆栈可以看见这两个函数:
相当于在我们main函数的栈帧空间之前,还应该有两个栈帧空间是为 tmainCRTStartup( ) 函数
和 __tmainCRTStartup( ) 函数开辟的:
正在调用哪个函数,ebp和esp就指向哪块栈帧!
3. 利用反汇编深入了解栈帧建立🚩
首先,我们写好简单的代码后按F11,然后什么都别点,点击右键转到反汇编,这里就可以看见我们的汇编代码了!(注意:视频用的是VS2022,所以可能会和我们之后的图片不符合,这里按照vs2013编译器来):
转到反汇编
这里我们在vs2013上可以看见汇编代码:我们一行一行往下走
3.1 反汇编过程(创建栈帧)🏁
首先我们可以看见它上来正在准备调用main函数,我们知道前面有一个函数是用来调用main函数的,所以我们先把调用main函数的函数的图给画出来:
当我们画好图后就开始一步一步走汇编代码: 紧跟着我的节奏!
3.11 创建栈帧第一步🏳️🌈
第一步的操作是:push,push的对象是ebp,push是压栈的意思,相当于在__tmainCRTStartup函数的栈帧上面插入了一个元素.并且esp跟着向上运动一格(地址减一,因为栈区是从高地址向低地址走
3.12 创建栈帧第二步🏳️🌈
第二步的操作是 mov,就是move的意思,就是将esp的值赋值给ebp,相当于esp和ebp现在指向同一块地址
3.13 创建栈帧第三步🏳️🌈
第三步的操作是sub,就是减法的意思,这里就是将esp减去一个0E4h,0E4h是一个8进制数字,它的值是228.不管怎么说,就是esp减去了一个值,相当于esp变小了,它就不指向原先的位置了,而是指向上面的某块位置
而当我们执行到这一步会发现,esp和ebp中间又重新多出来了一块空间,实际上这块空间就是为我们main函数开辟的栈帧
3.14 创建栈帧第四步🏳️🌈
当我们为main函数开辟好栈帧后,紧接着又执行了三次push,分别将ebx,esi,edi压栈,这三个元素这里我们暂且不做深入讨论,我们只需要知道它压入了三个元素.push一次,我们的esp就会变化一次,所以最终push完成三个元素后:
3.15 创建栈帧第五步🏳️🌈
第五步的操作是lea,它的意思是: load effective address,是加载有效地址的意思,它的意思就是将后面的 [ebp-0E4h] 加载到edi里面去.现在我们还看不出上面的效果,暂时先放着,我们接着往后走:
3.16 创建栈帧第六步(随机值的产生)🏳️🌈
这三步比较抽象,具体意思就是 dword 就是四个字节的意思,就是每次操作四个字节,将我们从 edi 开始的位置向下ecx个空间也就是 39h 这么多个空间全部改成 eax 的内容,也就是 CCCCCCCC. 我们再去内存中查看我们刚刚修改的空间时会看见它们全部被赋值成了C:
上面的这三步操作相当于把我们为main函数开辟的栈帧的内存全部初始化成了CCCC,这也就是我们未初始化的局部变量是随机值的原因.
3.2 反汇编(执行代码过程)🏁
在我们为main开辟好栈帧并且初始化之后,我们终于要来到执行代码的这一步了!
3.21 执行代码第一步(随机值的展现)🏳️🌈
第一步操作是mov,就是将0Ah赋值给我们的 [ebp-8] ,注意,这里的0Ah实际上就是我们初始化变量a的值,为10.而[ebp-8]就是ebp往上移动8格的位置.这里相当于我们的ebp-8的位置就是a变量的空间,空间里面放上10.
然而假设我们的a没有初始化给10,这里就不会把有效值放在内存中,内存里还是我们之前初始化的CCCCCC.这里就形成了随机值!
同理,这里定义的变量b和c也是以这种方式被存储在栈帧中.
最终得到:
注意:a,b,c三个变量存放位置的间隔不同编译器下可能不一样,这里不需要细究它为什么在什么位置,我们只需要了解局部变量是怎样创建的就好了
3.22 执行代码第二步(调用Add函数)🏳️🌈
这里就走到调用Add函数的地方了,函数调用需要传参,我们来看看是怎么回事
先看第一步,将[ebp-14h]的值赋值到eax上.我们会发现[ebp-14h]实际上就是存放b变量的值的地方,相当于就是将b的值20放到eax里面去
3.23 Add函数内🏳️🌈
这里首先将eax压栈,eax就放在了最上面,并且eax的值是20,然后esp再往上走一格得到:
紧接着往下走两步:
这里和前面非常类似,先将[ebp-8]的值赋值给ecx,再将ecx压栈最上面,再将esp往上走一格.并且这里的[ebp-8]其实就是我们变量a存放的值,为10.相当于就是将10赋值给ecx,再将ecx压栈.
我们上面的两个动作实际上是在传参,将参数a,b传到Add函数当中,并且,函数传参的顺序是先传参数b后传参数a!
3.24 Call调用函数🏳️🌈
这里我们要先把执行call这一代码的地址记下来:00C2144B.并且记下call下一行的地址:00C21450.当我们执行到call语句后.我们去查看内容:
它的栈帧中新压栈进入了一个东西:00 C2 14 50(这里要倒着读).这其实就是我们刚刚说要记住的地址,也就是call的下一行指令的地址 ,这里当我们执行完call指令后,我们就跳转到Add函数中去执行指令了,这里将call指令的下一条指令的地址给记住,把它放这儿,一会调用完Add函数后我们还要继续往下走,记住了下一条指令才能继续往下走,这里我们只需要知道这个被我们存储的地址等会儿要被使用到就可以了!
3.3 Add函数内部🏁
当我们执行完call指令后我们将跳转到Add函数的调用中.
不知道大家有没有发现,上面图片的汇编过程和我们之前探究main函数创建的汇编过程是一模一样的!这上面所有的内容其实就是在为我们的Add函数开辟栈帧,并且初始化为CCCC.这里和之前将的main函数开辟过程一样,所以我们直接往下走.
我们直接将执行完后这段汇编代码后的直观图带给大家:
3.31 Add函数汇编第一步🏳️🌈
我们初始化z为0,所以这里汇编代码将我们的0赋值给[ebp-8]这个位置,而ebp-8这个位置实际上就是变量z的空间
3.32 Add函数汇编第二步🏳️🌈
这里将[ebp+8]的值赋值给eax,而我们的ebp+8就是ebp这个指针往下走两格,我们看看其实就是我们刚刚存储的a的值10.
其实就是将我们形参a的值10赋值给我们的eax.紧接着看下面的操作
将a的值10赋值到eax后,下一步操作将[ebp+0Ch]的值加到eax上,而这里的[ebp+0Ch]刚好是我们保存的形参b的值20,这里将b的值加上去也就是得到了30.所以现在eax存放的值是30!紧接着我们将eax的值赋值给[ebp-8],而大家还记不记得,其实这里的[ebp-8]就是我们之前给C的一块空间,所以这里就是将eax的值30赋值给变量z,z现在也就变成了30.
讲到这儿,你可能就会对这句话有了一定的了解:形参是实参的一份临时拷贝.我们在调用函数之前,函数的参数就已经通过压栈的方式存储在了栈帧中,而当函数中需要用到参数时就会回去取,并且我们可以发现为函数形参开辟的空间并不在esp和ebp维护的函数中,也就是说函数的参数的空间和函数的空间是两个不一样的空间!
4. 利用反汇编深入了解函数栈帧的销毁🚩
在我们把Add函数中所有代码走了一遍后,现在要将z返回到main函数中去!
4.1 保存返回值🏁
这里return z的意思就是说将[ebp-8]也就是z的值30放在eax这个寄存器当中.我们说过当函数调用结束后里面创建的值会销毁,为了保存返回值z的值,这里就是将z的值赋值到eax寄存器中,寄存器是不会随着函数的销毁而销毁的,所以我们就成功的保存了z的值!
再往下走
pop就是出栈的意思,这里的三个pop就是将edi,esi,ebx分别出栈,出栈也就是从栈帧中删除的意思!每出栈一个元素,我们的esp就应该对应向下移动一格(和出栈刚好相反)
4.2 销毁栈帧第二步🏁
当Add函数调用完后,应该回收这块Add函数的栈帧,我们接着往下走:
这里先将ebp的值赋值给esp,相当于esp就不指向原先内个位置了,而是往下走了许多格和ebp指向一块栈帧.然后再将ebp给pop掉,也就是删除掉
而当我们pop掉这个ebp后,ebp就从维护Add函数的栈帧的位置重新跳到维护main函数的栈帧的位置了,因为这里Add函数以及调用完了,esp和ebp又要重新去维护正在调用的main函数了!
4.3 销毁栈帧第三步🏁
走到ret这个地方后,我们需要知道,此时我们已经将维护main函数的两个寄存器带到了正确的位置,但是我们执行完Add函数后要接着往下执行时,我们的return也就是ret要返回到什么位置呢?我们应该怎样找到这个位置?那大家还记不记得我们之前保存的call指令的下一个指令的地址,我们说call指令就是在调用函数,而调用完函数我们需要接着刚刚的call指令往下走,所以这里我们之前保存的call指令的下一条指令就起到作用了!我们的ret返回的位置也就是刚刚我们保存的00 C2 14 50.
这里箭头就指向call函数的下一个指令去了所以我们最初存call的下一个的指令的地址的意义就在这儿体现出来了
现在我们跳回去了接着看汇编指令:
这里将esp的值加上一个8,相当于esp向下移动了两个位置.这是因为我们调用完函数之后,形参x和y已经没有用了,所以我们将esp往下跳过两格就是刚好把形参x和y的栈帧给跳过去了,就把空间还给操作系统了
4.4 返回main函数🏁
将esp指向正确位置后,再将eax的值赋值给[ebp-20h],大家还记不记得,我们最开始将Add函数中z的值赋值给了eax这个寄存器,现在再将eax也就是z的值30赋值到[ebp-20h]这个位置.而这个位置刚好就是我们main函数里面的C.这一步相当于就是将Add的返回值赋值给C了
讲到这儿,Add函数的栈帧是怎样创建和销毁的,返回值是怎样被带出来了相信大家都有了一定的认识,后面的main函数的销毁和返回和Add函数大同小异,这里就不过多做介绍了.
5. 总结🚩
本篇文章主要是通过我们c语言的反汇编过程带大家深入了解了函数栈帧的创建和销毁,也许你在看文章的过程中看的云里雾里,摸不着头脑,没关系!本章作为C语言学习的番外篇着重带大家了解更底层的原理,你能理解,可能会对后面的学习又帮助,不能理解也没有关系!
其实我们可以看见C语言的编译过程是逻辑非常严谨的,不仅能走出去,还能回得来,什么时候创建栈帧,什么时候销毁栈帧它是给你安排的明明白白清清楚楚的,希望看了本篇文章你会更加喜欢C语言!
现在再去看看文章开头的问题,相信你也能回答出来了!