Lesson 11.9 — The runtime library, object emission and linking¶
Techniques: runtime library design: the code a compiled program calls but its source never defines, such as output, traps, program start and helpers for operations the target cannot do in a few instructions. You decide which operations are inline code and which are calls, fix the symbol and calling-convention contract, and choose which object file each piece lives in (libgcc/compiler-rt, Rust's
std::rt, Go'sruntime, Pebble's 200-line C runtime) · object emission and linking: LLVM IR → machine code → a relocatable object file through the target'sTargetMachine, then a system linker combines it with start-up files, the runtime archive and libc into an executable. Clang, rustc andpebblecall delegate that link to the C compiler driver · Pebble implements: both. The runtime (pebble/runtime/, runtime ABI) and thepebblecback half (pebble/lib/Driver/) are provided; your lowering (E1–E3) and code generator (E5) decide which runtime calls appear · Prerequisites: Lesson 0.6 (objects, relocations, static linking: Algorithm 0.6.6); Lesson 11.6 (traps) · Time: 3 hours
Everything so far ended in an LLVM module. A user runs pebblec hello.pbl -o hello && ./hello, and between the module and the running process there are two more design decisions. The first is what the module is allowed to call: print("hello") becomes call void @pebble_print_str(ptr @.str), a function that exists in no Pebble source. The second is how the module becomes a process: an object file with a relocation for that call, which a linker resolves against libpebble_runtime.a and next to C's start-up code, which calls main, which calls pebble_main. The running example is hello.pbl:
1. Problem and motivation¶
Runtime library design¶
Some operations are not worth emitting inline. Printing an integer in decimal takes dozens of instructions and a system call. A 128-bit division on x86-64 is a loop, because idiv takes at most a 128-bit dividend and a 64-bit divisor. A trap must format a message, flush stdout and exit with a fixed status. Starting a program means receiving argc/argv from the C start-up code and turning the program's result into an exit status. A compiler puts such code in a runtime library once and emits calls to it. That creates a contract: symbol names, signatures, calling convention, which calls never return, and who owns which memory. The contract outlives any single compiler version. Pebble's is docs/runtime-abi.md; C's is the libgcc/compiler-rt builtins that LLVM calls for operations the target lacks (__divti3 for 128-bit / [COMPILERRT-Divti3]); Rust's is std::rt::lang_start, which runs before main and turns a panic into exit status 101 [RUST-Rt], the same status pebble_trap uses.
Object emission and linking¶
LLVM's code generator turns the optimized module into machine code for one target. The target triple and a TargetMachine choose the instruction set, the object format (ELF, Mach-O, COFF) and the relocation model. A relocatable object file comes out, and its calls to not-yet-placed functions become relocations. A compiler rarely links by itself. The system linker (GNU ld, ld64, lld) knows the platform's start-up files, library search paths and dynamic linker, and the C compiler driver knows how to invoke it. So Clang's driver builds an ld command line [CLANG-GnuLink], and rustc and pebblec run cc [LLVM-CodeGenTM]. The chapter's outcome, a native executable for every Pebble program, depends on getting this last step right on both Linux and macOS.
2. Definitions and algorithms¶
Definition 11.9.1 (Runtime library, runtime interface, libcall)
A runtime library is object code linked into every program of a language that provides functions the
generated code calls but the source does not define. Its runtime interface is the set of symbols the
compiler may reference, each with a signature, a calling convention and attributes such as noreturn and
nounwind, together with the symbols the program must provide (Pebble: pebble_main). A libcall is
a call to the runtime that the code generator, not the front end, introduces for an operation
the target cannot do inline (LLVM's RTLIB::Libcall enumeration [LLVM-RuntimeLibcalls]).
Algorithm 11.9.2 (Inline code or runtime call, with declarations on first use)
- Input: an operation
opof the lowered program (a print, a check failure, a 128-bit division, …), the target's cost model, and the runtime interface \(R\). - Output: instructions that perform
op, and possibly a declaration added to the module. - Precondition: every symbol of \(R\) has a fixed LLVM declaration (Pebble: runtime ABI §2).
- Postcondition: the module declares exactly the runtime functions it calls, each with its declaration from \(R\) (Theorem 11.9.3).
- Invariant: every function declaration in the module is either a program
extern fnor a symbol of \(R\).
function Emit(op):
if the target has an instruction for op: return that instruction # Legal
if op expands to a short inline sequence: return the sequence # Expand
f ← R.symbolFor(op) # LibCall
if module has no declaration of f: # first use
add R.declaration(f) # name, signature, noreturn/nounwind/cold
return call f(operands of op)
Theorem 11.9.3 (Link completeness)
Suppose the invariant of Algorithm 11.9.2 holds, the runtime archive defines every symbol of \(R\), every
extern fn is defined by some linked library, and the program defines the symbols \(R\) requires of it.
Then linking the object with the runtime and libc (Algorithm 0.6.6) leaves no undefined symbol.
Proof
Code generation adds no global symbols other than libcalls, which Algorithm 11.9.2 also declares. So the
undefined symbols of the object file are exactly its function declarations. By the invariant each is a
symbol of \(R\) or an extern fn, and by hypothesis each has a definition in a linked input. The
runtime's own undefined symbols are either libc functions (defined by -lc) or the symbols it requires of
the program, which the object defines. Algorithm 0.6.6 therefore ends with \(U = \emptyset\). The ordering
condition that makes the archive search actually find these definitions is Theorem 11.9.6.
Definition 11.9.4 (Target machine, object emission)
A target machine fixes the target triple (architecture, vendor, OS, environment), a CPU and feature set, a relocation model (static, PIC) and a code-generation optimization level. Object emission runs the target's code-generation passes (instruction selection, register allocation, frame lowering; Chapters 21–22) and an object streamer that encodes instructions, emits symbols, and records a relocation for every reference whose final address is unknown.
Algorithm 11.9.5 (The pebblec back half: module → executable)
- Input: a verified LLVM module \(M\) (from E5, or
--from-llvm), an optimization level, the host triple. - Output: an executable file, or an error from the linker.
- Precondition: \(M\) satisfies the runtime ABI: it defines
i64 @pebble_main()and declares only runtime symbols andextern fns. - Postcondition: running the executable runs
pebble_mainand exits with its result modulo 256, or with 101 after a trap. - Invariant: \(M\) passes the LLVM verifier after every step that changes it.
function BuildExecutable(M, level):
TM ← createTargetMachine(hostTriple, cpu = "generic", reloc = PIC, level)
M.setTargetTriple(TM.triple); M.setDataLayout(TM.dataLayout)
optimize(M, level) # -O0: O0 pipeline; -O1: the course pipeline, or default<O1> if no
# chapter has registered a step; -O2: LLVM's default<O2>
obj ← temporary file
TM.addPassesToEmitFile(PM, obj, ObjectFile); PM.run(M) # legacy PM; see §7
run cc -o out obj libpebble_runtime.a [-lm] # cc picks the linker, crt files, libc
What -O1 is in your build. The course pipeline is the concatenation of the PEBBLE_COURSE_PIPELINE_STEP
registrations linked into pebblec; with the reference passes of Chapter 12 it is function(pebble-strength)
and nothing else (opt -load-pass-plugin=<build>/lib/PebblePasses.so -passes='print<pebble-passes>' prints it).
So pebblec -O1 does not promote allocas or rotate loops until a later chapter registers those steps; the
boxes of this chapter that show LLVM's transformations use -O2 or an explicit --passes=.
Theorem 11.9.6 (Archive order)
With the single-pass archive search of Algorithm 0.6.6, a member of archive \(A\) is linked if and only if it defines a symbol that is undefined when the linker reaches \(A\), or one that a member of \(A\) linked earlier leaves undefined. Consequently the runtime archive must come after every object that uses it, and a symbol that a runtime member needs from an archive placed earlier stays undefined.
Proof
By Algorithm 0.6.6, an archive is handled when the loop reaches it, by repeatedly adding the members that
define a name in \(U\) until none is added. \(U\) at that point contains the undefined names of the inputs
before \(A\) and of the members of \(A\) already added. A member is added exactly when it defines one of these
names, and after the loop leaves \(A\) it never returns to it. If libpebble_runtime.a came before
hello.o, \(U\) would not yet contain pebble_print_str when the archive is searched, no member would be
added, and hello.o's reference would stay in \(U\): an "undefined reference" error. Symmetrically, a
member that needs a name from an archive already passed cannot pull it. Hence -lm goes after the
runtime, and mutually dependent archives need --start-group … --end-group, which repeats the search.
3. Worked example¶
Runtime library design¶
Lowering hello.pbl (E3) reaches print("hello"). Its argument is a str literal, so it emits print_str then print_newline. The code generator (E5) maps both to runtime calls. The first time each appears it adds the declaration from the runtime ABI (Algorithm 11.9.2, first use):
@.str = private unnamed_addr constant [6 x i8] c"hello\00", align 1
define i64 @pebble_main() {
entry:
br label %bb0
bb0: ; preds = %entry
call void @pebble_print_str(ptr @.str)
call void @pebble_print_newline()
ret i64 0
}
declare void @pebble_print_str(ptr)
declare void @pebble_print_newline()
pebble_print_int, pebble_trap and the others are not declared, because this program never calls them (the invariant of Algorithm 11.9.2). The program's main becomes pebble_main. The C main lives in the runtime (pebble_main.c) and calls it, so the start-up contract (C's argc/argv, exit flushing stdout) stays in C, which already implements it.
Object emission and linking¶
pebblec --emit=obj produces an object with one defined symbol and two relocations (box in §7). The link line that cc builds on Linux is, in order: Scrt1.o crti.o crtbeginS.o hello.o libpebble_runtime.a -lm -lgcc -lc … crtendS.o crtn.o. Trace Algorithm 0.6.6 over the inputs that matter:
| step | input | defines | adds to \(U\) | \(U\) after |
|---|---|---|---|---|
| 1 | Scrt1.o |
_start |
main, __libc_start_main |
{main, __libc_start_main} |
| 2 | hello.o |
pebble_main |
pebble_print_str, pebble_print_newline |
{main, __libc_start_main, pebble_print_str, pebble_print_newline} |
| 3 | archive member pebble_main.c.o (defines main ∈ \(U\)) |
main |
pebble_main, already defined |
{__libc_start_main, pebble_print_str, pebble_print_newline} |
| 4 | archive member pebble_runtime.c.o (defines pebble_print_str ∈ \(U\)) |
all 8 runtime functions | printf, fputs, exit, stdout, … |
{__libc_start_main, printf, fputs, …} |
| 5 | -lc (shared) |
the libc names | — | {} |
Two design points show up. First, main is in its own member: pir-run links the same archive but has its own main, so it never has main undefined and never pulls pebble_main.c.o, which would need a pebble_main that pir-run does not have (the comment at the top of pebble_main.c says exactly this). Second, member granularity is the whole object file. Step 4 links pebble_print_int and pebble_trap even though hello never calls them (the llvm-nm hello box in §7). Runtimes that care about size put one function per member (compiler-rt's builtins are one file per function) or compile with -ffunction-sections and link with --gc-sections.
4. Invariants and correctness¶
Runtime library design¶
Theorem 11.9.3 is what the runtime ABI buys: the code generator and the runtime are developed separately and meet only at the symbol table. Three contract details matter for correctness, not just linking. (1) pebble_trap is declared noreturn nounwind cold. noreturn lets LLVM treat the trap block as a dead end, so the facts proved on the non-trapping path survive (Lesson 11.6), and it is a promise: if the runtime returned, the behavior would be undefined. (2) bool crosses the boundary as zeroext i1, because C's ABI requires the caller to extend it. (3) Pebble's internal functions are internal and carry nobuiltin at their call sites, so a user function called sqrt is not mistaken for the libm function the optimizer knows (Chapter 11's prog-float-newton end-to-end test failed at -O2 until codegen added the attribute; see the E5 contract in PIRToLLVM.h).
Object emission and linking¶
Theorem 11.9.6, and the platform differences that cc hides. Linux executables are position-independent by default (-pie in the link line), so pebblec creates its TargetMachine with Reloc::PIC_ and calls go through R_X86_64_PLT32. macOS requires PIC and names symbols with a leading underscore (_pebble_main: the m:o mangling of the Mach-O data layout, which llc -mtriple=x86_64-apple-macosx shows). Its driver links libSystem (darwin::Linker::ConstructJob adds -lSystem), which contains libm's functions, so pebblec adds -lm only on Linux (linkExecutable in pebble/lib/Driver/CodeEmission.cpp). Theorem 11.9.6 describes GNU ld's single pass; whether another linker (lld, Apple's ld) keeps archive members lazily available regardless of order is that linker's documented choice, and pebblec puts the archive after the object on every platform so the question never arises. The runtime's exit status is part of the contract too: main returns (int)pebble_main(), so the shell sees the result modulo 256, and a trap exits with 101 (the exit-status* and trap-* end-to-end tests check both).
5. Complexity¶
\(n\) = instructions in the module, \(s\) = symbols, \(r\) = relocations, \(m\) = archive members, \(k\) = archives in a --start-group cycle.
| Technique | Compile/link time | Run-time cost | Justification |
|---|---|---|---|
| Runtime library design | \(O(1)\) per operation for the choice; one hash lookup per first use | a call: argument setup plus call/return, and the callee cannot be inlined across the object boundary unless LTO is used | the decision is a table lookup (LLVM: getOperationAction); declarations are a map from name to function |
| Object emission and linking | emission \(O(n)\) after instruction selection and register allocation (Chapters 21–22); link \(O(s + r)\) plus archive search \(O(m)\) per archive pass | relocations resolved at link time cost nothing; PLT calls to shared libraries cost an indirect jump | each relocation is applied once (Algorithm 0.6.6); the archive index maps names to members |
Pathological family (archive search). With archives \(A_1, \dots, A_k\) in a group where each member of \(A_i\) needs one symbol from \(A_{i-1}\) (and \(A_1\)'s from \(A_k\)), a group search adds one member per pass, so it needs up to \(k \cdot m\) passes over the indexes: \(O(k m^2)\) lookups instead of \(O(m)\). GNU ld documents that --start-group has "a significant performance cost" for this reason; lld avoids it by remembering lazy archive symbols in the symbol table. Runtime calls in hot loops: a libcall per iteration (for example fmod for Pebble's float %) costs the call overhead every time. That is the reason to expand an operation inline once it is common enough.
6. Variants and refinements¶
Runtime library design¶
- Runtime written in the language itself (Go's
runtimepackage, Rust'score/std): the compiler compiles the runtime and may inline its functions. The runtime must avoid features that need the runtime (Go's runtime restricts itself with directives such as//go:nosplit; Rust'scorehas no allocator). - Runtime as LLVM bitcode, linked before optimization (CUDA's libdevice, the ROCm device libraries): calls to the runtime can be inlined and optimized with the program.
- Compiler-generated start-up: rustc emits a C-ABI
mainthat callslang_start(box in §7), while Pebble putsmainin the runtime. Both keep argument decoding and exit-status handling out of the generated code.
Object emission and linking¶
- Integrated linker (Zig and Go's own linkers, or
lldcalled as a library): no dependency on a systemcc, at the cost of reimplementing each platform's start-up and library conventions. - Link-time optimization (
-flto): objects hold LLVM bitcode and the linker runs the optimizer on the whole program, so runtime functions written in C can be inlined into Pebble code (Chapter 20 on interprocedural optimization). - JIT instead of object files:
pir-runinterprets PIR, and the Chapter 11 lab's measurement tool uses LLVM's ORC JIT (LLJIT), so no object file or linker is involved.
7. In real compilers¶
Runtime library design¶
LLVM lists every libcall and the symbol it maps to in llvm/include/llvm/IR/RuntimeLibcalls.td (def __divti3 : RuntimeLibcallImpl<SDIV_I128>;) [LLVM-RuntimeLibcalls]. When type legalization has to split a 128-bit sdiv, DAGTypeLegalizer::ExpandIntRes_SDIV in llvm/lib/CodeGen/SelectionDAG/LegalizeIntegerTypes.cpp asks RTLIB::getSDIV(VT) for the libcall and emits it with TLI.makeLibCall [LLVM-LegalizeInt]. compiler-rt's lib/builtins/divti3.c defines it [COMPILERRT-Divti3]. Rust's library/std/src/rt.rs has lang_start and lang_start_internal, which catches a panic escaping main and returns 101 [RUST-Rt].
clang: operations the target lacks become runtime calls
Reproduce (clang 23.1.2):
cat > rt.c <<'EOF'
__int128 quot(__int128 a, __int128 b) { return a / b; }
double rem(double a, double b) { return __builtin_fmod(a, b); }
long cube(long x) { return x * x * x; }
EOF
clang-23 -O2 -c rt.c && llvm-nm rt.o
clang-23 -O2 -S rt.c -o - | sed -n '/^quot:/,/Lfunc_end0/p' | grep -v '^\s*\.\(cfi\|p2align\)'
Output:
U __divti3
0000000000000010 T cube
U fmod
0000000000000000 T quot
0000000000000008 T rem
quot: # @quot
# %bb.0:
pushq %rax
callq __divti3@PLT
popq %rcx
retq
.Lfunc_end0:
What to notice: cube is inline code (two imuls, no undefined symbol). The 128-bit division is a
libcall to __divti3 (libgcc or compiler-rt), and fmod is a call to libm, even at -O2. These are
Algorithm 11.9.2's three outcomes. The object records both as undefined symbols (U) that the link must
satisfy, just as Pebble's float % needs -lm.
rustc: the generated main calls the runtime's lang_start
Reproduce (rustc 1.94.1):
cat > m.rs <<'EOF'
fn main() { println!("hi"); }
EOF
rustc -C opt-level=1 --emit=llvm-ir m.rs -o m.ll && sed -n '/^define.* @main(/,/^}/p' m.ll
Output:
define noundef i32 @main(i32 %0, ptr %1) unnamed_addr #5 {
top:
%_7.i = alloca [8 x i8], align 8
%2 = sext i32 %0 to i64
call void @llvm.lifetime.start.p0(i64 8, ptr nonnull %_7.i)
store ptr @_ZN1m4main17h75aee98a3666cac9E, ptr %_7.i, align 8
; call std::rt::lang_start_internal
%_0.i = call noundef i64 @_ZN3std2rt19lang_start_internal17hc68d929ebd5f7eeaE(ptr noundef nonnull align 1 %_7.i, ptr noalias noundef readonly align 8 captures(address, read_provenance) dereferenceable(48) @vtable.0, i64 noundef %2, ptr noundef %1, i8 noundef 0)
call void @llvm.lifetime.end.p0(i64 8, ptr nonnull %_7.i)
%3 = trunc i64 %_0.i to i32
ret i32 %3
}
What to notice: rustc generates the C main itself. It stores a pointer to the user's m::main and
passes it with argc/argv to std::rt::lang_start_internal, which initializes the runtime, calls
it, catches a panic (exit status 101) and returns the status that becomes main's result. Pebble makes
the other choice: main is in the runtime and the compiler emits only pebble_main.
Find where LLVM does it. In llvm/lib/CodeGen/SelectionDAG/LegalizeIntegerTypes.cpp, which function expands a 128-bit signed division, what does it do if the target handles ISD::SDIVREM custom, and which call produces the libcall otherwise? (Quiz llvm-where-libcall.)
Object emission and linking¶
CodeGenTargetMachineImpl::addPassesToEmitFile in llvm/lib/CodeGen/CodeGenTargetMachineImpl.cpp adds the code-generation passes and then an AsmPrinter connected to an object streamer (addAsmPrinter) [LLVM-CodeGenTM]. pebblec calls it through emitObjectFile in pebble/lib/Driver/CodeEmission.cpp, with a legacy PassManager, because machine-code emission still uses the legacy pass manager in LLVM 23. The x86-64 ELF writer chooses R_X86_64_PLT32 for calls in X86ELFObjectWriter::getRelocType (llvm/lib/Target/X86/MCTargetDesc/X86ELFObjectWriter.cpp) [LLVM-X86ELF]. Clang's driver builds the GNU ld command in tools::gnutools::Linker::ConstructJob (clang/lib/Driver/ToolChains/Gnu.cpp) and the macOS ld64 command in darwin::Linker::ConstructJob (Darwin.cpp) [CLANG-GnuLink]. Linkers themselves are Lesson 0.6's subject [Lev00].
pebblec: an object file with two relocations to the runtime
Reproduce (pebblec from this repository with -DPEBBLE_USE_SOLUTION=all, LLVM 23.1.2; hello.pbl is the example at the top of this lesson; x86-64 Linux):
pebblec --emit=obj hello.pbl -o hello.o && llvm-nm hello.o
llvm-objdump -dr --no-show-raw-insn hello.o | sed -n '/<pebble_main>:/,$p'
Output:
0000000000000000 r .L.str
0000000000000000 T pebble_main
U pebble_print_newline
U pebble_print_str
0000000000000000 <pebble_main>:
0: pushq %rax
1: jmp 0x3 <pebble_main+0x3>
3: leaq (%rip), %rdi # 0xa <pebble_main+0xa>
0000000000000006: R_X86_64_PC32 .L.str-0x4
a: callq 0xf <pebble_main+0xf>
000000000000000b: R_X86_64_PLT32 pebble_print_str-0x4
f: callq 0x14 <pebble_main+0x14>
0000000000000010: R_X86_64_PLT32 pebble_print_newline-0x4
14: xorl %eax, %eax
16: popq %rcx
17: retq
What to notice: the object defines only pebble_main (T); the two runtime functions are
undefined (U), exactly the declarations of §3. Each call is a callq with a zero displacement plus an
R_X86_64_PLT32 relocation (\(S + A - P\), Lesson 0.6), and the string address is R_X86_64_PC32. The
jmp to the next instruction is the entry → bb0 branch left at -O0 (the default of pebblec),
which emits code without optimizing.
clang's driver: the link line pebblec gets from cc
Reproduce (clang 23.1.2 driver, GNU ld 2.42, gcc 14 start-up files, Ubuntu 24.04; -L search paths removed from the output):
pebblec --emit=obj hello.pbl -o hello.o
clang-23 -### hello.o -lpebble_runtime -lm -o hello 2>&1 | tail -1 | tr ' ' '\n' | grep -v '^"-L' | xargs -n 4 echo
Output:
/usr/bin/ld -z relro --hash-style=gnu
--eh-frame-hdr -m elf_x86_64 -pie
-dynamic-linker /lib64/ld-linux-x86-64.so.2 -o hello
/lib/x86_64-linux-gnu/Scrt1.o /lib/x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/14/crtbeginS.o hello.o
-lpebble_runtime -lm -lgcc --as-needed
-lgcc_s --no-as-needed -lc -lgcc
--as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-linux-gnu/14/crtendS.o
/lib/x86_64-linux-gnu/crtn.o
What to notice: -### prints the linker command without running it. The driver adds what a
compiler would otherwise have to know about the platform: the start-up objects (Scrt1.o defines
_start and references main), the dynamic linker path, -pie, and libgcc and libc after the user's
inputs, in the order Theorem 11.9.6 requires. pebblec passes the archive by path instead of -l, in
the same position.
The executable contains the whole runtime member
Reproduce (pebblec from this repository with -DPEBBLE_USE_SOLUTION=all, LLVM 23.1.2, linked by gcc 13.3's driver and GNU ld 2.42):
Output:
hello
00000000000018e8 T main
0000000000001230 T pebble_format_float
00000000000011c0 T pebble_main
00000000000017b0 T pebble_print_bool
0000000000001200 T pebble_print_float
00000000000011e0 T pebble_print_int
00000000000017f0 T pebble_print_newline
00000000000017e0 T pebble_print_str
0000000000001830 T pebble_trap
0000000000001810 T pebble_trap_message
What to notice: step 4 of the trace in §3 in practice. hello calls two runtime functions, but all
eight from pebble_runtime.c.o are linked, because a static archive contributes whole members; main
comes from the separate pebble_main.c.o.
Find where LLVM does it. In llvm/lib/CodeGen/CodeGenTargetMachineImpl.cpp, which function does addPassesToEmitFile call to attach the object writer, and what does addPassesToEmitFile return when the target cannot emit the requested file type? (Quiz llvm-where-emit-file.)
8. Comparison¶
| Technique | Power / precision | Speed (asymptotic · practical) | Output / error quality | Implementation effort | Typical use |
|---|---|---|---|---|---|
| Runtime library design | Anything expressible in the runtime's language; the ABI contract fixes names, types and noreturn |
a call per use · fine for I/O and traps, costly for hot arithmetic (then expand inline) | one place to print messages and choose exit statuses (Pebble 101, Rust 101) | Low for a C runtime; high when the runtime has a GC or a scheduler (Go) | Pebble's C runtime, compiler-rt builtins, Rust std::rt, Go runtime |
| Object emission and linking | Any target LLVM supports; the system linker handles the platform | emission linear after codegen; link \(O(s + r)\) · link time dominates large builds | linker errors ("undefined reference") name symbols, not source lines | Low with a TargetMachine and cc; high for an integrated linker |
pebblec, rustc and Clang (via cc/ld), Zig and Go (own linkers) |
Choose a runtime call when the operation is long, rare or needs the OS (printing, traps, 128-bit division); choose inline code when it is a few instructions on a hot path. Choose linking through cc when you target the platforms a C toolchain already supports; choose an integrated linker when you ship cross-compilers without a system toolchain.
9. Assessment¶
- Quiz (
./course quiz 11):rt-libcall-choice,llvm-where-libcall(tagruntime-library);link-archive-order,llvm-where-emit-file(tagobject-linking). - Drill: none: the archive-order reasoning is Chapter 0's linking material, and the quiz asks it on new link lines (
link-archive-order). - Flashcards: tags
runtime-library,object-linking. - Exercises: E4 (every program runs as a native executable at
-O0,-O1and-O2), E5 (runtime declarations on first use).
References¶
See the chapter references.