0%

关于DLL的一些你不会想要知道的知识

英文版本 Everything You Never Wanted To Know About DLLs.


最近因为一些原因,我需要调研动态链接在Windows平台上的实现细节。这篇文章主要是总结我在这个问题上所学到的知识,用于我将来的回顾和参考,但同时我也希望这篇文章对其他人所有帮助,因为我将要总结的这些内容,你可能需要东找西找才能找到。

废话不多说,让我们开始这趟旅程吧:

导出和导入

Windows可执行文件加载器(Windows executable loader)负责在运行程序前完成所有动态加载和符号解析工作。链接器会分别计算出每一个可执行镜像(可执行镜像是一个DLL或者一个EXE文件)导出和导入了哪些函数,这个过程是通过检查可执行镜像的.edata段和.idata段来进行的。

关于.edata段和.idata段的详细信息,在PE/COFF specification这篇文档中有详细说明。

.edata段

.edata段记录了可执行镜像导出的符号(是的,EXE也可以导出符号)。主要包括:

  • 导出地址表(export address table):一个长度为N的数组,保存了导出的函数/变量的地址(相对于可执行镜像起始地址的相关地址)。这张表的索引称之为序号(ordinals)。
  • 导出名称地址表(export name pointer table):一个长度为M的并行数组(译者记:其实就是两个大小一样的数组,一个保存key,一个保存对应的value),保存的是符号地址到导出名称的映射。这个平行数组是按照导出名称的字典序排序的,从而允许进行对一个给定的导出符号名称进行二分查找。
  • 导出序号表(export ordinal table):也是一个长度为M的并行数组,保存了序号到对应的导出名称的映射,其中导出名称对应的是导出名称地址表中的键key。

(作为通过名称导入符号这一做法的另一种替代方法,也可以通过指定序号来导入一个符号。通过序号来导入符号的做法在运行时会稍微快一些,因为这种情况下动态链接器(dynamic linker)不需要进行查找(译者记:根据名称在导出名称地址表中找到名称对应的地址)。此外,如果导出符号的DLL并没有给某个导出项分配名称,那么通过序号来导入符号是唯一的可行之路。)

那么.edata段最初是怎样被创建的呢?主要有两种方法:

  1. 最常见的一种情况,在编译生成一些目标文件(object files)时会创建.edata段。这些目标文件对应的源码中,定义了一些带__declspec(dllimport)修饰符的函数或者变量。于是编译器就会产生一个包含了这些导出项的.edata段。

  2. 另一种比较少见的情况,开发人员会写一个.def文件,指定那些函数需要被导出。将这个.def文件提供给dll tool --output-exp,就能产生一个导出文件(export file)。导出文件是一个仅包含.edata段的目标文件,导出了在.def文件中声明的符号(导出了一些未解析的引用,通常链接器会填写这些引用到一个实际的地址)。程序员在将这些目标文件链接成DLL的时候,必须对这些导出库(export library)进行命名。

    对于以上两种情况,链接器在链接时都会从所有的目标(objects)中收集.edata段,用于给整个可执行镜像文件创建一个.edata段。最后一种可能的方式是.edata段可以被链接器自身所创建,不需要将.edata段放入到任何目标文件中:

  3. 链接器可以选择在链接时导出目标文件中的所有符号。例如,这是GNU ld的默认行为(也可以通过–export-all-symbols显式地指定这种行为)。在这种情况下,由链接器来产生.edata段。(GNU ld也支持在命令行中指定一个.def文件,然后产生的.edata段就只会导出这个.def文件中声明的符号)。

.idata段

.idata段记录了可执行镜像导入的符号信息。包括:
对于导入符号涉及到的每一个可执行镜像:

  • 可执行镜像的文件名。被动态链接器用于在磁盘上查找该文件。
  • 导入符号查找表(import lookup table):一个长度为N的数组,每一项要么是一个序号,要么是一个指向导入名称字符串的指针。
  • 导入符号地址表(import address table):一个长度为N的指针数组。动态链接器会用从导入符号查找表中对应顺序的符号的地址来填写该数组。

.idata段中的条目以如下方式被创建:

  1. 最常见的一种情况,这些条目来自目标文件中的导入库(import library)。可以对你希望导出符号的DLL或者我们之前讨论过的.def文件使用dlltool工具来创建导入库。和导出库一样,用户必须在链接这些导入库时指定名称。

  2. 或者,有一些链接器(像GNU ld)也可以让你在链接时直接指定DLL文件。对于你需要从这些DLL文件中导入的符号,链接器会自动产生相应的.idata段条目。

