Win32动态链接库的创建与使用 -> VC++深入浅出

1. 静态库与动态库的区别

静态库

函数和数据被编译进一个二进制文件(.LIB)。在使用静态库下,在编译连接可执行文件时,链接器从库中复制这些函数和数据,并把它们和应用程序的其他模块组合起来创建最终的可执行文件(.EXE)。当产品发布时,只需要发布可执行文件,不需要发布使用的静态库。

特点

  1. 编译后的可执行文件包含了所需要的函数的代码,占用磁盘空间较大。(但是可以避免出现用户的电脑上没有你开发时所用的库的尴尬情形。)
  2. 如果多个调用相同库的进程在内存中同时运行,内存中会存放多份相同的代码

动态库

在使用动态库的时候,往往提供两个文件:引入库(.lib)文件和DLL(.dll)文件引入文件包含DLL导出的函数和变量的符号名,而.dll包含了该DLL的实际的函数和数据。在使用动态库的情况下,在编译连接可执行文件时,只需要连接该DLL的引入库文件,而该DLL的函数代码和数据并不复制到可执行文件中,直到可执行程序运行时,才加载所需的DLL,将该DLL映射到进程的地址空间中,然后访问DLL中导出的函数。此时,在发布产品时,除了发布可执行文件外,还要发布程序将要调用的动态链接库

特点

  • 使用动态库的好处在于能够节省磁盘空间和内存。如果多个应用程序需要访问同样的功能,那么可以将该功能以DLL的形式提供,这样一台机器上只需要存在一份该DLL就可以了,从而节省了磁盘空间。如果多个程序调用同一个DLL,该DLL的页面只需要存放在内存一次,所有的应用程序都可以共享它的页面了。

2. 示例

首先,我们新建一个空的Win32动态链接库,为其增加两个函数:

int add(int a, int b)
{
    return a + b;
}

int subtract(int a, int b)
{
    return a - b;
}

当build之后,在这个工程的Debug目录下,就会有一个对应于工程明的dll文件。这个dll目前是无法使用的,因为这两个函数都没有被“导出”。我们可以利用Visual Studio提供的命令行工具Dumpbin来查看:

D:\MyPrograms\mfc\CH_19_DLL1\Debug>dumpbin -exports CH_19_DLL1.dll
Microsoft (R) COFF Binary File Dumper Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.


Dump of file CH_19_DLL1.dll

File Type: DLL

  Summary
        7000 .data
        1000 .idata
        2000 .rdata
        2000 .reloc
       2A000 .text

其中并没有与函数有关的信息。

为了让DLL导出一些函数,需要在每个要被导出的函数前面加上标示符_declspec(dllexport)

此时再次查看,就可以看到:

 ordinal hint RVA      name

        1    0 0000100A ?add@@YAHHH@Z
        2    1 00001005 ?subtract@@YAHHH@Z

其中?add@@YAHHH@Z和?subtract@@YAHHH@Z的意思在
https://blog.csdn.net/VonSdite/article/details/81165308这里有解释过。

  • 大致过程如下:
    首先,编译器会给函数名前加一个?;其次,因为是__cdecl调用,所以后面加上YA;再次,H代表的是int类型,3个H分别表示的是返回值,第一个参数,第二个参数,最后以@Z结尾。

隐式链接方式和显示加载

有了动态链接库以后,我们就要在程序中加载它们。
有两种可选的方式:

  • 隐式链接方式加载DLL
  • 显示加载DLL。

隐式加载

我们先看隐式加载。我们新建一个MFC对话框应用程序,然后给上面放两个按钮,一个用来做加法,另一个用来做减法。消息响应函数如下:

extern int add(int a, int b);
extern int subtract(int a, int b);

void CCH_19_DllTestDlg::OnBtnAdd() 
{
    // TODO: Add your control notification handler code here
    CString str;
    str.Format("5 +3 = %d",add(5,3));
    MessageBox(str);
}

void CCH_19_DllTestDlg::OnBtnSubtract() 
{
    // TODO: Add your control notification handler code here
    CString str;
    str.Format("5 - 3 = %d",subtract(5,3));
    MessageBox(str);    
}

在其中因为要调用动态链接库中的add和subtract函数,所以在这里声明为外部函数

具体如何加载呢?
从我们的动态链接库的debug目录下,将*.lib文件复制到MFC程序所在的文件夹下,然后在工程->设置->链接中增加整个lib文件,程序就能编译链接通过了。我们可以使用Dumpbin来查看:

