Главная страница » Какие динамические библиотеки использует ваш исполняемый файл

Какие динамические библиотеки использует ваш исполняемый файл

  • автор:

Как узнать список библиотек, которые используются программой(бинарём)?Какие библиотеки использует программа?

Есть собранный бинарь, надо узнать что он подтягивает(использует) для своей работы. Точно помню, что есть такая утилита на Linux, вот почему-то не могу найти.

Если не знаем где лежит программа — ищем её

переходим в директорию с бинарём и запускаем утилиту ldd

Вообще зависимость от библиотек на программерском сленге называется dependency

В хорошем случае зависимости даже динамические можно посмотреть через gdb genue debugger загружакшь прогу в отладчик и смотришь deps.

По нубский — тяп-ляп, ldd (list dynamic dependencies) даёт информацию о статический загруженных библиотеках.

Добавлено спустя. Вот пример догрузки на самом простейшем паскаль варианте под винду (добавлено просто для понимания как это вообще работает)

A.1 – Статические и динамические библиотеки

Библиотека – это пакет кода, который предназначен для повторного использования многими программами. Обычно библиотека C++ состоит из двух частей:

  1. заголовочный файл, который определяет функциональность, которую библиотека предоставляет (предлагает) программам, использующим ее;
  2. предварительно скомпилированный двоичный файл, который содержит реализацию этой функциональности, предварительно скомпилированную в машинный код.

Некоторые библиотеки могут быть разделены на несколько файлов и/или иметь несколько файлов заголовков.

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

Существует два типа библиотек: статические библиотеки и динамические библиотеки.

Статическая библиотека (иногда называемая archive, «архив») состоит из подпрограмм, которые скомпилированы и линкуются непосредственно с вашей программой. Когда вы компилируете программу, использующую статическую библиотеку, все функции статической библиотеки, которые использует ваша программа, становятся частью вашего исполняемого файла. В Windows статические библиотеки обычно имеют расширение .lib , а в Linux – расширение .a (archive, архив). Одним из преимуществ статических библиотек является то, что вам нужно распространять только исполняемый файл, чтобы пользователи могли запускать вашу программу. Поскольку библиотека становится частью вашей программы, это гарантирует, что с вашей программой всегда будет использоваться правильная версия библиотеки. Кроме того, поскольку статические библиотеки становятся частью вашей программы, вы можете использовать их так же, как функции, которые вы написали для своей программы. С другой стороны, поскольку копия библиотеки становится частью каждого исполняемого файла, который ее использует, это может привести к потере большого количества места. Статические библиотеки также не могут быть легко обновлены – для обновления библиотеки необходимо заменить весь исполняемый файл.

Динамическая библиотека (также называемая shared library, «общая библиотека») состоит из подпрограмм, которые загружаются в ваше приложение во время выполнения. Когда вы компилируете программу, использующую динамическую библиотеку, библиотека не становится частью вашего исполняемого файла – она ​​остается отдельной единицей. В Windows динамические библиотеки обычно имеют расширение .dll (dynamic link library, библиотека динамической компоновки), а в Linux – расширение .so (shared object, общий объект). Одним из преимуществ динамических библиотек является то, что многие программы могут совместно использовать одну копию библиотеки, что экономит место. Возможно, большим преимуществом является то, что динамическую библиотеку можно обновить до более новой версии без замены всех исполняемых файлов, которые ее используют.

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

Библиотека импорта – это библиотека, которая автоматизирует процесс загрузки и использования динамической библиотеки. В Windows это обычно делается с помощью небольшой статической библиотеки ( .lib ) с тем же именем, что и динамическая библиотека ( .dll ). Статическая библиотека подключается к программе во время компиляции, и затем функциональные возможности динамической библиотеки можно эффективно использовать, как если бы это была статическая библиотека. В Linux файл общих объектов ( .so ) выполняет функции динамической библиотеки и библиотеки импорта. Большинство компоновщиков при создании динамической библиотеки могут создать библиотеку импорта для этой динамической библиотеки.

