小公司做网站,虚拟主机搭建网站源码,如何在百度做网站,WordPress多网络目录
前言
一、什么是函数栈帧#xff1f;
二、理解函数栈帧能解决什么问题呢
三、函数栈帧的创建和销毁解析
3.1 什么是栈#xff1f;
3.2 认识相关寄存器和汇编指令
3.3 剖析函数栈帧的创建和销毁
3.3.1 esp寄存器与ebp寄存器的重要性
3.3.2 函数的调用堆栈
3.3.…
目录
前言
一、什么是函数栈帧
二、理解函数栈帧能解决什么问题呢
三、函数栈帧的创建和销毁解析
3.1 什么是栈
3.2 认识相关寄存器和汇编指令
3.3 剖析函数栈帧的创建和销毁
3.3.1 esp寄存器与ebp寄存器的重要性
3.3.2 函数的调用堆栈
3.3.3 准备环境
3.3.4 转到反汇编
3.3.5 函数栈帧的创建
3.3.6 执行有效代码
3.3.7 回答理解函数栈帧能解决什么问题中的六个问题
总结 前言 函数为C语言中最基本的一个单位但它是如何创建的您了解吗它又是如何传参的呢又是如何返回的呢本文章将深入底层详细讲解关于函数栈帧的创建与销毁一系列知识看完本文您将收获匪浅。 一、什么是函数栈帧 我们在写C语言代码的时候经常会把一个独立的功能抽象为函数所以C程序是以函数为基本单位的。 那函数是如何调用的函数的返回值又是如何待会的函数参数是如何传递的这些问题都和函数栈帧有关系。 函数栈帧stack frame就是函数调用过程中在程序的调用栈call stack所开辟的空间这些空间是用来存放 函数参数和函数返回值临时变量包括函数的非静态的局部变量以及编译器自动生产的其他临时变量保存上下文信息包括在函数调用前后需要保持不变的寄存器。 简单理解就是创建函数时会在栈区创建一块空间而这块空间正是函数栈帧。 二、理解函数栈帧能解决什么问题呢 理解函数栈帧有什么用呢只要理解了函数栈帧的创建和销毁以下问题就能够很好的理解了 局部变量是如何创建的为什么局部变量不初始化内容是随机的函数调用时参数时如何传递的传参的顺序是怎样的函数的形参和实参分别是怎样实例化的函数的返回值是如何带回的 函数栈帧的知识是偏向底层的当理解透彻后对理解变量的存储、静态变量的创建、动态内存的申请与销毁等等知识点有很大的帮助 三、函数栈帧的创建和销毁解析 讲解函数栈帧之前需要了解一些预备知识点。 3.1 什么是栈 栈stack是现代计算机程序里最为重要的概念之一几乎每一个程序都使用了栈没有栈就没有函 数没有局部变量也就没有我们如今看到的所有的计算机语言。 在经典的计算机科学中栈被定义为一种特殊的容器用户可以将数据压入栈中入栈push也可 以将已经压入栈中的数据弹出出栈pop但是栈这个容器必须遵守一条规则先入栈的数据后出 栈First In Last Out FIFO。就像叠成一叠的术先叠上去的书在最下面因此要最后才能取出。 在计算机系统中栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中也可以将数据 从栈顶弹出。压栈操作使得栈增大而弹出操作使得栈减小。 在经典的操作系统中栈总是向下增长由高地址向低地址的。 在我们常见的i386或者x86-64下栈顶由成为 esp 的寄存器进行定位的。 栈最主要的特性先进后出。这里简单介绍如想了解栈相关知识点可以看《超详细之实现栈》 3.2 认识相关寄存器和汇编指令 在函数栈帧的创建与销毁过程中涉及到了寄存器和汇编指令知识点。 寄存器的功能是存储二进制代码它是由具有存储功能的触发器组合起来构成的。一个触发器可以存储1位二进制代码故存放n位二进制代码的寄存器需用n个触发器来构成。 [1] 按照功能的不同可将寄存器分为基本寄存器和移位寄存器两大类。基本寄存器只能并行送入数据也只能并行输出。移位寄存器中的数据可以在移位脉冲作用下依次逐位右移或左移数据既可以并行输入、并行输出也可以串行输入、串行输出还可以并行输入、串行输出或串行输入、并行输出十分灵活用途也很广。详细可见百度百科寄存器 汇编指令是汇编语言中使用的一些操作符和助记符还包括一些伪指令如assumeend汇编指令同机器指令一一对应。每一种CPU都有自己的汇编指令集。 [1] 计算机是通过执行指令来处理数据的为了指出数据的来源、操作结果的去向及所执行的操作一条指令一般包含操作码和操作数两部分。详细可见百度百科汇编指令 相关寄存器 函数栈帧创建与销毁相关寄存器 eax通用寄存器保留临时数据常用于返回值ebx通用寄存器保留临时数据ebp栈底寄存器esp栈顶寄存器eip指令寄存器保存当前指令的下一条指令的地址 相关汇编命令 函数栈帧创建与销毁相关汇编命令 mov数据转移指令push数据入栈同时esp栈顶寄存器也要发生改变pop数据弹出至指定位置同时esp栈顶寄存器也要发生改变sub减法命令add加法命令call函数调用1. 压入返回地址 2. 转入目标函数jump通过修改eip转入目标函数进行调用ret恢复返回地址压入eip类似pop eip命令 3.3 剖析函数栈帧的创建和销毁
3.3.1 esp寄存器与ebp寄存器的重要性 esp栈顶指针。ebp栈底指针。每一次函数调用都要为本次函数调用开辟空间就是函数栈帧的空间。要理解清楚函数栈帧就要理解ebp与esp寄存器。这块空间的维护使用了2个寄存器esp和ebpebp存储的是栈底的地址ebp存储的是栈顶的地址。ebp寄存器与esp寄存器中存放的是地址这两个寄存器共同维护函数栈帧空间。 如图所示 关于栈区的特点 栈区的分配习惯是先使用高地址再使用低地址。栈区往往是从高地址开始往上使用空间。 注意函数栈帧的创建和销毁过程在不同的编译器上实现的方式大同小异主要掌握的是实现过程。本文将以VS2019为例。 3.3.2 函数的调用堆栈 演示代码 #include stdio.h
int Add(int x, int y)
{
int z 0;
z x y;
return z;
}
int main()
{
int a 3;
int b 5;
int ret 0;
ret Add(a, b);
printf(%d\n, ret);
return 0;
} 在VS2019编译器上进行调试F10进入调试我们可以调用函数堆栈看看如下图 函数调用堆栈是反馈函数调用逻辑的那我们可以清晰的观察到 main 函数调用之前是由invoke_main 函数来调用main函数。这也解释了为什么main函数总是有return 0了因为main函数也是被其它函数调用的这里不做过多讲解只需要知道每个函数都会有自己的栈帧。 3.3.3 准备环境 为了让我们研究函数栈帧的过程足够清晰不要太多干扰我们可以关闭下面的选项让汇编代码中排 除一些编译器附加的代码 3.3.4 转到反汇编 观察函数栈帧需要再返汇编内观看里边用到了寄存器与汇编指令。 为了方便观看地址我们鼠标右键将显示外部符号取消勾选。 int main()
{
//函数栈帧的创建
002E18B0 push ebp
002E18B1 mov ebp,esp
002E18B3 sub esp,0E4h
002E18B9 push ebx
002E18BA push esi
002E18BB push edi
002E18BC lea edi,[ebp-24h]
002E18BF mov ecx,9
002E18C4 mov eax,0CCCCCCCCh
002E18C9 rep stos dword ptr es:[edi]
//main函数中的核心代码int a 3;
002E18D5 mov dword ptr [ebp-8],3 int b 5;
002E18DC mov dword ptr [ebp-14h],5 int ret 0;
002E18E3 mov dword ptr [ebp-20h],0 ret Add(a, b);
002E18EA mov eax,dword ptr [ebp-14h]
002E18ED push eax
002E18EE mov ecx,dword ptr [ebp-8]
002E18F1 push ecx
002E18F2 call 002E10B4
002E18F7 add esp,8
002E18FA mov dword ptr [ebp-20h],eax printf(%d\n, ret);
002E18FD mov eax,dword ptr [ebp-20h]
002E1900 push eax
002E1901 push 2E7B30h
002E1906 call 002E10D2
002E190B add esp,8 return 0;
002E190E xor eax,eax
} 以上汇编代码为main函数这个函数栈帧中一系列操作其实可以分成两部分 函数栈帧的创建每个函数都有这部执行有效代码 3.3.5 函数栈帧的创建 我们先来看看第一部分如何创建函数栈帧。我们知道main函数也是被其它函数调用的前文提到一个函数栈帧是被esp和ebp进行维护的此时esp与ebp还在维护着invoke_main函数栈帧我们看看图 接下来开始为main函数创建栈帧我们逐条分析 002E18B0 push ebp push压栈将ebp压栈简单理解就是拷贝一份ebp放到栈顶此时栈顶存储着栈底的地址push后esp的指向也会改变看图 002E18B1 mov ebp,esp mov移动相当于赋值操作将esp的值赋给ebpebp一开始是指向栈底的赋值后ebp指向栈顶即esp指向的位置看图 002E18B3 sub esp,0E4h sub减操作将esp减去0E4h0E4h是一个8进制数字如想知道具体值可以打开监视查看这条指令的意思就是将esp指向位置减0E4h就是改变esp位置esp会向上移动因为栈区下面是高地址上边是低地址具体移动到了哪个位置我们可以通过内存窗口看下面是具体的图解 此时esp与ebp各自指向的地址之间就是共同维护的新一块空间。当然这块空间有多大是由编译器决定的我们也不知道这块空间也可以说是编译器为某函数预开辟的空间。 002E18B9 push ebx
002E18BA push esi
002E18BB push edi 接下来会进行三次push也就是压栈三次分别将ebx、esi、edi压栈。 接下来的4条指令作用是为函数栈帧初始化 先把ebp-24h的地址放在edi中把9放在ecx中把0xCCCCCCCC放在eax中将从ebp-24h到ebp这一段的内存的每个字节都初始化为0xCC 002E18BC lea edi,[ebp-24h] leaload effecitve address 加载有效地址将后面这个地址加载到edi中其实就是将edi的值改为(ebp-24h)。其实在刚刚sub操作时将esp-0E4h让esp向上走那时候esp的指向和ebp是一样的sub后指向了新的地址此时让edi指向(ebp-24h)的值那此时ebi的值为在3次push之前esp指向的位置了. 002E18BF mov ecx,9 mov移动将9赋值给ecx。 002E18C4 mov eax,0CCCCCCCCh mov操作将0cccccccch赋值给eax。 002E18C9 rep stos dword ptr es:[edi] 关键一步这条指令的意思是将edi以下9次或ecx次的dword(double word1个word为2字节double word为4字节)每次初始化4个字节全部改为0cccccccch的内容到ebp结束。 以上操作后此时为该函数的函数栈帧就开辟好了接下来开始执行有效代码了。 3.3.6 执行有效代码
int a 3;
int b 5;
int ret 0;
ret Add(a, b);
printf(%d\n, ret);
return 0;
int a 3;
002E18D5 mov dword ptr [ebp-8],3 mov移动意思为将3赋值给[ebp-8]dword为4字节意思就是将3放在[ebp-8]4字节位置[ebp-8]其实就是栈底向上8字节的位置该位置用来存储3该位置也是为局部变量a开辟的空间。 此时也能解是局部变量是如何创建的为什么局部变量创建的时候建议初始化了。如果不初始化那a的值就为0cccccccch这就是为什么会打印出烫烫烫.....的原因了。 int b 5;
002E18DC mov dword ptr [ebp-14h],5 同理与上一条指令的操作思路是一样的。还需要注意的是在哪个地方为局部变量开辟空间是不一定的这由编译器决定。 int ret 0;
002E18E3 mov dword ptr [ebp-20h],0 很多人都不清楚函数是如何传参的接下来4条指令就是函数的传参过程 res Add(a,b);
002E18EA mov eax,dword ptr [ebp-14h] mov操作将[ebp-14h]的值赋给eax[ebp-14h]是什么呢就是b所在的空间中的5。 002E18ED push eax push压栈将eax压栈。 002E18EE mov ecx,dword ptr [ebp-8] mov操作将[ebp-8]的值赋给ecx[ebp-8]就是a所在的空间里的3。 002E18F1 push ecx push压栈将ecx压栈。 eax与ecx分别存储了b与a的值并且进行压栈操作注意此时还没有调用函数Add那也就是说函数的传参其实是在调用之前完成的而形参是实参的一份临时拷贝这句话也能理解了eax与[ebp-14h]、ecx与[ebp-8] 均不在一块空间并且eax与ecx存储的值分别为b与a的值这就是临时拷贝。关于函数传参是从右到左执行的。 接下来就准备进行函数调用了。 002E18F2 call 002E10B4 call指令是要执行函数调用逻辑的在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方继续往后执行。按F11跳转 002E10B4 jmp 002E1770 这里不用过多关注再按一次F11就进入Add函数了。 int Add(int x, int y)
{
//函数栈帧的创建
002E1770 push ebp
002E1771 mov ebp,esp
002E1773 sub esp,0CCh
002E1779 push ebx
002E177A push esi
002E177B push edi
002E177C lea edi,[ebp-0Ch]
002E177F mov ecx,3
002E1784 mov eax,0CCCCCCCCh
002E1789 rep stos dword ptr es:[edi]
//执行有效代码int z 0;
002E1795 mov dword ptr [ebp-8],0 z x y;
002E179C mov eax,dword ptr [ebp8]
002E179F add eax,dword ptr [ebp0Ch]
002E17A2 mov dword ptr [ebp-8],eax return z;
002E17A5 mov eax,dword ptr [ebp-8]
}
//函数销毁并回到main函数内销毁函数内部的局部变量
002E17A8 pop edi
002E17A9 pop esi
002E17AA pop ebx
002E17B8 mov esp,ebp
002E17BA pop ebp
002E17BB ret 此时的逻辑跟创建main函数栈帧逻基本辑相同但要分为三部分前两部分与main函数逻辑相同在执行有效代码部分如何使用形参也是重点第三部分为函数销毁、如何将返回值带回、如何回到main函数内、如何销毁形参。下面进行解析 //Add函数栈帧的创建
002E1770 push ebp
002E1771 mov ebp,esp
002E1773 sub esp,0CCh
002E1779 push ebx
002E177A push esi
002E177B push edi
002E177C lea edi,[ebp-0Ch]
002E177F mov ecx,3
002E1784 mov eax,0CCCCCCCCh
002E1789 rep stos dword ptr es:[edi] 第一部分为创建Add函数栈帧指令因为上文解释过了这里不进行过多解释了看看图解吧 第二部分是执行有效代码这里需要关注形参的使用。 int z 0;
002E1795 mov dword ptr [ebp-8],0 与上文逻辑相同为z开辟一个空间。不过多解释。 z x y;
002E179C mov eax,dword ptr [ebp8]
002E179F add eax,dword ptr [ebp0Ch]
002E17A2 mov dword ptr [ebp-8],eax 以上3条指令的作用是使用形参xy并且相加赋给z。一条条解释吧 002E179C mov eax,dword ptr [ebp8] mov操作将[ebp8]的值赋给eax[ebp8]是什么呢看看下图 一块内存空间就为4字节那ebp8的话就是ebp向下8字节那就是ecx内存空间也就是a所在的空间看到这里相信聪明的您明白了吧。函数内的形参并不会在它的函数栈帧中分配一块空间而是当要使用时寻找之前压栈的ecx与eax所有严格上来说函数的形参是创建在主调函数栈帧里的。 002E179F add eax,dword ptr [ebp0Ch] add增加指令其实就是找到[ebp0Ch]然后eax [ebp0Ch]而[ebp0Ch]的值也就是eax空间b的值了找到后进行相加也就对应着x y了。 这里有个点就是当寻找形参时是从左向右执行的这点与函数传参是不同。 002E17A2 mov dword ptr [ebp-8],eax mov操作将eax的值赋给[ebp-8]逻辑跟上文创建局部变量相同eax的值就是刚刚相加后的值[ebp-8]就是Add函数栈帧内的一处空间为z开辟了一处空间这块空间就是[ebp-8]里面的内容是eax就是形参相加后的值。 继续执行接下来是return返回语句那函数中怎么带回返回值呢 return z;
002E17A5 mov eax,dword ptr [ebp-8] 我们知道在函数返回后也代表这个函数结束进行销毁而进行销毁那z的值不也销毁了吗那我们怎么带回去呢 mov指令将[ebp-8]的值赋给eax这里非常巧妙[ebp-8]为z而eax是什么呢eax是一个寄存器寄存器是独立于内存之外的不会随着函数的结束而销毁因此将返回值放到eax中等回到主调函数再拿出来。 此时Add函数内的有效代码全部执行完毕但并还没有结束接下来将进行函数栈帧的销毁、返回到主调函数。 002E17A8 pop edi
002E17A9 pop esi
002E17AA pop ebx 还记得在创建函数栈帧的时候push了3个寄存器到栈顶吗而此时又进行3次pop将edi、esi、ebx依次弹出esp的指向也改变。 002E17B8 mov esp,ebp
002E17BA pop ebp 进行mov操作将ebp的值赋给esp此时esp指向ebp位置。 pop操作将栈顶元素弹出并赋值放到ebp中而正好此时的栈顶正好是刚开始执行的第一次指令push ebp而此时栈顶的值就是上一个函数栈帧的栈底因此ebp此时指向上一个函数的栈底位置这就是精妙之处因为上一个函数的栈底位置很难找到那在创建函数栈帧时就会push ebp将上一个函数的栈底地址压栈栈顶放置栈底的地址方便回来的时候找到栈底此时因为pop了esp与ebp执行均改变esp执行下一个ebp被pop赋值而pop的位置正好存放了栈底的值因此ebp又指向栈底了现在esp与ebp又维护这main函数。 002E17BB ret 最后ret指令首先是从栈顶弹出一个值此时栈顶的值就是call指 令下一条指令的地址然后直接跳转到call指令下一条指令的地址处继续往下执行。此时就真正的回到了main函数栈帧内了。这就能解释之前为什么将call指令的下一条指令的地址存储起来了就是为了当调用函数结束后返回后能继续执行代码。 此时我们观察上图发现形参还没有销毁因此说明形参不是随着函数栈帧的结束而销毁的而是要回到主调函数栈帧内再进行销毁。 002E18F7 add esp,8 add操作让esp8就是让esp向下走8字节。 此时函数调用结束要继续执行main函数有效代码部分了。 002E18FA mov dword ptr [ebp-20h],eax mov操作将eax的值赋给[ebp-20h]eax里的值是什么呢里面就是Add函数的返回值而[ebp-20]所处的空间为res的空间这条指令就是接受返回值。 printf(%d\n, ret);
002E18FD mov eax,dword ptr [ebp-20h]
002E1900 push eax
002E1901 push 2E7B30h
002E1906 call 002E10D2
002E190B add esp,8
return 0;
002E190E xor eax,eax 最后就是打印和return返回了。这里不再解释了。函数栈帧的创建与销毁这个流程也将结束。 3.3.7 回答理解函数栈帧能解决什么问题中的六个问题 问题1局部变量是如何创建的 在创建初始化完函数栈帧后通过栈底指针ebp-某个数这个数是由编译器决定的从而得到一块内存空间mov指令后这块空间就是属于局部变量的空间。 问题2为什么局部变量不初始化内容是随机的 在创建函数栈帧时会有4条指令用于初始化函数栈帧的内存空间每个内存中会存放0cccccccch这个值而如果在创建局部变量时不进行初始化那分配的空间中的内容就为0cccccccch。 问题3函数调用时参数是如何传递的 对于参数是如何传递的也就是如何进行函数传参问题在调用函数之前会将()内的参数从右到左依次进行push压栈操作此时参数处于主调函数的栈帧之间并且存储了实参的值内存空间与实参不同。 问题4传参的顺序是怎样的 函数传参的顺序是从右到左执行的。 问题5函数的形参和实参分别是怎样实例化的 函数的实参实例化在传参时压栈操作后与相对应的局部变量建立了联系相当于临时拷贝这是实参进行了实例化。而函数的形参实例化是当要使用该形参时会通过栈底指针ebp某个值找到调用函数前push压栈后所对应的空间使用里边的值这就是形参的实例化。 问题6函数的返回值是如何带回的 当函数执行到return返回语句时会将返回值赋值给eax寄存器因为寄存器不会随着函数的结束而销毁当回到主调函数时再进行使用eax寄存器里的值。 总结 这就是函数栈帧的创建与销毁相关知识点希望这篇文章对您有帮助如果有帮助可以点个赞关个注后续还会出更多的干货希望大家多多支持❤❤❤❤