我们学习函数的时候总会有诸多疑惑:
- 局部变量是怎么创建的?
- 为什么局部变量不初始化的值是随机值?
- 函数是怎么传参的?传参的顺序是怎样的?
- 形参和实参是什么关系?
- 函数调用是怎么做的?
- 函数调用结束后是怎么返回的?
接下来来带大家解答疑惑:
使用的编译环境是VS2013,不同的环境会有差异
寄存器:
eax
ebx
ecx
edx
ebp
esp
函数栈帧由这两个寄存器维护
ebp 栈底指针
esp 栈顶指针
这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的
注意:
每一个函数调用,都要在栈区创建一个空间
栈区的习惯是先使用高地址再使用低地址,由高指向低
是往上使用的
压栈 从栈顶放入一个元素 push
出栈 从栈顶删除一个元素 pop
main函数也是被其他函数调用的
__tmainCRTStarup调用
mainCRTStarup
也为这两个函数调用空间
边调用边分配栈帧
具体研究如何调用
F10 右击鼠标反汇编 就可以看到C语言的汇编代码
去掉显示符号名,就只显示地址,好观察 (右击鼠标)
接下来,我们开始解读,反汇编代码:
在调用main函数之前,是先调用__tmainCRTStarup函数的
有ebp和esp来维护,所以首先会先开辟一块空间给__tmainCRTStarup函数
#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函数的,所以先看main函数的解析
将代码加粗标红
int Add(int x, int y)
{
003913C0 push ebp
压栈来自main函数的ebp
esp往上走
003913C1 mov ebp,esp
赋值 ebp指向ebp
003913C3 sub esp,0CCh
esp往上走
为Add函数分配栈帧
003913C9 push ebx
003913CA push esi
003913CB push edi
三次压栈 esp跟着往上走
003913CC lea edi,[ebp+FFFFFF34h]
003913D2 mov ecx,33h
003913D7 mov eax,0CCCCCCCCh
003913DC rep stos dword ptr es:[edi]
初始化 cc cc cc cc
开始执行计算任务
int z = 0;
003913DE mov dword ptr [ebp-8],0
z = x + y;
003913E5 mov eax,dword ptr [ebp+8]
将[ebp+8]的的地址值的值放入eax中,[ebp+8]就是刚才的值10 ecx
003913E8 add eax,dword ptr [ebp+0Ch]
把[ebp+0Ch(12)] 中的值(20)加到eax里面去
所以此时eax中存放的值是30
003913EB mov dword ptr [ebp-8],eax
把eax中的值放到 [ebp-8]这个位置
此时会发现形参根本不是在Add内部创建的,而是回到找当初调用时候压栈进去的空间a和b
所以说,形参是实参的一份临时拷贝
之前都是在说函数的调用,现在开始返回:
return z;
003913EE mov eax,dword ptr [ebp-8]
把[ebp-8] 里面的值 30 放到eax中去
为什么要这么做?
因为程序接着走之后,出Add函数z的值(局部变量)就跟着销毁了,所以要把它的值放到eax中
eax是一个寄存器,寄存器是不会随程序的退出而销毁的
}
003913F1 pop edi
003913F2 pop esi
003913F3 pop ebx
弹出,每次弹出esp都会++一次,往下走
003913F4 mov esp,ebp
回收空间,把ebp的值赋给esp,esp就会往下走
003913F6 pop ebp
弹出的是ebp—main
意味着ebp往下走找到原先的ebp
此时就会回到main函数里
003913F7 ret
跳到原来main函数里保存的call指令的地址的下一条指令add上去
int main()
{
先push(压栈)
首先push ebp
esp地址值变小了代表着(图中)往上走了,因为是从高到低所以地址值会变小
mov 赋值
把esp的值给ebp 往上走
sub 减法
给esp-0E4h(八进制数 228)
意味着esp地址值变小了,就往上走指向上面的某一区域
此时esp与ebp都变了,就相当于进入了main函数,给main函数预开辟了一块空间,这块空间很大(编译器决定)
接下来就要使用这块空间
接下来压栈三次,开辟的空间在main函数空间之上
push ebx
push esi
push edi
此时esp已经指向edi,ebp还是指向ebp
lea edi ,[ebp-0E4h]
load effective address 指向有效地址
使ebp指向ebx 往上走,减法都是往上走
mov move移动
mov ecx,39h ecx是次数
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
把从edi开始向下的39h空间dword(doubleword 双字 四个字节)全部改成0CCCCCCCCh
这个动作意味着把内存中所有的值全部初始化成 cc cc cc cc
int a = 10;
mov dword ptr [ebp-8],0Ah
把0Ah(10)这个八进制数放到[ebp-8]的位置
说明为a开辟了ebp到[ebp-8]的一块空间
如果定义一个变量,初始化的时候不给赋值,此事内存中放入的就是cc cc cc cc(随机值)
所以这就是有时候我们会打印出烫烫烫烫烫烫这类乱码的原因
放置的位置取决于编译器
int b = 20;
mov dword ptr [ebp-14h],14h
int c = 0;
mov dword ptr [ebp-20h],0
当abc创建好之后,开始调用Add函数
c = Add(a, b);
mov eax,dword ptr [ebp-14h]
把[ebp-14h] 里面的值(20 b)放到eax里去
push eax
压栈20
注意:每一次压栈push都是压到栈顶,如此刻eax就是栈顶
mov ecx,dword ptr [ebp-8]
把 [ebp-8] 里面的值(10 a)放到ecx里去
push ecx
压栈10
刚刚这两个动作是在传参,实参
调用的时候就已经开始传参了,参数从右向左传
0039144B call 003910E1
调用
注意:此时压栈的是call指令的下一条指令add的地址00391450
原因:记住下一条指令的地址,因为此时需要跳出main函数进入Add函数,所以记住地址方便下一次跳回之后接着往下继续走
此时程序会跳往Add函数里面去,就不执行下面的了,此时才算真正来到Add函数里面
跳到这里(不仅要走出去,还要回得来)
00391450 add esp,8
esp+8 往下走 指到edi
mov dword ptr [ebp-20h],eax
把eax中的值30,放到 [ebp-20h] c 中去
printf("%d\n", c);
00391456 mov esi,esp
00391458 mov eax,dword ptr [ebp-20h]
0039145B push eax
0039145C push 395858h
00391461 call dword ptr ds:[00399114h]
00391467 add esp,8
0039146A cmp esi,esp
0039146C call 0039113B
return 0;
00391471 xor eax,eax
}
结合图一起看比较清晰
注意:寄存器是集成到CPU上的
计算机的硬盘、内存、寄存器都是独立存在的