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、 __cdecl是C/C++和很多编译器默认使用的调用约定,也可以在函数声明时加上__cdecl关键字来手工指定。采用__cdecl约定时,函数参数按照从右到左的顺序入栈,并且由调用函数者把参数弹出栈以清理堆栈。因此,实现可变参数的函数只能使用该调用约定。由于每一个使用__cdecl约定的函数都要包含清理堆栈的代码,所以产生的可执行文件大小会比较大。__cdecl可以写成_cdecl。
2、__stdcall调用约定用于调用Win32 API函数。采用__stdcall约定时,函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,函数参数个数固定。由于函数体本身知道传进来的参数个数,因此被调用的函数可以在返回前用一条ret n指令直接清理传递参数的堆栈。__stdcall可以写成_stdcall。
3、__fastcall约定用于对性能要求非常高的场合。__fastcall约定将函数的从左边开始的两个大小不大于4个字节(DWORD)的参数分别放在ECX和EDX寄存器,其余的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的堆栈。__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的样子出现)。
5、 Microsoft COFF 二进制文件转储器 (DUMPBIN.EXE) 显示有关通用对象文件格式 (COFF) 二进制文件的信息。可以使用 DUMPBIN 检查 COFF 对象文件、标准 COFF 对象库、可执行文件和动态链接库 (DLL)等。
为了防止导出函数的名称发生变化,我们在定义导出函数时用上关键字:extern "C",这样我们解决了C和C++的问题,但是类中就不能确保了。另外一种情况我们害怕导出函数的调用约定出现问题,所有即使使用了extern "C",还可能因为调用约定的不同而失败,为了解决这个问题我们使用这样的标准调用约定的函数声明,即在函数声明是加上_stdcall,如下:
#else
#def DLL_API extern "C" _declspec(dllimport)
#endif
DLL_API 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)#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文件内容会是这样:add @ 1
HelloWorld @ 2
若对C写的dll采用__stdcall 调用约定:
# define DLLIMPORT __declspec (dllimport) __stdcall
则所生成的def文件的内容为
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
小总结:如果想要我们自己写的dll只有C/C++能调用,我们就使用编译器的默认调用方式(__cdecl)编译就行,如果想要dll也同时能被 VB、Delphi、.NET等调用,需要使用 __stdcall 调用方式! (注意,这些调用方式关键字只能用来修饰函数,放在函数返回类型的右边,函数名的左边,我想用意是为了使C++方式的函数重载得以很好的名字修饰,保持兼容)
链接库头文件:
//head.hclass 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 "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 "dlfcn.h"
#define SOFILE "sec.so"
int (*f)();
int main()
{
void *dp;
dp=dlopen(SOFILE,RTLD_LAZY);
f=dlsym(dp,"f");
f();
return 0;
}
运行Z$./myapp
10
$
使用关键字 extern "C" 可以使得C++编译器生成的函数名满足C语言的要求。
posted on 2010-08-27 12:08 hicjiajia 阅读(2287) 评论(0) 编辑 收藏 举报