Windows动态链接库简介

要说为什么写这篇,大概只是希望它能“有用”吧。

Windows下面可执行的代码通常会存在两种文件里面,一种是 EXE 一种是 DLL。其中 EXE 大家都知道,双击就能运行。但是 DLL 双击的话,只会弹一些什么不知道如何打开文件啊、选择一个应用程序用来打开,这种。反正就是不让你开。

1. 链接

要说动态链接库的话,我觉得要从链接开始说起才能大概讲得清楚。那么先说链接,比如我有两个 C 语言的代码文件,一个叫 a.c 一个叫 b.c。完了在 a.c 里面只有这么一点内容:

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

简单到爆了,就是给两个整数算和。然后在 b.c 里面调用它:

#include <stdio.h>
int add(int a, int b);
int main() { printf("%d", add(1, 2)); return 0; }

这样子的代码,编译链接之后执行,输出的结果是 3。……嗯,差不多是废话了。

现在我们来看看编译链接的过程。首先是编译, a.c 变成 a.obj,然后 b.c 变成 b.obj。也就是说,一个 .c 代码文件变成一个 .obj。这么说来,计算加法的那个代码,虽然在 b.c 里面调用,但是并不在 b.obj 里面,它在 a.obj 里面。
大概来研究一下 a.obj 里面到底是什么样的东西:这里要关注的,其实就里面的两个部分:一个部分叫“符号表”,a.c 里面的 add 函数,“add”这个名字就叫“符号”,编译成 obj 之后这个符号还保留着,链接的时候要用到;另一个部分叫“代码区”,add 函数里“把 a 和 b 拿出来加在一起又放到存返回值的地方”这一操作所对应的机器代码,就放在“代码区”里。然后符号表的“add”那边,就写了这个“add”对应的东西在代码区的哪里哪里。

然后再来看一下 b.obj 里面是什么。符号表里有 main,另外还多了个叫“重定位表”的东西,写明了有用到 printf 和 add,在哪里哪里用到,并没有说用到的东西在哪里(知道在哪里了还要链接器干嘛)。

好了现在我们拿到这么些东西,b.obj 里面写了“在这里这里用到 add”, a.obj 里写了“我这里有 add”。链接器看到了这两个,就把 a.obj 里面,代码区的 add 函数所在的位置,往 b.obj 里写了“我要add”的地方一填,就大功告成了。同理,printf 就是在标准库的符号表里面,找到 printf,然后把符号表的 printf 的那一栏里面标明的 printf 在代码区的哪里哪里,往 b.obj 里写了“我要printf”的地方一填,事情就差不多了。还有一步就是那个 main,程序运行起来的时候会先执行 C 语言标准库里的初始化代码,然后进 main 函数。至于 main 函数在哪里,标准库也不知道,于是标准库也就留了个地方,写上“我要main”,完了链接器把 main 在代码区里的位置朝这边一填,程序能运行了。

pic1

每个大格子里,最上面是文件名、库名;第二格是符号表,声明“我有什么”;第三格是代码区,放编译后的代码的;第四格是重定位表(可有可无),代表“我要什么”。这个是简化过的模型,实际场景中的 obj 文件和 库 文件要比这个复杂很多。

2. 动态链接

现在我们来改一改,刚才是 a.c 编译成 a.obj,然后 b.c 编译成 b.obj,最后 a.obj 和 b.obj 和标准库链接在一起,变成能跑的代码。现在改为, a.c 编译成 a.obj 然后链接成 a.dll, b.c 编译成 b.obj 链接成 b.exe,那么这其中会有什么变化……

根据上面的描述,要达到这样的效果,至少要有这么几个东西:一个是“我要什么”,一个是“我有什么”,然后才能搞起来。Windows系统确实是提供了这样的一套方式,但并不是上面 a.obj 的“我要什么”:你见过这么用的,是 DLL 而不是什么 OBJ 对吧。DLL 有 DLL 自己的“我有什么”的表示方式,这个就是“导出表”。导出表里面说白了也是上面说的符号表里的那些东西,但是和符号表不同:符号表的使用时机是链接的时候,这个时候有很多“内部符号”还留着的,这些“内部符号”在链接完成之后其实并没有什么用了。但是导出表就可以不必写这些东西:这个导出表又不是给链接器用的……

导出表存放在 exp 文件中。以刚才的 a.c 为例子,在 a.c 编译成 a.obj 之后,假如我要做的是一个 a.dll 文件,我有个 a.exp 文件里面是说了我要导出 add 函数,那就把 a.obj 和 a.exp 链接一下,生成 a.dll 就好了。画个图容易理解一些:

pic2

这个生成的 DLL 就包含了“我有什么”的部分了,有人要来找 add 的话,就能看到 a.dll 里有 add,还知道它在代码区的什么地方,要调用能调用了。

接下来还有一个,就是 b.obj 那边。现在 a.obj 变成了 a.dll, a.dll 又没有什么符号表之类的给你用,链接器没办法直接往 a.dll 上面链的:这一步是操作系统干的事情,其实不算链接器要干的事情。那么现在 b.obj 要做成 b.exe,需要有一种“DLL的仪式”,来声明“我要什么”。这个是“导入表”。导入表通常是 lib 文件,这种类型,写程序的人看到的应该比 exp 要多的多。导入表里其实也没什么太多东西,大致就是个“我要 xxx.dll 文件里的 xxx 函数,给我放到 xxx 全局变量里”。这个“全局变量”呢,就是个函数指针。这么讲有点抽象,我再画个图就简单多了:

