Перейти к содержанию

Линковка и библиотеки: основы

Что такое линковка

Линковка — финальная стадия сборки, на которой из набора объектных файлов и библиотек создаётся готовый исполняемый файл или разделяемая библиотека. Линковщик (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:

g++ main.cpp -ldl -o main

Это основа плагинных архитектур: основная программа загружает .so по имени во время работы.

Ручная линковка с помощью ld

g++ скрывает детали линковки, автоматически подставляя нужные объекты C runtime. Чтобы понять, что именно происходит, можно вызвать ld напрямую.

Исходный файл main.cpp:

#include <iostream>

int main() {
    std::cout << "Hello caos!!!" << std::endl;
}

Сначала получаем объектный файл:

g++ -c main.cpp -o main.o

Минимальный вызов 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

Такой бинарь не запустится:

./main: cannot execute: required file not found

Причина: в секции .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++, можно узнать из подробного вывода:

g++ -v main.o -o main

Подробнее о различиях между вариантами crtbegin: stackoverflow.com/questions/22160888.

Подробнее о том, что происходит от _start до main — в статье Запуск и завершение программы.

Источники

  • man ld — документация линковщика GNU
  • man ar — работа с архивами статических библиотек
  • man dlopen — динамическая загрузка библиотек во время выполнения
  • Linkers and Loaders — John R. Levine