前言
继上文对于函数栈帧的基础知识的讲解,本文我们将继续探究对于从汇编的角度详解函数栈帧的创建和销毁
一、详解函数栈帧的创建和销毁
1.入栈和出栈
入栈(压栈):先将栈顶指针向上移动四字节的大小空间,再将寄存器的数据放入那四字节空间。这里的向上移动是指向低地址处移动。
入栈指令:push a。
出栈:将栈顶指针向下移动四字节,这里的向下是往低地址处移动四个字节的空间。并将这四个字节的数据放入某个寄存器中。
出栈指令:pop a。
我们可以理解就像子弹的弹夹一样,怎么压子弹和怎么把子弹退出来,如下图所示
在这其中,对于这块空间使用了两个寄存器,ebp和esp,在上面的预备知识中我们讲到,ebp 记录的是栈底的地址, esp 记录的是栈顶的地址,并且栈中的地址都是由高地址向低地址延申的。
2.main()函数调用
在调试中,打开调用堆栈,我们可以发现,add()函数被main()函数调用,而main 函数是由invoke_main 函数来调用的。在 invoke_main 函数之前的函数调用我们就暂时不考虑了
3.细说汇编
以下这个程序运行时的汇编代码,我将对列出来的相关汇编做一下详细的解释
int add(int x, int y)
{
push ebp
mov ebp,esp
sub esp,0CCh
push ebx
push esi
push edi
lea edi,[ebp-0Ch]
mov ecx,3
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
mov ecx,0BBC003h
call 00BB131B
int sum = 0;
mov dword ptr [ebp-8],0
sum = x + y;
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]
mov dword ptr [ebp-8],eax
return sum;
mov eax,dword ptr [ebp-8]
}
pop edi
pop esi
pop ebx
add esp,0CCh
cmp ebp,esp
call 00BB1244
mov esp,ebp
pop ebp
ret
int main()
{
push ebp
mov ebp,esp
sub esp,0E4h
push ebx
push esi
push edi
lea edi,[ebp-24h]
mov ecx,9
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
mov ecx,0BBC003h
call 00BB131B
int a = 10;
mov dword ptr [ebp-8],0Ah
int b = 20;
mov dword ptr [ebp-14h],14h
int c = 0;
mov dword ptr [ebp-20h],0
add(a, b);
mov eax,dword ptr [ebp-14h]
push eax
mov ecx,dword ptr [ebp-8]
push ecx
call 00BB1023
add esp,8
printf("%d", c);
mov eax,dword ptr [ebp-20h]
push eax
push 0BB7B30h
call 00BB10D2
add esp,8
return 0;
xor eax,eax
}
pop edi
pop esi
pop ebx
add esp,0E4h
cmp ebp,esp
call 00BB1244
mov esp,ebp
pop ebp
ret
main函数的栈帧创建
-
- 将ebp压入栈,存放的是invoke_main函数栈帧的ebp
push ebp
-
- mov ebp,esp
mov ebp,esp
-
- sub会让esp中的地址减去一个16进制数字0xe4,产生新的esp,此时的esp是main函数栈帧的esp,此时结合上一条指令的ebp和当前的esp,ebp和esp之间维护了一个块栈空间,这块栈空间就是为main函数开辟的,就是main函数的栈帧空间,这一段空间中将存储main函数中的局部变量,临时数据,已经调试信息等。
sub esp,0E4h
-
- 将ebx esi edi 压入栈
push ebx
push esi
push edi
-
- 这四句话主要是将main函数的栈帧的每个字节都初始化为0xCC
lea edi,[ebp-24h]
mov ecx,9
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
热知识:为什么出现这么多‘烫烫烫’,是因为在main函数调用的时候栈区开辟的空间每个字节都被初始化为0XCC,且arr数组没有被初始化,又恰好在此空间上开辟,所以0xCCCC(两个连续排列的0xCC)的汉字编码就是“烫”,所以0xCCCC被当作文本就是“烫”。
-
- 以下是对局部变量的创建与初始化
int a = 10;//将10存储到ebp-8的地址处,ebp-8的位置存放的是a变量
mov dword ptr [ebp-8],0Ah
int b = 20; //将20存储到ebp-14h的地址处,ebp-14h的位置存放的是b变量
mov dword ptr [ebp-14h],14h
int c = 0;//将0存储到ebp-20h的地址处,ebp-20h的位置存放的是ret变量
mov dword ptr [ebp-20h],0
add函数创建并初始化栈帧
这里的创建栈帧与上面的main函数大同小异,我们直接将add函数语句内的语句执行
-
- 将0放在ebp-8的地址处,其实就是创建并初始化sum
int sum = 0;
mov dword ptr [ebp-8],0
- 2.将新的地址存到sum的地址中
sum = x + y;
mov eax,dword ptr [ebp+8]//将ebp+8地址处的数字存储到eax中
add eax,dword ptr [ebp+0Ch]//将ebp+12(即ebp+0Ch)地址处的数字加到eax寄存中
mov dword ptr [ebp-8],eax//将eax的结果保存到ebp-8的地址处,其实就是放到sum中
-
- 将处理的结果返回
return sum;
mov eax,dword ptr [ebp-8] //将ebp-8地址处的值放在eax中
add函数栈帧的销毁
pop edi
pop esi
pop ebx
add esp,0CCh
cmp ebp,esp
call 00BB1244
mov esp,ebp
pop ebp
ret
将edi、esi、ebx出栈
0CCh是Add函数栈帧的大小,所以esp向下移动到dbp的位置,之后pop ebp,由于栈顶指向的是main函数栈帧的栈底,因此出栈ebp指向main函数栈帧的栈底。
之后的指令就和上述讲的差不多了,这里就不细细讲解了
4. 总结
最后,通过本文的了解,我们就可以对很多问题有了一个答案
函数是如何调用的?
答:先传参,也就是把参数的值分别放在寄存器中,然后再push压入栈中;把主调函数ebp的值和下一条指令的地址push压入栈中,随后进入调用的函数中,创建函数栈帧并初始化,然后执行函数内的语句。
为什么局部变量若不初始化,内容是随机的?
答:函数栈帧创建后会自动将空间中存储的值全部初始化为一个特定值(如VS2019下为0xcccccccc),编译器不同值也不同。
函数调用时参数时如何传递的?传参的顺序是怎样的?
其实传参就是把参数push到栈帧空间中,传参时先压入的是后面参数的值,(参数,参数,…)从右往左压入。
函数的形参和实参分别是怎样实例化的?
形参通过寄存器的值压栈创建,而实参通过 ebp 存储的地址进行偏移,由编译器决定一块未使用空间创建。
形参和实参是什么关系?
形参是实参的临时拷贝,只是值相同却是不同的地址
局部变量是如何创建的?
函数栈帧创建后编译器分配由高到低地址创建变量
感谢观看,你的支持就是对我的最大鼓励,有什么问题欢迎在评论区指正,谢谢!