为什么Windows要求导入DLL数据?

时间:2018-11-09 14:05:35

标签: c windows dll dllimport

在Windows上,可以从DLL加载数据,但是它需要通过导入地址表中的指针进行间接寻址。结果,编译器必须知道是否正在使用__declspec(dllimport)类型说明符从DLL导入正在访问的对象。

这很不幸,因为这意味着Windows库的标头(旨在用作静态库或动态库)需要知道程序所链接到的库的版本。此要求不适用于使用存根函数调用real函数的存根函数透明模拟的DLL,这些存根函数的地址存储在导入地址表中。

在Linux上,动态链接程序(ld.so)将所有链接数据对象的值从共享对象复制到每个进程的专用映射区域。不需要间接寻址,因为私有映射区域的地址是模块的本地地址,因此在链接程序时就可以确定其地址(对于位置独立的可执行文件,则使用相对寻址)。

Windows为什么不这样做?是否存在DLL可能加载不止一次并因此需要链接数据的多个副本的情况?即使是这种情况,也不适用于只读数据。

似乎MSVCRT通过在定位动态C运行时库(带有_DLL/MD标志时定义/MDd宏,然后在所有标准中使用它来解决此问题标头使用__declspec(dllimport)有条件地声明所有导出的符号。我想如果您仅在使用静态C运行时时支持静态链接而在使用动态C运行时时支持动态链接,则可以重用此宏。

参考文献:

LNK4217 - Russ Keldorph's WebLog(强调我的意思)

  

__ declspec(dllimport)可以在代码和数据上使用,并且它们的语义在两者之间略有不同。当应用于例行调用时,它纯粹是性能优化。对于数据,它是正确性所必需的。

     

[...]

     

导入数据

     

如果从DLL导出数据项,则必须在访问它的代码中用__declspec(dllimport)声明它。在这种情况下,而不是从内存生成直接负载,而是编译器通过指针生成负​​载,从而导致一个附加的间接调用。与调用不同,链接器将在不声明例程__declspec(dllimport)的情况下正确地修复代码,而访问导入的数据则需要__declspec(dllimport)如果省略,代码将结束访问IAT条目而不是DLL中的数据,这可能会导致意外行为。

Importing into an Application Using __declspec(dllimport)

  

在函数声明中使用__declspec(dllimport)是可选的,但是如果您使用此关键字,则编译器将产生更有效的代码。但是,对于导入的可执行文件,必须使用__declspec(dllimport)来访问DLL的公共数据符号和对象。

Importing Data Using __declspec(dllimport)

  

将数据标记为__declspec(dllimport)时,编译器会自动为您生成间接代码。

Importing Using DEF Files(有关直接访问IAT的有趣历史注释)

How do I share data in my DLL with an application or with other DLLs?

  

默认情况下,每个使用DLL的进程都有其自己的所有DLL全局和静态变量实例。

Linker Tools Warning LNK4217

What happens when you get dllimport wrong?(似乎不知道数据语义)

How do I export data from a DLL?

CRT Library Features(记录了_DLL宏)

1 个答案:

答案 0 :(得分:2)

Linux和Windows使用不同的策略来访问存储在动态库中的数据。

在Linux上,对对象的未定义引用在链接时解析为库。链接器在可执行文件的.bss.rdata段中查找对象的大小并为其保留空间。执行后,动态链接器(ld.so)会将符号解析为动态库(再次),并将对象从动态库复制到进程的内存中。

在Windows上,对对象的未定义引用在链接时解析为导入库,并且没有为该对象保留空间。执行模块时,动态链接器将符号解析为动态库,并在该过程中的写内存映射上创建副本,并由动态库中的共享数据段支持。

在写内存映射上进行复制的优点是,如果链接的数据不变,则可以与其他进程共享。实际上,这是一个微不足道的好处,对于使用动态库的工具链和程序而言,这极大地增加了复杂性。对于实际编写的对象,这总是效率较低。

尽管我没有证据,但我怀疑此决定是针对特定且已过时的用例做出的。也许在16位Windows上的动态库中(在正式的Microsoft程序中或其他情况下)使用大型(暂时)只读对象是常见的做法。无论哪种方式,我都怀疑Microsoft的任何人都具有专业知识和时间来进行更改。

为了调查这个问题,我创建了一个程序,该程序从动态库中写入对象。它在对象中每页写入一个字节(4096字节),然后写入整个对象,然后每页写入重试最初的一个字节。如果在调用main之前为该进程保留了对象,则第一个和第三个循环应该花费大约相同的时间,而第二个循环应该花费比这两个过程更长的时间。如果该对象是写入映射到动态库的副本,则第一个循环的时间至少应与第二个循环的时间相同,而第三个循环的时间应少于两个循环。

结果与我的假设一致,并且对反汇编的分析证实了Linux在相对于程序计数器的链接时间地址处访问动态库数据。令人惊讶的是,Windows不仅间接访问数据,而且每次启用循环优化时,都会在每次循环迭代时从导入地址表中重新加载指向数据的指针及其长度,。这已经在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    $0x1000,%eax               # add PAGESIZE to data offset
  4006b7:   c6 04 0a 00             movb   $0x0,(%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;
}

我衷心希望其他人能从我的研究中受益。感谢您的阅读!