D:\MyPrograms\mfc\CH_19_DllTest\Debug>dumpbin -imports CH_19_D
Microsoft (R) COFF Binary File Dumper Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

Dump of file CH_19_DllTest.exe

File Type: EXECUTABLE IMAGE

  Section contains the following imports:

    CH_19_Dll1.dll
                4052C0 Import Address Table
                40508C Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference


                   1  ?subtract@@YAHHH@Z
                   0  ?add@@YAHHH@Z

其他部分省略。
我们看到,这个可执行文件需要导入subtract和add两个函数。
此时,程序还是不能执行的,因为.exe文件并不知道从哪里获得.dll文件,此时,编译器会依次在当前可执行文件的目录(.exe所在的目录)当前目录(.cpp所在的目录)系统目录环境变量目录中查找。我们可以将对应的.dll文件也拷过去。程序就能成功执行了。

除了使用Dumpbin之外,我们也可以使用vc++提供的一个可视化的工具:Depends来查看

除了使用extern之外,我们还可以使用__declspec(dllimport)来表明该函数是从动态链接库中加载的。即:

//extern int add(int a, int b);
//extern int subtract(int a, int b);
__declspec(dllimport) int add(int a, int b);
__declspec(dllimport) int subtract(int a, int b);

这只是一个说明性的例子,在实际中,我们通常不是这样编写程序的。因为当我们把dll交给用户以后,用户并不知道dll中拥有那些函数,只能通过前面介绍的Dumpbin和Depends等编译工具来猜测函数的原型,这是很不方便的。正确的做法是,为这个dll增加一个头文件,在头文件添加函数的声明及注释。在头文件添加:

__declspec(dllimport) int add(int a, int b);
__declspec(dllimport) int subtract(int a, int b);

注意,因为头文件是给用户使用的,所以是指明这些函数是从dll中导入的。而在我们的对话框程序中,添加:

#include "..\CH_19_Dll1\CH_19_Dll1.h"

有的时候,我们希望dll库中的函数不仅可以为客户使用,也能够让库自身调用,那么我们就要利用预编译指令来实现了,在头文件:

#ifdef DLL1_API
#else
#define DLL1_API __declspec(dllimport)
#endif
DLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);

首先判断是否定义DLL1_API,如果定义,则什么也不做,否则就会将DLL1_API定义为__declspec(dllimport),然后用__declspec(dllimport)代替DLL1_API
而在源文件中:

#define DLL1_API _declspec(dllexport)
#include "CH_19_Dll1.h"


int add(int a, int b)
{
    return a + b;
}

int subtract(int a, int b)
{
    return a - b;
}

此时这些函数就跟我们之前写的函数一样了,可以互相调用
我们仔细分析一下。在编译该dll时,源文件会定义DLL1_API,然后将头文件展开。此时因为已经定义了DLL1_API,所以直接编译函数的声明,表明这个函数是从动态链接库中导出的。
而当用户的程序调用该dll时,只要用户程序中没有定义DLL1_API,那么就会定义DLL1_API__declspec(dllimport)

导出类

我们下面看看如何从动态链接库中导出C++类:
在头文件中声明:

class DLL1_API Point
{
public:
    void output(int x, int y);
};

在源文件中定义:

void Point:: output(int x, int y)
{
    //获得当前窗口的句柄
    HWND hwnd = GetForegroundWindow();
    //获取DC
    HDC hdc = GetDC(hwnd);
    char buf[20];
    memset(buf,0,20);
    sprintf(buf,"x = %d, y= %d", x, y);
    TextOut(hdc,0,0,buf,strlen(buf));
    ReleaseDC(hwnd,hdc);
}

注意到,这里使用了windows函数,和输出函数,所以得包含头文件Windows.h、stdio.h。
在应用程序中,新增一个按钮来调用这个类的成员函数:

void CCH_19_DllTestDlg::OnBtnOutput() 
{
    // TODO: Add your control notification handler code here
    Point pt;
    pt.output(5,3);
}

我们可以再利用Dumpbin看看这个dll:

         1    0 00001014 ??4Point@@QAEAAV0@ABV0@@Z
         2    1 0000100A ?add@@YAHHH@Z
         3    2 0000100F ?output@Point@@QAEXHH@Z
         4    3 00001005 ?subtract@@YAHHH@Z

其中的?4Point@@QAEAAV0@ABV0@@Z是构造函数,?output@Point@@QAEXHH@Z中,@Point是表明它是Point类的函数,QAE表示它是public函数,X表示返回值类型为void,HH表示有两个int类型的参数。
其实,我们完全可以不导出整个类,而是只导出其中的若干函数:

