前言
💖作者:龟龟不断向前
✨简介:宁愿做一只不停跑的慢乌龟,也不想当一只三分钟热度的兔子。
👻专栏:C++初阶知识点👻工具分享:
如果觉得文章对你有帮助的话,还请点赞,关注,收藏支持博主🙊,如有不足还请指点,博主及时改正
我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。
那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问题都和函数栈帧有关系。
函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:
函数参数和函数返回值
临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)
保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。
局部变量是如何创建的?
为什么局部变量不初始化内容是随机的?
函数调用时参数时如何传递的?传参的顺序是怎样的?
函数的形参和实参分别是怎样实例化的?
函数的返回值是如何带会的?
寄存器:
eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数据
ebp:栈底寄存器
esp:栈顶寄存器
eip:指令寄存器,保存当前指令的下一条指令的地址
指令:
mov:数据转移指令
push:数据入栈,同时esp栈顶寄存器也要发生改变
pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub:减法命令
add:加法命令
call:函数调用,1. 压入返回地址 2. 转入目标函数
jump:通过修改eip,转入目标函数,进行调用
ret:恢复返回地址,压入eip,类似pop eip命令
这里不理解没有任何关系,我们在下面会逐句指令,进行相关解释。
以下都以该程序代码来讲解内容。环境:visual stdio2013
#include
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 3;
int b = 5;
int ret = 0;
ret = Add(a, b);
printf("%d\n", ret);
return 0;
}
大家都知道上述代码中,main
函数会调用Add
函数,那请问main函数会不会也是某个函数调用的呢?确实如此
按F10将程序调试起来,并且将代码跑到Add函数的内部,查看调用堆栈的窗口。
其中下面的函数先被调用,我们不难发现,Add
是被main
函数调用,并且main
函数是被__tmainCRTStartup
函数所调用的,而且__tmainCRTStartup
函数是被mainCRTStarup
函数调用的。这就是该程序函数调用的关系。
接下来咱们会从汇编语言的角度,带着同学们逐步分析函数调用的过程,以及解释一些对我们理解比较重要的指令和寄存器。
F10调式起来,进入反汇编。
esp
和ebp
是维护函数栈帧的两个寄存器,esp–存放栈顶指针,ebp
–存放栈底指针提示:栈是向低地址处生长的,这也是为什么压栈,越要-esp的值,因为在向低地址处生长。
00FD1410 push ebp //1.将ebp的内容压栈,esp-4
00FD1411 mov ebp,esp //2.将esp的内容赋值给ebp
00FD1413 sub esp,0E4h //3.0E4h--十六进制的0E--十进制的228,即esp-228
00FD1419 push ebx //ebx压栈
00FD141A push esi //esi压栈
00FD141B push edi //edi压栈
00FD141C lea edi,[ebp-0E4h]
00FD1422 mov ecx,39h
00FD1427 mov eax,0CCCCCCCCh
00FD142C rep stos dword ptr es:[edi]
//这四句指令的内容意思:
//将edi到ebp之间的内容全部修改成十六进制的CCCCCCCC,这也是烫烫烫烫的缘由
动画演示
int a = 3;
00FD142E mov dword ptr [ebp-8],3 //ebp-8的内容放上3 ,a
int b = 5;
00FD1435 mov dword ptr [ebp-14h],5 //ebp-20的内容放上5,b
int ret = 0;
00FD143C mov dword ptr [ebp-20h],0 //ebp-32的内容放上0,ret
动画演示
ret = Add(a, b);
//传参
00FD1443 mov eax,dword ptr [ebp-14h] //将ebp-20(b)的内容放进eax里面
00FD1446 push eax //将eax压栈
00FD1447 mov ecx,dword ptr [ebp-8] //将ebp-8(a)的内容放进ecx
00FD144A push ecx //将ecx压栈
//调用Add函数
00FD144B call 00FD10E1 //call指令
00FD1450 add esp,8 //call指令的下一句指令
call 指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。
动画演示
此时我们发现,函数传参的顺序是:从右到左进行传参
我们按F11,会跳转进下面的场景
_Add:
00FD10E1 jmp 00FD13C0 //转入Add函数,进行调用
此时我们再次按F11,即可跳进Add函数当中
int Add(int x, int y)
{
int Add(int x, int y)
{
00FD13C0 push ebp
00FD13C1 mov ebp,esp
00FD13C3 sub esp,0CCh
00FD13C9 push ebx
00FD13CA push esi
00FD13CB push edi
00FD13CC lea edi,[ebp-0CCh]
00FD13D2 mov ecx,33h
00FD13D7 mov eax,0CCCCCCCCh
00FD13DC rep stos dword ptr es:[edi]
//栈帧的建立是类似的,大家直接看动画演示
int z = 0;
00FD13DE mov dword ptr [ebp-8],0 //将ebp-8(z)的内容赋为0
z = x + y;
00FD13E5 mov eax,dword ptr [ebp+8] //将ebp+8的内容(a)放进eax里
00FD13E8 add eax,dword ptr [ebp+0Ch] //将ebp+12的内容(b)加进eax里面
00FD13EB mov dword ptr [ebp-8],eax //将eax的内容放进ebp-8(z)里面
return z;
00FD13EE mov eax,dword ptr [ebp-8] //将ebp-8的内容(z)放在eax里面
}
动画演示
通过上一步我们发现,在Add函数开始销毁之前,它将返回值先保存在了eax里面
00FD13F1 pop edi //edi出栈
00FD13F2 pop esi //esi出栈
00FD13F3 pop ebx //ebx出栈
00FD13F4 mov esp,ebp //将ebp的值赋值给esp
00FD13F6 pop ebp //ebp出栈
00FD13F7 ret
pop
:弹出栈顶的值存放到ebp,栈顶此时的值恰好就是main函数的ebp,esp+4,此时恢复了main函数的栈帧维护,esp指向main函数栈帧的栈顶,ebp指向了main函数栈帧的栈底。
ret
:ret指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是call指令下一条指令的地址,此时esp+4,然后直接跳转到call指令下一条指令的地址处,继续往下执行。
动画演示:
00FD1450 add esp,8
ret = Add(a, b);
00FD1453 mov dword ptr [ebp-20h],eax //将eax的内容放进ebp-32的内容(ret)里面
//以下的功能大家可以不用琢磨,不影响我们理解函数栈帧
printf("%d\n", ret);
00FD1456 mov esi,esp
00FD1458 mov eax,dword ptr [ebp-20h]
00FD145B push eax
00FD145C push 0FD5858h
00FD1461 call dword ptr ds:[00FD9114h]
00FD1467 add esp,8 //将两个函数参数出栈
00FD146A cmp esi,esp
00FD146C call 00FD113B
return 0;
00FD1471 xor eax,eax
}
注意:call指令的下一句指令的地址,其实早已经在上一步的ret
指令中就出栈了
同样也是与Add函数栈帧销毁类似
00FD1473 pop edi
00FD1474 pop esi
00FD1475 pop ebx
00FD1476 add esp,0E4h
00FD147C cmp ebp,esp
00FD147E call 00FD113B
00FD1483 mov esp,ebp
00FD1485 pop ebp
00FD1486 ret
动画演示
main函数和Add函数的调用过程结束。
希望大家看了这篇文章对函数的调用能有不一样的理解,咱们下期间!