Установка и использование библиотек

Теперь, когда вы знаете о различных типах библиотек, давайте поговорим о том, как на самом деле использовать библиотеки в вашей программе. Установка библиотеки на C++ обычно состоит из 4 шагов:

  1. Получите библиотеку. Лучший вариант – загрузить предварительно скомпилированный пакет для вашей операционной системы (если он существует), чтобы вам не пришлось компилировать библиотеку самостоятельно. Если для вашей операционной системы не предусмотрен пакет, вам придется загрузить пакет, содержащий только исходный код, и скомпилировать его самостоятельно (что выходит за рамки этого урока). В Windows библиотеки обычно распространяются в виде файлов .zip . В Linux библиотеки обычно распространяются в виде пакетов (например, .RPM ). В вашем диспетчере пакетов могут быть некоторые из наиболее популярных библиотек (например, SDL ), которые уже перечислены для упрощения установки, поэтому сначала проверьте там.
  2. Установите библиотеку. В Linux это обычно включает вызов диспетчера пакетов и предоставление ему возможности выполнить всю работу. В Windows это обычно включает разархивирование библиотеки в каталог по вашему выбору. Для облегчения доступа рекомендуем хранить все свои библиотеки в одном месте. Например, используйте каталог C:\libs и поместите каждую библиотеку в отдельный подкаталог.
  3. Убедитесь, что компилятор знает, где искать файл(ы) заголовков для данной библиотеки. В Windows обычно это подкаталог include каталога, в который вы установили файлы библиотеки (например, если вы установили свою библиотеку в C:\libs\SDL-1.2.11 , файлы заголовков, вероятно, находятся в C:\libs\SDL-1.2.11\include ). В Linux файлы заголовков обычно устанавливаются в /usr/include , который уже должен быть частью пути поиска включаемых файлов. Однако если файлы установлены в другом месте, вам придется указать компилятору, где их найти.
  4. Сообщите компоновщику, где искать файл(ы) библиотеки. Как и в шаге 3, это обычно включает добавление каталога в список мест, где компоновщик ищет библиотеки. В Windows это обычно подкаталог /lib каталога, в который вы установили файлы библиотеки. В Linux библиотеки обычно устанавливаются в /usr/lib , который уже должен быть частью пути поиска ваших библиотек.

После того, как библиотека установлена, ​​и среда IDE знает, где ее искать, обычно необходимо выполнить следующие 3 шага для каждого проекта, который хочет использовать библиотеку:

  1. Если вы используете статические библиотеки или библиотеки импорта, сообщите компоновщику, какие файлы библиотеки нужно линковать.
  2. Включите с помощью #include заголовочный файл(ы) библиотеки в вашу программу. Это сообщит компилятору обо всех функциях, предлагаемых библиотекой, чтобы ваша программа могла правильно компилироваться.
  3. Если вы используете динамические библиотеки, убедитесь, что программа знает, где их найти. В Linux библиотеки обычно устанавливаются в /usr/lib , который находится в пути поиска по умолчанию после путей в переменной среды LD_LIBRARY_PATH . В Windows путь поиска по умолчанию включает каталог, из которого запускается программа, каталоги, установленные вызовом SetDllDirectory() , каталоги Windows, System и System32 , а также каталоги в переменной среды PATH . Самый простой способ использовать .dll – скопировать .dll в расположение исполняемого файла. Поскольку вы обычно распространяете .dll вместе со своим исполняемым файлом, в любом случае имеет смысл хранить их вместе.