注意,跟导出符号不一样,__declspec(dllimport)修饰符并不会导致产生相应的.idata段。

比起第一次出现,导入库有点更复杂了。Windows动态加载器将导入符号(例如,函数Func的地址)的地址填入到导入符号地址表中。然而,当其它目标文件中的汇编代码执行call Func时,它们期待的是用Func来命名那段code的地址。但我们直到运行时的时候才知道这个地址:我们能够静态地得知的事情只有动态链接器会将这个地址存放在哪个地方。我们称这个地方为_imp_Func。
为了处理这一层额外的中间层,导入库导出的函数Func仅仅间接引用了_imp_Func(来获得实际的函数指针),然后执行jmp跳转到它。同一个工程中的所有其它的目标文件现在可以调用call Func,仿佛Func已经在其它目标文件而不是其它DLL中定义过了一样。基于这个原因,动态链接的函数的声明上的__declspec(dllimport)只是可有可无的(尽管实际上如果加上这个修饰符的话,代码的效率会有轻微的提升,我们之后会谈到这一点)。
不幸的是,如果你想要从另一个DLL中导入变量,则并没有类似的技巧。如果我们有一个导入的变量myData,并没有一种方法来定义一个导入库,使得链接到这个导入库的目标文件可以通过执行mov $eax, myData来写入到myData所在的内存位置。取而代之的是,导入库定义了一个符号__imp__myData,这个符号解析到一个可以找到myData链接地址的地方。然后编译器就会保证当你在读写用__declspec(dllimport)定义的变量时,这些读写其实是通过__imp_myData来间接进行的。因为需要在使用的时候再产生不同的代码,因此在导入变量时的__declspec声明是不可省去的。

应用实例

理论都很好,但在实践中看看所有这些的这些部分会对我们很有帮助。

构建DLL

首先,让我们来构建一个简单的DLL,同时导出了函数和变量。为了最大化地进行说明,我们将使用显式地导出库,而不是用declspec(dllexport)来修饰我们的函数,也不是提供一个.def文件给链接器。
先创建一个.def文件,library.def:

1
2
3
4
LIBRARY library
EXPORTS
function_export
data_export DATA

DATA关键词和LIBRARY这一行仅仅影响到导入库如何被产生,之后本文会解释这一点。现在请暂时忽略这个。)
然后构建一个导出文件:

1
$ dlltool --output-exp library_exports.o -d library.def

产生的目标文件基本上只包含了一个.edata段,导出了符号_data_export_function_export,名称分别是data_exportfunction_export

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
$ objdump -xs library_exports.o

...

There is an export table in .edata at 0x0

The Export Tables (interpreted .edata section contents)

Export Flags 0
Time/Date stamp 4e10e5c1
Major/Minor 0/0
Name 00000028 library_exports.o.dll
Ordinal Base 1
Number in:
Export Address Table 00000002
[Name Pointer/Ordinal] Table 00000002
Table Addresses
Export Address Table 00000040
Name Pointer Table 00000048
Ordinal Table 00000050

Export Address Table -- Ordinal Base 1

