在看文章之前来了解两个指针:
ebp:指向函数栈底的指针
esp:指向函数栈顶栈顶指针
#include<stdio.h>
int sum(int a, int b)
{
int temp = 0;
temp = a + b;
return temp;
}
int main()
{
int a = 10;
int b = 20;
int ret = 0;
ret = sum(a, b);
printf("ret = %d\n",ret);
return 0;
}
一、函数堆栈的建立过程
接下我给程序打个断点,看一下执行时的部分汇编代码是什么样的:
1.main函数第一个大括号到函数第一条语句之间的汇编代码:
17: int main()
18: {
00151410 push ebp
00151411 mov ebp,esp
00151413 sub esp,0E4h
00151419 push ebx
0015141A push esi
0015141B push edi
0015141C lea edi,[ebp-0E4h]
00151422 mov ecx,39h
00151427 mov eax,0CCCCCCCCh
0015142C rep stos dword ptr es:[edi]
19: int a = 10;
##############################################################################
这一段汇编程序是干什么的?
main函数也是一个函数,它的执行是被其他函数调用的
这段汇编代码实际上是给main函数的执行开辟栈帧
00151413 sub esp,0E4h 指定了main函数的栈帧大小 大小为0E4h
00151427 mov eax,0CCCCCCCCh
0015142C rep stos dword ptr es:[edi]
循环的对main函数的栈帧进行初始化,初始值为0CCCCCCCCh
这就是我们打印没有初始化的变量看到的”烫烫烫“
2.再来看一下汇编层面上,局部变量是怎么被cpu拿到的:
19: int a = 10;
0015142E mov dword ptr [ebp - 4],0Ah
20: int b = 20;
00151435 mov dword ptr [ebp - 8],14h
21: int ret = 0;
0015143C mov dword ptr [ebp - 0Ch],0
########################################################
我们可以看到对a、b、ret等局部变量的时候不是在内存中取。
而是通过ebp指针的偏移来取的,不是操作 a b ret 等名字,因为他们都是指令
他们不会产生符号,保存在.text段
3.sum函数被调用时的汇编代码
//这里展示的是压实参的过程
22: ret = sum(a, b);
00151443 mov eax,dword ptr [ebp -8]
00151446 push eax
00151447 mov ecx,dword ptr [ebp -4]
0015144A push ecx
0015144B call sum (015105Fh)
00151450 add esp,8
00151453 mov dword ptr [ebp - 0Ch],eax
#################################################
着重看一下这两行代码:
下面两行汇编之前的汇编代码是分别将形参a b入栈
0015144B call sum (015105Fh)
00151450 add esp,8
call指令做了两件事情:
1.跳入sum函数
2.在进入sum函数之前,把call指令的下一行指令的地址入栈,即00151450 add esp,8
这样能够保证sum函数执行完以后,cpu知道自己该干嘛,pc寄存器将存储add esp,8 这一行的地址
4.进入sum函数大括号和第一条指令之间也有一大堆的汇编代码:
这堆汇编是干什么的?
10: int sum(int a, int b)
11: {
001513C0 push ebp
001513C1 mov ebp,esp
001513C3 sub esp,0CCh
001513C9 push ebx
001513CA push esi
001513CB push edi
001513CC lea edi,[ebp-0CCh]
001513D2 mov ecx,33h
001513D7 mov eax,0CCCCCCCCh
001513DC rep stos dword ptr es:[edi]
12: int temp = 0;
####################################
这里的汇编是给sum函数的执行开辟栈帧,通过移动esp和ebp
001513C3 sub esp,0CCh 这行代码说明了sum栈帧的大小,移动了0CCH字节
001513D7 mov eax,0CCCCCCCCh
001513DC rep stos dword ptr es:[edi]
循环的对main函数的栈帧进行初始化,初始值为0CCCCCCCCh
这就是我们打印没有初始化的变量看到的”烫烫烫“
扩展:对于没有初始化堆上的内存,我们看到是”屯屯屯…”
5.sum函数的返回值如何带出去的呢:
14: return temp;
001513EE mov eax,dword ptr [temp]
由于sum函数类型为int,返回值== 4B,我们可以看到汇编上是通过寄存器将temp带回去的
6.sum函数的栈帧回退时,对使用过的内存进行处理了吗?
15: }
001513F1 pop edi
001513F2 pop esi
001513F3 pop ebx
001513F4 mov esp,ebp
001513F6 pop ebp
001513F7 ret
####################################################
实际上,我们能看到,sum函数在回退栈帧的时候,并没有对这块内存进行清零啊之类的操作
仅仅是esp回退而已
所以,有时我们尝试试图用非正常的去访问无效的内存可以看到有效的值,就不觉得奇怪了。
001513F6 pop ebp 通过该操作就知道主调函数的栈底地址,因为函数调用前call就把主调函数的栈底地址push进了被调函数的栈帧中。
二、函数堆栈的回退过程:
整个过程如上。
二、函数的返回值传递方式:
在函数开辟堆栈时,我们根据函数返回值的类型的大小,判断是否要产生临时量来将返回值带出去
<= 4B:一个寄存器带回
8B=< > =4B:通过两个寄存器带回
.>8B:通过产生临时量的方式处理返回值
大于8个字节时处理返回值的过程:返回值太大,寄存器无法带回去。
整个过程如下:
1.函数调用前根据函数的返回值类型,确定函数返回值的大小
2.提前在主调函数的栈帧上开辟一块内存,然后调用函数的时候将这块内存的地址压栈进去,等到所以的实参入栈以后就将这块内存的地址入栈
3.被调函数产生的数据将根据临时量的内存的地址直接拷贝回主调函数
注意:
只要不是内置类型,只要是自定义的类型,返回值的时候都会产生临时量。
三、函数的调用约定
_cdecl:c调用约定,默认是该约定
_stdcall:windows标准的调用约定
_fastcall:快速调用约定
_thiscall:c++的成员函数的调用约定
1.函数产生的符号名字不同
2.函数参数的入栈顺序不同
3.谁来清理形参的内存
解答:
_cdecl:调用方开辟形参内存,调用方自己清理形参内存
_stdcall:调用方开辟形参内存,被调方自己释放形参内存
_fastcall:调用方开辟形参内存,但是把最后8个字节的实参通过寄存器带到被调用函数,被调方自己释放形参内存
int _cdecl sum(int a, int b)
{
//...
}
int _fastcall sum(int a, int b)
{
//...
}
int _stdcall sum(int a, int b)
{
//...
}