Embedded HandbookПриложения
Все разделы › XVII. Системное программирование на C
· уровень средний · 10 мин чтения
Открыть в интерактивном справочнике

143. Статические и динамические библиотеки: .a, .so, soname, rpath, LD_PRELOAD, dlopen

Раздел 74 разбирал стандартную библиотеку — ту, что дают. Здесь — как сделать свою: собрать код в библиотеку, отдать её другим программам и обновлять, не пересобирая их. В прошивке библиотека всегда статическая: её код вклеивается в образ. В Linux есть второй путь — динамическая библиотека, один файл на диске для всех программ сразу, — и вокруг него целое хозяйство: soname, ldd, rpath, LD_PRELOAD, dlopen. Библиотека libgeom из четырёх маленьких функций (две открытые, одна скрытая и одна static) проходит весь путь в _verify/sys143/ (stm-sys:1, gcc 12, glibc 2.36).

Как это называется в документации

Оригинал Перевод
static library, archive (.a) статическая библиотека, архив объектных файлов
shared library, shared object (.so) динамическая (разделяемая) библиотека
position-independent code (PIC) код, не зависящий от адреса загрузки
dynamic linker / loader (ld.so) динамический компоновщик
soname имя библиотеки, зашитое в программу
NEEDED, RUNPATH что нужно программе и где искать
symbol visibility видимость символа снаружи библиотеки
interposition подмена символа библиотекой, загруженной раньше
dlopen, dlsym открыть библиотеку и найти в ней имя во время работы
plugin плагин: библиотека, загружаемая по имени
ABI двоичный интерфейс: что можно менять, не ломая старые программы

143.1 Библиотека из четырёх функций

static double square(double x) { return x * x; }                 /* static: не видна даже компоновщику */

double GEOM_HIDDEN geom_internal(double x) { return square(x) + 1; }   /* видна в .a, скрыта в .so */

GEOM_API double geom_dist(double x1, double y1, double x2, double y2)
{
    return sqrt(square(x2 - x1) + square(y2 - y1));
}

Три уровня видимости: static — имя не покидает файл; обычное внешнее — видно всем, кто компонуется; GEOM_HIDDEN — атрибут visibility("hidden"), имя есть в объектном файле, но из динамической библиотеки не экспортируется. Программа app.c зовёт geom_dist и geom_version (четвёртая функция, отдаёт строку версии) и не знает, какая библиотека ей достанется.

Один объектный файл — статический архив, который вклеивается, или .so, который ld.so находит при запуске


143.2 Статическая: архив объектных файлов

gcc -O2 -c geom.c -o geom.o
ar rcs libgeom.a geom.o
gcc -O2 -o app_static app.c -L. -lgeom -lm

libgeom.a — просто архив: ar t перечисляет geom.o. Компоновщик берёт из архива только те объектные файлы, в которых есть нужные символы, и вклеивает их в программу: app_static — 16 152 байта и не зависит от libgeom.a после сборки. nm показывает, что в архиве:

T geom_dist
T geom_internal
T geom_version
U sqrt

T — определён в секции кода, U — не определён, нужен снаружи (здесь sqrt из libm). square в списке нет: при -O2 компилятор встроил её в вызывающие; при -O0 она есть с пометкой t — локальная.

Порядок в командной строке важен: компоновщик идёт слева направо и берёт из архива то, что уже понадобилось. gcc -lgeom app.c — ошибка undefined reference to geom_dist: в момент чтения libgeom.a символ ещё никому не нужен. gcc app.c -lgeom — собралось. Библиотеки — после объектов, и те, что зависят, — раньше тех, от кого зависят.

Две командные строки: архив раньше объекта — неразрешённый символ, после объекта — сборка


143.3 Динамическая: PIC, soname, три имени

gcc -fPIC -fvisibility=hidden -c geom.c -o geom_pic.o
gcc -shared -Wl,-soname,libgeom.so.1 -o libgeom.so.1.0 geom_pic.o -lm
ln -s libgeom.so.1.0 libgeom.so.1
ln -s libgeom.so.1 libgeom.so

-fPIC — код без абсолютных адресов: библиотека загружается в каждый процесс по своему адресу. -fvisibility=hidden прячет всё, что не помечено GEOM_API (visibility("default")): экспортированы ровно две функции —

geom_dist
geom_version

а geom_internal снаружи не существует. Это и есть ABI библиотеки: чем меньше экспортировано, тем меньше обязательств.

