什么是函数栈帧❓
我们在写C 语言代码的时候,经常会把一个独立的功能抽象为函数,所以 C程序是以函数为基本
单位的。那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问
题都和函数栈帧有关系。
函数栈帧(stack frame) 就是函数调用过程中在程序的调用栈(call stack)所开辟的空间, 这些空间是用来存放:
函数参数和函数返回值 临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量) 保存上下文信息(包括在函数调用前后需要保持不变的寄存器)
理解函数栈帧能解决什么问题呢?
理解函数栈帧有什么用呢?
只要理解了函数栈帧的创建和销毁,以下问题就能够很好的额理解了:
局部变量是如何创建的? 为什么局部变量不初始化内容是随机的? 函数调用时参数时如何传递的?传参的顺序是怎样的? 形参和实参的关系是什么呢? 函数调用结束后是如何返回的?
让我们一起走进函数栈帧的创建和销毁的过程中。
函数栈帧的创建和销毁解析
预备知识
什么是栈?
栈(stack )是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。 在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push ),也可以将已经压入栈中的数据弹出(出栈,pop ),但是栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out , FIFO )。就像叠成一叠的术,先叠上去的书在最下面,因此要最后才能取出。 在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。 在经典的操作系统中, 栈总是向下增长(由高地址向低地址)的 。 在我们常见的i386 或者 x86-64 下,栈顶由成为 esp 的寄存器进行定位的。
认识相关寄存器和汇编指令
相关寄存器
寄存器名称 |
简介 |
eax | 通用寄存器,保留临时数据,常用于返回值 |
ebx | 通用寄存器,保留临时数据 |
ebp | 栈底寄存器(Stack bottom) |
esp | 栈顶寄存器 (stack top) |
eip | 指令寄存器,保存当前指令的下一条指令的地址 |
相关汇编命令
汇编命令 |
解释 |
mov |
数据转移指令(赋值) |
push | 数据入栈,同时 esp栈顶寄存器 也要发生改变 |
pop | 数据弹出至指定位置,同时 esp栈顶寄存器 也要发生改变 |
sub | 减法命令 |
add | 加法命令 |
call | 函数调用, 1 . 压入返回地址 2. 转入目标函数 |
jump | 通过修改 eip ,转入目标函数,进行调用 |
ret | 恢复返回地址,压入 eip ,类似 pop eip 命令 |
必备知识
每一次函数调用,都要为本次函数调用开辟空间,就是函数栈帧的空间。
这块空间的维护是使用了2个寄存器: esp 和 ebp , ebp 记录的是栈底的地址, esp 记录的是栈顶的地址。
第2点如图所示:
3.函数栈帧的创建和销毁过程,在不同的编译器下创建和销毁是略有差异的,但是大体逻辑是相差不大的,当编译器越高级的时候,函数栈帧的封装越不容易看,所以编译器的环境采用vs2013
演示代码:
#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; }
大体思路:
每一个函数调用,都要在栈区创建一个空间
由于栈区使用内存的时候,每一次函数调用都要在栈区上分配空间,是先使用高地址,再使用低地址
打开调试窗口,接着打开调用堆栈
从调用堆栈看到,原来main函数也被调用了,那么它是被谁调用呢?
在VS2013中,main函数也是被其他函数调用的,调用逻辑如下:
反汇编代码:
右击鼠标,打开反汇编
int main() { //函数栈帧的创建 00BE1820 push ebp 00BE1821 mov ebp,esp 00BE1823 sub esp,0E4h 00BE1829 push ebx 00BE182A push esi 00BE182B push edi 00BE182C lea edi,[ebp-24h] 00BE182F mov ecx,9 00BE1834 mov eax,0CCCCCCCCh 00BE1839 rep stos dword ptr es:[edi] //main函数中的核心代码 int a = 3; 00BE183B mov dword ptr [ebp-8],3 int b = 5; 00BE1842 mov dword ptr [ebp-14h],5 int ret = 0; 00BE1849 mov dword ptr [ebp-20h],0 ret = Add(a, b); 00BE1850 mov eax,dword ptr [ebp-14h] 00BE1853 push eax 00BE1854 mov ecx,dword ptr [ebp-8] 00BE1857 push ecx 00BE1858 call 00BE10B4 00BE185D add esp,8 00BE1860 mov dword ptr [ebp-20h],eax printf("%d\n", ret); 00BE1863 mov eax,dword ptr [ebp-20h] 00BE1866 push eax 00BE1867 push 0BE7B30h 00BE186C call 00BE10D2 00BE1871 add esp,8 return 0; 00BE1874 xor eax,eax }
1. _tmainCRTStartup函数栈帧的创建(调用main函数的函数)
我们已经知道了,main函数也是被调用的,画出函数栈帧图详解一波:
栈空间的使用是,由高地址到低地址,而main函数是被_tmainCRTStartup的,所以esp与ebp就维护当前的栈帧
①执行push操作
这时候F10按一下, 执行一下push让ebp这个地址压栈
怎么证明ebp压栈成功?
所以说,esp这个栈顶指针指向了ebp这个压栈的值:
②接下来执行mov指令,就是把esp的值赋值给ebp
如下图:
③然后执行sub指令,让esp减去0E4h,换成二进制就是228,,整体流程下图:
当①②执行完后,其实_tmainCRTStartup栈帧的空间已经开辟完毕,当③执行完后,调用了main函数,此时esp、ebp就预开辟好了一块空间给main函数,并维护该栈帧,如下图
2.函数栈帧的创建
接着上文的内容,画出该图:
接着依次push三个寄存器ebx,esi,edi的值入栈中,esp往低地址处移动
通过监视可以看一看
画出图如下:
接下来看这四条指令:
①lea edi, [ebp+FFFFFF1Ch]
解析:
[ebp+FFFFFF1Ch]显示符号名去掉,也就是[ebp-0E4h] (也就是和[esp - OE4h]是同一个位置)
lea - 加载有效地址,即将[ebp-0E4h]的地址加载到edi寄存器中,[ebp-0E4h] - 指向ebp(基准指针寄存器)上减去0E4h(232)个字节位置的内存单元
②mov ecx,39h(准确的次数)
解析:
将立即数 39h 复制到 ecx 寄存器中,使 ecx 寄存器的内容变为 39h(十进制的57)。
③mov eax,0cccccccCh
解析:
这条指令将立即数 0cccccccCh 复制到 eax 寄存器中,使 eax 寄存器的内容变为 0cccccccCh
④rep stos dword ptr es : [edi]
解析:
rep 是重复前缀,用于指示指令要重复执行多次,执行的次数由 ecx 寄存器中的计数值决定。
stos 是字符串存储 (Store String) 的缩写,用于将数据存储到字符串中。
dword ptr 指明操作数的大小为双字(32位),用于指示要存储的数据的大小。
es:[edi] 是目标操作数,表示将数据存储到以 es 寄存器为段地址,edi 寄存器为偏移地址的内存位置。
第④点整体来看:该指令的作用是将 eax 寄存器中的值重复写入到以 es:[edi] 为起始地址的内存位置。执行次数由 ecx 寄存器中的计数值确定。
整体①②③④来看:
要把edi这个位置开始(也就是[ebp-0E4h]的地址),向下空间的ecx(次数)放的39h这个值,这么多个dword(4个字节)的数据全部都改成0CCCCCCCCh,图解在下面:
到这,main函数的开辟已经执行完了。
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-2
https://developer.aliyun.com/article/1456965?spm=a2c6h.13148508.setting.30.2e124f0eZtymxB