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 (четвёртая функция, отдаёт строку версии) и не знает, какая библиотека ей достанется.
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 Итоги
.a— архив: код вклеивается, порядок-lпосле объектов..so:-fPIC,-shared,-Wl,-soname; три имени; экспорт толькоGEOM_APIпри-fvisibility=hidden.- Версия 1.0 → 1.1 под тем же soname — без пересборки программы.
- Поиск:
LD_LIBRARY_PATH→RUNPATH($ORIGIN) → кэш →/lib,/usr/lib; старыйRPATH— раньше переменной. LD_PRELOADподменяет символы (994mallocуpython3 -c pass); внутри — безprintf.dlopen/dlsym— плагины;hiddenскрывает и от них.
Полные исходники
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.