一、动态连接库的用途
动态连接库,dynamic-link libraries(DLL),是微软公司提供的一项软件技术。
它实质上是包含了一些函数和数据的可执行模块,它可以被应用程序(.EXE)或其它DLL
调用。这种技术有以下好处:共享资源、节省内存、支持多语种、可重复利用、便于大
项目的开发等。这样说是不是有点老套,也是,教科书都有的嘛。咳,就当复习一下功课了....
下面说一下我的理解。
没有总结,就没有进步。这话好象听谁说过的。作为一种载体,用来对过去经验作
个总结,动态库得天独厚。比方说你在以往的项目开发或编程中积累下了很多的经验、
技巧、想法(?)和专业资料,而且它们在特定的领域很有价值。但是随着开发工具的
发展、执行平台的升级,已往的这些经验、技巧和资料可能就会被丢弃。其实将它们作
为对以前劳动成果的一种总结,汇集到特定的动态库中,不失为一种两全其美的方法。
由于动态库与编程语言无关,如此得到的资源可以得到更广泛地应用。作为一种长远
考虑,资源的重复利用不但没有使以往的劳动浪费,而且使原来的劳动增值,使工作
更有效。尤其是资源的重复利用问题,如果系统地考虑软件复用则是解决软件开发中
重复劳动问题的一种方案,动态库则是一种途径和方法。以已有的工作为基础,充分
利用过去应用系统开发中积累的知识和经验,将开发的重点集中于应用的特有构成成
分上,消除重复劳动,避免重新开发可能引入的错误,从而提高软件开发的效率和质量。
另外,作为混合编程的一种特例,动态库当仁不让。由于动态库与具体的编程语言
无关,只要这种语言支持动态库技术,则这种语言就能拿来用,目的只有一个“取长补
短”。各类编程语言的存在是由于它们各有所长。我们可以通过动态库将一个大的任务
分割成一个个子任务,这些子任务可以分别由不同的语言来实现。
还有一个最成功的例子:微软的应用程序接口API。
二、动态连接库的有关约定
关于动态库输出函数的约定有两种:调用约定和名字修饰约定。
调用约定决定着函数参数传送时入栈和出栈的顺序,以及编译器用来识别函数名字
的修饰约定。名字修饰约定随调用约定和编译种类(C或C++)的不同而变化。为了让不
同的编程语言共享动态库带来的方便,函数输出时必须使用正确的调用约定,并且最好
不带有任何由编译器生成的名字修饰。
下面就以VC5和VB5为例,结合具体情况来说明如何实现这些要求。
(一)调用约定
VC++5.0支持的函数调用约定有多种,在这里仅讨论以下三种:__stdcall调用约定
、C调用约定和__fastcall调用约定。
__stdcall调用约定相当于16位动态库中经常使用的PASCAL调用约定。在32位的VC+
+5.0中PASCAL调用约定不再被支持(实际上它已被定义为__stdcall。除了__pascal外,
__fortran和__syscall也不被支持),取而代之的是__stdcall调用约定。两者实质上
是一致的,即函数的参数自右向左通过栈传递,被调用的函数在返回前清理传送参数的
内存栈,但不同的是函数名的修饰部分(关于函数名的修饰部分在后面将详细说明)。
C调用约定(即用__cdecl关键字说明)和__stdcall调用约定有所不同,虽然参数传
送方面是一样的,但对于传送参数的内存栈却是由调用者来维护的(也正因为如此,实
现可变参数的函数只能使用该调用约定),另外,在函数名修饰约定方面也有所不同。
__fastcall调用约定是“人”如其名,它的主要特点就是快,因为它是通过寄存器
来传送参数的(实际上,它用ECX和EDX传送前两个双字或更小的参数,剩下的参数仍旧
自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定
方面,它和前两者均不同。
关键字 __stdcall、__cdecl和__fastcall可以直接加在要输出的函数前,也可以在
编译环境的Setting...\C/C++ \Code Generation项选择。当加在输出函数前的关键字与编
译环境中的选择不同时,直接加在输出函数前的关键字有效。它们对应的命令行参数分别
为/Gz、/Gd和/Gr。缺省状态为/Gd,即__cdecl。
顺便说明一下,要完全模仿PASCAL调用约定首先必须使用__stdcall调用约定,至于
函数名修饰约定,可以通过其它方法模仿。还有一个值得一提的是WINAPI宏,Windows.h支
持该宏,它可以将输出函数翻译成适当的调用约定,在WIN32中,它被定义为__stdcall。
建议:使用WINAPI宏,这样你就可以创建自己的APIs了。
(二)函数名修饰约定
函数名修饰约定随编译种类和调用约定的不同而不同,下面分别说明。
对于C编译,__stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一
个“@”符号和其参数的字节数,格式为_functionname@number。__cdecl调用约定仅在
输出函数名前加上一个下划线前缀,格式为_functionname。__fastcall调用约定在输出
函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,格式为
@functionname@number。它们均不改变输出函数名中的自符大小写,这和PASCAL调用约定
不同,PASCAL约定输出的函数名无任何修饰且全部大写。说到这里,我给出一种完全模仿
PASCAL调用约定的方法,在.DEF文件的EXPORTS段通过别名来实现。例如:
int __stdcall MyFunc (int a, double b);
void __stdcall InitCode (void);
在 .DEF 文件中:
EXPORTS
MYFUNC=_MyFunc@12
INITCODE=_InitCode@0
C++编译输出的函数名修饰较为复杂,VC++5.0的随机文档中也没有给出说明。经过
一些实验和摸索,
我发现了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++输出函数时使用__declspec(dllexport),而不再用_export修饰字。
__declspec(dllexport)在C调用约定、C编译情况下可以去掉输出函数名的下划线
前缀。extern "C"
使得在C++中使用C编译方式成为可能,在一个C++文件中,用extern "C"来指明该函数使
用C编译方式。例
如,在一个C++文件中,有如下函数:
extern "C" {void __declspec(dllexport) __cdecl Test(int var);}
其输出函数名为:Test
为了方便,你可以使用下列预处理语句:
#if defined(__cplusplus)
extern "C"
{
#endif
//函数原型说明
#if defined(__cplusplus)
}
#endif
如此以来,经过上面的特殊处理,不管在C中,还是在C++中都可以得到一个无任何修饰
的函数名了。
下面再介绍另一条途径:不用__declspec(dllexport)修饰字输出函数,而用.DEF文
件来输出函数。
将要输出的函数修饰名罗列在EXPORTS之下,这个名字必须与定义函数的名字完全一致,
如此就得到一
个没有任何修饰的函数名了。
至此,我们已有至少三种方法可以获得“没有任何修饰的函数名”了。
我在开始时就提到过“函数输出时....最好不带有任何由编译器生成的名字修饰”
,这一点在多语
种混合编程时尤其重要。
(四)实验
下面做一个实验来加深一下上面介绍内容的印象。
实验设想:有这样一个软件系统,用VB5设计它的界面,用VC5写一个动态库,用于
执行一些繁琐的
计算,在计算过程中有一些中间结果要作简单的显示,我们用VB5来完成显示任务,于是
在VB5中定义了
一个显示函数,由动态库来回调它,并且将计算结果作为回调时的参数....
首先用VB5编写界面并定义显示函数。新建一个工程,添加一个模块文件,在该模块
文件中定义我们
的显示函数(即回调函数):
Public Sub ShowResult(result As Long)
form1.Print result '简单模拟一下显示而已
End Sub
另外,给出动态库输出函数的描述:
Declare Sub TestShow Lib "test32.dll" (ByVal Show As Long, Param As Any)
之后,在窗体上放一个命令按钮并添加如下代码:
Private Sub Command1_Click()
Dim i As Long
TestShow AddressOf ShowResult, i
End Sub
现在用VC5写我们的动态库。
新建一个项目。选择New Projects | Win32 Dynamic-Link Library,并输入项目名
Test32;然后添
加下面内容到.CPP文件:
#include
BOOL WINAPI DllEntryPoint( HINSTANCE hinstDll,DWORD fdwRreason,
LPVOID plvReserved)
{
return 1; // Indicate that the DLL was initialized successfully.
}
void TestShow(int AppShow(int*),int *flag)
{
for(int i=0;i<10;i++)
{
*flag=11011+i; //为简单起见,这里用直接赋值替代“复杂计算”的结果
AppShow(flag); //回调
}
}
这里使用.DEF文件输出函数。添加下列内容到.DEF文件:
LIBRARY TEST32
DESCRIPTION 'TEST32.DLL'
EXPORTS
TestShow @1
将调用约定设置为__stdcall,编译生成Test32.dll,将其拷入系统目录。
最后运行上面编写的VB5项目。OK?!
实验一:将调用约定改为缺省设置,即C调用约定,其它不变,重新编译生成Test3
2.dll并将其拷
入系统目录,试运行VB5项目看看......
实验二:将调用约定改为缺省设置,即C调用约定,在上面的TestShow函数前加上_
_stdcall关键字
或WINAPI宏,其它不变,重新编译生成Test32.dll并将其拷入系统目录,试运行VB5项目
看看......
实验三:将调用约定改为缺省设置,即C调用约定,在上面的TestShow函数前加上_
_stdcall关键字
或WINAPI宏,并且在其第一个参数AppShow前加上__stdcall关键字,其它不变,即
void __stdcall TestShow(int __stdcall AppShow(int*),int *flag)
重新编译生成Test32.dll并将其拷入系统目录,试运行VB5项目看看......
提示:VB5的函数调用遵循API调用约定(__stdcall,即原来的PASCAL)。
关于回调函数的概念和约定请参阅相关书籍。
三、参数传递
有关WIN32动态库的输出函数的参数传递上面也说了一些,这里主要再进一步详细说
明。在32位动态
库中,所有的参数都被扩展为32位(如字符型参数、短整型参数),自右向左反向入栈
。函数的返回值
也被扩展为32位,放在EAX寄存器中,8字节的返回值放在EDX:EAX寄存器对中,返回值
为更大结构时使用
EAX作为指向隐形返回结构的指针返回。当函数用到一些相关寄存器(如ESI, EDI, EBX
和 EBP)时,编译
器会自动生成一个函数头和一个函数尾,用于保存和恢复这些用到的寄存器。下面举例
描述参数传递的情
况。我们已经知道,__stdcall和__cdecl调用约定的参数传递是相同的,__fastcall调
用约定和它们有所
不同。
void MyFunc( char c, short s, int i, double f );
.
.
.
void MyFunc( char c, short s, int i, double f )
{
.
.
.
}
.
.
.
MyFunc ('a', 22, 8192, 2.1418);
其执行时参数传递情况将是这样的:
__stdcall和__cdecl调用约定
位置 栈
ESP+0x14 2.1418
ESP+0x10
ESP+0x0c 8192
ESP+0x08 22
ESP+0x04 a
ESP 返回值
__fastcall调用约定
位置 栈
ESP+0x0c 2.1418
ESP+0x08
ESP+0x04 8192
ESP 返回值
ECX a
EDX 22
四、栈
前面曾提到不同的调用约定在传送参数时对栈的不同处理。这里再重点说一下不同
的调用约定是如
何来维护栈的正常工作的,同时也更深刻地理解保持相同调用约定的重要性。我们已经
知道,上面所提
到的三种调用约定传送参数时都是自右至左压栈,这里的压栈的动作是由调用者来完成
的。当调用开始,
被调用者得到控制权,它可以对寄存器操作,而当调用结束,被调用者失去控制权,调
用者重新得到控
制权,此时它期望它所用到的某些寄存器恢复其调用前的状态,尤其是栈指针,这就牵
涉到栈的维护问
题。前面提到__stdcall和__fastcall调用约定均是被调用的函数在返回前清理传送参数
的内存栈,而
__cdecl调用约定是由调用者来维护用于传送参数的栈。下面举例来说明。
void MyFunc1(int c );
.
.
.
void MyFunc2( )
{
int i=1;
....
MyFunc1( i );
....
}
我们看一下MyFunc2的实现过程:
1、__stdcall和__fastcall调用约定
....
mov eax,dword ptr [i]
push eax
call @ILT+445(?MyFunc1@@YGXH@Z)(0x014a11bd)
//调用结束栈指针已恢复,由被调用者在返回前恢复
....
2、__ cdecl调用约定
....
mov eax,dword ptr [i]
push eax
call @ILT+30(?MyFunc1@@YAXH@Z)(0x014a101e)
//调用结束栈指针未恢复
add esp,4
//调用者自己恢复栈指针
....
现在再回过头来看一下前面设计的实验,由于VB5支持的是标准API调用约定,类同
于__stdcall调
用约定,所以当动态库用__stdcall调用约定编译时,实验正常通过。而当动态库用__c
decl调用约定
编译时,实验一和实验二的现象能很好地说明问题,其实此时由于调用约定的不统一,
用于传送参数的
栈已遭到破坏,现象就是工作不正常。实验三中虽然仍用__cdecl编译,但在函数名前的
__stdcall才是
真正起作用的调用约定,故它也顺利通过。
五、总结与补充
上面结合实验描述了动态库技术的几个关键点:调用约定(或称调用协议)、名字
修饰约定、堆栈
与参数传递等。目的就是为了更深刻地理解该项技术,更好地在实际应用中使用该项技
术。
另外需要补充的是关于输出函数名的问题。前面一再强调,函数输出时“最好不带
有任何由编译(充实--学海无涯)
生成的名字修饰”,这一点是受限于编程语言中对函数命名的规则。VB虽然也有此规则
,但它仍然可以
通过别名使用带修饰的输出函数。VB使用动态库的语法:
Declare Sub name Lib "libname" Alias "aliasname" (arglist)
Declare Function name Lib "libname" Alias "aliasname" (arglist) As type
其中Alias(别名)可以作为一条使用带修饰字函数的途径。例如
int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z”
这是在C++环境__stdcall调用约定下得到的一个输出函数,在VB中可以如此描述:
Declare Function Test Lib "test32.dll" Alias "?Test1@@YGHPADK@Z"
(var1 as Byte,Byval var2 as long) As Long
这样一来,在VB应用程序中就可以使用Test来实际调用动态库Test32.dll中的Test1了。
我在实际应用中
有时也这样使用动态库,带修饰的函数名虽然有些复杂古怪,但它本身能够表达更多的
可用信息。