• 函数调用的几种方式:__cdecl、__stdcall、__fastcall、__thiscall、__clrcall、__vectorcall


            调用约定(Calling Convention) 是计算机编程中一个比较底层的设计,它主要涉及:

    • 函数参数通过寄存器传递还是栈?
    • 函数参数从左到右还是从右到左压栈?
    • 是否支持可变参数函数(vararg function or variadic function)?
    • 是否需要函数原型?
    • 调用者(caller)还是被调用者(called or callee)清理堆栈?

            具体参见:Argument Passing and Naming Conventions | Microsoft Docs 

            函数的调用过程是通过函数栈帧的不断变化实现的:

             函数的调用,涉及参数传递、返回值传递、调用后返回,这都是通过栈的变化来实现的。对于常见的三种调用约定而言:

    1. __cdecl 

            C/C++默认方式;

            参数从右向左依次入栈;

            由主调函数(调用者)负责栈平衡(清栈)。

    2. __stdcall

            windows API 默认方式,在头文件里查看这些API的声明时候用了WINAPI的宏进行了代替,而这个宏就是__stdcall;

            参数从右向左依次入栈;

            由被调函数自身(即被调用者)负责栈平衡(清栈)。

    3. __fastcall

            快速调用方式;所谓快速,这种方式将参数优先从寄存器传入(ECX和EDX),剩下的参数再从右向左入栈。栈位于内存区域,而寄存器位置CPU,故存取方式快于内存;

            由被调函数自身(即被调用者)负责栈平衡(清栈)。

    ​​​​

     

    4. 为什么要从右向左入栈?

            每个参数都有自己的地址,但不定长参数无法确认地址,并且函数的个数无法确定。

            C/C++中规定了函数参数压栈顺序从右向左,对于不定参数,最后入栈的参数个数,只需要取栈顶就可以得到。

            对于含有不定参数的printf函数,其原型是printf(const char* format,…);其中format确定了printf的参数(通过format的%个数判断)。

            假设是从左至右压栈,那么先入栈的是format,然后依次入栈未知参数,此时想要知道参数个数,就必须找到format,而要找到format,就必须知道参数个数,这样就会陷入一个死胡同里面了。

    5. 参数计算顺序

            先执行哪个参数和参数的计算顺序有关,而c/c++中没有规定函数参数的计算顺序,这个和编译器有关,代码参数的计算顺序决定了实际输出。

            vs的计算顺序是从右至左,clang的计算顺序是从左至右,具体的计算流程分析就很简单了。

            对于c/c++函数参数的读取顺序,参数入栈时顺序从右向左入栈,但是在入栈前会先把参数列表里的表达式从右向左算一遍得到表达式的结果,最后再把这些运算结果统一入栈。

            在参数入栈前,编译器会先把参数的表达式都处理掉,对于一般的操作来说,参数入栈时取值是直接从变量的内存地址里取的,但是对于a++操作,编译器会开辟一个缓冲区来保存当前a的值,然后再对a继续操作,最后参数入栈时的取值是从缓冲区取,而不是直接从a的内存地址里取。

            因为函数参数的计算顺序依照编译器的实现,所以在编码中避免编写诸如 fun(++x, x+y)这种的程序,其在不同的平台得到的结果可能不一样,但是在面试中可能遇到这样的问题,所以我们需要知其然更要知所以然。 

    6. 函数名修饰约定 

    6.1 C编译时函数名修饰约定规则:

    • __stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_functionname@number。
    • __cdecl调用约定仅在输出函数名前加上一个下划线前缀,格式为_functionname。
    • __fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,格式为@functionname@number。

            它们均不改变输出函数名中的字符大小写,这和PASCAL调用约定不同,PASCAL约定输出的函数名无任何修饰且全部大写。

    6.2 C++编译时函数名修饰约定规则:

    • __stdcall调用约定:

    1、以“?”标识函数名的开始,后跟函数名;

    2、函数名后面以“@@YG”标识参数表的开始,后跟参数表;

    3、参数表以代号表示:

    X--void ,

    D--char,

    E--unsigned char,

    F--short,

    H--int

    I--unsigned int,

    J--long,

    K--unsigned long,

    M--float,

    N--double,

    _N--bool,

    ....

    PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代

    表一次重复;

    4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前

    5、参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。

    其格式为“?functionname@@YG*****@Z”或“?functionname@@YG*XZ”,例如

    int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z”

    void Test2() -----“?Test2@@YGXXZ”

    __cdecl调用约定:

    规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YA”。

    __fastcall调用约定:

    规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YI”。

    VC++对函数的缺省声明是"__cdecl",将只能被C/C++调用。

    7. 另外三种不常用调用约束:__thiscall、__clrcall、__vectorcall

    • __thiscall

            主要用于解决this指针问题,使用寄存器传递this指针。返回方式同__stdcall.

    • __clrcall

            __clrcall是C++ .Net里面的。

    • __vectorcall

            要求尽可能在寄存器中传递参数。函数名改编为”@@函数名@参数字节数十进制”。这是微软自己添加的标准。/

  • 相关阅读:
    Koa 源码剖析
    世微 电动车摩托车灯 5-80V 1.2A 一切二降压恒流驱动器AP2915
    牛客网刷题-(2)
    【论文笔记】Dynamic Convolution: Attention over Convolution Kernels
    云计算(一):弹性计算概述
    【VMware vSphere】使用RVTools中的PowerShell脚本创建导出vSphere环境信息的自动化任务。
    从零到一学FFmpeg:AVCodecContext 结构体详析与实战
    【leetcode】单词长度的最大乘积
    [Java Framework] [MQ] SpringBoot 集成RabbitMQ
    Android端ReactNative环境搭建——下
  • 原文地址:https://blog.csdn.net/weixin_43712770/article/details/126080701