Senior Pimiento

Линковщики и Загрузчики

Ссылки

Линковка

Линковка это процесс компоновки различных кусков кода и данных вместе, в результате чего получается один исполняемый файл. Линковка может быть выполнена во время компиляции, во время загрузки (загрузчиком) и также во время исполнения (исполняемой программой). Раньше (конец 40-х) линковка выполнялась вручную, сейчас мы имеем программы линковщики (linkers), которые дают возможность динамической линковки разделяемых библиотек (shared libraries).

Основы

Пусть у нас есть два файла с кодом a.c и b.c. Чтобы скомпилировать эти два файла при помощи GCC, мы вызываем следующий код

gcc a.c b.c

Это вызывает следующую последовательность:

  1. Запустить препроцессор на файле a.c и сохранить результат в промежуточный файл a.i
cpp other-command-line options a.c /tmp/a.i
  1. Запустить компилятор на a.i и сгенерировать ассемблерный код в a.s
cc1 other-command-line options /tmp/a.i -o /tmp/a.s
  1. Запустить ассемблер на a.s и сгенерировать объектный файл a.o
as other-command-line options /tmp/a.s -o /tmp/a.o
  1. Повторить шаги 1-4 для файла b.c, получить объектный файл b.o
  2. Работа линковщика состоит в том, чтобы получить на вход сгенерированные объектные файлы a.o и b.o и сгенерировать из них финальный исполняемый файл a.out
ld other-command-line-options /tmp/a.o /tmp/b.o -o a.out

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

Линковщики и Загрузчики

Линковщики (linkers) и загрузчики (loaders) выполняют концептуально разные, но в целом похожие задачи:

  • Загрузка программ. Копирование образа программы с жёсткого диска в RAM. В некоторых случаях загрузка программы (loading) также может включать выделение дисковой памяти или отображение виртульного адресного пространства на дисковое пространство.
  • Релокация (relocation). Компиляторы и ассемблеры генерируют объектный код для каждого входного модуля программы с началом адресации в нуле. Релокация — это процесс изменения адреса загрузки различных частей программы во время объединения всех секций одного типа в одну секцию. Секции кода и данных таким образом будут указывать на корректные адреса в рантайме.
  • Symbol Resolution. Программы имеют внутри себя множество подпрограмм; указание одной подпрограммы на другую подпрограмму происходит через символьные таблицы. Работа линковщика — подменять указания на символ подпрограммы на указание адреса расположения подпрограммы, изменяя объектный код.

В итоге, получается что загрузчик выполняет загрузку программ; линковщик выполняет symbol resolution; оба выполняют релокацию.

Объектные файлы

  • Перемещаемый объектный файл (relocatable object file) — содержит бинарный код и данные в форме, которая может быть скомпонована с другими перемещаемыми объектными файлами во время компиляции. В итоге получаем исполняемый объектный файл, скомпонованный из перемещаемых объектный файлов.
  • Исполняемый объектный файл (executable object file) — содержат бинарный код и данные в форме, которая может быть напрямую загружена в память и выполнена.
  • Разделяемый объектный файл (shared object file) — специальный тип перемещаемого объектного файла, который может быть загружен в память и слинкован динамически либо во время загрузки в память, либо во время выполнения.

Компиляторы и ассемблеры генерируют перемещаемые объектные файлы (а так же разделяемые объектные файлы). Линковщики компонуют эти объектные файлы вместе и генерируют исполняемые объектные файлы.

ELF

Объектные файлы разнятся в разных ОС. Первые UNIX системы использовали формат a.out. Ранние System V использовали формат COFF (common object file format). Windows NT использует разновидность формата COFF, называемую PE (portable executable); IBM использует собственный формат IBM 360. Современные UNIX системы, такие как Linux и Solaris используют формат UNIX ELF (executable and linking format).

Заголовки Elf

  • .text: the machine code of the compiled program.
  • .rodata: read-only data, such as the format strings in printf statements.
  • .data: initialized global variables
  • .bss: uninitialized global variables. BSS (начало блока данных — block storage start), эта секция обычно пустует в объектных файлах; этакая заглушка.
  • .symtab: таблица символов, содержащая информацию о функциях и глобальных переменных, определённых и адресованных в коде программы. Эта таблица не содержит записей о локальных переменных, эта информация содержится на стеке.
  • .rel.text: список мест в секции .text, которые необходимо модифицировать, когда линковщик будет компоновать этот объект с другими объектными файлами.
  • .rel.data: информация о релокации глобальных переменных, которые объявлены, но не определены в текущем модуле программы.
  • .debug: таблица отладочных символов с записями о локальных и глобальных переменных. Эта секция будет присутствовать только если компилятору был передан флаг компиляции с таблицей отладочных символов (-g для gcc).
  • .line: отображение номеров строк в исходном C-файле и машинными кодами инструкций. Эта информация необходима для отладки программ.
  • .strtab: таблица строк для таблицы символов .symtab и секции .debug

Символы и адресация символов

Каждый перемещаемый объектный файл содержит таблицу символов связанные символы. В контексте линковщика представлены следующие виды символов:

  • Глобальные символы объявленые на уровне модуля — могут быть адресованы из других модулей.Все не-статические и глобальные переменные попадают в эту категорию.
  • Глобальные символы адресованные в коде, но объевленные где-то вне. Все функции и переменные с модификатором extern попадают в эту категорию.
  • Локальные символы объявленные и адресованные исключительно во входном модуле. Все статические функции и статические переменные попадают в эту категорию.

Линковщик разрещает адресацию символов путём соотношения каждой ссылки на символ только к одному определению символу из таблицы символов.

Линковка статических библиотек

