Что происходит с глобальными и статическими переменными в разделяемой библиотеке, когда она динамически связана?
Я пытаюсь понять, что происходит, когда модули с глобальными и статическими переменными динамически связаны с приложением. Под модулями я подразумеваю каждый проект в решении (я много работаю с visual studio!). Эти модули либо встроены в *.lib или *.dll, либо в сам файл *.exe.
Я понимаю, что двоичный файл приложения содержит глобальные и статические данные всех отдельных единиц перевода (объектных файлов) в сегменте данных (и сегмент данных только для чтения, если const).
Что происходит, когда это приложение использует модуль A с динамическим связыванием во время загрузки? Я предполагаю, что в DLL есть раздел для глобалов и статики. Загружает ли их операционная система? Если так, куда они загружаются?
И что происходит, когда приложение использует модуль B с динамическим связыванием во время выполнения?
Если в моем приложении есть два модуля, в которых оба используют A и B, создаются ли копии глобалов A и B, как указано ниже (если это разные процессы)?
Получают ли библиотеки A и B доступ к глобалам приложений?
(Пожалуйста, укажите также свои причины)
Цитата из MSDN:
Переменные, которые объявлены как глобальные в файле исходного кода DLL, обрабатываются компилятором и компоновщиком как глобальные переменные, но каждый процесс, который загружает данную DLL, получает свою собственную копию глобальных переменных этой DLL. Область действия статических переменных ограничена блоком, в котором объявлены статические переменные. В результате каждый процесс по умолчанию имеет свой собственный экземпляр глобальных и статических переменных DLL.
и отсюда:
При динамическом связывании модулей может быть неясно, есть ли у разных библиотек свои собственные экземпляры глобалов или общие глобалы.
Благодарю.
3 ответа
Это довольно известная разница между Windows и Unix-подобными системами.
Не важно что:
- Каждый процесс имеет свое собственное адресное пространство, что означает, что между процессами никогда не будет общей памяти (если вы не используете какую-либо библиотеку межпроцессного взаимодействия или расширения).
- Одно правило определения (ODR) по-прежнему применяется, то есть вы можете иметь только одно определение глобальной переменной, видимое во время соединения (статическое или динамическое связывание).
Таким образом, ключевой вопрос здесь действительно видимость.
Во всех случаях, static
глобальные переменные (или функции) никогда не видны снаружи модуля (dll/so или исполняемый файл). Стандарт C++ требует, чтобы они имели внутреннюю связь, а это означает, что они не видны за пределами модуля перевода (который становится объектным файлом), в котором они определены. Итак, это решает эту проблему.
Где это сложно, когда у вас есть extern
глобальные переменные. Здесь Windows и Unix-подобные системы совершенно разные.
В случае Windows (.exe и.dll), extern
глобальные переменные не являются частью экспортируемых символов. Другими словами, разные модули никоим образом не знают о глобальных переменных, определенных в других модулях. Это означает, что вы получите ошибки компоновщика, если попытаетесь, например, создать исполняемый файл, который должен использовать extern
переменная определена в DLL, потому что это не разрешено. Вам потребуется предоставить объектный файл (или статическую библиотеку) с определением этой внешней переменной и статически связать его как с исполняемым файлом, так и с DLL, что приведет к двум разным глобальным переменным (одна принадлежит исполняемому файлу, а другая - DLL).).
Чтобы фактически экспортировать глобальную переменную в Windows, вы должны использовать синтаксис, аналогичный синтаксису функции экспорта / импорта, то есть:
#ifdef COMPILING_THE_DLL
#define MY_DLL_EXPORT extern "C" __declspec(dllexport)
#else
#define MY_DLL_EXPORT extern "C" __declspec(dllimport)
#endif
MY_DLL_EXPORT int my_global;
Когда вы делаете это, глобальная переменная добавляется в список экспортируемых символов и может быть связана, как и все другие функции.
В случае Unix-подобных сред (например, Linux), динамические библиотеки, называемые "общими объектами" с расширением .so
экспортировать все extern
глобальные переменные (или функции). В этом случае, если вы делаете привязку во время загрузки из любого места к файлу общего объекта, то глобальные переменные являются общими, то есть связываются вместе как единое целое. По сути, Unix-подобные системы спроектированы так, что практически нет разницы между связыванием со статической или динамической библиотекой. Опять же, ODR применяется по всем направлениям: extern
Глобальная переменная будет разделена между модулями, что означает, что она должна иметь только одно определение для всех загруженных модулей.
Наконец, в обоих случаях для Windows или Unix-подобных систем вы можете выполнять динамическое связывание во время выполнения, т. Е. Используя LoadLibrary()
/ GetProcAddress()
/ FreeLibrary()
или же dlopen()
/ dlsym()
/ dlclose()
, В этом случае вам нужно вручную получить указатель на каждый из символов, которые вы хотите использовать, и это включает в себя глобальные переменные, которые вы хотите использовать. Для глобальных переменных вы можете использовать GetProcAddress()
или же dlsym()
точно так же, как вы делаете для функций, при условии, что глобальные переменные являются частью экспортированного списка символов (по правилам предыдущих абзацев).
И, конечно, в качестве необходимого заключительного замечания: глобальных переменных следует избегать. И я полагаю, что текст, который вы цитировали (о вещах, которые "неясны") относится именно к платформенным различиям, которые я только что объяснил (динамические библиотеки на самом деле не определяются стандартом C++, это территория, зависящая от платформы, то есть гораздо менее надежный / портативный).
Ответ, оставленный Микаэлем Перссоном, хотя и очень тщательный, содержит серьезную ошибку (или, по крайней мере, вводящую в заблуждение) в отношении глобальных переменных, которую необходимо устранить. Исходный вопрос задавался, были ли отдельные копии глобальных переменных или глобальные переменные были общими для процессов.
Правильный ответ таков: существуют отдельные (множественные) копии глобальных переменных для каждого процесса, и они не являются общими для процессов. Таким образом, заявление о том, что применяется правило одного определения (ODR), также вводит в заблуждение, оно не применяется в том смысле, что они НЕ являются одними и теми же глобальными переменными, используемыми каждым процессом, поэтому на самом деле это не «одно определение» между процессами.
Кроме того, несмотря на то, что глобальные переменные не «видимы» для процесса, они всегда легко «доступны» для процесса, потому что любая функция может легко вернуть значение глобальной переменной процессу или, если уж на то пошло, процессу. может установить значение глобальной переменной через вызов функции. Таким образом, этот ответ также вводит в заблуждение.
На самом деле, «да», процессы имеют полный «доступ» к глобальным переменным, по крайней мере, через вызовы функций в библиотеку. Но повторюсь, у каждого процесса есть своя собственная копия глобальных переменных, так что это не будут те же самые глобальные переменные, которые использует другой процесс.
Таким образом, весь ответ, касающийся внешнего экспорта глобальных переменных, действительно не по теме, не нужен и даже не связан с исходным вопросом. Поскольку для доступа к глобальным переменным не требуется внешний доступ, к глобальным переменным всегда можно получить непрямой доступ через вызовы функций библиотеки.
Единственная часть, которая является общей для процессов, это, конечно же, собственно «код». Код загружается только в одно место в физической памяти (ОЗУ), но то же самое место в физической памяти, конечно же, отображается в «локальные» ячейки виртуальной памяти каждого процесса.
Напротив, статическая библиотека имеет копию кода для каждого процесса, уже встроенного в исполняемый файл (ELF, PE и т. д.), и, конечно же, как и динамические библиотеки, имеет отдельные глобальные переменные для каждого процесса.
В системах unix:
Следует отметить, что компоновщик не жалуется, если две динамические библиотеки экспортируют одни и те же глобальные переменные. но во время выполнения может возникнуть segfault в зависимости от нарушений доступа. Обычным числом, демонстрирующим такое поведение, будет segmentation fault 15.
segfault at xxxxxx ip xxxxxx sp xxxxxxx error 15 in a.out