zoukankan      html  css  js  c++  java
  • 动态链接库、名字修饰约定、调用约定

      调用约定(Calling Convention)是指在程序设计语言中为了实现函数调用而建立的一种协议。这种协议规定了该语言的函数中的参数传送方式、参数是否可变和由谁来处理堆栈等问题。不同的语言定义了不同的调用约定。

      在C++中,为了允许操作符重载和函数重载,C++编译器往往按照某种规则改写每一个入口点的符号名,以便允许同一个名字(具有不同的参数类型或者是不同的作用域)有多个用法,而不会打破现有的基于C的链接器。这项技术通常被称为名称改编(Name Mangling)或者名称修饰(Name Decoration)。许多C++编译器厂商选择了自己的名称修饰方案。

      因此,为了使其它语言编写的模块(如Visual Basic应用程序、Pascal或Fortran的应用程序等)可以调用C/C++编写的DLL的函数,必须使用正确的调用约定来导出函数,并且不要让编译器对要导出的函数进行任何名称修饰。

     

    1.调用约定(Calling Convention

    调用约定用来处理决定函数参数传送时入栈和出栈的顺序(由调用者还是被调用者把参数弹出栈),以及编译器用来识别函数名称的名称修饰约定等问题。在Microsoft VC++ 6.0中定义了下面几种调用约定,我们将结合汇编语言来一一分析它们:

    1、__cdecl

    __cdecl是C/C++和MFC程序默认使用的调用约定,也可以在函数声明时加上__cdecl关键字来手工指定。采用__cdecl约定时,函数参数按照从右到左的顺序入栈,并且由调用函数者把参数弹出栈以清理堆栈。因此,实现可变参数的函数只能使用该调用约定。由于每一个使用__cdecl约定的函数都要包含清理堆栈的代码,所以产生的可执行文件大小会比较大。__cdecl可以写成_cdecl。

    下面将通过一个具体实例来分析__cdecl约定:

    在VC++中新建一个Win32 Console工程,命名为cdecl。其代码如下:

    int __cdecl Add(int a, int b);         //函数声明

    void main()

    {

           Add(1,2);                                   //函数调用

    }

    int __cdecl Add(int a, int b)          //函数实现

    {

           return (a + b);

    }

    函数调用处反汇编代码如下:

    ;Add(1,2);

    push                     2                                        ;参数从右到左入栈,先压入2

    push        1                                         ;压入1

    call          @ILT+0(Add) (00401005)    ;调用函数实现

    add           esp,8                                   ;由函数调用清栈

    2、__stdcall

    __stdcall调用约定用于调用Win32 API函数。采用__stdcal约定时,函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,函数参数个数固定。由于函数体本身知道传进来的参数个数,因此被调用的函数可以在返回前用一条ret n指令直接清理传递参数的堆栈。__stdcall可以写成_stdcall。

    还是那个例子,将__cdecl约定换成__stdcall:

    int __stdcall Add(int a, int b)

    {

    return (a + b);

    }

    函数调用处反汇编代码:

          

           ; Add(1,2);

    push                     2                                               ;参数从右到左入栈,先压入2

    push        1                                                ;压入1

    call          @ILT+10(Add) (0040100f)          ;调用函数实现

    函数实现部分的反汇编代码:

    ;int __stdcall Add(int a, int b)

    push                     ebp

    mov          ebp,esp

    sub                esp,40h

    push               ebx

    push               esi

    push               edi

    lea           edi,[ebp-40h]

    mov          ecx,10h

    mov        eax,0CCCCCCCCh

    rep stos       dword ptr [edi]

    ;return (a + b);

    mov          eax,dword ptr [ebp+8]

    add                eax,dword ptr [ebp+0Ch]

    pop           edi

    pop          esi

    pop           ebx

    mov          esp,ebp

    pop          ebp

    ret           8                 ;清栈

    3、__fastcall

    __fastcall约定用于对性能要求非常高的场合。__fastcall约定将函数的从左边开始的两个大小不大于4个字节(DWORD)的参数分别放在ECX和EDX寄存器,其余的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的堆栈。__fastcall可以写成_fastcall。

    依旧是相类似的例子,此时函数调用约定为__fastcall,函数参数个数增加2个:

    int __fastcall Add(int a, double b, int c, int d)

    {

    return (a + b + c + d);

    }

    函数调用部分的汇编代码:

    ;Add(1, 2, 3, 4);

    push                     4                          ;后两个参数从右到左入栈,先压入4

    mov          edx,3                    ;将int类型的3放入edx

    push         40000000h            ;压入double类型的2

    push         0

    mov          ecx,1                    ;将int类型的1放入ecx

    call          @ILT+0(Add) (00401005)                ;调用函数实现

    函数实现部分的反汇编代码:

                 

    ; int __fastcall Add(int a, double b, int c, int d)

    push                     ebp

    mov        ebp,esp

    sub          esp,48h

    push               ebx

    push               esi

    push               edi

    push               ecx

    lea           edi,[ebp-48h]

    mov          ecx,12h

    mov           eax,0CCCCCCCCh

    rep stos       dword ptr [edi]

    pop          ecx

    mov        dword ptr [ebp-8],edx

    mov        dword ptr [ebp-4],ecx

    ;return (a + b + c + d);

    fild           dword ptr [ebp-4]

    fadd          qword ptr [ebp+8]

    fiadd         dword ptr [ebp-8]

    fiadd         dword ptr [ebp+10h]

    call          __ftol (004011b8)

    pop          edi

    pop          esi

    pop          ebx

    mov          esp,ebp

    pop          ebp

    ret           0Ch                              ;清栈

    关键字__cdecl、__stdcall和__fastcall可以直接加在要输出的函数前,也可以在编译环境的Setting...->C/C++->Code Generation项选择。它们对应的命令行参数分别为/Gd、/Gz和/Gr。缺省状态为/Gd,即__cdecl。当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效。

    解释一: 通过栈传递,被调用的函数在返回前清理传送参数的内存栈,但不同的是函数名的修饰部分(关于函数名的修饰部分在后面将详细说明)。 

    _stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。 

    2、C调用约定(即用__cdecl关键字说明)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。 

    _cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀。是MFC缺省调用约定。 

    3、__fastcall调用约定是“人”如其名,它的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。 

    _fastcall方式的函数采用寄存器传递参数,VC将函数编译后会在函数名前面加上"@"前缀,在函数名后加上"@"和参数的字节数。 

    4、thiscall仅仅应用于“C++”成员函数。this指针存放于CX寄存器,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。 

    5、naked call采用1-4的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。naked call不产生这样的代码。naked call不是类型修饰符,故必须和_declspec共同使用。 

    关键字 __stdcall、__cdecl和__fastcall可以直接加在要输出的函数前,也可以在编译环境的Setting...\C/C++ \Code Generation项选择。当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效。它们对应的命令行参数分别为/Gz、/Gd和/Gr。缺省状态为/Gd,即__cdecl。 

    要完全模仿PASCAL调用约定首先必须使用__stdcall调用约定,至于函数名修饰约定,可以通过其它方法模仿。还有一个值得一提的是WINAPI宏,Windows.h支持该宏,它可以将出函数翻译成适当的调用约定,在WIN32中,它被定义为__stdcall。使用WINAPI宏可以创建自己的APIs。 

    2)名字修饰约定 

    1、修饰名(Decoration name) 

    “C”或者“C++”函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字符串。有些情况下使用函数的修饰名是必要的,如在模块定义文件里头指定输出“C++”重载函数、构造函数、析构函数,又如在汇编代码里调用“C””或“C++”函数等。 

    修饰名由函数名、类名、调用约定、返回类型、参数等共同决定。 

    2、名字修饰约定随调用约定和编译种类(C或C++)的不同而变化。函数名修饰约定随编译种类和调用约定的不同而不同,下面分别说明。 

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

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

    __cdecl调用约定仅在输出函数名前加上一个下划线前缀,格式为_functionname。 

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

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

    b、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++对函数的省缺声明是"__cedcl",将只能被C/C++调用. 

    CB在输出函数声明时使用4种修饰符号 
    //__cdecl 
    cb的默认值,它会在输出函数名前加_,并保留此函数名不变,参数按照从右到左的顺序依次传递给栈,也可以写成_cdecl和cdecl形式。 
    //__fastcall 
    她修饰的函数的参数将尽肯呢感地使用寄存器来处理,其函数名前加@,参数按照从左到右的顺序压栈; 
    //__pascal 
    它说明的函数名使用Pascal格式的命名约定。这时函数名全部大写。参数按照从左到右的顺序压栈; 
    //__stdcall 
    使用标准约定的函数名。函数名不会改变。使用__stdcall修饰时。参数按照由右到左的顺序压栈,也可以是_stdcall; 


    解释二: 

    #define CALLBACK __stdcall 
    #define WINAPI __stdcall 

    class="Apple-style-span" style="font-family: ����; font-size: 12px;"> 

    例如对于函数: 
    void func(int a, int b, int c, int d) { } 
    使用函数如下 
    int main() 

    func(1, 2, 3, 4); 






    1.如果函数func是__cdecl(默认调用方式),调用时情况如下 
    int main() 

    //参数从右到左压栈 
    push 4 
    push 3 
    push 2 
    push 1 
    call func 
    add esp 0x10 //调用者恢复堆栈指针esp,4个参数的大小是0x10(4x4) 


    2.如果函数func是__stdcall,调用时情况如下 
    int main() 

    //参数从右到左压栈 
    push 4 
    push 3 
    push 2 
    push 1 
    call func 
    //恢复堆栈指针由被调用者func负责,方法是"ret 0x10" 


    3.如果函数func是__pascal,调用情况如下 
    int main() 

    //参数从左到右压栈 
    push 1 
    push 2 
    push 3 
    push 4 
    call func 
    //恢复堆栈指针由被调用者func负责,方法是"ret 0x10" 


    4.如果函数func是__fastcall,调用情况如下 
    int main() 

    //参数先用ecx, edx, eax传递,然后再压栈 
    //不进栈 
    //(不知为什么,帮助中写的是从左到右传递的, 
    //是不是错了,还是BCB6和BCB5的不一样) 
    push 4 
    mov ecx 3 
    mov edx 2 
    mov eax 1 
    call func 
    //恢复堆栈指针由被调用者func负责,方法是"ret 0x04", 
    //因为只进栈一个参数,其余用寄存器传递,所以用ret 0x04恢复 
    }

  • 相关阅读:
    Hadoop集群(三) Hbase搭建
    Hadoop集群(二) HDFS搭建
    Hadoop集群(一) Zookeeper搭建
    Redis Cluster 添加/删除 完整折腾步骤
    Redis Cluster在线迁移
    Hadoop分布式HA的安装部署
    Describe the difference between repeater, bridge and router.
    what is the “handover” and "soft handover" in mobile communication system?
    The main roles of LTE eNodeB.
    The architecture of LTE network.
  • 原文地址:https://www.cnblogs.com/renyuan/p/2799898.html
Copyright © 2011-2022 走看看