Шаги 3-5 включают настройку вашей IDE – к счастью, когда дело доходит до выполнения этих вещей, почти все IDE работают одинаково. К сожалению, поскольку каждая среда IDE имеет свой интерфейс, самая сложная часть этого процесса – просто определить правильное место для выполнения каждого из этих шагов. Следовательно, в следующих нескольких уроках этого раздела мы расскажем, как выполнить все эти шаги как для Visual Studio, так и для Code::Blocks. Если вы используете другую IDE, прочтите оба урока – к тому времени, когда вы закончите, у вас должно быть достаточно информации, чтобы сделать то же самое с вашей собственной IDE и небольшим гуглением.

Путешествие по динамическим библиотекам C и их отличиям от статических библиотек

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

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

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

Зачем использовать библиотеки в C?

Основная причина использования библиотеки на C — это когда у вас есть фрагмент кода, который может быть полезен для многих других программ или файлов. Таким образом, вместо того, чтобы тратить время на написание кода и, возможно, на исправление некоторых ошибок (повторяя часть кода, который у вас уже есть), вы могли бы использовать библиотеки, чтобы сделать свою жизнь счастливее. В двух словах, если вы хотите быть более эффективным при кодировании, используйте библиотеки, это вам очень поможет.

После того, как мы ответим на вопросы о библиотеках и почему, мы начнем путешествие, и наша первая остановка будет посвящена понятиям статических и динамических библиотек в C.

Что такое статическая библиотека?

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

Преимущества

  • Не требуется во время выполнения.
  • Связывание с этим типом библиотек выполняется быстрее, чем связывание отдельных файлов.

Недостатки

  • Каждый раз, когда вы вносите изменения в свою библиотеку, вы должны перекомпилировать свой код.
  • Каждый файл, использующий статическую библиотеку, содержит копию в своем исполняемом файле.
  • Размер исполняемого файла становится больше, поскольку код библиотеки хранится внутри исполняемого файла.
Что такое динамическая библиотека?

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

Преимущества

  • Одна копия библиотеки загружается в память, когда запущена программа, которая ее использует, и когда несколько запущенных программ используют эту библиотеку, мы можем сэкономить много памяти.
  • Если какие-либо изменения были сделаны внутри библиотеки, нам не нужно перекомпилировать нашу программу.
  • Вышеупомянутое преимущество также упрощает обслуживание библиотеки.

Недостатки

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

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

Как работают статические библиотеки и как их создавать

Статические библиотеки работают следующим образом: когда вы компилируете файл, перед завершением процесса компиляции на этапе компоновки компилятор ищет символы (функции, переменные и т. д.), которые требуются программе. На этом шаге, когда вы используете статические библиотеки, компоновщик ищет эти символы внутри указанной библиотеки, и когда символы находят, они связываются с программой в исполняемом файле:

Для создания статических библиотек мы используем программу под названием ar (аббревиатура от «архиватор»). Эта программа позволяет нам создавать статические библиотеки, изменять и извлекать объектные файлы из библиотеки. Для создания статической библиотеки мы используем следующую команду:

Флаг r указывает программе заменить старые объектные файлы в библиотеке новыми объектными файлами. С другой стороны, флаг c указывает программе создать библиотеку, если она еще не существует.

Следует отметить, что после создания или изменения файла (объектного файла) необходимо его проиндексировать, чтобы компилятор мог ускорить поиск символов внутри библиотеки. По этой причине мы используем команду:

Как работают динамические библиотеки и как их создавать

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

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

Когда программа выполняется, системный динамический загрузчик ищет некоторые библиотеки в определенных каталогах. Если библиотеки нет, значит, это библиотека, которую мы создаем. Таким образом, в этом случае мы можем указать динамическому загрузчику искать в других каталогах, используя переменную окружения LD_LIBRARY_PATH следующим образом (в данном случае мы работаем в sh):

Если LD_LIBRARY_PATH не определен:

Если LD_LIBRARY_PATH уже определен:

И, наконец, чтобы убедиться, что ваша библиотека находится в вашей системе, вы используете следующую команду:

В отличие от статических библиотек, для создания динамических библиотек сначала нам нужно скомпилировать файлы, которые будут частью библиотеки, следующим образом:

