Windows API编程之动态链接库(DLL)

2020-12-28 12:30

阅读:390

/*
 * testMain.c
 */
#include 
#include 

typedef int (* PGetMax)(int, int);

int main()
{
    int a = 2;
    int b = 3;
    
    HINSTANCE hDll;  // DLL句柄 
    PGetMax pGetMax; // 函数指针
    
    hDll = LoadLibrary(".\\Debug\\DLL_lib.dll");
    if (hDll == NULL) {
        printf("Can‘t find library file \"dll_lib.dll\"\n");
        exit(1);
    }
    
    pGetMax = (PGetMax)GetProcAddress(hDll, "GetMax");
    if (pGetMax == NULL) {
        printf("Can‘t find function \"GetMax\"\n");
        exit(1);
    }

    printf(" max(2, 3) = %d\n", pGetMax(2, 3));
    
    FreeLibrary(hDll);

    return 0;
}

此时,不再需要动态的.h文件和.lib文件,只需要提供.dll文件即可。在具体使用时,先用LoadLibrary加载Dll文件,然后用GetProcAddress寻找函数的地址,此时必须提供该函数的在Dll中的名字(不一定与函数名相同)。

    然后编译链接、运行,结果与前面的运行结果相同。

    下面将解释,为什么前面要去掉WINAPI调用约定(即采用默认的__cdecl方式)。我们可以先看看DLL_Lib.dll里面的链接符号。在cmd中运行命令:
    dumpbin /exports DLL_Lib.dll
得到如下结果:

Dump of file f:\code\DLLTest\Debug\Dll_lib.dll

File Type: DLL

  Section contains the following exports for DLL_Lib.dll

           0 characteristics
    4652C3B1 time date stamp Tue May 22 18:19:29 2007
        0.00 version
           1 ordinal base
           1 number of functions
           1 number of names

    ordinal hint RVA      name

          1    0 0000100A GetMax

  Summary

        4000 .data
        1000 .idata
        3000 .rdata
        2000 .reloc
        28000 .text

可以看到GetMax函数在编译后在Dll中的名字仍为GetMax,所以在前面的程序中使用的是:
    pGetMax = (PGetMax)GetProcAddress(hDll, "GetMax");

    然后,我们把WINAPI添加回去,重新编译DLL_Lib工程。运行刚才的DLL_Test程序,运行出错,结果如下:
> process attach of dll
Can‘t find function "GetMax"
> process detach of dll
Press any key to continue

    显然,运行失败原因是因为没有找到GetMax函数。再次运行命令:dumpbin /exports DLL_Lib.dll,结果如下(部分结果):

 

  1 ordinal base
           1 number of functions
           1 number of names

    ordinal hint RVA      name

          1    0 0000100A _GetMax@8

 

从上面dumpbin的输出看,GetMax函数在WINAPI调用约定方式下在DLL里的名字与源码中的函数定义时的名字不再相同,其导出名是"_GetMax@8"。此时,你把testMain.c中的函数指针类型声明和函数查找语句作如下修改:
    typedef int (WINAPI* PGetMax)(int, int);
    pGetMax = (PGetMax)GetProcAddress(hDll, "_GetMax@8");
再次编译链接,然后运行,发现结果又正确了。

    现在找到了问题所在。很显然,这种修改方式并不适用,而默认生成的名字又不是我们所想要的。那么该怎么解决这个问题呢?这就需要用到.def文件来解决。

模块定义文件(.def)

    模块定义文件(.def文件)是一个描述DLL的各种属性的文件,可以包含一个或多个模块定义语句。如果你不使用关键字__declspec(dllexport)关键字导出DLL中的函数,那么DLL就需要一个.def文件。

    一个最小的.def文件必须包含下面的模块定义语句:
    (1)文件中第一个语句必须是LIBRARY语句。该语句标记该.def文件属于哪个DLL。语法形式为:LIBRARY 
    (2)EXPORTS语句列表。第一个导出语句的形式为:entryname[=internalname] [@ordinal],列出DLL中要导出的函数的名字和可选的序号(ordinal value)。要导出的函数名可以是程序源码中的函数名,也可以定义新的函数别名(但后面必须紧跟[=]);序号必须在范围1到N之间且不能重复,其中N是DLL中导出的函数个数。因此,EXPORTS语句语法形式为:
    EXPORTS
        [=]
        [=]
        ;...
    (3)虽然不是必须的,一个.def文件也常常包含DESCRIPTION语句,用来描述该DLL的用途之类,语法形式为:
    DESCRIPTION ""
    (4)在任意位置,可以包含注释语句,以分号(;)开始。

    例如,在本文中后面将用到的.def文件为:

; DLL_Lib.def

LIBRARY DLL_Lib     ; the dll name
DESCRIPTION "Learn how to use the dll."

EXPORTS
    GetMax @1
    Max=GetMax @2   ; alias name of GetMax

; Ok, over

现在,让我们回到DLL_Lib工程,修改GetMax函数的声明,把EXPORT去掉,重新编译该工程。然后,运行dumpbin命令,我们发现此时没有导出函数。再将上面的DLL_Lib.def文件添加进DLL_Lib工程,再次编译,并运行dumpbin命令,得到如下结果(引用部分结果):

     1 ordinal base
          2 number of functions
          2 number of names

   ordinal hint RVA      name

         1    0 0000100A GetMax
         2    1 0000100A Max