pic3

那么这样的 lib 文件和 b.obj 链接的时候,就是这么一种景象:

pic4

从这里面可以看到,它也有 b.obj 中重定位表需要找的“符号表”,里面有个 add。然后它也有代码区,里面确实是 add 的代码,不过是假的 add 的代码:它的功能就是把调用转发给 __imp_add 函数指针指向的函数。这个函数指针是属于导入表的一部分,导入表写明了这个函数指针要指向的是“a.dll 文件里的 add 函数”。这样 Windows 在启动 b.exe 文件的时候,就会看到它导入表里面写了“我要 a.dll 文件里的 add 函数”,Windows 就会加载 a.dll 文件,加载了之后把 a.dll 里导出表里写的 add 函数所在的地址,填入 __imp_add 全局变量。这样在 b.exe 执行到这个函数的时候,就会调用 a.dll 里的 add 了。

根据实际情况,有的时候也会跳过 先调用 add 再调用 __imp_add 函数指针 这样两步的步骤,而是简化为一步、直接去调用 __imp_add 函数指针。比如在声明 int add(int a, int b) 的时候加上 __declspec(dllimport) 这样的声明,生成的代码就会直接调用 __imp_add 函数指针。

3. 使用方法

说这么多,那么实际操作的时候应该是什么样的呢……我就以上面为例子,说说最简单的情况。

首先要介绍 def 文件,这种文件定义了导入导出的 DLL 叫什么名字,以及函数名是什么。基本语法非常简单,上面的情况就是:

LIBRARY a.dll
EXPORTS
add

第一行是 LIBRARY 后面加上 DLL 的文件名,第二行写 EXPORTS 单独一行,后面就都是函数名了,一个一行。比方说除了 add 还有一个 sub 函数,那就在下面加一行 sub 就行了。

之所以要介绍这种文件,是因为生成上面提到的 exp 文件和生成 lib 文件都要用到它。

1. 在 MSVC 里使用

不同编译器生成的方法不同,对于微软的 Visual Studio 来说,在命令行下面生成的方式是

>link /lib /out:a.lib /def:a.def
Microsoft (R) Library Manager Version 9.00.30729.207
Copyright (C) Microsoft Corporation.  All rights reserved.

LINK : warning LNK4068: /MACHINE not specified; defaulting to X86
   Creating library a.lib and object a.exp

第一行是命令,后面几行是输出。可以看到它同时生成了 exp 文件和 lib 文件。输出的时候有个提示说没有指定机器类型默认给你选 x86 了。这个地方可以用 /MACHINE 来指定你要做 32 位的 exp/lib 文件还是 64 位的 exp/lib 文件。然后拿着这个 exp 文件就可以生成 DLL 了,同样是命令行:

>cl /nologo /c a.c
a.c

>link /nologo /dll /out:a.dll a.exp a.obj

>

这里把 a.c 编译成了 a.obj 然后再和 exp 文件链接在一起,生成了 a.dll。为了验证确实导出了 add 函数,可以用 CFF Explorer 工具打开这个 DLL,切换到 Export Directory 视图。

然后对于上面说的 b.c,这么编译链接就能运行了:

>cl /nologo /c b.c
b.c

>link /nologo /out:b.exe b.obj a.lib

>b.exe
3
>

2. 在 Visual Studio 里使用

用图形化开发环境的来做这种东西确实是容易。在 Visual Studio 里面,这个例子在同一个解决方案里给 a.dll 和 b.exe 分别创建工程 a 和工程 b 就行。假设两个创建的时候都选了“空工程”,那么要设置的地方,一个是右键点工程 a 选属性,弹出来的“属性页”里在“配置属性——常规——项目默认值——配置类型”那边,改成“动态库”,然后到“配置属性——链接器——输入——模块定义文件”,输入 def 文件的文件名。def 文件和源代码文件放在同一个目录下,为了方便编辑也可以把它添加到工程中。这样在构建工程 a 的时候,就会同时生成 lib 文件和 exp 文件了。

对于工程 b 来说,因为和工程 a 是同属于同一个解决方案,所以直接在工程 b 的“引用”那一栏添加对工程 a 的引用就行了。如果不是同属于一个解决方案,那么同样是右键点工程 b 选属性,弹出来的“属性页”里在“配置属性——链接器——输入——附加依赖项”那边添加 a.lib 即可。

3. 在 MinGW-GCC 里使用

Windows 下开发,如果是做开源项目,那么用到 GCC 的概率还是蛮大的。在 MinGW 里,也有一套工具用来搞这个 DLL 的。也还是这么几个套路:生成 .exp 和生成 .lib,然后用什么命令编译链接。
生成 exp 和 lib 文件用的工具是 dlltool,命令行非常直白,看得懂英语的话,就是指定输入 def 文件是啥输出 lib 文件是啥输出 exp 文件是啥。

> dlltool --input-def=a.def --output-exp=a.exp --output-lib=a.lib

这货很高冷,啥也不输出(

得到 def 文件和 exp 文件之后,可以编译链接 a.c 和 b.c 了。命令行如下:

> gcc -c a.c

> gcc -shared -o a.dll a.exp a.o

> gcc -c b.c

> gcc -o b.exe b.o a.lib

> b.exe
3
>

《Windows动态链接库简介》上有2条评论

发表评论