Как видите, мы используем флаг -fPIC (код, независимый от позиции PIC), чтобы объектный файл можно было использовать в общей библиотеке. Это означает, что разделяемая библиотека будет совместно использоваться процессами, но не всегда возможно загрузить библиотеку по одному и тому же адресу в обоих процессах. Таким образом, если бы объектный файл не был PIC, то каждому процессу потребовалась бы его собственная копия.

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

Это долгий путь, зная, как работают статические и динамические библиотеки, как их создавать и как их использовать, мы почти закончили… мы собираемся продолжить нашу последнюю остановку, добро пожаловать в различия между статическими и общие библиотеки!

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

Основные различия между статическими и динамическими библиотеками:

  • Статические прикрепляются к исполняемому файлу во время компиляции.
  • Динамические — это файлы вне исполняемого файла (во время компиляции исполняемый файл получает ссылку, указывающую на библиотеку, но объектные файлы не загружаются внутрь исполняемого файла).
  • Код, связанный с исполняемым файлом из Static, нельзя изменить без перекомпиляции.
  • Динамическая библиотека может быть изменена без перекомпиляции.
  • Статическая библиотека копируется в код каждой исполняемой программы во время компиляции.
  • Динамическая копия создается только один раз.
  • Программа более уязвима с Dynamic, потому что, если библиотека повреждена, исполняемый файл не будет работать.
  • Статическая библиотека дает исполняемому файлу больше автономии, потому что код внутри него.

«После того как вы отправились в путешествие, путешествие никогда не заканчивается, а разыгрывается снова и снова в самых тихих покоях. Ум никогда не может оторваться от путешествия».

C2017/Динамические библиотеки

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

В UNIX-системах библиотеки имеют расширение so (shared object), в Windows — расширение dll (dynamic link library).

Динамическая загрузка

Динамические библиотеки могут использоваться двумя способами:

  • динамическая компоновка (dynamic linking)
  • динамическая загрузка (dynamic loading)

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

Переадресация (relocation)

Разные программы имеют различные размеры и различный набор подгружаемых динамических библиотек, и если разделяемая библиотека отображается в адресное пространство различных программ, она будет иметь различные адреса. Это в свою очередь означает, что все функции и переменные в библиотеке будут на различных местах. Если все обращения к адресам относительные («значение +1020 байта отсюда») нежели абсолютные («значение в 0x102218BF»), то это не проблема, однако так бывает не всегда. В таких случаях всем абсолютным адресам необходимо прибавить подходящий офсет — это и есть relocation.

Это практически всегда скрыто от C/C++ программиста — очень редко проблемы компоновки вызваны трудностями переадресации.

Таблица перемещений (relocation table) — это список указателей, созданный транслятором (компилятором или ассемблером) и хранимый в объектном или исполняемом файле. Каждая запись в таблице, или «fixup», является указателем на абсолютный адрес в объектном коде, который должен быть изменен, когда загрузчик перемещает программу так, чтобы она ссылалась на правильное местоположение. Fixup’ы предназначены для поддержки переноса программы в виде цельной единицы.

ASLR (англ. address space layout randomization — «рандомизация размещения адресного пространства») — технология, применяемая в операционных системах, при использовании которой случайным образом изменяется расположение в адресном пространстве процесса важных структур данных, а именно образов исполняемого файла, подгружаемых библиотек, кучи и стека.

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

API и ABI

API: Application Program Interface

Это набор публичных типов, переменных, функций, которые вы делаете видимыми из вашего приложения или библиотеки.

В C и C++ API обычно поставляется в виде заголовочного файла (h) вместе с библиотекой.

С API работают люди, когда пишут код.

ABI: Application Binary Interface

Детали реализации этого интерфейса. Определяет такие вещи, как

  • Способ передачи параметров в функции (регистры, стек).
  • Кто извлекает параметры из стека (вызывающий код или вызываемый, caller/callee).
  • Как происходит возврат значений из функции.
  • Как реализован механизм исключений.
  • Декорирование имён в C++ (mangling).

