В Windows данные могут быть загружены из DLL, но для этого требуется косвенный указатель в таблице адресов импорта. В результате компилятор должен знать, импортируется ли объект, к которому осуществляется доступ, из библиотеки DLL, используя спецификатор типа __declspec(dllimport).
Это прискорбно, потому что это означает, что заголовок библиотеки Windows, предназначенной для использования в качестве статической или динамической библиотеки, должен знать, с какой версией библиотеки связана программа. Это требование не применимо к функциям, которые прозрачно эмулируются для библиотек DLL с функцией-заглушкой, вызывающей реальную функцию, адрес которой хранится в таблице адресов импорта.
В Linux динамический компоновщик (ld.so) копирует значения всех связанных объектов данных из общего объекта в частную отображаемую область для каждого процесса. Это не требует косвенного обращения, поскольку адрес частной отображаемой области является локальным для модуля, поэтому его адрес определяется при компоновке программы (а в случае исполняемых файлов, не зависящих от позиции, используется относительная адресация).
Почему Windows не делает то же самое? Есть ли ситуация, когда DLL может загружаться более одного раза и, следовательно, требовать нескольких копий связанных данных? Даже если бы это было так, это было бы неприменимо к данным только для чтения.
Кажется, что MSVCRT решает эту проблему, определяя макрос _DLL при нацеливании на динамическую библиотеку времени выполнения C (с флагом /MD или /MDd), а затем используя это во всех стандартных заголовках для условного объявления всех экспортированных символов с помощью __declspec(dllimport). Я полагаю, вы могли бы повторно использовать этот макрос, если бы вы поддерживали статическое связывание только при использовании статической среды выполнения C и динамическое связывание при использовании динамической среды выполнения C.
Использованная литература:
LNK4217 - 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.
Импорт в приложение с использованием __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.
Импорт данных с использованием __declspec (dllimport)
When you mark the data as __declspec(dllimport), the compiler automatically generates the indirection code for you.
Импорт с использованием файлов DEF (интересные исторические заметки о прямом доступе к IAT)
Как поделиться данными в моей DLL с приложением или с другими DLL?
By default, each process using a DLL has its own instance of all the DLLs global and static variables.
Предупреждение средств компоновщика LNK4217
Что произойдет, если вы неправильно укажете dllimport? (похоже, не знает семантику данных)
Как экспортировать данные из библиотеки DLL?
Особенности библиотеки CRT (документирует макрос _DLL)
@movcmpret Способ Windows строго менее эффективен. Это косвенный доступ (через IAT) вместо прямого доступа к адресу, известному во время связывания (содержимое которого инициализируется во время загрузки).
@JAlan - адрес внутри внешнего модуля не может быть известен во время компоновки. доступ к данным во внешнем модуле через указатель на него - единственный способ. окна эффективны. однако ваш вопрос мне непонятен
Косвенный переход через указатель IAT или dllimport предотвращает изменение кодовых страниц, когда целевая DLL не может быть загружена по ее базовому адресу времени компоновки. Это имело большое значение, когда ему приходилось работать с 16 МБ оперативной памяти. Все равно не больно.
@RbMm Данные копируются из внешнего модуля в локальный модуль при загрузке внешнего модуля. См. Цитируемую ссылку в сообщении: «По умолчанию каждый процесс, использующий DLL, имеет собственный экземпляр всех глобальных и статических переменных DLL».
Я здесь на заборе: можно ли ответить на этот вопрос? это "почему?" об этом предмете в основном к мнению? или есть технический ответ, который может дать специалист? даже если это технический вопрос, является ли Stackoverflow сайтом Лучший StackExchange для этого типа вопросов? Моя цель состоит в том, чтобы этот вопрос был перед лучшей аудиторией, которая ответит на него; HTH
@HansPassant Не имеет значения, куда загружена DLL, потому что данные копируются в модуль, загружающий ее, по адресу, известному ему во время компоновки. См. Мой предыдущий комментарий.
@ landru27 Я ищу технический ответ, но я бы принял исторический ответ вместо этого. Это определенно лучший сайт для этого, потому что StackOverflow широко занимается C, Win32 API, компиляторами и операционными системами. Я также думаю, что есть смысл оставить этот вопрос, потому что я не нашел каких-либо ссылок, в которых обсуждается это конкретное ограничение.
Нет, ничего не копируется, вы обращаетесь к данным в DLL. Но данные нельзя переместить, поэтому это нужно делать с помощью указателя. Вот почему dllimport является жестким требованием для экспортируемых данных, он сообщает компилятору всегда использовать указатель, даже если ваш код этого не делает.
Думаю, я наконец понял. Данные из DLL сопоставляются каждому процессу с копией на карту памяти для записи, вместе с исполняемым текстовым сегментом только для чтения и т. д. Динамический компоновщик загружает всю DLL в непрерывную область пространства, поэтому он не знает ее адрес во время компоновки. Это верно? Похоже, что он может создать вторую карту по известному адресу, охватывающую только сегменты данных, с тщательной компоновкой адреса. Вы знаете, как работает Linux в этом отношении? Или где найти дополнительную информацию?
Обратите внимание, что даже на целевых объектах ELF (таких как Linux) существует такая же структура таблицы (называемая GOT [глобальная таблица смещения]). Он используется, когда общие объекты относятся к любым объектам внешних данных. Просто у ELF есть оптимизация для общего случая, когда исполняемый файл ссылается на объект данных, объявленный в библиотеке.
Я голосую за то, чтобы закрыть этот вопрос как не по теме, потому что это вопрос разработки программного обеспечения, а не программирования, и его следует задавать на softwareengineering.stackexchange.com





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;
}
Я искренне надеюсь, что мое исследование принесет пользу кому-то другому. Спасибо за прочтение!
Конструкция Windows означает, что если два модуля в процессе связаны с одной и той же DLL, данные DLL используются совместно. Например, A.DLL и B.DLL используют LIBC.DLL. A устанавливает errno = 3. B читает errno и получает 3. Версия для Linux предоставляет A.DLL и B.DLL свои собственные отдельные копии errno.
Это именно та разница, которую я искал, но не нашел. Я действительно искал способ связаться с вами или дать вам эту тему в качестве предложения, прежде чем публиковать это, но я не мог найти способа. Спасибо за ответ!
Если функция в динамической библиотеке Linux хочет получить доступ к своей собственной глобальной переменной, как она это делает? Предположим, в вашей библиотеке также есть функция reset_libdat, которая устанавливает значение libdat_dat обратно в ноль. Есть несколько копий этого libdat_dat (по одной на каждого клиента). Как он узнает, какой сбросить? Сбрасывает их все? Как найти их всех?
Я проверил разборку, и функция (reset_libdat) вычисляет адрес локальной копии модуля, используя относительную адресацию программного счетчика. Дизассемблирование комментируется с помощью `<libdat_dat @@ Base-0x28>`. Я не знаю, как это работает, но для этого используются некоторые спецификации. Я попробую разобраться в этом позже, хотя это будет хороший новый вопрос.
Означает ли это (в Linux), что если A и B оба ссылаются на C, а затем C ссылаются на D, то каждый из A и B получает отдельную копию данных C, но они разделяют копию D (через общую копию модуля C). Модель Windows заключается в том, что программа и все ее библиотеки DLL действуют так, как будто все они статически связаны в одну гигантскую программу.
@RaymondChen Это неверно. Конечно, оба получают одну и ту же копию errno! Общие библиотеки обращаются к глобальным переменным через таблицу GOT, которая очень похожа на структуру в Windows.
Но это не то, о чем говорится в этом ответе. В этом ответе говорится, что данные копируются в каждого клиента, и asm поддерживает это. Клиент получает свою личную копию libdat_dat.
@fuz правильный. Я протестировал ваш пример ABCD, и все библиотеки используют одну копию всех данных, которые хранятся в сегменте .bss исполняемого файла. Исполняемый файл обращается к нему напрямую. Каждая динамическая библиотека обращается к ней косвенно через GOT, специфичный для каждой библиотеки. Динамический компоновщик инициализирует GOT каждой динамической библиотеки, создавая карту частной памяти в процессе по адресу, по которому загружена динамическая библиотека GOT, а затем записывает в нее адреса фактических данных в сегменте .bss.
@JAlan Ага, значит предложение «Компоновщик определяет размер объекта и резервирует для него место в сегменте .bss или .rdata модуля». было неверно. Он хранится в .bss / .rdata в исполняемый файл. Тест не продемонстрировал этого различия, потому что модуль импорта было исполняемый файл.
@RaymondChen Это верно. Я зафиксировал ответ. Когда я изначально писал это, я не понимал, что динамические библиотеки разделяют данные и что это соглашение важно.