zoukankan      html  css  js  c++  java
  • Dll的分析与编写(二)

    1、调用约定基本概念

    2、C/C++ 常用的几种调用约定

    3、调用约定与名称修饰

    4、 __cdecl  与  __stdcall  的区别

    5、保证与其他调用程序的兼容性

    6、 几个重要的关键字解释!

    7、乱七八糟

    8、C程序中调用C++写的dll


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

        调用约定决定以下内容:1)函数参数的压栈顺序,2)由调用者还是被调用者把参数弹出栈,3)以及产生函数修饰名的方法

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

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


    2、 常用的可以说有三种: 1、 __cdecl   2、__stdcall    3、 __fastcall

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

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

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

        (这里有一篇详细的,利用汇编来分析各种约定的文章: http://blog.csdn.net/chief1985/archive/2008/05/04/2385099.aspx)

    3、


        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

    在TURBO C中用修饰符cdecl说明的函数或不加说明的函数按照从右向左的顺序将参数压入堆栈,即给定调用函数(a,b,c)后,a最先进栈,然后是b和c。
    在进行函数调用时,有几种调用方法,分为C式,Pascal式。在C和C++中C式调用是缺省的,除非特殊声明。二者是有区别的。


    4、几乎我们写的每一个WINDOWS API函数都是__stdcall类型的,首先,需要了解两者之间的区别: WINDOWS的函数调用时需要用到栈(STACK,一种先入后出的存储结构)。当函数调用完成后,栈需要清除,这里就是问题的关键,如何清除??这就涉及到调用约定问题了,C/C++默认采用_cdedl约定。


         1、如果我们的函数使用了_cdecl,那么栈的清除工作是由调用者,用COM的术语来讲就是客户来完成的。这样带来了一个棘手的问题,不同的编

    译器产生栈的方式不尽相同,那么调用者能否正常的完成清除工作呢?答案是不能。


        2、如果使用__stdcall,上面的问题就解决了,函数自己解决清除工作。所以,在跨(开发)平台的调用中,我们都使用__stdcall(虽然有时是以WINAPI的样子出现)。


        那么为什么还需要_cdecl呢?当我们遇到这样的函数如fprintf()它的参数是可变的,不定长的,被调用者事先无法知道参数的长度,事后的清除工作也无法正常的进行,因此,这种情况我们只能使用_cdecl。到这里我们有一个结论,如果你的程序中没有涉及可变参数,最好使用__stdcall关键字。 

     5、 Microsoft COFF 二进制文件转储器 (DUMPBIN.EXE) 显示有关通用对象文件格式 (COFF) 二进制文件的信息。可以使用 DUMPBIN 检查 COFF 对象文件、标准 COFF 对象库、可执行文件和动态链接库 (DLL)等。

         为了防止导出函数的名称发生变化,我们在定义导出函数时用上关键字:extern "C",这样我们解决了C和C++的问题,但是类中就不能确保了。另外一种情况我们害怕导出函数的调用约定出现问题,所有即使使用了extern "C",还可能因为调用约定的不同而失败,为了解决这个问题我们使用这样的标准调用约定的函数声明,即在函数声明是加上_stdcall,如下:

    #ifdef DLL_API
    #else
    #def DLL_API     
    extern "C" _declspec(dllimport)
    #endif
    DLL_API 
    int  _stdcall add(int a,int b);
     同理,我们也要改变dll.cpp中的int  _stdcall add(int a,int b)的定义。

    6、

      __declspec (dllexport):这是关键,它标志着这个这个函数将成为对外的接口。
      使用包含在DLL的函数,必须将其导入。导入操作时通过dllimport来完成的,dllexport和dllimport都是C++编译器所支持的扩展的关键字。但是dllexport和dllimport关键字不能被自身所使用,因此它的前面必须有另一个扩展关键字__declspec。
    通用格式如下:__declspec(specifier)其中specifier是存储类标示符。对于DLL,specifier将是dllexport和dllimport。而且为了简化说明导入和导出函数的语句,用一个宏名来代替__declspec.在此程序中,使用的是DllExport。

          如果用户的DLL被编译成一个C++程序,而且希望C程序也能使用它,就需要增加“C”的连接说明。#define   DllExport   extern   "C "__declspec(dllexport),这样就避免了标准C++命名损坏。(当然,如果读者正在编译的是C程序,就不要加入extern   “C”,因为不需要它,而且编译器也不接受它)。 

    <8、是一个C调用C++写的dll的例子>

             再说说dllimport,它是为了更好的处理类中的静态成员变量的,如果没有静态成员变量,那么这个__declspec(dllimport)无所谓。因此为了更好的代码质量,我们在写dll的时候尽量要采用标准的头文件定义:

    (详细内容请看: http://blog.csdn.net/chief1985/archive/2008/05/04/2385099.aspx 
    #ifndef _DLL_H_
    #define _DLL_H_
     
    #if BUILDING_DLL
    # define DLLIMPORT __declspec (dllexport)
    #else /* Not BUILDING_DLL */
    # define DLLIMPORT __declspec (dllimport)
    #endif /* Not BUILDING_DLL */
     
    DLLIMPORT 
    void HelloWorld (void);
     .............

    #endif /* _DLL_H_ */

    7、 

            1、上面第二条的命名约定应该对应的是VC编译器来说的,而我用的是DEV-C++编译器,在实践过程中,发现他们的命名约定并不一样,对于一个用C写的dll(采用默认的 _cdecl 调用约定),其生成的def文件内容会是这样:
    EXPORTS
    add @ 
    1
    HelloWorld @ 
    2
     可以看出,它的函数约定是“函数名+@+函数顺序数”

    若对C写的dll采用__stdcall 调用约定:

    # define DLLIMPORT __declspec (dllexport) __stdcall
    # define DLLIMPORT __declspec (dllimport) __stdcall

     则所生成的def文件的内容为

    EXPORTS
    HelloWorld@
    0 @ 1
    add 
    = add@8 @ 2
    add@
    8 @ 3
    HelloWorld 
    = HelloWorld@0 @ 4

     我们可以很清楚的判别出来,这其中的差异,函数名后第一个@后的数字就是参数所占的字节数了,而第二个@后才是函数顺序数,函数重载的话就可以区分开了.

         2、对一个用C++写的dll(_cdecl),其生成的def文件会是如下这般:(而 __stdcall 会有些不同,就不贴代码了)

    EXPORTS

    ;DllClass::add()

    _ZN8DllClass3addEv @ 1

    ;DllClass::DllClass(int, int)

    _ZN8DllClassC1Eii @ 2

    ;DllClass::DllClass()

    _ZN8DllClassC1Ev @ 3

    ;DllClass::DllClass(int, int)

    _ZN8DllClassC2Eii @ 4

    ;DllClass::DllClass()

    _ZN8DllClassC2Ev @ 5

    ;DllClass::~DllClass()

    _ZN8DllClassD0Ev @ 6

    ;DllClass::~DllClass()

    _ZN8DllClassD1Ev @ 7

    ;DllClass::~DllClass()

    _ZN8DllClassD2Ev @ 8

    ;vtable for DllClass

    _ZTV8DllClass @ 9 DATA

    从中我们也可以发现一些规律,比如说,Ev 代表函数参数为空,Eii 代表有两个int型的参数...

             小总结:如果想要我们自己写的dll只有C/C++能调用,我们就使用编译器的默认调用方式(__cdecl)编译就行,如果想要dll也同时能被 VB、Delphi、.NET等调用,需要使用 __stdcall 调用方式! (注意,这些调用方式关键字只能用来修饰函数,放在函数返回类型的右边,函数名的左边,我想用意是为了使C++方式的函数重载得以很好的名字修饰,保持兼容) 

    8、这里有点麻烦了,C++写代码要用类的,如果不用类的话,C++还C没什么区别了,还不如用C,一旦C++用类,你想想你在C程序中如何调用函数啊。这面这是一个例子(经过了两次封装后,才能被C程序调用)。
    例子如下:

    链接库头文件: 

     //head.h
    #include <iostream>
    class A
    {
      
    public:
         A();
        
    virtual ~A();
        
    int gt();
        
    int pt();
      
    private:
           
    int s;
    };

    //firstso.cpp
    #i nclude <iostream>
    #i nclude 
    "head.h"

    A::A(){}
    A::
    ~A(){}
    int A::gt()
    {
        s
    =10;
    }
    int A::pt()
    {
        std::cout
    <<s<<std::endl;
    }
     编译命令如下:

    g++ -shared -o libmy.so firstso.cpp
    这时候生成libmy.so文件,将其拷贝到系统库里面:/usr/lib/
    进行二次封装:

     //secso.cpp

    #include <iostream>
    #include 
    "head.h"
    extern "C"
    {
        
    int f();
        
    int f()
        {
            A a;
            a.gt();
            a.pt();
            
    return 0;
        }
    }
     编译命令:

    gcc -shared -o sec.so secso.cpp -L. -lmy
    这时候生成第二个.so文件,此时库从一个类变成了一个c的接口.
    拷贝到/usr/lib
    下面开始调用:

     //test.c

    #include "stdio.h"
    #include 
    "dlfcn.h"

    #define SOFILE "sec.so"
    int (*f)();
    int main()
    {
        
    void *dp;
        dp
    =dlopen(SOFILE,RTLD_LAZY);
        f
    =dlsym(dp,"f");
        f();
        
    return 0;
    }
     编译命令如下:
    gcc -rdynamic -s -o myapp test.c
    运行Z$./myapp
    10
    $
    在C语言程序当中使用C++编写的函数,关键是函数名字解析问题。
    使用关键字 extern "C" 可以使得C++编译器生成的函数名满足C语言的要求。
  • 相关阅读:
    python--DenyHttp项目(1)--调用cmd控制台命令os.system()
    python--DenyHttp项目(1)--GUI:tkinter☞ module 'tkinter' has no attribute 'messagebox'
    python--DenyHttp项目(1)--socket编程:服务器端进阶版socketServer
    python--DenyHttp项目(1)--socket编程:客户端与服务器端
    平台后台编辑功能实现
    java中的map
    java 重写(override)与 重载(overload)
    java继承 extends
    java泛型
    java中Map和List的使用
  • 原文地址:https://www.cnblogs.com/hicjiajia/p/1810008.html
Copyright © 2011-2022 走看看