Статическая и динамическая линковка¶
Статическая линковка¶
При статической линковке код нужных функций из статических библиотек (.a) физически встраивается в итоговый
исполняемый файл. Архив .a — это набор объектных файлов, упакованных утилитой ar; линковщик извлекает из него только
те объектные файлы, которые содержат нужные символы.
Статическая линковка полезна когда:
- нужно запустить программу на машинах, где нет нужных версий библиотек;
- требуется поставить один самодостаточный бинарь (например, в distroless-контейнер);
- важен полный контроль над используемыми версиями библиотек.
Недостатки: бинарь крупнее; обновить библиотечный код без перекомпиляции нельзя.
Статическая линковка
main.o libc.a libfoo.a
│ │ │
│ ld │ извлекает нужные .o из архива
└────┬────┘─────────────┘
│
▼
┌──────────────────────────────────────┐
│ main (ET_EXEC) │
│ .text: код main + printf + foo + … │
│ всё в одном файле, ldd: none │
└──────────────────────────────────────┘
Динамическая линковка¶
При динамической линковке бинарь содержит только список зависимостей и таблицы для позднего связывания. При запуске
динамический линковщик (ld-linux-x86-64.so.2) находит нужные .so, загружает их в адресное пространство процесса,
выполняет релокации и настраивает PLT/GOT.
Динамические библиотеки имеют расширение .so и являются ELF-файлами типа DYN. Несколько процессов могут совместно
использовать одно физическое отображение .so в памяти, что экономит RAM.
Динамическая линковка
main.o libfoo.so (только ссылка, без кода)
│ │
│ ld │ записывает DT_NEEDED=libfoo.so
└──────┬──────┘
│
▼
┌────────────────────────────┐
│ main (ET_DYN) │
│ .text: код main │ адресное пространство процесса
│ .dynamic: DT_NEEDED=… │ ┌──────────────┐
│ .plt / .got │───▶│ libfoo.so │ (shared pages)
└────────────────────────────┘ │ libc.so │
└──────────────┘
Несколько процессов делят одни физические страницы .so в RAM
Просмотр динамических зависимостей: ldd¶
Команда ldd показывает, какие .so потребуются бинарю при запуске:
Пример вывода:
linux-vdso.so.1 (0x00007fd247062000)
libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x00007fd246c00000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007fd246a10000)
libm.so.6 => /usr/lib/libm.so.6 (0x00007fd246f3c000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd247064000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007fd246f0f000)
Механизм: ldd запускает тот же динамический линковщик с переменной LD_TRACE_LOADED_OBJECTS=1 — вместо реального
запуска программы он только печатает список загружаемых объектов. То же самое можно сделать вручную:
Создание и использование своей динамической библиотеки¶
Исходный файл lib.cpp:
Флаг -fPIC генерирует позиционно-независимый код (Position-Independent Code), необходимый для .so:
Основная программа main.cpp:
Линковка с библиотекой из текущего каталога:
Запуск — динамический линковщик ищет libmy.so только в стандартных путях, поэтому нужно указать текущий каталог:
Или зашить путь в бинарь через rpath:
Нестандартное расположение библиотек¶
Если библиотека лежит в нестандартном пути (например /opt/mylibs):
-L/opt/mylibs— добавить путь в список поиска при линковке;-Wl,-rpath,/opt/mylibs— записать путь в ELF-полеDT_RUNPATH, чтобы динамический линковщик нашёл библиотеку при запуске безLD_LIBRARY_PATH.
rpath¶
rpath (runtime path) — список каталогов, хранящийся прямо в ELF-файле в секции .dynamic (тег DT_RPATH или
DT_RUNPATH). Динамический линковщик просматривает эти каталоги при поиске .so.
Просмотр записанного rpath:
Разница между DT_RPATH и DT_RUNPATH: DT_RPATH применяется до LD_LIBRARY_PATH, DT_RUNPATH — после. Современные
линковщики предпочитают DT_RUNPATH.
Переменные окружения динамического линковщика¶
| Переменная | Назначение |
|---|---|
LD_LIBRARY_PATH |
Список каталогов (через :), в которых линковщик ищет .so до стандартных путей |
LD_PRELOAD |
Список .so, которые загружаются раньше всех остальных; их символы имеют приоритет |
LD_DEBUG |
Отладочный вывод динамического линковщика (LD_DEBUG=all ./prog) |
LD_PRELOAD позволяет перехватывать функции стандартной библиотеки:
Это используется в профилировщиках памяти, инструментах трассировки и тестовых заглушках.
Порядок поиска динамических библиотек¶
Динамический линковщик ищет .so в следующем порядке:
- Каталоги из
DT_RPATHв ELF-файле (устаревший тег, применяется первым); - Каталоги из
LD_LIBRARY_PATH; - Каталоги из
DT_RUNPATHв ELF-файле; - Кэш
/etc/ld.so.cache(заполняется командойldconfig); - Стандартные пути
/lib,/usr/lib(и их 64-битные варианты).
Чтобы добавить системный каталог в кэш ld.so.cache:
После этого программы найдут библиотеки из /opt/mylibs без LD_LIBRARY_PATH.
Трассировка вызовов библиотечных функций: ltrace¶
ltrace перехватывает вызовы функций из динамических библиотек и печатает их с аргументами и возвращаемыми значениями:
Для трассировки системных вызовов используется strace (см.
статью Системные вызовы: введение).
PLT/GOT: ленивое связывание¶
При первом вызове функции из .so её адрес ещё не известен — он резолвится «на ходу». Механизм: каждый вызов внешней
функции компилятор направляет через PLT (Procedure Linkage Table). PLT — это набор крошечных trampoline-заглушек; каждая
заглушка считывает указатель из GOT (Global Offset Table) и прыгает по нему.
Первый вызов printf (адрес в GOT ещё не проставлен)
main: PLT[printf]:
┌──────────────────┐ ┌─────────────────────────────────┐
│ call printf@plt │────────▶│ jmp *GOT[printf] │
└──────────────────┘ │ │ │
│ │ GOT[printf] == stub ──▶ │
│ ▼ │
│ push reloc_index │
│ jmp PLT[0] (resolver) │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ ld.so: _dl_runtime_resolve() │
│ находит printf в libc.so │
│ записывает адрес в GOT[printf] │
│ прыгает в реальный printf │
└─────────────────────────────────┘
Второй и последующие вызовы (GOT уже заполнен)
main: PLT[printf]:
┌──────────────────┐ ┌──────────────────────┐
│ call printf@plt │────────▶│ jmp *GOT[printf] │──────────▶ printf в libc.so
└──────────────────┘ └──────────────────────┘
GOT[printf] == реальный адрес
(resolver больше не вызывается)
Ленивое связывание ускоряет запуск: адреса разрешаются только для реально вызванных функций. Отключить его:
LD_BIND_NOW=1 ./prog или линковать с -Wl,-z,now — тогда всё резолвится при старте.
Сравнение статической и динамической линковки¶
| Свойство | Статическая | Динамическая |
|---|---|---|
| Размер бинаря | Крупнее | Меньше |
| Зависимости при запуске | Нет | Нужны .so нужных версий |
| Обновление библиотеки | Требует перекомпиляции | Достаточно заменить .so |
| Разделение кода между процессами | Нет | Да (физические страницы .so общие) |
| Переносимость | Высокая | Зависит от наличия библиотек |
Подробнее о том, как динамический линковщик загружает .so в адресное пространство — в
статье Запуск и завершение программы. Как отображение файлов в память устроено на уровне
ОС — в статье mmap и отображение файлов.
Источники¶
man ld.so— динамический линковщик, переменные окружения, поиск библиотекman ldd— утилита lddman dlopen— загрузка.soиз кода во время выполненияman ltrace— трассировка библиотечных вызововman 8 ldconfig— управление кэшем путей к динамическим библиотекам