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语言的要求。

posted on 2010-08-27 12:08  hicjiajia  阅读(2287)  评论(0编辑  收藏  举报