深入动态库

一、动态连接库的用途 
   动态连接库,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 <windows.h> 

     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了。 

我在实际应用中 

有时也这样使用动态库,带修饰的函数名虽然有些复杂古怪,但它本身能够表达更多的 

可用信息。 
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章