Компоновка, статические и динамические библиотеки, ldd

⏱ 30 мин · 🧩 заданий: 5

Что делает компоновщик

Объектный файл 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):

  1. execve загружает ELF-файл программы. В его заголовке указан «интерпретатор» — путь к загрузчику.
  2. Ядро отображает в память программу и загрузчик, запускает загрузчик.
  3. Загрузчик читает список нужных библиотек (DT_NEEDED), ищет их, отображает через mmap.
  4. Выполняет перемещения (заполняет таблицу GOT адресами функций, часто лениво — при первом вызове через PLT).
  5. Передаёт управление в _start программы → main.

Где загрузчик ищет библиотеки:

  1. DT_RPATH/DT_RUNPATH, записанные в программу;
  2. переменная окружения LD_LIBRARY_PATH;
  3. кеш /etc/ld.so.cache, который строит ldconfig по каталогам из /etc/ld.so.conf;
  4. стандартные каталоги /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

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Один вариант ответа

Что показывает программа ldd ./prog?

Задание 2 Заполнить таблицу

Сравните статические и динамические библиотеки.

Статическая (.a)Динамическая (.so)
Код библиотеки копируется в исполняемый файл при компоновке
Одна копия кода в физической памяти на все процессы
Обновление библиотеки без пересборки программ
Программа запускается без внешних файлов библиотек
Связывание происходит при запуске программы загрузчиком ld-linux.so
Отметьте ячейку — «+», пустая — «−».
Задание 3 Ввод ответа

Как называется переменная окружения, задающая дополнительные каталоги поиска разделяемых библиотек при запуске программы?

Задание 4 Расставить по порядку

Расставьте шаги запуска динамически скомпонованной программы ./prog:

  1. Ядро отображает в память программу и загрузчик и передаёт управление загрузчику
  2. Загрузчик выполняет перемещения (заполняет адреса функций) и передаёт управление в точку входа программы
  3. Ядро читает ELF-заголовок и видит секцию PT_INTERP — путь к загрузчику /lib64/ld-linux-x86-64.so.2
  4. Shell вызывает fork, потомок вызывает execve("./prog", ...)
  5. Загрузчик находит и отображает через mmap нужные .so (libc.so.6 и др.)
Задание 5 Написать код

Соберите разделяемую библиотеку libmem.so из mem.c (позиционно-независимый код) и скомпонуйте с ней main.c в программу prog. Библиотека лежит в текущем каталоге.

Войдите, чтобы отмечать пройденные уроки.