ABI важно, когда приложение использует внешние библиотеки. Если при обновлении библиотеки ABI не меняется, то менять программу не надо. API может остаться тем же, но поменяется ABI. Две версии библиотеки, имеющие один ABI, называют binary compatible (бинарно совместимыми): старую версию библиотеки можно заменить на новую без проблем.

Иногда без изменений ABI не обойтись. Тогда приходится перекомпилировать зависящие программы. Если ABI библиотеки меняется, а API нет, то версии называют source compatible.

Разработчики библиотеки стараются поддерживать ABI стабильным. Новые функции и типы данных могут добавляться, но старые должны сохраняться.

Linux

Полностью статическая сборка

Рассмотрим такой простейший код:

Даже такая программа использует динамические библиотеки. Вы можете просмотреть библиотеки, используемые приложением, через команду ldd.

Обратите внимание, что адреса меняются — это ASLR.

С помощью ключа -static компилятора gcc можно создать статически скомпонованный исполняемый файл.

Динамический загрузчик ld-linux.so

Когда операционная система загружает приложение, скомпонованное динамически, она должна найти и загрузить динамические библиотеки, необходимые для выполнения программы. В ОС Linux эту работу выполняет ld-linux.so.2.

Когда запускается программа ls, ОС передаёт управление в ld-linux.so.2 вместо нормальной точки входа в приложение. В свою очередь ld-linux.so.2 ищет и загружает требуемые библиотеки, затем передаёт управление на точку старта приложения.

Справочная страница (man) к ld-linux.so.2 даёт высокоуровневое описание работы динамического компоновщика. По сути это рантайм-компонент компоновщика (ld), который отыскивает и загружает в память динамические библиотеки, используемые приложением. Обычно динамический компоновщик неявно задаётся в процессе компоновки. Спецификация ELF предоставляет функциональность динамической компоновки. Компилятор GCC включает в исполняемые файлы специальный заголовок (program header) под названием INTERP, он указывает путь к динамическому компоновщику.

Спецификация гласит, то если присутствует заголовок PT_INTERP, то ОС должна создать образ процесса интерпретатора вместо приложения. Управление передаётся интерпретатору, который отвечает за загрузку динамических библиотек. Спецификация закладывает достаточную гибкость.

linux-vdso.so.1

В те времена, когда процессоры с архитектурой x86 только появились, взаимодействие пользовательских приложений со службами операционной системы осуществлялось с помощью прерываний. По мере создания более мощных процессоров эта схема взаимодействия становилась узким местом системы. Во всех процессорах, начиная с Pentium® II, Intel® реализовала механизм быстрых системных вызовов (Fast System Call), в котором вместо прерываний используются инструкции SYSENTER и SYSEXIT, ускоряющие выполнение системных вызовов.

Библиотека linux-vdso.so.1 является виртуальной библиотекой, или виртуальным динамически разделяемым объектом (VDSO), который размещается только в адресном пространстве отдельной программы. В более ранних системах эта библиотека называлась linux-gate.so.1. Эта виртуальная библиотека содержит всю необходимую логику, обеспечивающую для пользовательских приложений наиболее быстрый доступ к системным функциям в зависимости от архитектуры процессора – либо через прерывания, либо (для большинства современных процессоров) через механизм быстрых системных вызовов.

Система нумерации версий

Во всём UNIX-мире принята система нумерации вида major.minor.patchlevel:

  • Мажорная версия библиотеки изменяется всякий раз, когда у неё меняется ABI.
  • Минорная версия изменяется при добавлении в библиотеку новой функциональности без изменения ABI.
  • Patchlevel изменяется при исправлении ошибок без добавления новой функциональности.

Смена мажорной версии библиотеки — это всегда событие, переход на неё — это всегда трудозатраты.

