继上文对于函数栈帧的基础知识的讲解,本文我们将继续探究对于从汇编的角度详解函数栈帧的创建和销毁
入栈(压栈):先将栈顶指针向上移动四字节的大小空间,再将寄存器的数据放入那四字节空间。这里的向上移动是指向低地址处移动。
入栈指令:push a。
出栈:将栈顶指针向下移动四字节,这里的向下是往低地址处移动四个字节的空间。并将这四个字节的数据放入某个寄存器中。
出栈指令:pop a。
我们可以理解就像子弹的弹夹一样,怎么压子弹和怎么把子弹退出来,如下图所示
在这其中,对于这块空间使用了两个寄存器,ebp和esp,在上面的预备知识中我们讲到,ebp 记录的是栈底的地址, esp 记录的是栈顶的地址,并且栈中的地址都是由高地址向低地址延申的。
在调试中,打开调用堆栈,我们可以发现,add()函数被main()函数调用,而main 函数是由invoke_main 函数来调用的。在 invoke_main 函数之前的函数调用我们就暂时不考虑了
以下这个程序运行时的汇编代码,我将对列出来的相关汇编做一下详细的解释
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
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]
热知识:为什么出现这么多‘烫烫烫’,是因为在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
这里的创建栈帧与上面的main函数大同小异,我们直接将add函数语句内的语句执行
int sum = 0;
mov dword ptr [ebp-8],0
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中
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函数栈帧的栈底。
之后的指令就和上述讲的差不多了,这里就不细细讲解了
最后,通过本文的了解,我们就可以对很多问题有了一个答案
函数是如何调用的?
答:先传参,也就是把参数的值分别放在寄存器中,然后再push压入栈中;把主调函数ebp的值和下一条指令的地址push压入栈中,随后进入调用的函数中,创建函数栈帧并初始化,然后执行函数内的语句。
为什么局部变量若不初始化,内容是随机的?
答:函数栈帧创建后会自动将空间中存储的值全部初始化为一个特定值(如VS2019下为0xcccccccc),编译器不同值也不同。
函数调用时参数时如何传递的?传参的顺序是怎样的?
其实传参就是把参数push到栈帧空间中,传参时先压入的是后面参数的值,(参数,参数,…)从右往左压入。
函数的形参和实参分别是怎样实例化的?
形参通过寄存器的值压栈创建,而实参通过 ebp 存储的地址进行偏移,由编译器决定一块未使用空间创建。
形参和实参是什么关系?
形参是实参的临时拷贝,只是值相同却是不同的地址
局部变量是如何创建的?
函数栈帧创建后编译器分配由高到低地址创建变量
感谢观看,你的支持就是对我的最大鼓励,有什么问题欢迎在评论区指正,谢谢!