关于函数的调用堆栈有如下几个问题:
1.形参开辟内存吗?由谁开辟?
2.形参的入栈顺序?
3.返回值如何带出?
4.被调用方结束后如何知道回退到调用方栈帧上?
5.函数调用完成如何知道执行下一行指令?
测试用例:
#include<stdio.h>
typedef struct Node{
int data[1];
}Node,*PNode;
Node fun(int a,int b)
{
Node tmp={0};
tmp.data[0]=a+b;
return tmp;
}
int main()
{
int a=5;
int b=6;
Node tmp=fun(a,b);
return 0;
}
上面代码的汇编代码:
首先 ebp是栈底指针寄存器(存放调用方栈底指针的地址)
esp是栈顶指针寄存器 (每次往栈里面压入一个东西,esp就会向上挪动一次,保证esp一直指的是栈顶)
解析:
00401060 push ebp 将调用main函数的mainCRTStartup函数的栈底的地址压栈
00401061 mov ebp,esp 将esp的值赋给ebp,就是将ebp所指向的内容改变为mainCRTStartup函数栈的顶部也是
main函数栈的底部
00401063 sub esp,4Ch 将esp的值减去16进制数的4Ch个,首先我们要知道内存中栈是由高地址向低地址使用, 那么减去4Ch就是向再去开辟4Ch大小的空间,esp指向esp-4Ch的地址
00401066 push ebx
00401067 push esi
00401068 push edi 分别压入了 ebx,esi和edi
00401069 lea edi,[ebp-4Ch] 将ebp-4Ch的地址放进edi中,即让edi指向ebp-4Ch的地址也就是esp指向的地址
0040106C mov ecx,13h 将16进制的13放进ecx中
00401071 mov eax,0CCCCCCCCh 将CCCCCCCC放入eax中
00401076 rep stos dword ptr [edi] 从edi指向的地址开始,拷贝eax值 ecx次,一次拷贝dword个字节
(d代表double,word代表两个字节),即一次拷贝4个字节
刚好把开辟的4Ch个空间置为CCCCCCCC
16: int a=5;
00401078 mov dword ptr [ebp-4],5 将5放进从ebp-4开始4个字节大小的地址中
17: int b=6;
0040107F mov dword ptr [ebp-8],6 将6放进从ebp-8开始4个字节大小的地址中
18: Node tmp=fun(a,b);
00401086 mov eax,dword ptr [ebp-8] 将从ebp-8开始4个字节大小的地址所存放的数据(即 b的值)放进eax中
00401089 push eax 将eax压栈
0040108A mov ecx,dword ptr [ebp-4] 将从ebp-4开始4个字节大小的地址所存放的数据(即 a的值)放进ecx中
0040108D push ecx 将ecx压栈
由此可知:
问题1:形参开辟内存,由调用方开辟
问题2: 形参的入栈顺序为从右至左
0040108E call @ILT+0(_fun) (00401005) call语句,也就是跳转语句,转去另一条指令
(_fun后面的遗传16进制数字 就是转至的位置)
call在被运行时,首先会将call指令的下一条指令的地址压入栈中
00401005 jmp (fun )00401020 指令已经跳转过来了,但是过来时候是一句jmp语句,
即跳转到后面的函数,也就是fun函数地址为(00401020)
进入fun()函数
00401020 push ebp 将调用fun函数的main函数的栈底的地址压栈
00401021 mov ebp,esp 将esp的值赋给ebp,就是将ebp所指向的内容改变为
main函数栈的顶部也是fun函数栈的底部
00401023 sub esp,44h
00401026 push ebx
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-44h]
0040102C mov ecx,11h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi] 这和main函数创建时的代码是一样的,目的是为函数开辟空间
和将所开辟的空间初始化为CCCCCCCC,让ebp从指向main函数 栈底改为指向fun函数的栈底
Node tmp={0};
00401038 mov dword ptr [ebp-4],0 将0放进从ebp-4开始4个字节大小的地址中
10: tmp.data[0]=a+b;
0040103F mov eax,dword ptr [ebp+8] 将从ebp+8开始4个字节大小的地址所存放的数据(即 形参a的值)放进 eax中
00401042 add eax,dword ptr [ebp+0Ch] 将从ebp+0C开始4个字节大小的地址所存放的数据(即 形参b的值) 与eax相加并将结果放入eax中
00401045 mov dword ptr [ebp-4],eax 将eax的值放到从ebp-4开始4个字节大小的地址(即 tmp的内存中)
11: return tmp;
00401048 mov eax,dword ptr [ebp-4] 将ebp-4开始4个字节大小的地址所存放的数据(即 tmp的值)
} 放到eax中
由此可知:
问题3:通过将返回值放入eax寄存器中将返回值带出 但这只适用于返回值大于0 并且小于等于4个字节之间。
当返回值大于四个字节并且小于等于8个字节时:
将结构体Node中data数组的长度改为2测试:
从图中可以看出当返回值大于四个字节并且小于等于8个字节时 :通过eax和edx两个寄存器将返回值带出
如果当返回值大于8个字节时:
将结构体Node中data数组的长度改为3测试:
main函数:
lea eax,[ebp-20h] 的含义是将ebp-20的地址赋给eax寄存器
push eax 将ebp-20的地址压栈
fun函数
上面代码用图描述:
从图中可以看出:
当返回值大于8个字节时,将调用方函数栈帧的一部分内存当做临时变量并将这个临时变量地址压栈,将返回值数据
存入此临时变量内存中,并将此临时变量的地址存入eax寄存器中,利用临时变量将返回值带出。
下面继续:
0040104B pop edi 将esp当前所指的内容赋给edi,再将其弹出,即esp向下移四个字节
0040104C pop esi 将esp当前所指的内容赋给esi,再将其弹出,即esp向下移四个字节
0040104D pop ebx 将esp当前所指的内容赋给ebx,再将其弹出,即esp向下移四个字节
0040104E mov esp,ebp 将ebp 的内容赋给esp,即让esp直接指向fun函数的栈底
00401050 pop ebp 当前ebp和esp指向的都是fun函数的栈底,所以如果再进行pop指令
弹出的就是刚才为fun函数开辟空间之前压入的main函数的栈底的地 址,所以现在这句指令正好就是将ebp重新指向main函数的栈底。
由此可知:
问题4:通过将调用方函数的栈底的地址压入被调用方函数的栈底实现被调用函数结束后回退到调用方函数栈帧上。
00401051 ret ret 这个指令的意思是再次弹出一个值并付给 PC寄存器
而在跳转到fun函数将main函数栈底地址压进fun函数栈底之前
在main函数中,将call指令的下一句指令地址压栈,
即此时出栈的数据为main函数中调用fun函数的下一行指令的地址
由此可知:
问题5:在将调用方函数栈底的地址压入被调用方函数栈底前先将调用方的call指令下一行指令的地址压栈实现函数调用完成后知道执行下一行指令的地址。
00401093 add esp,8 将esp向下移动8个字节,刚好把main中为形参a,b开辟的空间销毁
由此也知,系统销毁空间时只是将栈顶指针指向的位置进行修改
而并没有将使用过的内存中的数据进行清空。
00401096 mov dword ptr [ebp-0Ch],eax 将eax中的值放进从ebp-0C开始4个字节大小的地址中
(即 tmpd的内存中)