The linker: the stage that is not rustc¶
Level: 201 · working knowledge
One line: Compilation leaves holes — every call to something defined elsewhere is a name with no address yet — and the linker is the separate program that matches those names against definitions in other object files and libraries, which is why its errors look nothing like Rust's.
An object file is not a program. It is machine code with a list attached: symbols I define, and symbols I need. Compile ten files and you get ten of these, each full of blanks where a call to another file should go. Linking is filling in the blanks and emitting one executable.
Two names this crate never defines¶
Neither function has a body anywhere in the program. The extern block is a promise: somewhere out there is a symbol called abs taking an i32 and returning one. rustc compiles the calls with the address left blank, records the two names in the object file's "needed" list, and stops caring.
Verified output of the_linker.rs — regenerated by tools/run_examples.py, never hand-typed.
The linker found both in the system C library, which every Rust program on this machine already links against.
unsafe is doing real work in that block. Rust checked the signatures against nothing at all — no header, no metadata, no cross-check. Write fn strlen(s: *const c_char) -> u8 and it compiles, links, and returns a truncated length forever. Inside a crate, a mismatch between a call and a definition is a type error; across the extern boundary, it is a run-time bug, and the keyword marks exactly that loss.
What a link error looks like¶
Ask for a symbol nobody defines and every earlier stage still passes — parsing, type checking and borrow checking all succeed, because none of them has anything to check against:
error: linking with `cc` failed: exit status: 1
|
= note: Undefined symbols for architecture x86_64:
"_definitely_not_a_symbol", referenced from:
__RNvCsdOC8EPUqYrp_9link_fail4main in link_fail...rcgu.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1
Three tells that this is not a Rust error:
- No error code, no
-->line, no span. rustc's diagnostics point at your source; the linker has never seen your source. It knows object files and symbol names. - The wording is the platform's. That is Apple's
ldviaclang. GNUldon Linux saysundefined reference to 'definitely_not_a_symbol'instead, for the identical mistake. - The names are mangled.
__RNvCsdOC8EPUqYrp_9link_fail4mainismainin cratelink_fail, encoded so that two functions with the same name in different modules — or the same generic instantiated at different types — get different symbols.#[unsafe(no_mangle)]opts a function out and gives it its plain name, which is how a Rust function becomes callable from C.
The most common version of this error is not about a symbol at all: error: linker 'cc' not found on a fresh machine. Rust does not ship a linker. It asks the system for one, which on Linux means installing build-essential or equivalent, and on macOS the Xcode command-line tools.
Static, dynamic, and what a Rust binary actually does¶
| Where the code goes | Resolved when | |
|---|---|---|
| Static | Copied into your executable | Link time |
| Dynamic | Referenced by name; loaded from a .so / .dylib / .dll |
Program start, by the OS loader |
A typical Rust binary is both. Every Rust crate you depend on, and the standard library itself, are linked statically — which is why a Rust program is several megabytes and why copying it to a machine with no Rust installed works. The C library is linked dynamically, because that is the only supported way to talk to the kernel on macOS and the normal way on glibc Linux. That split is exactly why a "static binary" story on Linux usually involves musl: x86_64-unknown-linux-musl swaps the one dynamic dependency for a statically linkable libc.
Which linker¶
rustc invokes whatever cc points at, and cc invokes the platform linker — ld64 on macOS, GNU ld on most Linux distributions. Neither is fast. LLD ↗ and mold ↗ are drop-in replacements that are much faster, and since linking is most of an incremental rebuild, swapping one is often the largest single win available on the edit-compile-test loop — the arithmetic is in Compile times.
If you are coming from another language¶
C and C++. Identical, including the error text, since it is the same linker. One difference is worth knowing: C++ mangles names too, but a header mismatch is caught only if the mangled names differ, which is why an ABI change with matching declarations produces a silent crash. Rust's version of that hazard is confined to extern blocks and marked unsafe at the site.
Python. There is no link step, because nothing is resolved until it runs: import locates a module at run time and an AttributeError is what a missing symbol looks like. The nearest equivalent to a link error is a C extension failing to load — ImportError: undefined symbol: PyFoo_Bar — and it is the same failure, just deferred to the moment of import instead of the moment of build. Deferring it is why a typo in a rarely-taken branch can ship.
ABAP. No linker at all. A program is activated into load form and every call is resolved by the runtime against the dictionary and the class pool — the moment of truth is activation for syntax and execution for a missing method, which is why a CX_SY_DYN_CALL_ILLEGAL_METHOD reaches a user rather than a build log. Rust moves nearly all of that to build time; the residue is this one stage, and it is the only place a Rust program can fail for a reason no compiler checked.
See also¶
- What a compiler does before your program runs — the two stages before this one, and the errors each can raise
- LLVM and its IR — where the object files came from, and what LLD is doing in the LLVM suite
- Compile times — the link phase measured, and the three knobs that reach it
- rustup —
rustup target addinstalls a target's standard library and not a linker, which is the usual reason a first cross-build still fails
Po polsku¶
Ten etap ma po polsku podręcznikową nazwę — konsolidator (linker), a czynność to konsolidacja — i warto ją znać, bo tak mówią polskie wykłady i tak jest w tłumaczeniach książek o C. W praktyce wszyscy piszą „linker” i „linkowanie”, i tylko ta druga forma coś znajduje w wyszukiwarce. Plik obiektowy (object file) to kod maszynowy plus dwie listy: symbole, które definiuję, i symbole, których potrzebuję; konsolidator dopasowuje jedne do drugich. A ponieważ jest osobnym programem, jego błędy nie wyglądają jak błędy Rusta: nie ma kodu E0…, nie ma strzałki -->, nie ma podkreślonego fragmentu twojego kodu — konsolidator twojego kodu nigdy nie widział. Słownictwo należy przy tym do systemu, a nie do Rusta: ld Apple'a mówi Undefined symbols for architecture x86_64, a GNU ld na Linuksie undefined reference to — o dokładnie tej samej pomyłce.
Najczęstsza wersja tego błędu w ogóle nie dotyczy symbolu: error: linker 'cc' not found na świeżo postawionej maszynie. Rust nie dostarcza konsolidatora, tylko prosi o niego system — na Linuksie trzeba build-essential albo odpowiednika, na macOS narzędzi wiersza poleceń Xcode, a na Windowsie (pierwszy próg, o który potyka się większość zaczynających) Build Tools for Visual Studio dla domyślnego celu -msvc, albo przesiadka na toolchain -gnu. Drugi znak rozpoznawczy komunikatu od konsolidatora to zniekształcone nazwy: __RNvCsdOC8EPUqYrp_9link_fail4main to zakodowana nazwa funkcji main z crate'a link_fail — po polsku mówi się o dekorowaniu nazw (name mangling). Kodowanie jest po to, żeby dwie funkcje o tej samej nazwie w różnych modułach dostały różne symbole; #[unsafe(no_mangle)] je wyłącza i zostawia gołą nazwę, i dokładnie tak udostępnia się funkcję Rusta dla C.
Blok unsafe extern "C" jest obietnicą, a nie deklaracją, którą ktoś sprawdza — i unsafe naprawdę tu pracuje, bo podpisu nie porównuje z niczym ani rustc, ani konsolidator. Napisz fn strlen(s: *const c_char) -> u8 zamiast usize, a to się skompiluje, zlinkuje i będzie po wsze czasy zwracać obciętą długość: wewnątrz crate'a niezgodność wywołania z definicją jest błędem typów, przez granicę extern jest błędem czasu wykonania. Na koniec podział, który po polsku pada zwykle jako „statycznie czy dynamicznie”: typowy program w Ruscie jest jednym i drugim naraz — wszystkie crate'y i biblioteka standardowa wchodzą statycznie (stąd kilka megabajtów i to, że plik działa na maszynie bez Rusta), a biblioteka C jest dołączana dynamicznie. Dlatego każda opowieść o „statycznej binarce” na Linuksie prędzej czy później kończy się na celu x86_64-unknown-linux-musl.
Szukaj po polsku: konsolidator · dekorowanie nazw · biblioteki statyczne i dynamiczne · rust linker cc not found · rust undefined reference to