Три имени одного файла:

Имя Кто использует
libgeom.so.1.0 — настоящий файл пакет, установка
libgeom.so.1 — soname, ссылка программа во время запуска (NEEDED)
libgeom.so — ссылка компоновщик при сборке (-lgeom)

-Wl,-soname зашивает в библиотеку имя libgeom.so.1, и именно его компоновщик записывает в программу:

(NEEDED) [libgeom.so.1]
(NEEDED) [libc.so.6]

Номер в soname — версия ABI: 1.0 → 1.1 совместимы, программа получит новую библиотеку без пересборки; 1 → 2 — несовместимы, старые программы продолжат искать libgeom.so.1. Опыт: собрать libgeom.so.1.1 с GEOM_VERSION "1.1", переключить ссылку libgeom.so.1 — и старая программа печатает:

geom 1.1: расстояние (0,0)-(3,4) = 5.0

Программа с динамической библиотекой — 16 048 байт, почти как статическая: код geom в ней не лежит, только имя и таблица нужных символов.


143.4 Где искать: ld.so, LD_LIBRARY_PATH, rpath

Запуск программы — это запуск /lib64/ld-linux-x86-64.so.2: он читает NEEDED, находит библиотеки, отображает их и связывает символы. Ищет он по порядку: RPATH в самой программе (старая запись; учитывается, только если нет RUNPATH), переменная LD_LIBRARY_PATH, RUNPATH в программе, кэш /etc/ld.so.cache (из каталогов в /etc/ld.so.conf.d/), затем /lib и /usr/lib. Современный компоновщик пишет RUNPATH, поэтому LD_LIBRARY_PATH его перекрывает. Текущий каталог в списке нет:

./app_shared: error while loading shared libraries: libgeom.so.1: cannot open shared object file: No such file or directory

LD_LIBRARY_PATH=. ./app_shared работает, и ldd показывает, откуда что взято:

libgeom.so.1 ./libgeom.so.1
libc.so.6 /lib/x86_64-linux-gnu/libc.so.6
libm.so.6 /lib/x86_64-linux-gnu/libm.so.6

Переменная окружения — для разработки; для установленной программы — либо библиотека в системном каталоге и ldconfig, либо путь, зашитый в программу: -Wl,-rpath,'$ORIGIN' записывает RUNPATH [$ORIGIN] — «рядом с исполняемым файлом», и программу можно переносить каталогом целиком.


143.5 LD_PRELOAD: подменить функцию без пересборки

Библиотека из LD_PRELOAD загружается раньше всех, и её символы перекрывают одноимённые в libc. Так считают вызовы malloc в любой программе:

void *malloc(size_t n)
{
    if (!real_malloc) real_malloc = dlsym(RTLD_NEXT, "malloc");   /* RTLD_NEXT: настоящий malloc из libc */
    calls++;
    return real_malloc(n);
}
[preload] malloc вызван 994 раз      ← python3 -c pass
[preload] malloc вызван 1 раз        ← app_rpath: буфер printf
[preload] malloc вызван 0 раз        ← /bin/true

Внутри подмены нельзя звать printf — он сам зовёт malloc, и получится рекурсия; отчёт пишется через snprintf в статический буфер и write. Так же подменяют malloc целиком (jemalloc, раздел 135), ловят ошибки памяти (libasan), подделывают время и сеть в тестах. Для программ с setuid LD_PRELOAD не действует: ядро помечает такой запуск как защищённый, и динамический загрузчик переменную игнорирует (кроме библиотек из системных каталогов).


143.6 dlopen: библиотека по имени во время работы

Плагин — библиотека, имя которой известно только при запуске:

    void *h = dlopen(path, RTLD_NOW);                          /* RTLD_NOW: все символы сразу, ошибка — здесь */
    if (!h) { printf("dlopen: %s\n", dlerror()); return 1; }
    double (*dist)(double, double, double, double) = dlsym(h, "geom_dist");
плагин ./libgeom.so.1.1: версия 1.1, dist = 10.0, geom_internal скрыт
плагин ./libgeom.so.1.0: версия 1.0, dist = 10.0, geom_internal скрыт
dlopen: ./nope.so: cannot open shared object file: No such file or directory