Пример

Пример динамической загрузки

Сборка выполняется так:

Решение проблемы перемещения

Скомпилируем объектный файл:

RIP — регистр instruction pointer, указывает на следующую инструкцию.

Адресация относительно RIP была введена в x86-64 в «длинном» режиме и используется по умолчанию. В старом x86 такая адресация применялась только для инструкций перехода call, jmp, . а теперь стала применяться в гораздо большем числе инструкций.

Когда потом объектый файл статически линкуется в исполняемый файл, нули превращаются в ненули:

Но это статическая линковка, а с динамической сложнее.

Есть два подхода:

  • Load-time relocation
  • Position independent code (PIC)
Load-time relocation

На x86-64 метод не применяется.

  • Замедление на стадии загрузки.
  • text-сегмент получается разным в разных копиях библиотеки, то есть не может разделяться между библиотеками, теряется преимущество экономии памяти.
  • text-сегмент доступен на запись (лишняя угроза безопасности)
Position Independent Code

Пример использования внешней библиотеки

LD_PRELOAD

Windows

Несмотря на то что общие принципы разделяемых библиотек примерно одинаковы как на платформах UNIX, так и на Windows, всё же есть несколько существенных различий.

Экспортируемые символы

Самое большое отличие заключается в том, что в библиотеках Windows символы не экспортируются автоматически. В UNIX все символы всех объектных файлов, которые были подлинкованы к разделяемой библиотеке, видны пользователю этой библиотеки. В Windows программист должен явно делать некоторые символы видимыми, т. е. экспортировать их.

Есть три способа экспортировать символ и Windows DLL (и все эти три способа можно перемешивать в одной и той же библиотеке).

Первый способ

В исходном коде объявить символ как __declspec(dllexport), примерно так:

Второй способ

При выполнении команды компоновщика использовать опцию LINK.EXE export:symbol_to_export

Третий способ

Скормить компоновщику файл определения модуля (DEF) (используя опцию /DEF:def_file), включив в этот файл секцию EXPORT, которая содержит символы, подлежащие экспортированию.

Как приходится иметь дело с C++, первая из этих опций становится самой простой, так как в этом случае компилятор берёт на себя обязательства позаботиться о декорировании имён.

.LIB и другие относящиеся к библиотеке файлы

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

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

Чтобы сделать всё ещё более запутанным, расширение .LIB также используется для статических библиотек.

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

Файлы на выходе компоновки
  • library.DLL: собственно код библиотеки; этот файл нужен (во время исполнения) любому бинарнику, использующему библиотеку.
  • library.LIB: файл «импортирования библиотеки», который описывает, где и какой символ находится в результирующей DLL. Этот файл генерируется, если только DLL экспортирует некоторые её символы. Если символы не экспортируются, то смысла в .LIB файле нет. Этот файл нужен во время компоновки.
  • library.EXP: «Экспорт файл» компилируемой библиотеки, который нужен, если имеет место компоновка бинарников с циклической зависимостью.
  • library.ILK: Если опция /INCREMENTAL была применена во время компоновки, которая активирует инкрементную компоновку, то этот файл содержит в себе статус инкрементной компоновки. Он нужен для будущих инкрементных компоновок с этой библиотекой.
  • library.PDB: Если опция /DEBUG была применена во время компоновки, то этот файл является программной базой данных, содержащей отладочную информацию для библиотеки.
  • library.MAP: Если опция /MAP была применена во время компоновки, то этот файл содержит описание внутреннего формата библиотеки.