正如我们所预期的,有两个导出函数GetMax和Max。注意,此时源码中的GetMax函数的导出名不再是默认的“_GetMax@8”。另外,需要注意的是,两个导出函数有相同的相对虚拟地址(RVA),也说明了两个导出名实质是同一个函数的不同名字而已,都是源码中GetMax函数的导出名。

    现在,回到DLL_Test工程,修改testMain.c文件内容如下:

/*
 * testMain.c
 */
#include 
#include 

typedef int (WINAPI* PGetMax)(int, int);

int main()
{
    int a = 2;
    int b = 3;
    
    HINSTANCE hDll; // DLL句柄 
    PGetMax pGetMax; // 函数指针
    
    hDll = LoadLibrary(".\\Debug\\DLL_lib.dll");
    if (hDll == NULL) {
        printf("Can‘t find library file \"dll_lib.dll\"\n");
        exit(1);
    }
    
    pGetMax = (PGetMax)GetProcAddress(hDll, "GetMax");
    if (pGetMax == NULL) {
        printf("Can‘t find function \"GetMax\"\n");
        exit(1);
    }
    printf(" GetMax(2, 3) = %d\n", pGetMax(2, 3));

    pGetMax = (PGetMax)GetProcAddress(hDll, "Max");
    if (pGetMax == NULL) {
        printf("Can‘t find function \"GetMax\"\n");
        exit(1);
    }
    printf(" Max(2, 3) = %d\n", pGetMax(2, 3));
    
    FreeLibrary(hDll);
    return 0;
}

 编译链接、运行,结果如下:

> process attach of dll
 GetMax(2, 3) = 3
 Max(2, 3) = 3
> process detach of dll
Press any key to continue

    运行结果正如前面分析的那样,GetMax和Max都得到了相同的结果。

    到这里,我们解决了DLL导出函数名在各种调用约定下的默认名可能不同于源码中函数名的问题。此时,你就可以制作跟Windows的自带API函数库相同的库了:使用__stdcall调用约定以满足Windows下的任何语言都可以调用DLL库,同时使用函数名作为导出名,以方便用户使用DLL里的函数。

 
 
导出全局变量
 
    前面我们介绍了DLL中的函数的导出方法,这里也介绍一下DLL中全局变量的导出。
 
    首先需要明确的是,当多个应用程序同时使用同一个DLL时,系统中只有一个DLL实例(这里主要指代码段,一般不包含数据段)。也就是说,如果没有特殊处理,DLL中的数据都是每个使用DLL的应用都保留一份副本的(但是,可以根据需要实现DLL数据的共享,后面进行介绍)。因此,使用DLL的各应用程序之间不会发生干扰。
 
    要导出DLL中的全局变量,方法与导出函数基本一样。只是,在定义.def文件时,在EXPORTS定义语句之后用DATA标识符表明这是变量。例如:g_oneNumber DATA 或者 g_oneNumber @3 DATA
 
    在使用DLL中导出的全局变量时,对于前面DLL的两种链接方式,有不同的方法。其中,对于运行时链接的DLL,其使用方法与函数一样(流程:LoadLibrary, GetProcAddress),只是在使用时要知道这是一个变量的地址,而不再是一个函数的地址即可(其实,用dumpbin工具查看DLL的导出列表,会发现导出的数据也被当作函数计数)。 对于装载时链接,要导入DLL中的变量,有点与函数不一样的地方,那就是必须显示地用关键字__declspec(dllimport)导入DLL中的变量。例如,在使用前面的g_oneNumber前,应先导入:__declspec(dllimport) extern int g_oneNumber。然后,其它与函数的使用方法无异。
 
共享DLL中的数据
 
    有时,可能需要在使用DLL的多个应用之间共享DLL的数据,而默认情况下,DLL的数据是每个应用拥有一份副本的。要实现这个需求,就需要做些特殊处理。
 
    首先,定义一个数据段,里面有需要共享的变量,并要初始化这些变量。然后设置该数据段为共享即可,比较简单。例如,要在DLL中共享int型变量g_oneNumber,那么应按如下方式定义该变量:
#pragma data_seg ("shared")       
int g_oneNumber = 0;
#pragma data_seg ()
 
#pragma comment(linker,"/SECTION:shared,RWS")
 
    对上面的代码做些解释:#pragma data_seg ("shared")创建了一个数据段,命名为Shared;#pragma data_seg()标记该数据段的结束;它们之间定义的是该数据段中的变量。注意:这里对变量的初始化是必须的,否则,编译器会把未初始化的变量放在普通的未初始化数据段,而不是在共享的数据段。
    #pragma comment(linker, "SECTION:shared,RWS")告诉链接器shared数据段具有RWS属性。这里的RWS是指Read、Write和Shared三个属性。也可以在IDE中设置工程属性:在Settings|Link|Project Options中,添加链接参数:/SECTION:shared,RWS。
 
 
资源DLL的制作及使用
 
    有了前面的基础,资源DLL的制作及使用相对简单多了。如果是纯资源DLL的话(没有导出函数),那么只需要定义一个有DLLMain函数的文件即可,然后加入资源,编译成DLL库即可。在使用时,只需要动态加载这个资源库,然后加载库里的资源即可。例如,资源库里有位图资源,那么只需要LoadBitmap即可。
 

 


评论


亲,登录后才可以留言!