为什么Windows需要导入DLL数据?
Why does Windows require DLL data to be imported?
在 Windows 上,可以从 DLL 加载数据,但需要通过导入地址 table 中的指针进行间接访问。因此,编译器必须知道正在访问的 object 是否是通过使用 __declspec(dllimport)
类型说明符从 DLL 导入的。
这很不幸,因为这意味着 header 设计用作静态库或动态库的 Windows 库需要知道程序的库版本正在链接到。此要求不适用于函数,这些函数是为 DLL 透明地模拟的,存根函数调用实际函数,其地址存储在导入地址 table.
在 Linux 上,动态链接器 (ld.so
) 将所有链接数据 object 的值从共享 object 复制到每个进程的私有映射区域.这不需要间接寻址,因为私有映射区域的地址是模块本地的,所以它的地址是在程序链接时决定的(在位置无关的 executables 的情况下,使用相对寻址).
为什么 Windows 不这样做?是否存在 DLL 可能被加载不止一次的情况,因此需要链接数据的多个副本?即使是这样,它也不适用于只读数据。
似乎 MSVCRT 通过在针对动态 C 运行时库(使用 /MD
或 /MDd
标志)时定义 _DLL
宏来处理这个问题,然后在所有标准 headers 有条件地声明所有带有 __declspec(dllimport)
的导出符号。我想如果您仅在使用静态 C 运行时时支持静态链接而在使用动态 C 运行时时支持动态链接,则可以重用此宏。
参考文献:
LNK4217 - Russ Keldorph's WebLog(强调我的)
__declspec(dllimport) can be used on both code and data, and its semantics are subtly different between the two. When applied to a routine call, it is purely a performance optimization. For data, it is required for correctness.
[...]
Importing data
If you export a data item from a DLL, you must declare it with __declspec(dllimport)
in the code that accesses it. In this case, instead of generating a direct load from memory, the compiler generates a load through a pointer, resulting in one additional indirection. Unlike calls, where the linker will fix up the code correctly whether the routine was declared __declspec(dllimport)
or not, accessing imported data requires __declspec(dllimport)
. If omitted, the code will wind up accessing the IAT entry instead of the data in the DLL, probably resulting in unexpected behavior.
Importing into an Application Using __declspec(dllimport)
Using __declspec(dllimport)
is optional on function declarations, but the compiler produces more efficient code if you use this keyword. However, you must use `__declspec(dllimport) for the importing executable to access the DLL's public data symbols and objects.
Importing Data Using __declspec(dllimport)
When you mark the data as __declspec(dllimport), the compiler automatically generates the indirection code for you.
Importing Using DEF Files(关于直接访问 IAT 的有趣历史记录)
How do I share data in my DLL with an application or with other DLLs?
By default, each process using a DLL has its own instance of all the DLLs global and static variables.
What happens when you get dllimport wrong?(好像没有意识到数据语义)
How do I export data from a DLL?
CRT Library Features(记录 _DLL
宏)
Linux 和 Windows 使用不同的策略来访问存储在动态库中的数据。
在 Linux,对对象的未定义引用在 link 时解析为库。 linker 找到对象的大小并在 executable 的 .bss
或 .rdata
段中为它保留 space。执行时,动态 linker (ld.so
) 将符号解析为动态库(再次),并将对象从动态库复制到进程的内存中。
在 Windows,对对象的未定义引用在 link 时解析为导入库,并且没有为它保留 space。执行模块时,动态 linker 将符号解析为动态库,并在进程中创建写入内存映射的副本,由动态库中的共享数据段支持。
写入内存映射时复制的优点是,如果 linked 数据不变,则可以与其他进程共享。在实践中,这是一个微不足道的好处,它大大增加了工具链和使用动态库的程序的复杂性。对于实际写入的对象,这总是效率较低。
虽然我没有证据,但我怀疑这个决定是针对一个特定的、现在已经过时的用例做出的。也许在 16 位 Windows(在官方 Microsoft 程序或其他程序中)的动态库中使用大型(当时)只读对象是常见的做法。无论哪种方式,我怀疑 Microsoft 的任何人现在都有专业知识和时间来更改它。
为了调查这个问题,我创建了一个从动态库写入对象的程序。它在对象中每页(4096 字节)写入一个字节,然后写入整个对象,然后重试初始的每页写入一个字节。如果在调用 main
之前为进程保留该对象,则第一次和第三次循环的时间应该大致相同,而第二次循环的时间应该比两者都长。如果对象是写映射到动态库的副本,则第一个循环的时间至少应与第二个循环一样长,而第三个循环的时间应少于两者。
结果与我的假设一致,分析反汇编确认Linux在相对于程序计数器的link时间地址访问动态库数据。令人惊讶的是,Windows 不仅间接访问数据,而且 每次循环迭代都会从导入地址 table 重新加载指向数据的指针及其长度,并启用优化 .这是在 Windows XP 上用 Visual Studio 2010 测试的,所以也许事情已经改变了,虽然我认为它没有。
以下是 Linux 的结果:
$ dd bs=1M count=16 if=/dev/urandom of=libdat.dat
$ xxd -i libdat.dat libdat.c
$ gcc -O3 -g -shared -fPIC libdat.c -o libdat.so
$ gcc -O3 -g -no-pie -L. -ldat dat.c -o dat
$ LD_LIBRARY_PATH=. ./dat
local = 0x1601060
libdat_dat = 0x601040
libdat_dat_len = 0x601020
dirty= 461us write= 12184us retry= 456us
$ nm dat
[...]
0000000000601040 B libdat_dat
0000000000601020 B libdat_dat_len
0000000001601060 B local
[...]
$ objdump -d -j.text dat
[...]
400693: 8b 35 87 09 20 00 mov 0x200987(%rip),%esi # 601020 <libdat_dat_len>
[...]
4006a3: 31 c0 xor %eax,%eax # zero loop counter
4006a5: 48 8d 15 94 09 20 00 lea 0x200994(%rip),%rdx # 601040 <libdat_dat>
4006ac: 0f 1f 40 00 nopl 0x0(%rax) # align loop for efficiency
4006b0: 89 c1 mov %eax,%ecx # store data offset in ecx
4006b2: 05 00 10 00 00 add [=10=]x1000,%eax # add PAGESIZE to data offset
4006b7: c6 04 0a 00 movb [=10=]x0,(%rdx,%rcx,1) # write a zero byte to data
4006bb: 39 f0 cmp %esi,%eax # test loop condition
4006bd: 72 f1 jb 4006b0 <main+0x30> # continue loop if data is left
[...]
以下是 Windows 的结果:
$ cl /Ox /Zi /LD libdat.c /link /EXPORT:libdat_dat /EXPORT:libdat_dat_len
[...]
$ cl /Ox /Zi dat.c libdat.lib
[...]
$ dat.exe # note low resolution timer means retry is too small to measure
local = 0041EEA0
libdat_dat = 1000E000
libdat_dat_len = 1100E000
dirty= 20312us write= 3125us retry= 0us
$ dumpbin /symbols dat.exe
[...]
9000 .data
1000 .idata
5000 .rdata
1000 .reloc
17000 .text
[...]
$ dumpbin /disasm dat.exe
[...]
004010BA: 33 C0 xor eax,eax # zero loop counter
[...]
004010C0: 8B 15 8C 63 42 00 mov edx,dword ptr [__imp__libdat_dat] # store data pointer in edx
004010C6: C6 04 02 00 mov byte ptr [edx+eax],0 # write a zero byte to data
004010CA: 8B 0D 88 63 42 00 mov ecx,dword ptr [__imp__libdat_dat_len] # store data length in ecx
004010D0: 05 00 10 00 00 add eax,1000h # add PAGESIZE to data offset
004010D5: 3B 01 cmp eax,dword ptr [ecx] # test loop condition
004010D7: 72 E7 jb 004010C0 # continue loop if data is left
[...]
以下是用于两个测试的源代码:
#include <stdio.h>
#ifdef _WIN32
#include <windows.h>
typedef FILETIME time_l;
time_l time_get(void) {
FILETIME ret; GetSystemTimeAsFileTime(&ret); return ret;
}
long long int time_diff(time_l const *c1, time_l const *c2) {
return 1LL*c2->dwLowDateTime/100-c1->dwLowDateTime/100+c2->dwHighDateTime*100000-c1->dwHighDateTime*100000;
}
#else
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
typedef struct timespec time_l;
time_l time_get(void) {
time_l ret; clock_gettime(CLOCK_MONOTONIC, &ret); return ret;
}
long long int time_diff(time_l const *c1, time_l const *c2) {
return 1LL*c2->tv_nsec/1000-c1->tv_nsec/1000+c2->tv_sec*1000000-c1->tv_sec*1000000;
}
#endif
#ifndef PAGESIZE
#define PAGESIZE 4096
#endif
#ifdef _WIN32
#define DLLIMPORT __declspec(dllimport)
#else
#define DLLIMPORT
#endif
extern DLLIMPORT unsigned char volatile libdat_dat[];
extern DLLIMPORT unsigned int libdat_dat_len;
unsigned int local[4096];
int main(void) {
unsigned int i;
time_l t1, t2, t3, t4;
long long int d1, d2, d3;
t1 = time_get();
for(i=0; i < libdat_dat_len; i+=PAGESIZE) {
libdat_dat[i] = 0;
}
t2 = time_get();
for(i=0; i < libdat_dat_len; i++) {
libdat_dat[i] = 0xFF;
}
t3 = time_get();
for(i=0; i < libdat_dat_len; i+=PAGESIZE) {
libdat_dat[i] = 0;
}
t4 = time_get();
d1 = time_diff(&t1, &t2);
d2 = time_diff(&t2, &t3);
d3 = time_diff(&t3, &t4);
printf("%-15s= %18p\n%-15s= %18p\n%-15s= %18p\n", "local", local, "libdat_dat", libdat_dat, "libdat_dat_len", &libdat_dat_len);
printf("dirty=%9lldus write=%9lldus retry=%9lldus\n", d1, d2, d3);
return 0;
}
我真诚地希望其他人能从我的研究中受益。感谢阅读!
在 Windows 上,可以从 DLL 加载数据,但需要通过导入地址 table 中的指针进行间接访问。因此,编译器必须知道正在访问的 object 是否是通过使用 __declspec(dllimport)
类型说明符从 DLL 导入的。
这很不幸,因为这意味着 header 设计用作静态库或动态库的 Windows 库需要知道程序的库版本正在链接到。此要求不适用于函数,这些函数是为 DLL 透明地模拟的,存根函数调用实际函数,其地址存储在导入地址 table.
在 Linux 上,动态链接器 (ld.so
) 将所有链接数据 object 的值从共享 object 复制到每个进程的私有映射区域.这不需要间接寻址,因为私有映射区域的地址是模块本地的,所以它的地址是在程序链接时决定的(在位置无关的 executables 的情况下,使用相对寻址).
为什么 Windows 不这样做?是否存在 DLL 可能被加载不止一次的情况,因此需要链接数据的多个副本?即使是这样,它也不适用于只读数据。
似乎 MSVCRT 通过在针对动态 C 运行时库(使用 /MD
或 /MDd
标志)时定义 _DLL
宏来处理这个问题,然后在所有标准 headers 有条件地声明所有带有 __declspec(dllimport)
的导出符号。我想如果您仅在使用静态 C 运行时时支持静态链接而在使用动态 C 运行时时支持动态链接,则可以重用此宏。
参考文献:
LNK4217 - Russ Keldorph's WebLog(强调我的)
__declspec(dllimport) can be used on both code and data, and its semantics are subtly different between the two. When applied to a routine call, it is purely a performance optimization. For data, it is required for correctness.
[...]
Importing data
If you export a data item from a DLL, you must declare it with
__declspec(dllimport)
in the code that accesses it. In this case, instead of generating a direct load from memory, the compiler generates a load through a pointer, resulting in one additional indirection. Unlike calls, where the linker will fix up the code correctly whether the routine was declared__declspec(dllimport)
or not, accessing imported data requires__declspec(dllimport)
. If omitted, the code will wind up accessing the IAT entry instead of the data in the DLL, probably resulting in unexpected behavior.
Importing into an Application Using __declspec(dllimport)
Using
__declspec(dllimport)
is optional on function declarations, but the compiler produces more efficient code if you use this keyword. However, you must use `__declspec(dllimport) for the importing executable to access the DLL's public data symbols and objects.
Importing Data Using __declspec(dllimport)
When you mark the data as __declspec(dllimport), the compiler automatically generates the indirection code for you.
Importing Using DEF Files(关于直接访问 IAT 的有趣历史记录)
How do I share data in my DLL with an application or with other DLLs?
By default, each process using a DLL has its own instance of all the DLLs global and static variables.
What happens when you get dllimport wrong?(好像没有意识到数据语义)
How do I export data from a DLL?
CRT Library Features(记录 _DLL
宏)
Linux 和 Windows 使用不同的策略来访问存储在动态库中的数据。
在 Linux,对对象的未定义引用在 link 时解析为库。 linker 找到对象的大小并在 executable 的 .bss
或 .rdata
段中为它保留 space。执行时,动态 linker (ld.so
) 将符号解析为动态库(再次),并将对象从动态库复制到进程的内存中。
在 Windows,对对象的未定义引用在 link 时解析为导入库,并且没有为它保留 space。执行模块时,动态 linker 将符号解析为动态库,并在进程中创建写入内存映射的副本,由动态库中的共享数据段支持。
写入内存映射时复制的优点是,如果 linked 数据不变,则可以与其他进程共享。在实践中,这是一个微不足道的好处,它大大增加了工具链和使用动态库的程序的复杂性。对于实际写入的对象,这总是效率较低。
虽然我没有证据,但我怀疑这个决定是针对一个特定的、现在已经过时的用例做出的。也许在 16 位 Windows(在官方 Microsoft 程序或其他程序中)的动态库中使用大型(当时)只读对象是常见的做法。无论哪种方式,我怀疑 Microsoft 的任何人现在都有专业知识和时间来更改它。
为了调查这个问题,我创建了一个从动态库写入对象的程序。它在对象中每页(4096 字节)写入一个字节,然后写入整个对象,然后重试初始的每页写入一个字节。如果在调用 main
之前为进程保留该对象,则第一次和第三次循环的时间应该大致相同,而第二次循环的时间应该比两者都长。如果对象是写映射到动态库的副本,则第一个循环的时间至少应与第二个循环一样长,而第三个循环的时间应少于两者。
结果与我的假设一致,分析反汇编确认Linux在相对于程序计数器的link时间地址访问动态库数据。令人惊讶的是,Windows 不仅间接访问数据,而且 每次循环迭代都会从导入地址 table 重新加载指向数据的指针及其长度,并启用优化 .这是在 Windows XP 上用 Visual Studio 2010 测试的,所以也许事情已经改变了,虽然我认为它没有。
以下是 Linux 的结果:
$ dd bs=1M count=16 if=/dev/urandom of=libdat.dat
$ xxd -i libdat.dat libdat.c
$ gcc -O3 -g -shared -fPIC libdat.c -o libdat.so
$ gcc -O3 -g -no-pie -L. -ldat dat.c -o dat
$ LD_LIBRARY_PATH=. ./dat
local = 0x1601060
libdat_dat = 0x601040
libdat_dat_len = 0x601020
dirty= 461us write= 12184us retry= 456us
$ nm dat
[...]
0000000000601040 B libdat_dat
0000000000601020 B libdat_dat_len
0000000001601060 B local
[...]
$ objdump -d -j.text dat
[...]
400693: 8b 35 87 09 20 00 mov 0x200987(%rip),%esi # 601020 <libdat_dat_len>
[...]
4006a3: 31 c0 xor %eax,%eax # zero loop counter
4006a5: 48 8d 15 94 09 20 00 lea 0x200994(%rip),%rdx # 601040 <libdat_dat>
4006ac: 0f 1f 40 00 nopl 0x0(%rax) # align loop for efficiency
4006b0: 89 c1 mov %eax,%ecx # store data offset in ecx
4006b2: 05 00 10 00 00 add [=10=]x1000,%eax # add PAGESIZE to data offset
4006b7: c6 04 0a 00 movb [=10=]x0,(%rdx,%rcx,1) # write a zero byte to data
4006bb: 39 f0 cmp %esi,%eax # test loop condition
4006bd: 72 f1 jb 4006b0 <main+0x30> # continue loop if data is left
[...]
以下是 Windows 的结果:
$ cl /Ox /Zi /LD libdat.c /link /EXPORT:libdat_dat /EXPORT:libdat_dat_len
[...]
$ cl /Ox /Zi dat.c libdat.lib
[...]
$ dat.exe # note low resolution timer means retry is too small to measure
local = 0041EEA0
libdat_dat = 1000E000
libdat_dat_len = 1100E000
dirty= 20312us write= 3125us retry= 0us
$ dumpbin /symbols dat.exe
[...]
9000 .data
1000 .idata
5000 .rdata
1000 .reloc
17000 .text
[...]
$ dumpbin /disasm dat.exe
[...]
004010BA: 33 C0 xor eax,eax # zero loop counter
[...]
004010C0: 8B 15 8C 63 42 00 mov edx,dword ptr [__imp__libdat_dat] # store data pointer in edx
004010C6: C6 04 02 00 mov byte ptr [edx+eax],0 # write a zero byte to data
004010CA: 8B 0D 88 63 42 00 mov ecx,dword ptr [__imp__libdat_dat_len] # store data length in ecx
004010D0: 05 00 10 00 00 add eax,1000h # add PAGESIZE to data offset
004010D5: 3B 01 cmp eax,dword ptr [ecx] # test loop condition
004010D7: 72 E7 jb 004010C0 # continue loop if data is left
[...]
以下是用于两个测试的源代码:
#include <stdio.h>
#ifdef _WIN32
#include <windows.h>
typedef FILETIME time_l;
time_l time_get(void) {
FILETIME ret; GetSystemTimeAsFileTime(&ret); return ret;
}
long long int time_diff(time_l const *c1, time_l const *c2) {
return 1LL*c2->dwLowDateTime/100-c1->dwLowDateTime/100+c2->dwHighDateTime*100000-c1->dwHighDateTime*100000;
}
#else
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
typedef struct timespec time_l;
time_l time_get(void) {
time_l ret; clock_gettime(CLOCK_MONOTONIC, &ret); return ret;
}
long long int time_diff(time_l const *c1, time_l const *c2) {
return 1LL*c2->tv_nsec/1000-c1->tv_nsec/1000+c2->tv_sec*1000000-c1->tv_sec*1000000;
}
#endif
#ifndef PAGESIZE
#define PAGESIZE 4096
#endif
#ifdef _WIN32
#define DLLIMPORT __declspec(dllimport)
#else
#define DLLIMPORT
#endif
extern DLLIMPORT unsigned char volatile libdat_dat[];
extern DLLIMPORT unsigned int libdat_dat_len;
unsigned int local[4096];
int main(void) {
unsigned int i;
time_l t1, t2, t3, t4;
long long int d1, d2, d3;
t1 = time_get();
for(i=0; i < libdat_dat_len; i+=PAGESIZE) {
libdat_dat[i] = 0;
}
t2 = time_get();
for(i=0; i < libdat_dat_len; i++) {
libdat_dat[i] = 0xFF;
}
t3 = time_get();
for(i=0; i < libdat_dat_len; i+=PAGESIZE) {
libdat_dat[i] = 0;
}
t4 = time_get();
d1 = time_diff(&t1, &t2);
d2 = time_diff(&t2, &t3);
d3 = time_diff(&t3, &t4);
printf("%-15s= %18p\n%-15s= %18p\n%-15s= %18p\n", "local", local, "libdat_dat", libdat_dat, "libdat_dat_len", &libdat_dat_len);
printf("dirty=%9lldus write=%9lldus retry=%9lldus\n", d1, d2, d3);
return 0;
}
我真诚地希望其他人能从我的研究中受益。感谢阅读!