Привычная статическая компоновка с неперемещаемым исполняемым файлом (-no-pie):

  • compile time: компилируем код, получая объектные файлы, которые предоставляют символы, которые в них определены, и требуют символов, которые в них используются;
  • link time: во время компоновки объектных файлов в executable компоновщик разрешает (resolves) символы в их адреса и подставляет эти адреса в машинный код;
  • runtime: executable требует, чтобы его секции загрузили в память по фиксированным адресам, и рассчитывает на это в своей работе.

Динамическая компоновка:

  • link time: компоновщик оставляет некоторые символы неразрешёнными, но записывает в executable информацию о том, какие динамические библиотеки ему требуются для работы;
  • runtime: динамический загрузчик разрешает используемые символы в их адреса, разыскивая символы в загруженных библиотеках.

Как искать и загружать дополнительные библиотеки?

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

INTERP говорит, что нужно передать управление /lib/ld-linux.so.2

INTERP

Какие требования к коду разделяемых библиотек?

Что делать, если бинарник собирается по разным адресам памяти? В случае с переменными (в отличие от переходов) мы имеем не перемещаемый код (который работает только по фиксированному адресу памяти)

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

Есть один instruction pointer (регистр eip или rip), и на x86_32 мы не можем относительно него адресовать память, а на x86_64 можем.

Нужно предупредить компилятор, что у нас перемещаемый код.

Обращение по адресу памяти

Иначе компилятор обратится к переменной по адресу памяти.

-D

Опция -D - показать динамические предоставляемые символы. (puts нет)

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

Мы хотим, чтобы секция .text была отображена в память read-only (так она во всех процессах будет в одних и тех же физических страницах памяти — экономия), поэтому при релокации вытаскиваем в отдельную writable секцию, и вместо того, чтобы везде вызывать функцию puts, мы вызываем соответствующее место plt. А уже в соответствующем месте можем сделать jmp на нужную функцию. (на ее адрес в памяти)

puts

Главный бинарник по умолчанию собирается с -fpie:

  • более расслабленные требования к перемещаемому коду

  • компилятору проще найти функции и переменные

Таким образом, механизм разделяемых библиотек предоставляет нам 2 важные вещи:

  • код можно мапить в адресное пространство разных процессов

    • Таким образом экономится физическая и оперативная память.
  • динамическое связывание более слабое

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

Дополнительные темы для обсуждения:

  • ABI compatibility;
  • LD_PRELOAD.