Файлы на входе компоновки
  • library.LIB: Файл «импорта библиотеки», которые описывает где и какие символы находятся в других DLL, которые нужны для компоновки.
  • library.LIB: Статическая библиотека, которая содержит коллекцию объектов, необходимых при компоновке. Обратите внимание на неоднозначное использование расширения .LIB
  • library.DEF: Файл «определений», который позволяет управлять различными деталями скомпонованной библиотеки, включая экспорт символов.
  • library.EXP: Файл экспорта компонуемой библиотеки, который может сигнализировать, что предыдущее выполнение LIB.EXE уже создало файл .LIB для библиотеки. Имеет значение при компоновке бинарников с циклическими зависимостями.
  • library.ILK: Файл состояния инкрементной компоновки; см. выше.
  • library.RES: Файл ресурсов, который содержит информацию о различных GUI-виджетах, используемых исполняемым файлом. Эти ресурсы включаются в конечный бинарник.

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

Импортируемые символы

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

В LIB-файле для фунции FunctionName генерирутеся «заглушка», которая выглядит как

Здесь __imp__FunctionName является записью в таблице импортированных функций. То есть заглушка считывает адрес из таблицы импортированных адресов (IAT) и выполняет переход на тот код.

За счёт двухступенчатого процесса получается лишняя потеря производительности.

Если писать dllimport, компилятор будет генерировать код для непрямого вызова через таблицу IAT прямо по месту. Это уменьшает число индирекций и позволяет компилятору локально (в вызывающей функции) закешировать целевой адрес. Хотя при наличии LTO это уже не актуально.

Объявляем символ как __declspec(dllimport) в исходном коде примерно так:

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

Стандартный выход из этой ситуации — это использование макросов препроцессора.