[Ordinal/Name Pointer] Table
[ 0] data_export
[ 1] function_export

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000000 00000000 00000000 00000000 2**2
ALLOC, LOAD, READONLY, CODE
1 .data 00000000 00000000 00000000 00000000 2**2
ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000000 2**2
ALLOC
3 .edata 00000070 00000000 00000000 000000b4 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
SYMBOL TABLE:
[ 0](sec -2)(fl 0x00)(ty 0)(scl 103) (nx 1) 0x00000000 fake
File
[ 2](sec 4)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000028 name
[ 3](sec 4)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000040 afuncs
[ 4](sec 4)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000048 anames
[ 5](sec 4)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000050 anords
[ 6](sec 4)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000054 n1
[ 7](sec 4)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000060 n2
[ 8](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .text
AUX scnlen 0x0 nreloc 0 nlnno 0
[ 10](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .data
AUX scnlen 0x0 nreloc 0 nlnno 0
[ 12](sec 3)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .bss
AUX scnlen 0x0 nreloc 0 nlnno 0
[ 14](sec 4)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .edata
AUX scnlen 0x70 nreloc 8 nlnno 0
[ 16](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000000 _data_export
[ 17](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000000 _function_export


RELOCATION RECORDS FOR [.edata]:
OFFSET TYPE VALUE
0000000c rva32 .edata
0000001c rva32 .edata
00000020 rva32 .edata
00000024 rva32 .edata
00000040 rva32 _data_export
00000044 rva32 _function_export
00000048 rva32 .edata
0000004c rva32 .edata


Contents of section .edata:
0000 00000000 c1e5104e 00000000 28000000 .......N....(...
0010 01000000 02000000 02000000 40000000 ............@...
0020 48000000 50000000 6c696272 6172795f H...P...library_
0030 6578706f 7274732e 6f2e646c 6c000000 exports.o.dll...
0040 00000000 00000000 54000000 60000000 ........T...`...
0050 00000100 64617461 5f657870 6f727400 ....data_export.
0060 66756e63 74696f6e 5f657870 6f727400 function_export.

我们将会用一个简单的DLL实现来提供这些符号,library.c:

1
2
3
4
5
int data_export = 42;

int function_export() {
return 1337 + data_export;
}

打包到一个DLL中:

1
$ gcc -shared -o library.dll library.c library_exports.o

这个DLL的导出符号表如下,可见我们已经导出了我们所需的符号信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
The Export Tables (interpreted .edata section contents)

Export Flags 0
Time/Date stamp 4e10e5c1
Major/Minor 0/0
Name 00005028 library_exports.o.dll
Ordinal Base 1
Number in:
Export Address Table 00000002
[Name Pointer/Ordinal] Table 00000002
Table Addresses
Export Address Table 00005040
Name Pointer Table 00005048
Ordinal Table 00005050

Export Address Table -- Ordinal Base 1
[ 0] +base[ 1] 200c Export RVA
[ 1] +base[ 2] 10f0 Export RVA

[Ordinal/Name Pointer] Table
[ 0] data_export
[ 1] function_export

使用DLL

当我们回过头来看看如何使用DLL时,事情变得更有有趣了。首先,我们需要一个导出库:

1
$ dlltool --output-lib library.dll.a -d library.def

(我们使用导入库library.dll.a而不是直接使用导出符号的对象文件library_exports.o,是因为使用库来导入允许链接器忽略.idata段中并没有被使用到的符号。而相反的是链接器无法忽略.edata段中的任何符号,因为任何一个符号都可能被这个DLL的使用者用到)。
导入库是相当复杂的。对于每一个导入符号,导入库中都包含一个对应的目标文件(disds00000.odisds00001.o),同时也包含了其它两个目标文件(distdt.odisdh.o),用于设立导入列表的头部和尾部。(导入列表的头部除了其它的一些东西,还包含了在运行时需要链接的DLL的名字,这是从.def文件的LIBRARY一行派生而来的。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
$ objdump -xs library.dll.a
In archive library.dll.a:

disdt.o: file format pe-i386

...

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000000 00000000 00000000 00000000 2**2
ALLOC, LOAD, READONLY, CODE
1 .data 00000000 00000000 00000000 00000000 2**2
ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000000 2**2
ALLOC
3 .idata$4 00000004 00000000 00000000 00000104 2**2
CONTENTS, ALLOC, LOAD, DATA
4 .idata$5 00000004 00000000 00000000 00000108 2**2
CONTENTS, ALLOC, LOAD, DATA
5 .idata$7 0000000c 00000000 00000000 0000010c 2**2
CONTENTS, ALLOC, LOAD, DATA
SYMBOL TABLE:
[ 0](sec -2)(fl 0x00)(ty 0)(scl 103) (nx 1) 0x00000000 fake
File
[ 2](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .text
AUX scnlen 0x0 nreloc 0 nlnno 0
[ 4](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .data
AUX scnlen 0x0 nreloc 0 nlnno 0
[ 6](sec 3)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .bss
AUX scnlen 0x0 nreloc 0 nlnno 0
[ 8](sec 4)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .idata$4
AUX scnlen 0x4 nreloc 0 nlnno 0
[ 10](sec 5)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .idata$5
AUX scnlen 0x4 nreloc 0 nlnno 0
[ 12](sec 6)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .idata$7
AUX scnlen 0x7 nreloc 0 nlnno 0
[ 14](sec 6)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000000 __library_dll_a_iname


Contents of section .idata$4:
0000 00000000 ....
Contents of section .idata$5:
0000 00000000 ....
Contents of section .idata$7:
0000 6c696272 6172792e 646c6c00 library.dll.

disdh.o: file format pe-i386

...

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000000 00000000 00000000 00000000 2**2
ALLOC, LOAD, READONLY, CODE
1 .data 00000000 00000000 00000000 00000000 2**2
ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000000 2**2
ALLOC
3 .idata$2 00000014 00000000 00000000 00000104 2**2
CONTENTS, ALLOC, LOAD, RELOC, DATA
4 .idata$5 00000000 00000000 00000000 00000000 2**2
ALLOC, LOAD, DATA
5 .idata$4 00000000 00000000 00000000 00000000 2**2
ALLOC, LOAD, DATA
SYMBOL TABLE:
[ 0](sec -2)(fl 0x00)(ty 0)(scl 103) (nx 1) 0x00000000 fake
File
[ 2](sec 6)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000000 hname
[ 3](sec 5)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000000 fthunk
[ 4](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .text
AUX scnlen 0x0 nreloc 0 nlnno 0
[ 6](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .data
AUX scnlen 0x0 nreloc 0 nlnno 0
[ 8](sec 3)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .bss
AUX scnlen 0x0 nreloc 0 nlnno 0
[ 10](sec 4)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .idata$2
AUX scnlen 0x14 nreloc 3 nlnno 0
[ 12](sec 6)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000000 .idata$4
[ 13](sec 5)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000000 .idata$5
[ 14](sec 4)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000000 __head_library_dll_a
[ 15](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000000 __library_dll_a_iname


RELOCATION RECORDS FOR [.idata$2]:
OFFSET TYPE VALUE
00000000 rva32 .idata$4
0000000c rva32 __library_dll_a_iname
00000010 rva32 .idata$5


Contents of section .idata$2:
0000 00000000 00000000 00000000 00000000 ................
0010 00000000 ....

disds00001.o: file format pe-i386

...

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000008 00000000 00000000 0000012c 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 00000000 00000000 00000000 2**2
ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000000 2**2
ALLOC
3 .idata$7 00000004 00000000 00000000 00000134 2**2
CONTENTS, RELOC
4 .idata$5 00000004 00000000 00000000 00000138 2**2
CONTENTS, RELOC
5 .idata$4 00000004 00000000 00000000 0000013c 2**2
CONTENTS, RELOC
6 .idata$6 00000012 00000000 00000000 00000140 2**1
CONTENTS
SYMBOL TABLE:
[ 0](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000000 .text
[ 1](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000000 .data
[ 2](sec 3)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000000 .bss
[ 3](sec 4)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000000 .idata$7
[ 4](sec 5)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000000 .idata$5
[ 5](sec 6)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000000 .idata$4
[ 6](sec 7)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000000 .idata$6
[ 7](sec 1)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000000 _function_export
[ 8](sec 5)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000000 __imp__function_export
[ 9](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000000 __head_library_dll_a


RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000002 dir32 .idata$5


RELOCATION RECORDS FOR [.idata$7]:
OFFSET TYPE VALUE
00000000 rva32 __head_library_dll_a


RELOCATION RECORDS FOR [.idata$5]:
OFFSET TYPE VALUE
00000000 rva32 .idata$6


RELOCATION RECORDS FOR [.idata$4]:
OFFSET TYPE VALUE
00000000 rva32 .idata$6


Contents of section .text:
0000 ff250000 00009090 .%......
Contents of section .idata$7:
0000 00000000 ....
Contents of section .idata$5:
0000 00000000 ....
Contents of section .idata$4:
0000 00000000 ....
Contents of section .idata$6:
0000 01006675 6e637469 6f6e5f65 78706f72 ..function_expor
0010 7400 t.

disds00000.o: file format pe-i386

...

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000000 00000000 00000000 00000000 2**2
ALLOC, LOAD, READONLY, CODE
1 .data 00000000 00000000 00000000 00000000 2**2
ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000000 2**2
ALLOC
3 .idata$7 00000004 00000000 00000000 0000012c 2**2
CONTENTS, RELOC
4 .idata$5 00000004 00000000 00000000 00000130 2**2
CONTENTS, RELOC
5 .idata$4 00000004 00000000 00000000 00000134 2**2
CONTENTS, RELOC
6 .idata$6 0000000e 00000000 00000000 00000138 2**1
CONTENTS
SYMBOL TABLE:
[ 0](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000000 .text
[ 1](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000000 .data
[ 2](sec 3)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000000 .bss
[ 3](sec 4)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000000 .idata$7
[ 4](sec 5)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000000 .idata$5
[ 5](sec 6)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000000 .idata$4
[ 6](sec 7)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x00000000 .idata$6
[ 7](sec 5)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000000 __imp__data_export
[ 8](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000000 __head_library_dll_a


RELOCATION RECORDS FOR [.idata$7]:
OFFSET TYPE VALUE
00000000 rva32 __head_library_dll_a


RELOCATION RECORDS FOR [.idata$5]:
OFFSET TYPE VALUE
00000000 rva32 .idata$6


RELOCATION RECORDS FOR [.idata$4]:
OFFSET TYPE VALUE
00000000 rva32 .idata$6


Contents of section .idata$7:
0000 00000000 ....
Contents of section .idata$5:
0000 00000000 ....
Contents of section .idata$4:
0000 00000000 ....
Contents of section .idata$6:
0000 00006461 74615f65 78706f72 7400 ..data_export.

注意data_export对应的目标包含一个空的.text段,然而function_export却有定义一些代码。如果我们反汇编就会看到:

1
2
3
4
5
00000000 <_function_export>:
0: ff 25 00 00 00 00 jmp *0x0
2: dir32 .idata$5
6: 90 nop
7: 90 nop

类型dir32的重定位告诉链接器如何填写被jmp间接引用的地址。我们可以看到当进入_function_export时,会直接跳到从名为.idata$5的内存处读取的地址。通过彻底地检查.idata段,可以发现.idata$5对应的是导入地址表中function_export这个导入名称所对应的地址,于是就能找到加载的导入项function_export的绝对地址。

虽然只有function_export拥有一个对应的_function_export函数,但是以上的两个导入项在导入库中分别对应了一个符号,这个符号的名称带有__imp__前缀(__imp__data_export__imp__function_export)。就像我们之前探讨过的那样,这个符号代表了一个内存地址,这个地址中存放的是的指向函数或变量的指针,这个指针值由动态链接器负责填写。

通过一个导入库,我们就可以写一个使用这些导出函数的代码,例如这个main1.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

__declspec(dllimport) extern int function_export(void);
__declspec(dllimport) extern int data_export;

int main(int argc, char **argv) {
printf("%d\n", function_export());
printf("%d\n", data_export);

data_export++;

printf("%d\n", function_export());
printf("%d\n", data_export);

return 0;
}

编译这段代码并连接导入库,我们就会得到我们期待的结果:

1
2
3
4
5
$ gcc main1.c library.dll.a -o main1 && ./main1
1379
42
1380
43

之所以library.dll.a内没有定义data_export符号而这段代码仍能编译,是因为main.c文件中的data_export声明上的__declspec(dllimport)修饰符导致编译器生成了直接使用__imp_data_export符号的代码,反汇编的话我们就会看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
$ gcc -c main1.c -o main1.o && objdump --disassemble -r main1.o

main1.o: file format pe-i386


Disassembly of section .text:

00000000 <_main>:
0: 8d 4c 24 04 lea 0x4(%esp),%ecx
4: 83 e4 f0 and $0xfffffff0,%esp
7: ff 71 fc pushl -0x4(%ecx)
a: 55 push %ebp
b: 89 e5 mov %esp,%ebp
d: 51 push %ecx
e: 83 ec 14 sub $0x14,%esp
11: e8 00 00 00 00 call 16 <_main+0x16>
12: DISP32 ___main
16: a1 00 00 00 00 mov 0x0,%eax
17: dir32 __imp__function_export
1b: ff d0 call *%eax
1d: 89 44 24 04 mov %eax,0x4(%esp)
21: c7 04 24 00 00 00 00 movl $0x0,(%esp)
24: dir32 .rdata
28: e8 00 00 00 00 call 2d <_main+0x2d>
29: DISP32 _printf
2d: a1 00 00 00 00 mov 0x0,%eax
2e: dir32 __imp__data_export
32: 8b 00 mov (%eax),%eax
34: 89 44 24 04 mov %eax,0x4(%esp)
38: c7 04 24 00 00 00 00 movl $0x0,(%esp)
3b: dir32 .rdata
3f: e8 00 00 00 00 call 44 <_main+0x44>
40: DISP32 _printf
44: a1 00 00 00 00 mov 0x0,%eax
45: dir32 __imp__data_export
49: 8b 00 mov (%eax),%eax
4b: 8d 50 01 lea 0x1(%eax),%edx
4e: a1 00 00 00 00 mov 0x0,%eax
4f: dir32 __imp__data_export
53: 89 10 mov %edx,(%eax)
55: a1 00 00 00 00 mov 0x0,%eax
56: dir32 __imp__function_export
5a: ff d0 call *%eax
5c: 89 44 24 04 mov %eax,0x4(%esp)
60: c7 04 24 00 00 00 00 movl $0x0,(%esp)
63: dir32 .rdata
67: e8 00 00 00 00 call 6c <_main+0x6c>
68: DISP32 _printf
6c: a1 00 00 00 00 mov 0x0,%eax
6d: dir32 __imp__data_export
71: 8b 00 mov (%eax),%eax
73: 89 44 24 04 mov %eax,0x4(%esp)
77: c7 04 24 00 00 00 00 movl $0x0,(%esp)
7a: dir32 .rdata
7e: e8 00 00 00 00 call 83 <_main+0x83>
7f: DISP32 _printf
83: b8 00 00 00 00 mov $0x0,%eax
88: 83 c4 14 add $0x14,%esp
8b: 59 pop %ecx
8c: 5d pop %ebp
8d: 8d 61 fc lea -0x4(%ecx),%esp
90: c3 ret
91: 90 nop
92: 90 nop
93: 90 nop

实际上,我们可以看到生成的代码甚至都没有使用_function_export符号,取而代之的是使用了imp__function_export。本质上,导入库中的_function_export符号在每处使用的地方都已经被内联过了。这也就是为什么使用__declspec(dllimport)可以提高跨DLL调用的性能,不过这个修饰符在声明函数时不是必须写的。
我们也许会好奇,如果在声明时去掉__declspec(dllimport)修饰符会发生什么事情。鉴于我们之前讨论的关于导入变量和导入函数之间的差别,你也许以为会链接失败。我们的测试文件main2.c是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

extern int function_export(void);
extern int data_export;

int main(int argc, char **argv) {
printf("%d\n", function_export());
printf("%d\n", data_export);

data_export++;

printf("%d\n", function_export());
printf("%d\n", data_export);

return 0;
}

让我们来试一试:

1
2
3
4
5
$ gcc main2.c library.dll.a -o main2 && ./main2
1379
42
1380
43

见鬼了!编译居然通过了?这有点令人惊讶。之所以导入库library.dll.a没有定义_data_export符号但这仍能编译通过,是由于GNU ld的一个叫做自动导入的有趣的特性。如果没有自动导入特性,链接器就会如我们所愿地报错:

1
2
3
4
5
6
$ gcc main2.c library.dll.a -o main2 -Wl,--disable-auto-import && ./main2
/tmp/ccGd8Urx.o:main2.c:(.text+0x2c): undefined reference to `_data_export'
/tmp/ccGd8Urx.o:main2.c:(.text+0x41): undefined reference to `_data_export'
/tmp/ccGd8Urx.o:main2.c:(.text+0x49): undefined reference to `_data_export'
/tmp/ccGd8Urx.o:main2.c:(.text+0x63): undefined reference to `_data_export'
collect2: ld returned 1 exit status

微软的链接器没有实现自动导入的特性,因此如果你用的是微软的工具链的话,你就会看到类似的错误信息。

然而,有一个方法可以使得在写代码时既不用依赖于自动导入的特定,也不用使用__declspec(dllimport)关键字。我们新的代码main3.c就是这么写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

extern int (*_imp__function_export)(void);
extern int *_imp__data_export;

#define function_export (*_imp__function_export)
#define data_export (*_imp__data_export)

int main(int argc, char **argv) {
printf("%d\n", function_export());
printf("%d\n", data_export);

data_export++;

printf("%d\n", function_export());
printf("%d\n", data_export);

return 0;
}

在这段代码中,我们直接使用了源自导入库中的带__imp__前缀的符号。这些符号对应的是导入函数和导入变量的真实内存地址,就像代码中的预处理宏定义data_exportfunction_export所表示的那样。

即使没有自动编译特性,这段代码也能完美地编译通过:

1
2
3
4
5
$ gcc main3.c library.dll.a -o main3 -Wl,--disable-auto-import && ./main3
1379
42
1380
43

如果你一直阅读到了这里,你应该已经对Windows上上DLL的导入和导出有了透彻的理解。


本文地址:http://xnerv.wang/everything-you-never-wanted-to-know-about-dlls-cn/