class Point
{
public:
    void DLL1_API output(int x, int y);//导出该函数
    void test();
};

下面的问题与前面的问题紧密相关,C++编译器生成dll时,会对导出的函数名进行改编,但是不同的编译器的改变规则不完全相同,特别的,如果是一个纯C的编译器序使用这个dll,则由于改编名字的不同,dll中的函数就不能被找到了
因此,我们希望动态链接库文件在编译时,导出函数的名称不要发生改变。我们可以使用extern "C"来实现,指定的内容用C的方式编译:

#ifdef DLL1_API
#else
#define DLL1_API extern "C"__declspec(dllimport)
#endif
DLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);

#define DLL1_API extern "C"_declspec(dllexport)
#include "CH_19_Dll1.h"
//#include <Windows.h>
#include <stdio.h>

int add(int a, int b)
{
    return a + b;
}

int subtract(int a, int b)
{
    return a - b;
}

注意,由于是使用的C语言,所以就得把类相关的代码注释掉了。我们再用dumpbin看看这个dll:

   ordinal hint RVA      name


         1    0 0000100A add
         2    1 00001005 subtract

此时,名字没有发生改编。

引起名字改编的还有调用约定,在默认情况下,使用的cdecl:参数由右向左压栈,由调用者清栈;假如我们改为_stdcall:参数由右向左,由被调用的函数自己清栈,那么函数的名字又会变为:

   ordinal hint RVA      name


         1    0 00001005 _add@8
         2    1 0000100A _subtract@8

模块定义

我们可以通过模块定义文件来解决不同编译器、不同语言之间的编译器将函数明修饰的不一样的问题。我们重新建立一个动态链接库,为其增加一个.cpp文件:
int add(int a, int b)
{
return a + b;
}

int subtract(int a, int b)
{
return a - b;
}

然后,我们在它的目录下增加一个记事本文件,并把后缀名改为.def,使用vc++将其打开,添加如下代码:

LIBRARY CH_19_Dll2

EXPORTS
add
subtract

其中第一行是动态链接库的名称,EXPORTS语句表明的是将要导出的函数的名字,关于它的详细用法,可以参考MSDN。此时,导出函数的名称就统一了。

显示加载

说了这么多,都是隐式加载的。我们现在看看如何显式加载,先看程序:

void CCH_19_DllTestDlg::OnBtnAdd() 
{
    // TODO: Add your control notification handler code here
    HINSTANCE hInst;
    //加载动态链接库
    hInst = LoadLibrary("CH_19_Dll2.dll");
    typedef int(*ADDPROC)(int a, int b);
    //获取函数地址
    ADDPROC Add = (ADDPROC)GetProcAddress(hInst,"add");
    if(!Add)
    {
        MessageBox("获取函数地址失败");
        return ;
    }
    CString str;
    str.Format("5 +3 = %d",Add(5,3));
    MessageBox(str);
}

注意,其中GetProcAddress的第二个参数,函数名通过.def指定。

我们看到,隐式加载实现起来更为简单,加载好以后就可以直接使用;而显示加载更为灵活,可以在需要时才加载dll。但是他的麻烦之处就在于一旦编译器生成的新的函数名发生变化,那么就得修改。比如,如果dll中的函数调用方式改为stdcall,那么这里的函数指针也得改为stdcall。而且如果dll并没有使用.def来指定导出的名字,那么这里就得使用编译器修改后的名字:?add@@YAHHH@Z,或者使用访问序号:

    ADDPROC Add = (ADDPROC)GetProcAddress(hInst,MAKEINTRESOURCE(1));

这个访问序号也可以通过dumpbin获得

  ordinal hint RVA      name


        1    0 00001005 ?add@@YAHHH@Z

当然,在实际应用中,建议还是使用名字比较好,毕竟有意义的名字好于序号

其实,在windows加载dll时需要一个入口函数,就如同控制台或DOS程序需要main函数、WIN32程序需要WinMain函数一样。这个函数称为DllMain,声明如下:

BOOL WINAPI DllMain(  HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved  );

其中第二个参数是调用的原因,可以使用一个switch/case语句来对于每种情况分别处理。这个函数是一个可选的函数,由于我们的dll只完成一些简单的功能,所以没有使用。需要注意的是,在这个函数中不要进行太复杂的的调用。因为此时一些核心动态库,比如user32.dll或者GDI32.dll等还没有加载,如果我们的编写的DllMain函数需要调用这两个库的某些函数的话,则会出错。

猜你喜欢

转载自blog.csdn.net/VonSdite/article/details/81474388