от нескольких .o до одного бинарникаfrom several .o files to one binary
Компилятор видит только одну единицу трансляции за раз и оставляет в объектном файле «дырки» вместо адресов внешних функций и переменных. Линковщик — отдельная программа, которая сшивает несколько таких файлов в один исполняемый, разрешая эти дырки по именам символов.The compiler sees only one translation unit at a time and leaves "holes" in the object file instead of addresses for external functions and variables. The linker — a separate program — stitches several such files into one executable, resolving those holes by symbol name.
Каждый объектный файл содержит таблицу символов — список имён (замангленных для функций/шаблонов) с пометкой, определён ли символ в этом файле (defined) или только используется и ожидается извне (undefined/external). Линковка — это процесс сопоставления undefined-символов одних файлов с defined-символами других.Every object file contains a symbol table — a list of names (mangled, for functions/templates) marked as either defined in this file or merely used and expected to come from elsewhere (undefined/external). Linking is the process of matching up undefined symbols in some files with defined symbols in others.
// посмотреть таблицу символов объектного файла:// inspect an object file's symbol table:
nm main.o
// U _Z3addii ← undefined, ожидается извне (Undefined)// U _Z3addii ← undefined, expected from elsewhere
// 0000000000000000 T main ← defined в .text (Text)// 0000000000000000 T main ← defined, in .text
Статическая библиотека (.a/.lib) — просто архив объектных файлов. Линковщик копирует машинный код нужных функций прямо в итоговый исполняемый файл. Плюс: один самодостаточный файл, никаких проблем совместимости версий во время выполнения. Минус: каждый исполняемый файл, использующий библиотеку, содержит собственную копию её кода — раздувает дисковое пространство и, если библиотека уже загружена в память другим процессом, ОС не может переиспользовать эти страницы между процессами.A static library (.a/.lib) is just an archive of object files. The linker copies the machine code of the needed functions straight into the final executable. Upside: a single self-contained file, no runtime version-compatibility problems. Downside: every executable using the library carries its own copy of its code — this bloats disk space, and if the library is already loaded in memory by another process, the OS can't share those pages across processes.
Динамическая библиотека (.so на Linux, .dll на Windows) хранится отдельно от исполняемого файла. Загрузчик ОС находит и мапит её в память процесса при запуске (см. theory/01 — PE-файл). Если несколько процессов используют одну и ту же .so/.dll, физическая страница памяти с её кодом переиспользуется между ними — экономия оперативной памяти, которую статическая линковка дать не может.A dynamic library (.so on Linux, .dll on Windows) lives separately from the executable. The OS loader finds and maps it into the process's memory at launch (see theory/01 — the PE file). If several processes use the same .so/.dll, the physical memory page holding its code is shared between them — a RAM saving static linking simply cannot provide.
Правило C++: у каждой сущности (функции, переменной, класса) должно быть ровно одно определение во всей программе (с оговоркой для inline-сущностей и шаблонов — там определение может повторяться в разных единицах трансляции, но обязано быть идентичным байт в байт по смыслу). Нарушение ODR — одна из немногих категорий ошибок, которую компилятор в принципе не может обнаружить: каждая единица трансляции по отдельности выглядит корректной, проблема видна только при линковке, а иногда — вообще не видна и молча ведёт к UB.The C++ rule: every entity (function, variable, class) must have exactly one definition across the entire program (with an exception for inline entities and templates — there, the definition can repeat across translation units, but must be byte-for-byte identical in meaning). An ODR violation is one of the few categories of error a compiler cannot detect in principle: each translation unit looks correct in isolation, the problem only shows up at link time — and sometimes it doesn't show up at all, silently leading to UB.
// a.cpp
int add(int a, int b) { return a + b; }
// b.cpp
int add(int a, int b) { return a + b; }
// "duplicate symbol" — обе не-inline, обе внешне видимые// "duplicate symbol" — both non-inline, both externally visible
// header.hpp, включён в a.cpp с -DFAST и в b.cpp без него, included in a.cpp with -DFAST and in b.cpp without it
inline int compute(int x) {
#ifdef FAST
return x << 1;
#else
return x * 2;
#endif
}
// два "одинаковых" inline-определения физически разные —
// линковщик молча берёт одно из двух, ошибки нет// two "identical" inline definitions are physically different —
// the linker silently picks one of the two, no error at all
Классический линковщик (GNU ld) читает библиотеки один раз, слева направо, и подключает из них только те объектные файлы, которые нужны для уже накопленных неразрешённых символов на данный момент. Если библиотека A зависит от библиотеки B, но в команде линковки B указана раньше A, символы из B уже будут «пройдены мимо» и останутся неразрешёнными — классическая ошибка «undefined reference», решаемая перестановкой порядка (или флагом --start-group/--end-group для круговых зависимостей).A classic linker (GNU ld) reads libraries once, left to right, and pulls in from each only the object files needed to satisfy the unresolved symbols accumulated so far. If library A depends on library B, but B is listed before A on the link command, B's symbols will already have been passed over and remain unresolved — the classic "undefined reference" error, fixed by reordering (or the --start-group/--end-group flags for circular dependencies).
Если два зависимых компонента процесса ожидают разные, несовместимые версии одной и той же .so/.dll (частный случай — «DLL hell» на Windows), в память процесса может попасть только одна из них — второй компонент получит символы не той версии, с молчаливо иным поведением или крашем в рантайме, а не понятной ошибкой сборки.If two dependent components of a process expect different, incompatible versions of the same .so/.dll (the specific case commonly called "DLL hell" on Windows), only one version can end up loaded into the process's memory — the other component gets symbols from the wrong version, resulting in silently different behavior or a runtime crash rather than a clear build error.
Символы, помеченные как weak (компилятором — для inline-функций/шаблонных инстанциаций, или явно программистом), не вызывают ошибку «duplicate symbol», если встречаются в нескольких объектных файлах — линковщик молча выбирает один экземпляр. Это механизм, на котором построена легальная сторона ODR-исключения для шаблонов и inline-функций, но им же можно случайно замаскировать реальную ошибку дублирования, если использовать weak-атрибут неаккуратно вручную.Symbols marked weak (by the compiler, for inline functions/template instantiations, or explicitly by the programmer) don't trigger a "duplicate symbol" error when they appear in multiple object files — the linker silently picks one instance. This is the mechanism the legal ODR exception for templates and inline functions is built on — but it's also something that can accidentally mask a genuine duplication bug if the weak attribute is used carelessly by hand.