• 新书推荐:11.6 调用约定


    本节必须掌握的知识点:

            函数调用约定

    11.6.1 函数调用约定

    什么是函数调用约定?就是告诉编译器:怎么传递参数,怎么传递返回值,怎么平衡堆栈,函数怎样命名。当一个程序中所有的函数都是自己实现的,则不需要和任何人约定,但是如果程序中调用第三方的函数,则需要和对方约定上述事宜了。

    常见的调用约定有__stdcall__cdecl__fastcall, __pascal,Windows32位系统中,微软就开始使用__fastcall调用约定了,到了x64系统,则同一采用__fastcall调用约定了(参见《X86汇编基础教程》 第三部分64位汇编)。

    实验一百二十三:__cdecl调用约定

    在VS中新建项目11-6-1.c:

    /*

       __cdecl调用约定

    */

    #include

    #include

    int funAdd(int x, int y)

    {

        return x + y;

    }

    int main(void)

    {

        int rel = funAdd(1, 2);//这里是堆栈传参

        /*

            00401068   push        2  //将2压入堆栈

            0040106A   push        1  //将1压入堆栈

            0040106C   call        @ILT+0(funAdd)(00401005)//调用者

            00401071   add         esp, 8  //平衡堆栈(外平栈)

            00401074   mov         dword ptr[ebp - 4], eax

        */

        printf("%d\n",rel);

        system("pause");

        return 0;

    }

    ●运行结果:

    3

    请按任意键继续. . .

    ●参数传递:有堆栈传参、寄存器传参、堆栈+寄存器传参、call后续区传参这四种方式。

    那么怎么决定参数传递顺序的呢?要看具体的调用约定。

    ●调用约定:

    下面列举了常见的调用约定有三种形式:

    上面的代码中,并没有用告诉编译器是哪种调用约定,在VS下默认的调用约定是__cdecl,我们来一一验证下。

    参数压栈顺序:从右往左入栈,funAdd(1,2);执行从右往左压栈顺序,先push 2再 push 1,与__cdecl执行参数压栈顺序是一致的。我们接着看平衡堆栈,__cdecl的平衡堆栈要求是:调用者清理栈,且是外平栈。我们再来看下我们写的代码, call  @ILT+0(funAdd) (00401005),  funAdd(0x00401005)是当前的函数,main函数调用了 funAdd(0x00401005)函数,所以main函数是调用者,接着add  esp,8,是外平栈。

    以上完全符合__cdecl调用约定规则。

    我们可以函数定义中指定函数的调用约定,例如:

    int __cdecl  funAdd(int x,int y) //这样键入__cdecl就可以了

    实验一百二十四:__stdcall调用约定

    在VS中新建项目11-6-2.c:

    /*

       __stdcall调用约定

    */

    #include

    #include

    int __stdcall  funAdd(int x, int y)

    {

        return x + y;

    }

    int  main(void)

    {

        int rel = funAdd(1, 2);

        printf("%d\n", rel);

        system("pause");

        return 0;

    }

    ●运行结果:

    3

    请按任意键继续. . .

    ●代码分析:

    我们来看以下反汇编代码:

    int __stdcall  funAdd(int x, int y)

    {

    00301847  dec         dword ptr [ebx+45030845h] 

        return x + y;

    0030184D  or          al,5Fh 

    }

    0030184F  pop         esi 

    00301850  pop         ebx 

    00301851  add         esp,0C0h 

    00301857  cmp         ebp,esp 

    00301859  call        __RTC_CheckEsp (0301226h) 

    0030185E  mov         esp,ebp 

    00301860  pop         ebp 

    00301861  ret         8   ;内平栈

    int  main(void)

    {

    00301880  push        ebp 

    00301881  mov         ebp,esp 

        int rel = funAdd(1, 2);

    003018A8  push        2  ;从右往左入栈

    003018AA  push        1 

    003018AC  call        _funAdd@8 (03011E5h) 

    003018B1  mov         dword ptr [rel],eax 

        printf("%d\n", rel);

    003018B4  mov         eax,dword ptr [rel] 

    003018B7  push        eax 

    003018B8  push        offset string "%d\n" (0307B30h) 

    003018BD  call        _printf (030104Bh) 

    003018C2  add         esp,8 

        system("pause");

        return 0;

    003018DC  xor         eax,eax 

    }

    __stdcall调用约定:参数从右往左压入堆栈,且是函数自己本身清理堆栈。

    实验一百二十五:__fastcall调用约定

    在VS中新建项目11-6-3.c:

    /*

       __fastcall调用约定

    */

    #include

    #include

    int __fastcall funAdd(int x, int y, int i, int j)

    {

        return x + y + i + j;

    }

    int  main(void)

    {

        int rel = funAdd(1,2,3,4);

        printf("%d\n", rel);

        system("pause");

        return 0;

    }

    ●运行结果:

    10

    请按任意键继续. . .

    ●代码分析:

    我们来看以下反汇编代码:

    int __fastcall funAdd(int x, int y, int i, int j)

    {

    00B51720  push        ebp 

    00B51721  mov         ebp,esp 

     

        return x + y + i + j;

    00B51750  mov         eax,dword ptr [x] 

    00B51753  add         eax,dword ptr [y] 

    00B51756  add         eax,dword ptr [i] 

    00B51759  add         eax,dword ptr [j] 

    }

    00B5176C  mov         esp,ebp 

    00B5176E  pop         ebp 

    00B5176F  ret         8  ;平衡2个堆栈参数

    int  main(void)

    {

    00B51890  push        ebp 

    00B51891  mov         ebp,esp 

     

        int rel = funAdd(1,2,3,4);

    00B518B8  push        4  ;第四个参数

    00B518BA  push        3  ;第三个参数

    00B518BC  mov         edx,2   ;第二个参数

    00B518C1  mov         ecx,1   ;第一个参数

    00B518C6  call        @funAdd@16 (0B51082h) 

    00B518CB  mov         dword ptr [rel],eax 

        printf("%d\n", rel);

    00B518CE  mov         eax,dword ptr [rel] 

    00B518D1  push        eax 

    00B518D2  push        offset string "%d\n" (0B57B30h) 

    00B518D7  call        _printf (0B5104Bh) 

    00B518DC  add         esp,8 

        system("pause");

        return 0;

    00B518F6  xor         eax,eax 

    }

    由上述反汇编代码可知,第1,2个参数使用ecx和edx寄存器传参,第3,4个参数使用堆栈传参,堆栈传参仍然是从右往左顺序入栈。堆栈平衡为内平衡,被调函数负责平衡2个参数占用的8个字节堆栈空间。

    由于寄存器传参的速度最快,因此称为fastcall调用约定,可惜32位程序只能使用ecx和edx两个寄存器参与传参,剩余参数使用堆栈传参。x64处理器中的fastcall调用约定可以使用rcx,rdx,r8,r94个寄存器传参,剩余参数使用堆栈传参。

     

    总结

    __fastcall通过寄存器传参,传递参数不破坏堆栈的平衡,所以它不需要平栈,它的执行效率最高。但是当它的参数的数量大于2时,它就会采取堆栈+寄存器的方式传参,反而会降低执行效率。

    C语言中默认采用的调用约定是“_cdecl”;_stdcall为WINAPI的调用约定,_fastcall为Windows内核中常见的调用约定。此外还有C++中有this call调用约定(_thiscall)。

  • 相关阅读:
    Dotnet算法与数据结构:Hashset, List对比
    Springboot漫画网站j846u计算机毕业设计-课程设计-期末作业-毕设程序代做
    从 Clickhouse 到 Snowflake(一): 云原生
    Go程序是如何运行起来的?
    归并排序~
    使用CMake创建CUDA工程
    LINUX如何 部署ansible
    (02)Cartographer源码无死角解析-(23) 传感器数据类型自动推断与数据利用率计算
    使用 Docker 部署 TaleBook 私人书籍管理系统
    关于反向竞价的6个常见误解,采购一定要知道
  • 原文地址:https://blog.csdn.net/bcdaren/article/details/139383382