从汇编角度分析VS下C++函数调用

2019-07-31  本文已影响0人  loki528

记录函数调用的细节,深入汇编层面的。只分析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  

从中观察到的现象是:

  1. 函数调用使用栈传递参数,从右向左入栈;
  2. 函数返回值通过eax寄存器返回(如果返回值的大小不超过32位,4字节);
  3. 调用者负责清理配平栈,将参数占用的空间出栈;
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  

有几个地方值得分析一下:

  1. 在函数的入口地方,从寄存器中提取前4个参数,存入栈中
  2. 第一个参数存的地方是rsp + 8, 也就是栈顶往上8个字节的地方。因为在调用call指令的时候,会自动把call指令的下一条指令的地址入栈。ret指令会从栈顶出栈8个字节,存入指令寄存器(rpi),程序从之前call指令的下一条指令开始继续执行
上一篇下一篇

猜你喜欢

热点阅读