Одна программа открыла обе версии по очереди, и dlsym не нашёл geom_internal — видимость hidden действует и здесь. RTLD_NOW связывает всё при открытии, RTLD_LAZY — при первом вызове; dlerror — текст последней ошибки. Так грузят плагины редакторы, серверы и обработчики форматов; библиотека для dlopen обычно экспортирует одну функцию-«вход», которая возвращает таблицу указателей на остальные.


143.7 Windows: DLL

В Windows динамическая библиотека — .dll, и рядом с ней компоновщик создаёт библиотеку импорта .lib (.dll.a у MinGW): программа собирается с ней, а во время запуска ищет .dll в своём каталоге, потом по PATH. Экспорт — __declspec(dllexport) на функции или файл .def; по умолчанию ничего не экспортируется, как при -fvisibility=hidden. Аналог dlopen/dlsym — LoadLibrary/GetProcAddress. Формат файла — PE вместо ELF, и soname там нет: версии решают именем файла и каталогом рядом с программой.


143.8 Грабли

Симптом Причина
undefined reference при компоновке, хотя библиотека указана -l стоит раньше объекта, который её использует
cannot open shared object file при запуске нет в RUNPATH, LD_LIBRARY_PATH, кэше; текущий каталог не ищется
работает у меня, не работает у коллеги LD_LIBRARY_PATH в оболочке — зашить rpath или установить в систему
relocation R_X86_64_32 … recompile with -fPIC объект собран без -fPIC — в .so не годится
dlsym вернул NULL символ скрыт (visibility), C++-имя искажено, или нет extern "C"
после обновления библиотеки программа падает сломан ABI при том же soname: изменён размер структуры или порядок полей
LD_PRELOAD не действует программа setuid, или статическая
рекурсия и падение в подмене malloc внутри подмены вызван printf/malloc
два экземпляра одной переменной библиотека загружена дважды под разными путями
ldd показывает not found для одной из зависимостей у библиотеки свой NEEDED — искать её тем же путём

143.9 Итоги

Полные исходники

geom.c — библиотека «геометрия»: две открытые функции и одна скрытая.

/* Раздел 143: библиотека «геометрия» — две открытые функции и одна скрытая. */
#include <math.h>
#include "geom.h"

static double square(double x) { return x * x; }                 /* static: не видна даже компоновщику */

double GEOM_HIDDEN geom_internal(double x) { return square(x) + 1; }   /* видна в .a, скрыта в .so */

GEOM_API double geom_dist(double x1, double y1, double x2, double y2)
{
    return sqrt(square(x2 - x1) + square(y2 - y1));
}

GEOM_API const char *geom_version(void) { return GEOM_VERSION; }

preload_malloc.c — подмена malloc через LD_PRELOAD.

/* Раздел 143: подмена malloc через LD_PRELOAD — считаем вызовы в любой программе без пересборки.
   Внутри — только write и dlsym, без printf: printf сам зовёт malloc. */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>

static void *(*real_malloc)(size_t);
static long calls;
static char buf[64];

void *malloc(size_t n)
{
    if (!real_malloc) real_malloc = dlsym(RTLD_NEXT, "malloc");   /* RTLD_NEXT: настоящий malloc из libc */
    calls++;
    return real_malloc(n);
}

__attribute__((destructor)) static void report(void)
{
    int len = snprintf(buf, sizeof buf, "[preload] malloc вызван %ld раз\n", calls);
    write(2, buf, len);
}

plugin_host.c — загрузка библиотеки во время работы через dlopen и dlsym.

/* Раздел 143: загрузка библиотеки во время работы — dlopen/dlsym; так устроены плагины. */
#include <stdio.h>
#include <dlfcn.h>

int main(int argc, char **argv)
{
    const char *path = argc > 1 ? argv[1] : "./libgeom.so";
    void *h = dlopen(path, RTLD_NOW);                          /* RTLD_NOW: все символы сразу, ошибка — здесь */
    if (!h) { printf("dlopen: %s\n", dlerror()); return 1; }
    double (*dist)(double, double, double, double) = dlsym(h, "geom_dist");
    const char *(*ver)(void) = dlsym(h, "geom_version");
    void *hidden = dlsym(h, "geom_internal");
    printf("плагин %s: версия %s, dist = %.1f, geom_internal %s\n", path, ver(), dist(0, 0, 6, 8),
           hidden ? "найден" : "скрыт");
    dlclose(h);
    return 0;
}

Дальше: раздел 144 — ассемблер x86-64 глазами C.

Открыть в интерактивном справочнике