Перейти к содержанию

Статическая и динамическая линковка

Статическая линковка

При статической линковке код нужных функций из статических библиотек (.a) физически встраивается в итоговый исполняемый файл. Архив .a — это набор объектных файлов, упакованных утилитой ar; линковщик извлекает из него только те объектные файлы, которые содержат нужные символы.

Статическая линковка полезна когда:

  • нужно запустить программу на машинах, где нет нужных версий библиотек;
  • требуется поставить один самодостаточный бинарь (например, в distroless-контейнер);
  • важен полный контроль над используемыми версиями библиотек.
g++ -static main.cpp -o main_static
ldd main_static   # выведет: not a dynamic executable

Недостатки: бинарь крупнее; обновить библиотечный код без перекомпиляции нельзя.

 Статическая линковка

  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 потребуются бинарю при запуске:

ldd ./main

Пример вывода:

    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 — вместо реального запуска программы он только печатает список загружаемых объектов. То же самое можно сделать вручную:

LD_TRACE_LOADED_OBJECTS=1 ./main

Создание и использование своей динамической библиотеки

Исходный файл lib.cpp:

#include <iostream>

void hello() {
    std::cout << "Hello from lib\n";
}

Флаг -fPIC генерирует позиционно-независимый код (Position-Independent Code), необходимый для .so:

g++ -fPIC -shared lib.cpp -o libmy.so

Основная программа main.cpp:

void hello();

int main() {
    hello();
    return 0;
}

Линковка с библиотекой из текущего каталога:

g++ main.cpp -L. -lmy -o main

Запуск — динамический линковщик ищет libmy.so только в стандартных путях, поэтому нужно указать текущий каталог:

LD_LIBRARY_PATH=. ./main

Или зашить путь в бинарь через rpath:

g++ main.cpp -L. -lmy -Wl,-rpath,. -o main
./main   # работает без LD_LIBRARY_PATH

Нестандартное расположение библиотек

Если библиотека лежит в нестандартном пути (например /opt/mylibs):

g++ main.cpp -L/opt/mylibs -lstdc++ -Wl,-rpath,/opt/mylibs -o main
  • -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:

readelf -d ./main | grep -E 'RPATH|RUNPATH'

Разница между 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 позволяет перехватывать функции стандартной библиотеки:

LD_PRELOAD=./override_malloc.so ./prog

Это используется в профилировщиках памяти, инструментах трассировки и тестовых заглушках.

Порядок поиска динамических библиотек

Динамический линковщик ищет .so в следующем порядке:

  1. Каталоги из DT_RPATH в ELF-файле (устаревший тег, применяется первым);
  2. Каталоги из LD_LIBRARY_PATH;
  3. Каталоги из DT_RUNPATH в ELF-файле;
  4. Кэш /etc/ld.so.cache (заполняется командой ldconfig);
  5. Стандартные пути /lib, /usr/lib (и их 64-битные варианты).

Чтобы добавить системный каталог в кэш ld.so.cache:

echo /opt/mylibs > /etc/ld.so.conf.d/mylibs.conf
ldconfig          # обновить кэш (требует root)

После этого программы найдут библиотеки из /opt/mylibs без LD_LIBRARY_PATH.

Трассировка вызовов библиотечных функций: ltrace

ltrace перехватывает вызовы функций из динамических библиотек и печатает их с аргументами и возвращаемыми значениями:

ltrace ./prog

Для трассировки системных вызовов используется 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 — утилита ldd
  • man dlopen — загрузка .so из кода во время выполнения
  • man ltrace — трассировка библиотечных вызовов
  • man 8 ldconfig — управление кэшем путей к динамическим библиотекам