Что происходит с глобальными и статическими переменными в разделяемой библиотеке, когда она динамически связана?

127

Я пытаюсь понять, что происходит, когда модули с глобальными и статическими переменными динамически связываются с приложением. Под модулями я подразумеваю каждый проект в решении (я много работаю с Visual Studio!). Эти модули либо встроены в * .lib, либо * .dll, либо в сам * .exe.

Я понимаю, что двоичный файл приложения содержит глобальные и статические данные всех отдельных единиц перевода (объектных файлов) в сегменте данных (и сегмент данных только для чтения, если const).

  • Что происходит, когда это приложение использует модуль A с динамической компоновкой во время загрузки? Я предполагаю, что в DLL есть раздел для глобальных переменных и статики. Загружает ли их операционная система? Если да, то куда они загружаются?

  • А что происходит, когда приложение использует модуль B с динамической компоновкой во время выполнения?

  • Если в моем приложении есть два модуля, которые оба используют A и B, создаются ли копии глобальных объектов A и B, как указано ниже (если это разные процессы)?

  • Получают ли библиотеки DLL A и B доступ к глобальным приложениям?

(Также укажите свои причины)

Цитата из MSDN :

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

и отсюда :

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

Спасибо.

Раджа
источник
3
Под модулями вы, вероятно, имеете в виду библиотеки . На данный момент предлагается добавить модули в стандарт C ++ с более точным определением того, что это за модуль, и другой семантикой, чем у обычных библиотек.
Дэвид Родригес - дрибес
Ах, надо было это прояснить. Я рассматриваю в решении разные проекты (много работаю с visual studio) как модули. Эти модули встроены в файлы * .lib или * .dll.
Raja
3
@ DavidRodríguez-dribeas Термин «модуль» - это правильный технический термин для автономных (полностью связанных) исполняемых файлов, включая: исполняемые программы, библиотеки с динамической компоновкой (.dll) или общие объекты (.so). Здесь это совершенно уместно, а значение правильное и хорошо понимаемое. Как я объяснил, до тех пор, пока не будет стандартной функции под названием «модули», ее определение остается традиционным.
Микаэль Перссон

Ответы:

176

Это довольно известное различие между Windows и Unix-подобными системами.

Не важно что:

  • Каждый процесс имеет собственное адресное пространство, что означает, что между процессами никогда не используется общая память (если вы не используете какую-либо библиотеку или расширения межпроцессного взаимодействия).
  • One Definition Rule (УСО) все еще применяется, а это означает , что вы можете иметь только одно определение глобальной переменной видимой на время компоновки (статическое или динамическое связывание).

Итак, ключевым моментом здесь является наглядность .

Во всех случаях 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 ++, это территория, зависящая от платформы, то есть намного менее надежен / портативен).

Микаэль Перссон
источник
5
Отличный ответ, спасибо! У меня есть следующий вопрос: поскольку DLL представляет собой автономный фрагмент кода и данных, есть ли в нем раздел сегмента данных, аналогичный исполняемым файлам? Я пытаюсь понять, где и как эти данные загружаются (в) при использовании общей библиотеки.
Raja
18
@Raja Да, в DLL есть сегмент данных. Фактически, с точки зрения самих файлов, исполняемые файлы и библиотеки DLL практически идентичны, единственная реальная разница - это флаг, который установлен в исполняемом файле, чтобы сказать, что он содержит «основную» функцию. Когда процесс загружает DLL, его сегмент данных копируется куда-то в адресное пространство процесса, а статический код инициализации (который инициализирует нетривиальные глобальные переменные) также запускается в адресном пространстве процесса. Загрузка такая же, как и для исполняемого файла, за исключением того, что адресное пространство процесса расширяется, а не создается новое.
Микаэль Перссон
4
Как насчет статических переменных, определенных внутри встроенной функции класса? например, определите "class A {void foo () {static int st_var = 0;}}" в файле заголовка и включите его в модуль A и модуль B, будет ли A / B использовать одну и ту же st_var или у каждого будет собственная копия?
camino
2
@camino Если класс экспортируется (т.е. определяется с помощью __attribute__((visibility("default")))), то A / B будет использовать одну и ту же st_var. Но если класс определен с __attribute__((visibility("hidden"))), то модуль A и модуль B будут иметь свою собственную копию, а не общую.
Вэй Гуо
1
@camino __declspec (dllexport)
ruipacheco