文章目录
前言
在前期的学习过程中,我们可能会有很多的困惑:
1️⃣ 局部变量是怎么创建的?
2️⃣ 为什么未初始化的局部变量的值是随机值?
3️⃣ 函数是如何传参的?以及传参的顺序是怎样的?
4️⃣ 形参和实参是什么关系?
5️⃣ 函数调用是怎么做的?
6️⃣ 函数调用结束后是怎么返回的?
⚠ 这里使用的环境是 Visual Studio 2013 ,提示不要使用太过高级的编译器,因为越高级的编译器越不容易观察。同时这里需要注意的是在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器。
一、 寄存器的概念
寄存器的功能是存储二进制代码,它是由具有存储功能的触发器组合起来构成的。一个触发器可以存储1位二进制代码,故存放n位二进制代码的寄存器,需用n个触发器来构成。
按照功能的不同,可将寄存器分为基本寄存器和移位寄存器两大类。基本寄存器只能并行送入数据,也只能并行输出。移位寄存器中的数据可以在移位脉冲作用下依次逐位右移或左移,数据既可以并行输入、并行输出,也可以串行输入、串行输出,还可以并行输入、串行输出,或串行输入、并行输出,十分灵活,用途也很广。
二、 通用寄存器的结构
通用寄存器组包括AX、BX、CX、DX4个16位寄存器,用以存放16位数据或地址。也可用作8位寄存器。用作8位寄存器时分别记为AH、AL、BH、BL、CH、CL、DH、DL,只能存放8位数据,不能存放地址
1️⃣ AX(AH、AL):累加器。有些指令约定以AX(或AL)为源或目的寄存器。输入/输出指令必须通过AX或AL实现。
2️⃣ BX(BH、BL):基址寄存器。BX可用作间接寻址的地址寄存器和基地址寄存器,BH、BL可用作8位通用数据寄存器。
3️⃣ CX(CH、CL):计数寄存器。CX在循环和串操作中充当计数器,指令执行后CX内容自动修改,因此称为计数寄存器。
4️⃣ DX(DH、DL):数据寄存器。除用作通用寄存器外,在I/O指令中可用作端口地址寄存器,乘除指令中用作辅助累加器。
三、 指针寄存器和变址寄存器
1️⃣ BP( Base Pointer Register):基址指针寄存器。
2️⃣ SP( Stack Pointer Register):堆栈指针寄存器。
3️⃣ SI( Source Index Register):源变址寄存器。
4️⃣ DI( Destination Index Register):目的变址寄存器。
这组寄存器存放的内容是某一段内地址偏移量,用来形成操作数地址,主要在堆栈操作和变址运算中使用。BP和SP寄存器称为指针寄存器,与SS联用,为访问现行堆栈段提供方便。通常BP寄存器在间接寻址中使用,操作数在堆栈段中,由SS段寄存器与BP组合形成操作数地址即BP中存放现行堆栈段中一个数据区的“基址”的偏移量,所以称BP寄存器为基址指针。
SP寄存器在堆栈操作中使用,PUSH和POP指令是从SP寄存器得到现行堆栈段的段内地址偏移量,所以称SP寄存器为堆栈指针,SP始终指向栈顶。
寄存器SI和DI称为变址寄存器,通常与DS一起使用,为访问现行数据段提供段内地址偏移量。在串指令中,其中源操作数的偏移量存放在SⅠ中,目的操作数的偏移量存放在DI中,SI和DI的作用不能互换,否则传送地址相反。在串指令中,SI、DI均为隐含寻址,此时,SI和DS联用,Dl和ES联用。
四、 EBP和ESP
这是单独把 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; }
不知道大家有没有好奇 Add 被 main 函数调用,而 main 函数被谁调用呢 ❔❓
1、调试代码后,打开调用堆栈
2、这时看到调用堆栈这个窗口 main 函数被调用了,问题也就出现了 —— main函数被谁调用了
3、当程序继续走时,它跳到了crtexe.c文件中,这时再看堆栈窗口发现 main 函数被 __tmainCRTStartup() 调用
4、而 __tmainCRTStartup() 又被 mainCRTStartup() 调用
解决完疑问后,这里就细看代码的底层是如何执行的 ❔❓
这里观察C语言代码所对应的汇编代码 —— 调试代码后,右击鼠标转到反汇编
1️⃣ 先为调用 main 函数的这个函数 __tmainCRTStartup 开辟函数栈帧,并用 esp 和 ebp 维护
2️⃣ 为 main 函数开辟函数栈帧
2.1、push ebp
压栈 ebp,esp 指向的位置也随之改变 (地址减小)
▶ 压栈前
▶ 压栈后
📐 验证
1.2、mov ebp, esp
同 ebp = esp
▶ 赋值前
▶ 赋值后
1.3、sub esp, 0E4h
同 esp = esp - 0E4h
▶ 没减前
▶ 减完后
1.4、push ebx
压栈后 ebx,esp指向的位置也随之改变 (地址减小)
▶ 压栈前
▶ 压栈后
📐 验证
1.5、push esi
压栈 esi,esp指向的位置也随之改变 (地址减小)
▶ 压栈前
▶ 压栈后
📐 验证
1.6、push edi
▶ 压栈前
▶ 压栈后
📐 验证
1.7、lea edi, [ebp - 0E4h]
load effecitve address,把 ebp - 0E4h 这个地址加载到 edi 里
▶ 加载前
▶ 加载后
1.8、mov ecx, 39h
mov eax, 0cccccccch
rep stos dword ptr es : [edi]
把 edi 这个位置开始向下的 39h 次 dword 数据全部改为0xcccccccc (word是2个字节,dword是4个字节)
📐 验证
3️⃣ 初始化 a、b、c 局部变量
3.1、mov dword ptr [ebp-8], 0Ah
把 0Ah(10) 放到 ebp-8 的位置
📐 验证
2.2、mov dword ptr [ebp-14h], 14h
把 14h(20) 放到 ebp-14h(ebp-20) 的位置
📐 验证
2.3、mov dword ptr [ebp-20h], 0
把 0 放到 ebp-20h(ebp-32) 的位置
📐 验证
通过以上这里就可以验证未初始化的局部变量是随机值
4️⃣ 调用 Add 函数
4.1、mov eax, dword ptr [ebp-14h]
把 ebp-14h 的值 20 放到 eax 里去
4.2、push eax
压栈 eax(20),esp指向的位置也随之改变 (地址减小)
▶ 压栈前
▶ 压栈后
4.3、mov ecx, dword ptr [ebp-8]
把 ebp-8 的值 10 放到 ecx 里去
4.4、push ecx
压栈 ecx(10),esp指向的位置也随之改变 (地址减小)
▶ 压栈前
▶ 压栈后
4.5、
00C2144B call 00C210E1
00C21450 … (这是下一条指令的地址)
call 指令调用 Add 函数,这里逐语句(F11)执行,发现这里竟然存储着下一条指令的地址,事实上 call 指令把下一条指令的地址压栈了(为了 Add 函数结束后能找回来)
▶ 压栈前
▶ 压栈前
4.6、进入 Add 函数前,会先为 Add 函数开辟函数栈帧,这里开辟的方式类似于上面 main,所以这里就不细谈了
4.7、mov dword ptr [ebp-8], 0
把 0 放到 ebp-8 的位置
4.8、mov eax, dword ptr [ebp+8]
把 ebp+8 的值 10 放到 eax 里
4.9、add eax, dowrd ptr [ebp+0ch]
把 ebp+0ch 的值 20 和 eax 的值 10 相加
4.9.1、mov dowrd ptr [ebp-8], eax
把 eax 的值 30 放到 ebp-8(z) 里去
4.9.2、mov eax, dword ptr [ebp-8]
把 ebp-8 的值 30 放到 eax 里去,这也就是为什么函数结束、局部变量销毁,却能把返回值带回来的原因
4.9.3、
pop edi
pop esi
pop ebx
把栈顶的数据 edi 依次弹出放到 edi 寄存器里去,每一次弹出,esp都向下加一次
▶ pop前
▶ pop edi
▶ pop esi
▶ pop ebx
4.9.4、mov esp, ebp
同 esp = ebp
▶ 赋值前
▶ 赋值后
4.9.5、pop ebp
这里弹出的是 main 函数的栈底,ebp 就找到了 main 函数的栈底,且 esp 往下加了一步
▶ pop前
▶ pop后
4.9.5、ret
ret 指令就是让栈顶弹出 call 指令的下一条指令的地址,esp 往下加
▶ ret 前
▶ ret 后
4.9.6、add esp, 8
此时 esp 指向的形参,让 esp + 8 后,形参销毁
▶ add 前
▶ add 后
4.9.7、mov dword ptr [ebp-20h], eax
把 eax 的值 30 放到 ebp-20h ( c ) 的位置
动画演示:
五、总结
1️⃣ 局部变量是怎么创建的?
首先为这个函数分配好栈帧空间,并初始化一部分空间为0xcccccccc,再为局部变量分配空间
2️⃣ 为什么未初始化的局部变量的值是随机值?
在开辟好栈帧空间后,会初始化 0xcccccccc 这样的随机值,而局部变量的初始化操作就会将随机值覆盖
3️⃣ 函数是如何传参的?以及传参的顺序是怎样的?
在调用函数前,会先将函数参数从后向前依次压栈,而进入函数后,它会通过指针的偏移量找到形参
4️⃣ 形参和实参是什么关系?
形参是在压栈时开辟的空间,实参和形参只是值相同,空间是独立的。所以形参是实参的一份临时拷贝,改变形参不会改变实参
5️⃣ 函数调用是怎么做的?
函数调用前,它会记住下一条指令的地址,这样做是为了函数结束后能回的来
6️⃣ 函数调用结束后是怎么返回的?
通过寄存器 eax 返回的,在返回前它会将计算好的值放在 eax 里