纸上得来终觉浅, 绝知此事要躬行。
主页:June-Frost
专栏:C语言
局部变量为什么是随机值?函数是如何调用的?
✉️ 该篇将使用该编译器,通过介绍栈帧的创建和销毁来深入了解局部变量和函数调用的一些细节。
寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。
先了解一下这几个寄存器:
eax:
- 存储函数调用时返回值的地址
- 存储线程级别的全局变量、堆栈指针等数据
- 作为命令和参数传递时的参数值
ebx:
- 存储基址或偏移量,用于访问数组或结构体中的元素
- 存储线程级别的全局变量、堆栈指针等数据
ecx:
- 存储计算结果或临时变量的地址
- 存储线程级别的全局变量、堆栈指针等数据
edx:
- 存储动态链接库或共享库的入口地址
- 存储线程级别的全局变量、堆栈指针等数据
主角:
ebp:
- 存储函数调用时的临时变量的地址
- 存储函数调用时返回值的地址(在函数调用前由操作系统维护)
- 存储栈帧中的基址
esp:
- 存储当前栈帧的顶部指针,即下一个将要被压入栈中的内存地址
- 存储函数调用时的临时变量的地址(在函数调用前由编译器维护)
- 存储调用堆栈的回溯信息
ebp和esp 这两个寄存器中存放的是地址,用来维护函数栈帧。
每一次函数的调用,都会在栈区创建一块空间。
正在调用哪个函数,这两个寄存器就会去维护哪块空间。
对于函数栈帧,通过一个例子来具体讲解:
代码为:
#include
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);y
return 0;
}
在这个代码中,都知道是main函数在调用Add函数
但是main函数谁在调用?
通过调用堆栈来观察:
这说明 是mainCRTSTartup() 调用了_tmainCRTSTartup() , _tmainCRTSTartup() 调用了 main(…),main(…)调用了Add(int x, int y)。
我们将上述代码转到反汇编:
先对这部分代码进行详解:
在将要使用这部分main函数的时候,说明_tmainCRTSTartup() 的栈帧就已经创建完成了。
第一步:
push指令就是将指定的值压入函数的栈中。此处要将ebp的值压栈。
push ebp
在执行第一步之前是这样的:
执行后为:
可以见到,栈区从高到低使用,esp的值减少了,意味着它确实指向了新压入的空间。
第二步:
第三步:
sub esp,0E4h
sub指令是减法操作,这里相当于esp-0E4h,0E4h是八进制形式,也就是十进制228,所以为esp-228.
第四,五,六步:
第七,八,九,十步:
lea指令用于将一个值从内存中复制到寄存器中,它的全称是"Load Effective Address",意思是"加载有效地址"。
这里相当于给edi加载了一个值,那个值其实就是没压栈前的esp的值0x008FFAC0 。
mov ecx,39h
mov eax,0CCCCCCCCh
这里都是赋值,将39h赋值给ecx,0CCCCCCCCh赋值给eax。
rep stos dword ptr es:[edi]
word是2个字节,dword就是double word,是4个字节,这个操作就是将edi之后(包括自己)的39个dword赋为eax的值。
这里最终的效果其实就是将main函数的栈帧空间里的内存全部初始化为cc。
mov指令,将0Ah(10)赋值给[ebp-8]的那块空间,将14h(20)赋值给[ebp-14h]的空间,将0赋值给[ebp-20h]的空间。
初始化后才有确定值,说明如果没有初始化,那么对应的内存空间就是cc cc cc cc, 如果直接使用这种空间,那么对应的值就是随机值。
这步是将[ebp-14h]空间的值赋给eax,再将eax压栈,将[ebp-8]空间的值赋给ecx,再将ecx压栈。
这里也反映了一个情况:函数的参数式从右向左传参的。
当执行call指令的时候,就是调用函数,会出现两个效果,执行F11,跳转至该界面:
而且在内存上,会储存下一条指令的地址:
效果:
对上述界面再F11,就会进入Add函数。
Add函数内前面的这些汇编,和main有异曲同工之妙。都是在创建栈帧。
所以我将直接用动图直接展示这些步骤:
然后,找到[ebp + 8] 和 [ebp + 0Ch], 0Ch即12 。 先将[ebp + 8]对应的值赋给eax,再将[ebp + 0Ch]的值加在eax上,这样就完成了两数的相加,之后再将eax的值赋给[ebp - 8]对应的空间。
这里将 [ebp - 8]的值给了eax 。(变量z在出了函数体后,变量就被释放了,为了保存下这个值,编译器将该值放入寄存器中,就可以保证这个值的存在)。
pop用于将栈顶元素弹出到寄存器中 。栈顶原本就是dei的值,弹出后放入edi,这时栈顶就是esi的值,弹出后放入esi, 对于ebx同理。
这两步骤很重要:
这时候栈顶ebp(main)【 这个数据其实就是之前main函数的ebp】
将栈顶数据弹出后再放入ebp其实就找回了这个位置。
ret指令用于将函数的返回地址压入栈中,以便在下一次调用时使用。简单的讲,该处就是会把call指令的下一条指令的地址弹出,并跳转到那里。
随着Add函数的调用完成,形参就可以释放了。
这里将esp+8后,栈帧空间内就不再管理那两个形参。
然后将eax的值赋值给[ebp - 20h] 。相当于c接收了返回值。
到此为止,其实一个函数栈帧的创建与销毁就完成了。
文章到这里就结束了,本小白才疏学浅,如果文章存在问题,还请大佬们多多指出。