Компоновка, статические и динамические библиотеки, ldd
Что делает компоновщик
Объектный файл main.o содержит машинный код, но с «дырами»: адрес printf или глобальной переменной a из другого файла ещё неизвестен. В объектном файле есть:
- таблица символов — какие имена файл определяет (
main) и какие ему нужны извне (printf,a); - таблица перемещений — в каких местах кода нужно подставить адреса.
Компоновщик (ld) собирает все объектные файлы, находит каждому неопределённому символу определение и подставляет адреса.
nm main.o
# U a ← U: undefined, нужен извне
# U printf
# 0000000000000000 T main ← T: определён в секции кода
Типичные ошибки компоновки: undefined reference to 'foo' (определения нет нигде) и multiple definition of 'a' (определение в двух файлах — вместо extern написали определение).
Статические библиотеки
Статическая библиотека libX.a — архив объектных файлов. При компоновке из неё копируются нужные функции прямо в исполняемый файл.
gcc -c mem.c util.c
ar rcs libmem.a mem.o util.o # создать архив
gcc main.c -L. -lmem -o prog # -lmem → ищет libmem.so или libmem.a
gcc -static main.c -o prog_static # полностью статическая программа
Динамические (разделяемые) библиотеки
Разделяемая библиотека libX.so подключается при запуске программы. В исполняемом файле остаётся только запись «нужна libX.so.N».
gcc -fPIC -shared mem.c -o libmem.so # позиционно-независимый код
gcc main.c -L. -lmem -o prog
Статическая .a |
Динамическая .so |
|
|---|---|---|
| Когда связывается | при компоновке | при запуске (и даже во время работы: dlopen) |
| Размер программы | больше | меньше |
| Память | у каждого процесса своя копия | одна копия кода в физической памяти на всех |
| Обновление (исправление уязвимости) | пересобрать все программы | заменить один файл .so |
| Зависимость от окружения | нет | нужна библиотека нужной версии |
Почему одна копия в памяти
Код библиотеки отображается в адресные пространства процессов через mmap файла .so только для чтения. Страницы таблиц разных процессов указывают на одни и те же физические кадры со страницами кода libc. Поэтому сотни процессов не тратят по 2 МБ на свою libc.
Динамический загрузчик
Кто подключает .so при запуске? Не ядро, а программа пространства пользователя — динамический загрузчик ld-linux.so (/lib64/ld-linux-x86-64.so.2):
execveзагружает ELF-файл программы. В его заголовке указан «интерпретатор» — путь к загрузчику.- Ядро отображает в память программу и загрузчик, запускает загрузчик.
- Загрузчик читает список нужных библиотек (
DT_NEEDED), ищет их, отображает черезmmap. - Выполняет перемещения (заполняет таблицу GOT адресами функций, часто лениво — при первом вызове через PLT).
- Передаёт управление в
_startпрограммы →main.
Где загрузчик ищет библиотеки:
DT_RPATH/DT_RUNPATH, записанные в программу;- переменная окружения
LD_LIBRARY_PATH; - кеш
/etc/ld.so.cache, который строитldconfigпо каталогам из/etc/ld.so.conf; - стандартные каталоги
/lib,/usr/lib.
ldd
$ ldd /bin/ls
linux-vdso.so.1 (0x00007ffc...)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x...)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x...)
/lib64/ld-linux-x86-64.so.2 (0x...)
ldd показывает, какие разделяемые библиотеки нужны программе и где загрузчик их найдёт. Если библиотека не найдена — not found.
ldd запускает загрузчик
ldd во многих реализациях фактически запускает программу через загрузчик с переменной LD_TRACE_LOADED_OBJECTS=1. Поэтому не применяйте ldd к недоверенным файлам — безопаснее objdump -p prog | grep NEEDED или readelf -d.
Задача 2: к какому уровню относится ldd
ldd — инструмент системы программирования: он работает с понятиями компоновки — разделяемыми библиотеками, загрузчиком, поиском библиотек. Это программа пространства пользователя, она не входит в ядро. Подробно — в уроке «Уровни вычислительной системы».
ldconfig и пакеты
После установки новой библиотеки в /usr/lib нужно обновить кеш: ldconfig. Именно поэтому пакеты с библиотеками в скриптах postinst вызывают ldconfig (сейчас это часто делают триггеры пакетного менеджера) — вспомним в уроке про пакеты.
Итог
- Компоновщик разрешает символы и выполняет перемещения;
nmпоказывает символы. .aкопируется в программу при компоновке;.soподключается при запуске и разделяется между процессами.- Подключает
.soдинамический загрузчикld-linux.so, а не ядро; порядок поиска: RPATH,LD_LIBRARY_PATH, кешldconfig, стандартные каталоги. ldd— список зависимостей программы; инструмент системы программирования.
Практика решено 0 из 5
Что показывает программа ldd ./prog?
Сравните статические и динамические библиотеки.
| Статическая (.a) | Динамическая (.so) | |
|---|---|---|
| Код библиотеки копируется в исполняемый файл при компоновке | ||
| Одна копия кода в физической памяти на все процессы | ||
| Обновление библиотеки без пересборки программ | ||
| Программа запускается без внешних файлов библиотек | ||
| Связывание происходит при запуске программы загрузчиком ld-linux.so |
Как называется переменная окружения, задающая дополнительные каталоги поиска разделяемых библиотек при запуске программы?
Расставьте шаги запуска динамически скомпонованной программы ./prog:
- Ядро отображает в память программу и загрузчик и передаёт управление загрузчику
- Загрузчик выполняет перемещения (заполняет адреса функций) и передаёт управление в точку входа программы
-
Ядро читает ELF-заголовок и видит секцию PT_INTERP — путь к загрузчику
/lib64/ld-linux-x86-64.so.2 -
Shell вызывает
fork, потомок вызываетexecve("./prog", ...) -
Загрузчик находит и отображает через
mmapнужные.so(libc.so.6 и др.)
Соберите разделяемую библиотеку libmem.so из mem.c (позиционно-независимый код)
и скомпонуйте с ней main.c в программу prog. Библиотека лежит в текущем каталоге.
gcc -fPIC -shared mem.c -o libmem.so
gcc main.c -L. -lmem -o prog
Войдите, чтобы отмечать пройденные уроки.