Статические библиотеки это коллекция конкатенированных объектных файлов схожего типа. Эти библиотеки хранятся на диске в архиве. Архив также содержит мета-информацию для ускорения поиска в нём. Каждый архив с ELF начинается с магической последовательности !\n.Статические библиотеки передаются на вход линковщику, который копирует только объектные модули, упоминаемые в программе. В процессе разрешения адресации символов при работе со статическими библиотеками линковщик сканирует перемещаемые объектные файлы и архивы справа-налево в порядке указания аргументов вызова. В процессе сканирования линковщик создаёт набор O-файлов (перемащаемых объектных файлов, которые будут включены в исполняемый файл); набор U-файлов (неразрешённых пока символов); набор D-файлов (символы, объявленные в предыдущих модулях). Изначально все три набора пустые.

  • На каждый следующий входной аргумент линковщик определяет передаётся ли объектный файл или архив. Если это перемещаемый объектный файл, то линковщик добавляет его в набор O, обновляет наборы U и D и переходит к следующему входному аргументу
  • Если входной аргумент архив, линковщик сканирует список членов модулей, входящих в архив, чтобы отыскать любые неразрешённые символы, находящиеся в наборе U. Если такие символы находятся, то они добавляются в список O и обновляется список U. Список D дополняется символами, найденными в архиве.
  • Когда все входные аргументы пройдены, но если набор U не пуст, то линковщик сообщает об ошибке линковки и завершает свою работу. Иначе, если набор U пуст, линковщик компонует и релоцирует объектные файлы из набора O и генерирует финальный исполняемый файл.

Релокация

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

  1. Релокация секций и определения символов. Линковщик объединяет все секции одного типа в новую секцию. К примеру, линковщик объединяет все секции .data всех входных перемещаемых объектов в новую секцию .data результирующего исполняемого файла. Похожий процесс происходит для секции .code. Затем линковщик указывает текущий адрес памяти для этой сгенерированной секции. Так для каждой секции и символа. После завершения этого шага каждая инструкция и глобальная переменная в прогармме будет иметь уникальный адрес в момент загрузки.
  2. Релокация адресации символов внутри секций. На этом шаге линковщик изменяет адресации на символы в коде и секциях данных так, чтобы они указывали на корректный уникальный адрес в момент загрузки.

Ассемблер при релокации создаёт секции .relo.text и .relo.data, в которых содержится информация как разрешить адресацию (адрес для обращения к символу). ELF содержит в секциях релокации следующие данные:

  • Смещение (offset). Для перемещаемых файлов значение смещения это смещение в байтах от начала секции до получившегося после релокации адреса.
  • Символ (symbol). Индекс символа в символьной таблице.
  • Тип (type). Тип релокации.

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

Статические библиотеки, описанные выше, имеют существенный недостаток. Например, возьмём стандартные функции printf и scanf. Они используются почти что в каждой программе. Пусть на системе запущено 50-100 процессов, каждый процесс содержит свою копию исполняемого кода printf и scanf — это существенный объём затраченной памяти. Разделяемые библиотеки в свою очередь направлены на исправление этого недостатка статических библиотек. Разделяемые библиотеки это объектные модули, которые могут быть загружены в память в момент исполнения программы и после слинкованы с программой. Разделяемые библиотеки (shared libraries) называют так же разделяемые объекты (shared objects). На большинстве систем UNIX они именуются с суффиксом .so; на системах HP-UX — с суфиксом .sl; на системах Microsoft они называются DLL.Чтобы собрать разделяемый объектный файл, компилятор надо вызывать со специальным флагом

gcc -shared -fPIC -o libfoo.so a.o b.o

Эта команда сообщает компилятору, что надо сгенерировать разделяемую библиотеку libfoo.so, собранную из объектный файлов a.o и b.o. Флаг -fPIC сообщает компилятору, что надо сгенерировать адресо-независимый код (position independent code — PIC).Теперь представим что объектный модуль bar.o зависит от a.o и b.o. В этом случае мы компилируем его так:

gcc bar.o ./libfoo.so

Эта команда создаёт исполняемый файл a.out, который будет линковаться с libfoo.so в момент загрузки. Здесь a.out не содержит в себе объектный модулей a.o и b.o, которые были бы включены в него, если бы мы использовали статическую линковку. Исполняемый файл просто содержит некоторую информацию о релокации и таблицу символов, которые позволяют адресоваться к коду и данным в libfoo.so и эта адресация будет разрешена в процессе исполнения (runtime). Таким образом, a.out это не совсем исполняемый файл, который имеет зависимость от libfoo.so. Исполняемый файл содержит секцию .interp, где содержится имя динамического линковщика (который сам является разделяемым объектом в системах Linux — ld-linux.so). Таким образом, когда исполняемый файл загружается в память, загрузчик передаёт управление динамическому линковщику. Динамический линковщик содержит некоторый код, который отображает пространство адресов динамических библиотек на пространство адресов испольняемой программы.

  1. Происходит релокация кода и данных из libfoo.so в область памяти
  2. Происходит релокация адресации в a.out на символы объявленные в libfoo.so.

В конце работы динамический линковщик передаёт контроль исполняемой программе. С этого момента местоположение разделяемого объекта зафиксировано в памяти.

Загрузка разделяемой библиотеки из приложения

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

Утилиты для работы с объектными файлами

  • ar: создаёт статические библиотеки.
  • objdump: может быть использована для показа всей информации о бинарном объектном файле.
  • strings: показывает все строковые данные в бинарном файле, содержащие печатные символы.
  • nm: перечислить символы, определённые в символьной таблице объектного файла.
  • ldd: перечислить динамические библиотеки, от которых зависит объектный файл.
  • strip: удалить информацию из таблицы символов.