美文网首页
从汇编角度分析VS下C++函数调用

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

作者: loki528 | 来源:发表于2019-07-31 11:30 被阅读0次

    记录函数调用的细节,深入汇编层面的。只分析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指令的下一条指令开始继续执行

    相关文章

      网友评论

          本文标题:从汇编角度分析VS下C++函数调用

          本文链接:https://www.haomeiwen.com/subject/vieorctx.html