这篇博客,我们来了解一下函数是如何传参?,函数返回值 如何返回?,以及他们的栈帧的开辟。
首先我们先来看一个简单的函数调用。
int fun(int a,int b)
{
return a+b;
}
int main()
{
int a = fun(10,20);
return 0;
}
将汇编代码执行到函数调用的地方查看函数调用参数带入的指令。
int main()
{
00FD1780 push ebp
00FD1781 mov ebp,esp
00FD1783 sub esp,0CCh
00FD1789 push ebx
00FD178A push esi
00FD178B push edi
00FD178C lea edi,[ebp-0CCh]
00FD1792 mov ecx,33h
00FD1797 mov eax,0CCCCCCCCh
00FD179C rep stos dword ptr es:[edi]
int a = fun(10, 20);
00FD179E push 14h //从左向右
00FD17A0 push 0Ah
00FD17A2 call fun (0FD1168h)
00FD17A7 add esp,8 //向上偏移8个字节
00FD17AA mov dword ptr [a],eax //将a的值放入到eax寄存器中
return 0;
00FD17AD xor eax,eax
}
如果我们传入的参数大小为一个字节时
struct Tmp
{
char a;//大小为一个字节
}
int fun1(struct Tmp a,struct Tmp b)
{
return 0;
}
int main()
{
struct Tmp tmp1,tmp2;
tmp1.a = 10;
tmp2.a = 20;
int a = fun1(tmp1,tmp2);
return 0;
}
汇编代码:
int a = fun(tmp1, tmp2);
01053BF6 movzx eax,byte ptr [tmp2] //将tmp2的值给eax寄存器
01053BFA push eax //将eax压入栈中
01053BFB movzx ecx,byte ptr [tmp1] //将tmp1的值给ecx寄存器
01053BFF push ecx //压栈
01053C00 call fun (01051370h)
01053C05 add esp,8 //esp向上抬升8字节
01053C08 mov dword ptr [a],eax //将a的值压入到 eax寄存器中
return 0;
我们发现它是通过push ---eax和ecx寄存器进行参数的传入。
参数大小为 8 字节
struct Tmp
{
int a;
int b;
};//大小为8个字节
int fun1(struct Tmp a,struct Tmp b)
{
return 0;
}
int main()
{
struct Tmp tmp1,tmp2;
tmp1.a = 10;
tmp1.b = 20;
tmp2.a = 15
tmp2.b = 25;
int a = fun1(tmp1,tmp2);
return 0;
}
看一下8个字节大小的参数,是如何传参的。仍然是通过寄存器push。
int a = fun(tmp1, tmp2);
000E410A mov eax,dword ptr [ebp-18h]
000E410D push eax
000E410E mov ecx,dword ptr [tmp2]
000E4111 push ecx
000E4112 mov edx,dword ptr [ebp-8]
000E4115 push edx
000E4116 mov eax,dword ptr [tmp1]
000E4119 push eax
000E411A call fun (0E1370h)
000E411F add esp,10h
000E4122 mov dword ptr [a],eax
return 0;
//仍然是通过寄存器进行传参的。
参数的大小>8字节
struct Tmp
{
int a;
int b;
int c;
};//大小为12个字节,大于8个。
汇编代码:
int a = fun(tmp1, tmp2);
002D4112 sub esp,0Ch //现将栈顶寄存器向上抬了 0Ch的大小。esp减12
002D4115 mov eax,esp //将esp的值给eax寄存器
002D4117 mov ecx,dword ptr [tmp2] //将tmp.a的值写入ecx寄存器
002D411A mov dword ptr [eax],ecx // 将ecx寄存器的值写入到eax寄存器中
002D411C mov edx,dword ptr [ebp-20h] //将tmp.b的值写入edx寄存器中
002D411F mov dword ptr [eax+4],edx //将edx寄存器的值写入eax寄存器中
002D4122 mov ecx,dword ptr [ebp-1Ch] //将tmp.c的值写入ecx寄存器中
002D4125 mov dword ptr [eax+8],ecx //将ecx寄存器中的值写入eax寄存器中
002D4128 sub esp,0Ch
002D412B mov edx,esp
002D412D mov eax,dword ptr [tmp1]
002D4130 mov dword ptr [edx],eax
002D4132 mov ecx,dword ptr [ebp-0Ch]
002D4135 mov dword ptr [edx+4],ecx
002D4138 mov eax,dword ptr [ebp-8]
002D413B mov dword ptr [edx+8],eax
002D413E call fun (02D1370h)
002D4143 add esp,18h
002D4146 mov dword ptr [a],eax
return 0;
从上面的汇编代码可以看出,在函数参数为12字节的时候,其参数带入方式和小于以及等于8字节的时候不同,这里没有明显的push函数,而是现在main函数的栈顶向上移动12字节,然后将参数的数据拷贝到main函数栈顶开辟的内存中。其方式如图3
二:函数返回值返回
1.对于小于等于8字节的参数:其返回至是利用寄存器带回,然后将寄存器的值写入到接受返回值的变量中。
2.从上面的汇编代码中来看,当函数的返回值为12个字节的时间,还在参数入栈的最后入栈了一个寄存器该寄存器中储存main函数上的一块内存。
而在返回值的时候,先从ebp - 8位置取值,取出的正好是参数入栈之后入栈的一块main函数的地址,然后将返回的数据写入到该块内存上。
由此可见,当返回值大于8字节的时候是预先在调用方的栈帧上预留一块内存,作为函数返回值存储的位置,最后返回值的时候,将返回值的数据写入到该段内存。大致如图6
函数栈帧的回退
函数栈帧的回退分为两步,一步是函数栈帧的回退,另一步是函数参数的清楚。
三种调用的约定
①_cdecl
C/C++默认的调用约定,其调用方式如上面所示
②_stdcall
该调用约定和_cdecl相似,唯一区别在于,函数参数的清除是由被调用方直接完成,而非调用方完成。
③_fastcall
是快速调用约定,在参数入栈的时候,最后入栈的独立四字节或者8字节利用寄存器带入,其他的参数在当前栈顶开辟空间写入,寄存器带入的参数在被调用方栈帧开辟之后直接写入到其栈帧上。
栈帧回退清除函数的时候,在被调用方栈帧上的参数不用清楚,会跟随栈帧回退被清除,其他参数由被调用方自己清除