局部变量是怎么创建的?
在为main函数开辟栈帧空间时,在一定范围内初始化成0CCCCC,再把里面0CCCC的一些开辟空间给局部变量使用。
为什么局部变量的值是随机值?
因为我们在为main函数开辟栈帧空间时,会将一定范围内空间初始成0CCCCCC里面什么也没有,所以如果局部变量不给初始化,局部就会进入随意开辟栈帧空间,就是为随机值或者是烫烫烫。
函数是怎么传参的?传参的顺序是怎样的?
eax(b)和ecx(a)进行压栈,先传的b再传的a,从右向左。
形参和实参是什么关系?
形参是实参的一份临时拷贝,它们的值是相同的,但所使用的空间是不同的,所以形参的改变不影响实参,形参确实只是实参的一份临时拷贝。
函数调用是怎么做的?
下面我画图所解释的非常清楚了。(如果不明白的同学可以私信互相交流下)
函数调用是结束后怎么返回的?
由函数一步一步建立空间,再一步一步销毁空间返回
用保存call指令下一条指令地址与ebp-main函数的保存位置进行寄存器返回值
用寄存器eax返回最终的值。
知道和函数栈帧的创建和销毁就都会了,其实就是修炼了自己的内功,也能搞懂后期更多的知识。
进入正题
今天讲解使用的环境是VS2019
同时在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。
首先我们要了解什么是函数栈帧?
函数栈帧就是在函数调用过程中,程序为函数所开辟的栈空间,函数一般放在栈区。
而编译器为了方便动态内存管理,一般划分为了三个区域:栈区 堆区 静态区。
而什么又是栈呢?
栈的概念及结构
栈:一种特殊的线性表,其只允许在 固定的一端 进行 插入和删除 元素操作。 进行数据插入和删除 操作的一端称为 栈顶 ,另一端称为 栈底 。栈中的数据元素遵守 后进先出 LIFO (Last in First Out) 的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈(Push), 入数据在栈顶
出栈: 栈的删除操作叫做出栈(Pop)。 出数据也在栈顶 。
特点:栈只能在栈顶进行插入和删除。
为了更加清楚了解函数栈帧,我们还需要了解下以下寄存器
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\n", c);
return 0;
}
在调用main函数时ebp和esp会维护main函数的函数栈帧。
当我按F10进入调试,并且打开调用堆栈
我们可以看见函数的调用关系,调用堆栈是反应函数的调用关系的
由调试我们可以看见main函数是被invoke_main()函数调用的
而Add函数是被main()函数调用的
那么理所当然的invoke_main函数与Add函数也是有自己的函数栈帧空间的,并且由ebp和esp来维护函数栈帧空间。
F10进入调试点击右键转到反汇编
如下图所示是此次代码的反汇编指令
但我们为了方便更加清晰的观察,再次点右键取消显示符号名。
main函数第一条反汇编指令是Push(压栈) ebp。
如果是ebp压栈那么ebp的值是会变小的 因为是由高地址到低地址
这是原来的值
当我push以后的值,果然变小了
所以是真的压进去了吗?我们可以通过内存来查看
当我们查看esp内存的地址时,ebp确实压进去了,因为esp内存的值是ebp的地址
fc f9 f3 00 VS编译器一般是:小端字节存储 低位字节数据放低地址处 高位字节数据放高地址处
mov的意思是把esp的值给ebp 我们依然可以通常调试来查看是不是这样的
sub是subtraction减法的缩写。就是给esp减去0E4h
用16进制显示就是228
当我们给esp减去所对应的值时,那么esp不能再指向原来的位置,而是指向了上一块的某块区域
当我们查看内存ebp和esp
ebp
esp
这些内存空间都是为main函数所开辟的空间
接下来是在栈顶压3个元素
随着压栈esp也会随着压栈指向位置会发生变化
VS2019栈区内存存放习惯:先放高地址,再放低地址
下面esp的值会随着压栈 esp位置也产生了变化
lea指令是=load effective address. 意思是加载有效地址
这个指令有效果的其实是rep stos意思是把edi开始下面将内存空间改成OCCCCCCCCh 以双倍字节开辟 dword=double word es:[edi]把edi开始下面所有空间以双字节开辟成0CCCCCCCCh。
edi到ebp开辟内存空间
esp-24h的值如下图
将十进制数0Ah放进ebp-8 就是相当于把10放到了ebp-8里面
如果不给a初始值那么就是随机值,因此在为main函数开辟空间时使用的就是CCCCCC的值,所以会出现烫烫烫(字符串 字符)或者随机值(变量)。
0a 00 00 00 就是10的十六进制存储 内存存储一般是十六进制存储
又是隔着两个字节存放的C的值0 (不同编译器存放位置不同,取决于编译器)
把20的值给eax再把eax压栈压进去
下一步指令把10给ecx然后再把ecx进行压栈
按F11进入call令
内存中存放的是下一条add指令的地址00171987
当我们再按一次F11会跳到Add函数的反汇编指令当中去
这和main函数的反汇编极其相似,这是在为Add函数开辟函数栈帧空间
第一步Push压栈 第二步mov esp给ebp 第三步把0cch 给esp 相当于esp又往上走了
再进行压栈
随着压栈esp的值也变化 esp的指向位置也随着变化
把ebp-0ch值给edi 把3给ecx 然后从edi开始下面所有位置改成0CCCCCCCH
把0放到ebp-8
ebp+0ch相当于+12 epb+12
通过调试过程看见传参是从右向左,先传的b再传的a
最后返回的时候把ebp-8的值也就是z的值给了寄存器eax
下面指令pop三次 esp也随着产生位置变化
当pop了三次esp的值 增加了3次
当pop三次要返回main函数 那么Add创建的函数栈帧就要销毁 这几个指令完成了把esp的值给ebp
再pop ebp 我们这个位置所保存main函数的ebp-main 就是为了函数返回时找到main函数的ebp
返回以后ebp和esp又开始维护了main函数的函数栈帧空间
这条指令就是为了执行call指令下一条add的功能 弹出了add指针地址
所以我们在开辟函数栈帧时保存了add指令地址
当我们再按F10就回到了main函数Add的位置
main函数add指令 00171987与我们刚才在函数栈帧保存的call指令下一条指令的地址一模一样,也就是add指令函数的地址。就是为了方便回来,简直就是荣归故里!设计的太牛了!
给esp+8
随着esp+8形参的空间也销毁了。
把eax 30的值给ebp-20h 就是刚才c的位置
然后在程序结束时寄存器会返回所对应的值。