目录
1、阅读本文的价值
2、函数栈帧及栈的概念
3、部分寄存器及汇编指令
4、main函数的调用
5、main函数的栈帧创建
6、变量的栈帧创建
6、函数传参
7、函数内部运算及销毁
8、通过函数栈帧引发的思考
1、局部变量是如何创建的?
2、 为什么局部变量不初始化是随机的?
3、函数调用时参数时如何传递的?传参的顺序是怎样的?
4、 函数的形参和实参分别是怎样实例化的?
5、 函数的返回值是如何带回的?
话不多说先上图!
先从某招聘网站随便扒了两张岗位JD。由于博主在宁波,实体经济还是比较强的【外边真的全是嵌入式
那么面试官就会问了,小伙子你了解过汇编么?
我曾从汇编的角度描述过函数栈帧的创建与销毁!(本篇的意义在于了解汇编与深挖函数栈帧的创建与销毁,为后期理解栈区打好基础。)
函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:
1、函数参数和函数返回值
2、临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)
3、保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。
栈:用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入后出(First In Last Out, FILO)。在计算机系统中,栈则是一个具有以上属性的动态内存区域。压栈操作使得栈增大,而弹出操作使得栈减小。 在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。
eax (通用寄存器) |
通常用来执行加法,函数调用的返回值一般也放在这里面 |
|
ebx (通用寄存器) |
保留临时数据 |
|
esp (通用寄存器) |
栈顶寄存器,指向栈的顶部 |
|
ebp (通用寄存器) |
栈底寄存器,指向栈的底部,通常用ebp+偏移量的形式来定位函数存放在栈中的局部变量 |
|
eip (指令寄存器) |
最重要的寄存器,它指向了下一条要执行的指令所存放的地址 |
mov (通用数据传送指令) |
数据转移指令 |
|
push (通用数据传送指令) |
数据入栈,同时esp栈顶寄存器也要发生改变 |
|
pop (通用数据传送指令) |
数据弹出至指定位置,同时esp栈顶寄存器也要发生改变 |
|
sub (算术运算指令) |
减法 |
|
add (算术运算指令) |
加法 |
|
call (子程序调用指令) |
函数调用1. 压入返回地址 2. 转入目标函数 |
|
jump(无条件程序转移指令) |
通过修改eip,转入目标函数,进行调用 |
|
ret (子程序或函数返回指令) |
恢复返回地址,压入eip,类似pop eip命令 |
本次以加法函数为例:
#include
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()函数被static int __cdecl invoke_main()进行调用,从__cdecl中能看出C语言函数参数的压栈的顺序是从右向左压入栈中,例如C函数 Fun(a,b,c)函数调用时,参数压栈顺序为 c , b , a。
main()函数结束时的return 0,返回至int const main_result中。
int main()
{
003A18B0 push ebp//在栈中压入ebp的值
003A18B1 mov ebp,esp//把esp的值给ebp
003A18B3 sub esp,0E4h//把esp的值减去0E4h
003A18B9 push ebx//在栈中压入ebx的值
003A18BA push esi//在栈中压入esi的值
003A18BB push edi//在栈中压入edi的值
003A18BC lea edi,[ebp-24h]//把ebp-24h放入edi中
003A18BF mov ecx,9//把9的值给ecx
003A18C4 mov eax,0CCCCCCCCh//把0CCCCCCCCh放入eax
003A18C9 rep stos dword ptr es:[edi]//从edi开始向下ecx的区域放入eax
此处注意0CCCCCCCCh是从低地址向高地址创建的。
int a = 10;
003A18D5 mov dword ptr [ebp-8],0Ah//在ebp-8位置处放入0Ah,即a的值
int b = 20;
003A18DC mov dword ptr [ebp-14h],14h//在ebp-14h位置处放入14,即b的值
int c = 0;
003A18E3 mov dword ptr [ebp-20h],0//在ebp-20h位置处放入0,即c的值
c = Add(a, b);
003A18EA mov eax,dword ptr [ebp-14h]//把ebp-14h地址的值放入eax中
003A18ED push eax//压入eax
003A18EE mov ecx,dword ptr [ebp-8]//把ebp-8地址的值放入ecx中
003A18F1 push ecx//压入ecx
003A18F2 call 003A10B4//调用add函数,栈顶保存call指令的下一条指令
003A18F7 add esp,8//形参销毁
003A18FA mov dword ptr [ebp-20h],eax//形参销毁
注意传参时,形参是实参的一份临时拷贝,需要创建相同大小的空间用于传参,所以传值调用的效率往往不如传址调用。
int Add(int x, int y)
{
003A1770 push ebp
003A1771 mov ebp,esp
003A1773 sub esp,0CCh
003A1779 push ebx
003A177A push esi
003A177B push edi
003A177C lea edi,[ebp-0Ch]
003A177F mov ecx,3
003A1784 mov eax,0CCCCCCCCh
003A1789 rep stos dword ptr es:[edi]
003A178B mov ecx,3AC008h
003A1790 call 003A131B
int z = 0;
003A1795 mov dword ptr [ebp-8],0
z = x + y;
003A179C mov eax,dword ptr [ebp+8]
003A179F add eax,dword ptr [ebp+0Ch]
003A17A2 mov dword ptr [ebp-8],eax
return z;
003A17A5 mov eax,dword ptr [ebp-8]//把ebp-8的值放到eax寄存器中,让寄存器把结果带出函数
}
003A17A8 pop edi//弹出edi,同时esp地址增加
003A17A9 pop esi
003A17AA pop ebx
003A17AB add esp,0CCh
003A17B1 cmp ebp,esp
003A17B3 call 003A1244
003A17B8 mov esp,ebp
003A17BA pop ebp//通过ebp找回main的栈底
003A17BB ret
通过eax寄存器把函数计算结果带给c。
首先需要为函数分配栈帧空间,在函数的栈帧空间初始化完成后,再为局部变量分配空间。
因为在栈帧空间初始化的过程中,通过动图演示,可以看到栈帧空间的部分区域被初始化为0CCCCCCCCh,若局部变量不初始化,将会被赋值为这个值,这也是“烫烫烫”的成因。(全局变量不初始化为0)
函数调用时,会先在main函数内压入形参,通过从右向左的压栈方式向函数传递形参。当函数内部需要使用形参时,通过指针偏移量找到传参时生成的形参。
在函数调用时,形参才开辟空间,形参与实参的值相同,但所属的空间不同,改变形参不会改变实参。
在函数调用前,把call指令下一条指令的地址压入栈中,并且把上一个函数的ebp压入栈中,函数调用完毕,通过弹出ebp找到原始函数的栈底,同时使用压入栈中的地址找到下一条所要执行语句的地址。返回值是通过寄存器带出的。
关注!点赞!评论!收藏!关注!点赞!评论!收藏!关注!点赞!评论!收藏!关注!点赞!评论!收藏!关注!点赞!评论!收藏!