Линковка и библиотеки: основы¶
Что такое линковка¶
Линковка — финальная стадия сборки, на которой из набора объектных файлов и библиотек создаётся готовый исполняемый
файл или разделяемая библиотека. Линковщик (ld) выполняет следующие задачи:
- объединяет несколько объектных файлов и библиотек в один файл;
- по таблицам символов находит определения для всех использованных, но не определённых функций и переменных;
- расставляет окончательные адреса и выполняет релокации;
- формирует итоговый ELF-файл с корректной точкой входа.
Библиотеки¶
Библиотека — это коллекция скомпилированного кода, которую можно подключить к программе при линковке. Существует два вида библиотек:
- Статическая библиотека (
.a) — архив объектных файлов (ar-архив). Код из неё физически копируется в итоговый бинарь при линковке. Результат: самодостаточный исполняемый файл без внешних зависимостей от данной библиотеки. - Динамическая библиотека (
.so) — разделяемый объект, ELF типа DYN. Код не копируется в бинарь; при запуске его загружает и подключает динамический линковщик. Несколько процессов могут использовать одну.soв памяти одновременно.
Как линковщик разрешает символы¶
Каждый .o файл содержит таблицу символов. Линковщик читает все таблицы и ищет для каждого UND (undefined) символа
его определение (GLOBAL DEF) в другом объекте или библиотеке:
foo.o bar.o libmath.a
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Symbol table │ │ Symbol table │ │ Symbol table │
├──────────────────┤ ├──────────────────┤ ├──────────────────┤
│ GLOBAL DEF main │ │ GLOBAL DEF bar │ │ GLOBAL DEF sin │
│ UND bar │──────▶│ │ │ GLOBAL DEF cos │
│ UND sin │ │ UND sin │──────▶│ │
│ UND printf│ └──────────────────┘ └──────────────────┘
└──────────────────┘ ▲
│ │
└─────────────────────────────────────────────────────┘
После разрешения символов линковщик выполняет релокации:
foo.o: итоговый бинарь:
┌───────────────────────┐ ┌───────────────────────┐
│ call bar@PLT │ │ call 0x401020 │ ← адрес bar
│ call sin@PLT │ ──▶ │ call 0x401060 │ ← адрес sin
│ call printf@PLT │ │ call 0x401090 │ ← адрес printf
└───────────────────────┘ └───────────────────────┘
Виды записей в таблице символов (nm):
U — undefined (UND): используется, но не определено в этом .o
T — text: определено в секции .text (код)
D — data: определено в секции .data
B — bss: определено в .bss (неинициализированные данные)
W — weak: слабый символ (можно переопределить без ошибки линковщика)
Посмотреть таблицу символов объектного файла:
nm -C main.o # -C — демангл C++ имён
nm -u main.o # только неразрешённые (UND)
nm -D libfoo.so # только экспортируемые (dynamic) символы .so
readelf -s main.o # подробный вывод ELF таблицы символов
Если линковщик не находит определение для UND символа — ошибка undefined reference to 'foo'. Если находит несколько
определений GLOBAL DEF — ошибка multiple definition of 'foo'.
Статические библиотеки: создание¶
Статическая библиотека — это ar-архив. Порядок секций в архиве важен: символы ищутся слева направо, поэтому объекты нужно перечислять раньше библиотек.
# Компилируем исходники
g++ -c foo.cpp -o foo.o
g++ -c bar.cpp -o bar.o
# Создаём архив
ar rcs libfoo.a foo.o bar.o
# Линкуем
g++ main.cpp -L. -lfoo -o main
Посмотреть содержимое архива:
ar t libfoo.a # список объектных файлов
ar tv libfoo.a # подробный список
nm libfoo.a # символы из всех объектов архива
Динамическая загрузка библиотек: dlopen¶
Помимо линковки на этапе сборки, .so можно загрузить во время выполнения через dlopen:
#include <dlfcn.h>
#include <stdio.h>
int main(void) {
void *handle = dlopen("libfoo.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s\n", dlerror());
return 1;
}
typedef void (*hello_t)(void);
hello_t hello = (hello_t)dlsym(handle, "hello");
const char *err = dlerror();
if (err) {
fprintf(stderr, "%s\n", err);
dlclose(handle);
return 1;
}
hello();
dlclose(handle);
return 0;
}
Компиляция программы с dlopen требует флага -ldl:
Это основа плагинных архитектур: основная программа загружает .so по имени во время работы.
Ручная линковка с помощью ld¶
g++ скрывает детали линковки, автоматически подставляя нужные объекты C runtime. Чтобы понять, что именно происходит,
можно вызвать ld напрямую.
Исходный файл main.cpp:
Сначала получаем объектный файл:
Минимальный вызов ld (пути зависят от дистрибутива и версии компилятора):
ld main.o \
/usr/lib/libstdc++.so \
/usr/lib/libc.so \
/usr/lib/gcc/x86_64-pc-linux-gnu/13.3.1/crtbegin.o \
/usr/lib/gcc/x86_64-pc-linux-gnu/13.3.1/crtend.o \
-o main
Такой бинарь не запустится:
Причина: в секции .interp нет корректного пути к динамическому загрузчику.
Полный набор объектов C runtime¶
Чтобы получить работающий исполняемый файл, нужно явно указать:
- динамический линковщик (
-dynamic-linker) — путь кld-linux-x86-64.so.2; - CRT-объекты — они определяют секции
.init/.fini, метку_startи вызываютmainчерез__libc_start_main.
ld main.o \
/usr/lib/libstdc++.so \
/usr/lib/libc.so \
/usr/lib/gcc/x86_64-pc-linux-gnu/15.1.1/crtbegin.o \
/usr/lib/gcc/x86_64-pc-linux-gnu/15.1.1/crtend.o \
/usr/lib/crti.o \
/usr/lib/crtn.o \
/usr/lib/crt1.o \
-dynamic-linker /lib64/ld-linux-x86-64.so.2 \
-o main
Роли CRT-объектов:
| Объект | Назначение |
|---|---|
crt1.o |
содержит _start — точку входа, которую вызывает ядро |
crti.o |
пролог секций .init и .fini |
crtn.o |
эпилог секций .init и .fini |
crtbegin.o |
интеграция с конструкторами C++ (GCC-специфично) |
crtend.o |
интеграция с деструкторами C++ (GCC-специфично) |
Что именно подставляет g++, можно узнать из подробного вывода:
Подробнее о различиях между вариантами crtbegin: stackoverflow.com/questions/22160888.
Подробнее о том, что происходит от _start до main — в
статье Запуск и завершение программы.
Источники¶
man ld— документация линковщика GNUman ar— работа с архивами статических библиотекman dlopen— динамическая загрузка библиотек во время выполнения- Linkers and Loaders — John R. Levine