记录函数调用的细节,深入汇编层面的。只分析windows平台下,VS C++编译器的实现。
32位系统下
普通函数调用
int Add1(int a, int b)
{
return a + b;
}
int main()
{
int sum = Add1(1, 2);
return 0;
}
生成的汇编代码为:
int sum = Add1(1, 2);
00041D6A push 2
00041D6C push 1
00041D6E call Add1 (04149Ch)
00041D73 add esp,8
00041D76 mov dword ptr [sum],eax
从中观察到的现象是:
- 函数调用使用栈传递参数,从右向左入栈;
- 函数返回值通过eax寄存器返回(如果返回值的大小不超过32位,4字节);
- 调用者负责清理配平栈,将参数占用的空间出栈;
add esp, 8
延伸一个问题:
三种函数调用约定
__cdecl: C/C++默认方式,参数从右向左入栈,主调函数负责栈平衡。
__stdcall: windows API默认方式,参数从右向左入栈,被调函数负责栈平衡。
__fastcall: 快速调用方式。所谓快速,这种方式选择将参数优先从寄存器传入(ECX和EDX),剩下的参数再从右向左从栈传入。因为栈是位于内存的区域,而寄存器位于CPU内,故存取方式快于内存,故其名曰“__fastcall”。
参考这里:(仅适用于32位系统下)
https://www.cnblogs.com/-qing-/p/10674223.html
这个简单的Add1函数使用的就是__cdecl调用约定。
由Add1的调用者main函数负责栈平衡。
类成员函数调用
大部分情况下使用的__stdcall调用约定,即我们会在函数的最后看到ret 指令后面跟了一个数字,这个即是通过栈传递的参数的尺寸,不包括this指针,this指针通过ecx或rcx传递。
当参数长度可变的时候使用的__cdecl约定,待验证
64位系统下
普通函数调用
int Add(int i, int a, int b, int c= 4, int d=5, int e = 6, int f= 7, int g = 8)
{
return a + b * c + i;
}
int Add1(int a, int b)
{
return a + b;
}
int main()
{
int sum2 = Add(1, 2, 3);
int sum = Add1(1, 2);
return 0;
}
main函数的汇编代码
int main()
{
00007FF624101810 push rbp
00007FF624101812 push rdi
00007FF624101813 sub rsp,148h
00007FF62410181A lea rbp,[rsp+40h]
00007FF62410181F mov rdi,rsp
00007FF624101822 mov ecx,52h
00007FF624101827 mov eax,0CCCCCCCCh
00007FF62410182C rep stos dword ptr [rdi]
00007FF62410182E lea rcx,[__AA539275_main@cpp (07FF624111002h)]
00007FF624101835 call __CheckForDebuggerJustMyCode (07FF624101082h)
int sum2 = Add(1, 2, 3);
00007FF62410183A mov dword ptr [rsp+38h],8
00007FF624101842 mov dword ptr [rsp+30h],7
00007FF62410184A mov dword ptr [rsp+28h],6
00007FF624101852 mov dword ptr [rsp+20h],5
00007FF62410185A mov r9d,4
00007FF624101860 mov r8d,3
00007FF624101866 mov edx,2
00007FF62410186B mov ecx,1
00007FF624101870 call Add (07FF62410101Eh)
00007FF624101875 mov dword ptr [sum2],eax
int sum = Add1(1, 2);
00007FF624101878 mov edx,2
00007FF62410187D mov ecx,1
00007FF624101882 call Add1 (07FF62410123Fh)
00007FF624101887 mov dword ptr [sum],eax
return 0;
00007FF62410188A xor eax,eax
}
00007FF62410188C lea rsp,[rbp+108h]
00007FF624101893 pop rdi
00007FF624101894 pop rbp
00007FF624101895 ret
详细分析main函数的汇编代码:
rbp入栈 --具体操作伪码rsp -= 8, *rsp = rbp,从栈中申请8字节,rbp的值存入栈顶,将rsp指向栈顶
rdi入栈 --同上
从栈中分配148h字节,16*16 + 4*16+8=328字节
rbp = rsp + 40h --这里的40h = 64字节,即为Add调用使用的栈空间
将申请的栈空间全部初始化为0CCCCCCCCh --中断指令
忽略如下两条指令,vs添加的调试指令
00007FF62410182E lea rcx,[__AA539275_main@cpp (07FF624111002h)]
00007FF624101835 call __CheckForDebuggerJustMyCode (07FF624101082h)
--Add函数调用
将四个参数放入申请的栈空间中; --注意这里都是使用的8字节对齐,同时注意具体的偏移,参数存储的位置是申请栈空间的后32个字节 = 4个8字节
前4个参数使用寄存器传递;
调用Add函数; --返回值在eax中
将返回值拷贝到sum2中
--Add1函数调用
2存入edx
1存入ecx
调用Add1
返回值存入sum
eax=0
清理申请的栈空间,lea rsp,[rbp+108h] --之前rbp指向的位置为rsp + 40h, 所以这里总共清理的栈空间为148h字节,跟代码第3行,申请的空间大小是一样的。
rdi出栈
rbp出栈 --申请的所有栈空间全部还给了系统
函数返回
这里我定义了两个函数
Add: 有8个参数,一个返回值
Add1:有2个参数,一个返回值
看到的现象是:
调用Add时,参数从右向左入栈,后4个参数使用栈传递,前4个参数使用寄存器传递;
Add1参数都是用寄存器传递,返回值都用eax返回,因为返回值是4字节,如果是8字节,就是使用rax寄存器。
(这也是64位系统的进步,有更多更大的寄存器可供使用,函数调用优先使用寄存器传递,效率更高,此处仅代表参数传递效率更高,不代表64位程序就一定比32位快,还有其他因素,比如地址长度,指令长度问题等。)
在Add函数的汇编代码中没有看到ret指令后添加数字,说明是被调用者调整栈。
我在Add函数掉用的后面,紧接着Add1函数调用。本来是期望看到
add rsp, 0x40h
由调用者清理栈。
结果看到的是Add1的函数调用参数传递:将参数拷贝到调用者寄存器。
即在此处没有栈清理操作。
下面是Add函数的汇编代码:
int Add(int i, int a, int b, int c= 4, int d=5, int e = 6, int f= 7, int g = 8)
{
00007FF7E08F1AB0 mov dword ptr [rsp+20h],r9d
00007FF7E08F1AB5 mov dword ptr [rsp+18h],r8d
00007FF7E08F1ABA mov dword ptr [rsp+10h],edx
00007FF7E08F1ABE mov dword ptr [rsp+8],ecx
00007FF7E08F1AC2 push rbp
00007FF7E08F1AC3 push rdi
00007FF7E08F1AC4 sub rsp,0E8h
00007FF7E08F1ACB lea rbp,[rsp+20h]
00007FF7E08F1AD0 mov rdi,rsp
00007FF7E08F1AD3 mov ecx,3Ah
00007FF7E08F1AD8 mov eax,0CCCCCCCCh
00007FF7E08F1ADD rep stos dword ptr [rdi]
00007FF7E08F1ADF mov ecx,dword ptr [rsp+108h]
00007FF7E08F1AE6 lea rcx,[__AA539275_main@cpp (07FF7E0902028h)]
00007FF7E08F1AED call __CheckForDebuggerJustMyCode (07FF7E08F10A0h)
return a + b * c + i;
00007FF7E08F1AF2 mov eax,dword ptr [b]
00007FF7E08F1AF8 imul eax,dword ptr [c]
00007FF7E08F1AFF mov ecx,dword ptr [a]
00007FF7E08F1B05 add ecx,eax
00007FF7E08F1B07 mov eax,ecx
00007FF7E08F1B09 add eax,dword ptr [i]
}
00007FF7E08F1B0F lea rsp,[rbp+0C8h]
00007FF7E08F1B16 pop rdi
00007FF7E08F1B17 pop rbp
00007FF7E08F1B18 ret
有几个地方值得分析一下:
- 在函数的入口地方,从寄存器中提取前4个参数,存入栈中
- 第一个参数存的地方是rsp + 8, 也就是栈顶往上8个字节的地方。因为在调用call指令的时候,会自动把call指令的下一条指令的地址入栈。ret指令会从栈顶出栈8个字节,存入指令寄存器(rpi),程序从之前call指令的下一条指令开始继续执行。