Почему Windows требует импортировать данные DLL? - PullRequest
0 голосов
/ 09 ноября 2018

В Windows данные могут быть загружены из DLL, но это требует косвенного обращения через указатель в таблице адресов импорта. В результате компилятор должен знать, импортируется ли объект, к которому осуществляется доступ, из DLL с помощью спецификатора типа __declspec(dllimport).

Это прискорбно, поскольку это означает, что заголовок для библиотеки Windows, предназначенной для использования в качестве статической или динамической библиотеки, должен знать, на какую версию библиотеки ссылается программа. Это требование не применимо к функциям, которые прозрачно эмулируются для библиотек DLL с функцией-заглушкой, вызывающей реальную функцию, адрес которой хранится в таблице адресов импорта.

В Linux динамический компоновщик (ld.so) копирует значения всех связанных объектов данных из общего объекта в частную сопоставленную область для каждого процесса. Это не требует косвенного обращения, потому что адрес частного сопоставленного региона является локальным для модуля, поэтому его адрес определяется, когда программа связана (а в случае позиционно-независимых исполняемых файлов используется относительная адресация).

Почему Windows не делает то же самое? Существует ли ситуация, когда DLL может быть загружена более одного раза, и, таким образом, требуется несколько копий связанных данных? Даже если бы это было так, это было бы неприменимо для чтения только данных.

Кажется, что MSVCRT решает эту проблему, определяя макрос _DLL при нацеливании на динамическую библиотеку времени выполнения C (с флагом /MD или /MDd), а затем использует его во всех стандартных заголовках для условного объявления всех экспортируемых символы с __declspec(dllimport). Я полагаю, что вы могли бы повторно использовать этот макрос, если бы вы поддерживали статическое связывание только при использовании статической среды выполнения C и динамическое связывание при использовании динамической среды выполнения C.

Ссылки:

LNK4217 - Веб-журнал Русса Келдорфа (выделенный рудник)

__declspec (dllimport) может использоваться как для кода, так и для данных, и его семантика слегка различается между ними. Применительно к обычному вызову это просто оптимизация производительности. Для данных требуется правильность.

[...]

Импорт данных

Если вы экспортируете элемент данных из DLL, вы должны объявить его с помощью __declspec(dllimport) в коде, который обращается к нему. В этом случае вместо генерации прямой загрузки из памяти, компилятор генерирует нагрузку через указатель, что приводит к одной дополнительной косвенной ссылке . В отличие от вызовов, когда компоновщик исправит код правильно, независимо от того, была ли объявлена ​​подпрограмма __declspec(dllimport) или нет, для доступа к импортированным данным требуется __declspec(dllimport). Если опущен, код получит доступ к записи IAT вместо данных в DLL, что, вероятно, приведет к непредвиденному поведению.

Импорт в приложение с использованием __declspec (dllimport)

Использование __declspec(dllimport) необязательно в объявлениях функций, но компилятор создает более эффективный код, если вы используете это ключевое слово. Однако вы должны использовать `__declspec (dllimport) для импорта исполняемого файла, чтобы получить доступ к символам и объектам общедоступных данных DLL.

Импорт данных с использованием __declspec (dllimport)

Когда вы помечаете данные как __declspec (dllimport), компилятор автоматически генерирует для вас код косвенного обращения.

Импорт с использованием файлов DEF (интересные исторические заметки о прямом доступе к IAT)

Как поделиться данными в моей DLL с приложением или с другими DLL?

По умолчанию каждый процесс, использующий DLL, имеет собственный экземпляр всех глобальных и статических переменных DLL.

Предупреждение Linker Tools LNK4217

Что происходит, если вы неправильно вводите dllimport? (кажется, не знает о семантике данных)

Как экспортировать данные из DLL?

Функции библиотеки CRT (документирует макрос _DLL)

1 Ответ

0 голосов
/ 10 ноября 2018

Linux и Windows используют разные стратегии для доступа к данным, хранящимся в динамических библиотеках.

В Linux неопределенная ссылка на объект преобразуется в библиотеку во время ссылки. Компоновщик находит размер объекта и резервирует место для него в сегменте .bss или .rdata исполняемого файла. При выполнении динамический компоновщик (ld.so) разрешает символ в динамическую библиотеку (снова) и копирует объект из динамической библиотеки в память процесса.

В Windows неопределенная ссылка на объект преобразуется в библиотеку импорта во время ссылки, и для него не зарезервировано пространство. Когда модуль выполняется, динамический компоновщик преобразует символ в динамическую библиотеку и создает в процессе копию карты памяти записи в процессе, поддерживаемую общим сегментом данных в динамической библиотеке.

Преимущество копии на карте памяти записи состоит в том, что если связанные данные не изменяются, то они могут использоваться другими процессами. На практике это незначительное преимущество, которое значительно увеличивает сложность как для цепочки инструментов, так и для программ, использующих динамические библиотеки. Для объектов, которые действительно написаны, это всегда менее эффективно.

Я подозреваю, хотя у меня нет доказательств, что это решение было принято для конкретного и в настоящее время устаревшего варианта использования. Возможно, было обычной практикой использовать большие (пока что) объекты только для чтения в динамических библиотеках в 16-битной Windows (в официальных программах Microsoft или иным образом). В любом случае, я сомневаюсь, что у кого-то в Microsoft есть опыт и время, чтобы изменить это сейчас.

Чтобы исследовать проблему, я создал программу, которая пишет в объект из динамической библиотеки. Он записывает один объект на страницу (4096 байтов) в объекте, затем записывает весь объект, а затем повторяет начальный один байт на страницу записи. Если объект зарезервирован для процесса до вызова main, первый и третий циклы должны занять примерно одинаковое время, а второй цикл должен занять больше времени, чем оба. Если объект является копией при записи на карту в динамическую библиотеку, первый цикл должен занимать по меньшей мере столько же времени, сколько второй, а третий должен занимать меньше времени, чем оба.

Результаты согласуются с моей гипотезой, и анализ разборки подтверждает, что Linux обращается к данным динамической библиотеки по адресу времени ссылки относительно счетчика программы. Удивительно, но Windows не только косвенно обращается к данным, указатель на данные и их длина перезагружаются из таблицы адресов импорта при каждой итерации цикла с включенной оптимизацией . Это было протестировано в Visual Studio 2010 на Windows XP, поэтому, возможно, все изменилось, хотя я бы не подумал, что это изменилось.

Вот результаты для 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;
}

Я искренне надеюсь, что кто-то еще выиграет от моего исследования. Спасибо за чтение!

...