Файл с исходниками в DLL, который определяет функцию и переменную гарантирует, что переменная препроцессора EXPORTING_XYZ_DLL_SYMS определена (посредством #define) до включения соответствующего заголовочного файла и таким образом экспортирует символ. Любой другой код, который включает этот заголовочный файл, не определяет этот символ и таким образом импортирует его.

Циклические зависимости

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

Для большинства систем — это не проблема. Выполняемые файлы зависят от высокоуровевых библиотек, высокоуровневые библиотеки зависят от библиотек низкого уровня, и всё компонуется в обратном порядке — сначала библиотеки низкого уровня, потом высокого, а затем и выполняемый файл, который зависит от всех остальных

Однако, если имеет место циклическая зависимость между бинарниками, тогда всё немного усложняется. Если X.DLL нуждается в символе из Y.DLL, а Y.DLL нуждается в символе из X.DLL, тогда необходимо решить задачу про курицу и яйцо: какая бы библиотека ни компоновалась бы первой, она не сможет найти разрешение ко всем символам.

Windows предоставил обходной приём примерно следующего содержания. Сначала имитируем компоновку библиотеки X. Запускаем LIB.EXE (не LINK.EXE), чтобы получить файл X.LIB точно такой же, какой был бы получен с LINK.EXE. При этом X.DLL не будет сгенерирован, но вместо него будет получен файл X.EXP. Компонуем библиотеку Y как обычно, используя X.LIB, полученную на предыдущем шаге, и получаем на выходе как Y.DLL, так и Y.LIB. В конце концов компонуем библиотеку X теперь уже полноценно. Это происходит почти как обычно, используя дополнительно файл X.EXP, полученный на первом шаге. Обычное в этом шаге то, что компоновщик использует Y.LIB и производит X.DLL. Необычное — компоновщик пропускает шаг создания X.LIB, так как этот файл был уже создан на первом шаге, чему свидетельствует наличие .EXP файла.

Но несомненно лучше всё же реорганизовать библиотеки таким образом, чтоб избежать любых циклических зависимостей…

Relocation

DLL в Microsoft Windows используют вариант E8 инструкции CALL (относительный, смещение относительно следующей команды). Эти инструкции не нужно изменять при загрузке DLL.

Некоторые глобальные переменные (например, массивы строковых литералов, таблицы виртуальных функций) содержат адрес объекта в секции данных; поэтому сохраненный адрес в глобальной переменной необходимо обновить, чтобы он соответствовал адресу, по которому была загружена DLL. Динамический загрузчик вычисляет адрес, на который ссылается глобальная переменная, и сохраняет значение в глобальной переменной; это вызывает copy-on-write страницы памяти, содержащей такую ​​глобальную переменную. Страницы с кодом и страницы с глобальными переменными, которые не содержат указателей на код или глобальные данные, остаются разделяемыми между процессами. Эта операция должна выполняться в любой ОС, которая может загружать динамическую библиотеку по произвольному адресу.

В Windows Vista и более поздних версиях Windows перемещение DLL и исполняемых файлов выполняется диспетчером памяти ядра, который разделяет relocated-библиотеки между несколькими процессами. Образы всегда перемещаются с их предпочтительных базовых адресов, обеспечивая рандомизацию размещения адресного пространства (ASLR).

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

Динамические библиотеки и язык C++

Поддержка бинарной совместимости

Что делать можно
  • Можно добавить новую функцию, новый невиртуальный метод класса, новый конструктор и новые статические поля.
  • Можно добавлять новые перегрузки к существующим перегруженным функциям, создавать новые перегруженные функции или перегружать private-методы.
  • Можно добавлять новые виртуальные функции в конец класса, не имеющего наследников.
  • Можно изменить тело любой непоинлайненной функции (т. е. не определённой в публичном заголовочном файле).
  • Можно добавлять новые классы.
  • Можно произвольным образом изменять, добавлять и удалять friend declarations.
  • Можно переименовывать private-поля классов.
  • Можно делать всё, что угодно, с классами и функциями, не являющимися частью API (размещёнными в cpp-файле и имеющими static или обёрнутыми в анонимный namespace).
Что делать не стоит
  • Можно изменить тело поинлайненной функции или унести её в .cpp. Однако функциональность менять при этом нельзя (даже исправлять ошибки), поскольку собранные с библиотекой бинарники продолжат пользоваться старой версией функции.
  • Можно переопределить виртуальный метод, определённый в базовом классе. Однако функциональность менять при этом, опять же, нельзя, т. к. в ряде мест компилятор может заменять виртуальный вызов прямым (напр. Derived d; d.foo();).
  • Можно удалить невиртуальный закрытый (private) метод класса (или private static поле). Но перед этим нужно убедиться, что метод никогда за время жизни библиотеки не вызывался (или поле не использовалось) ни из одной inline-функции.
  • Можно изменять значения по умолчанию для функций и методов; однако уже собранные бинарники продолжат передавать старые значения по умолчанию.
  • Можно добавлять перегруженные варианты существующих неперегруженных public-методов. Это сохраняет бинарную совместимость, но код, который брал адрес такой функции (auto x = &MyClass::method) перестанет компилироваться.
Чего делать нельзя
  • Нельзя удалять существующие классы, являющиеся частью API.
  • У существующих классов нельзя удалять открытые, защищённые или виртуальные методы.
  • Нельзя как-либо менять существующую иерархию классов (т. е. изменять, добавлять, удалять или переупорядочивать базовые классы).
  • Нельзя как-либо менять список шаблонных параметров для существующих шаблонных классов и функций.
  • Нельзя менять сигнатуру существующих функций и методов.
  • Нельзя добавлять виртуальные методы в середину класса.
  • Нельзя добавлять виртуальные методы в класс, не имеющий виртуальных функций или виртуального деструктора.
  • Нельзя добавлять виртуальные методы в класс, имеющий наследников.
  • Нельзя добавлять, переупорядочивать или удалять поля существующих классов.

Идиома PImpl (pointer to implementation)

Другое, более общее название — opaque pointer (непрозрачный указатель).

  • реализация скрыта,
  • добавление новых полей в в приватную структуру не ломает бинарную совместимость,
  • ускоряется компиляция.

Минусы также очевидны:

  • усложняется код,
  • замедляются все вызовы за счёт лишнего разыменования указателя.

Похожий принцип можно использовать и на чистом C, эмулируя ООП и инкапсуляцию:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *