The Linker — Senior¶
This tier is the mental model of cmd/link: the pipeline from object files to executable, how deadcode reasons about interface/method liveness, what .gopclntab and DWARF really are, the precise triggers for external linking, and how layout and relocations work.
1. The linker pipeline, end to end¶
cmd/link (entry src/cmd/link/main.go, most logic in src/cmd/link/internal/ld/) runs roughly these phases:
1. setup / flag parse (lib.go, main.go)
2. load object files (goobj format → loader.Loader)
3. resolve symbols (build global symbol table, ABI wrappers)
4. dead-code elimination (deadcode.go — reachability)
5. type/itab generation (typelink, itablink, methods)
6. allocate sections (.text/.rodata/.data/.bss + Go sections)
7. address assignment (layout: give every symbol a final VA)
8. apply relocations (patch every reference with real address)
9. generate .gopclntab (pcln.go — PC→func/line tables)
10. generate DWARF (dwarf.go — debug info, unless -w)
11. write buildinfo (.go.buildinfo)
12. emit object format (elf.go / macho.go / pe.go) → file on disk
Modern Go (1.15+) uses the loader.Loader (src/cmd/link/internal/loader/loader.go): a compact, index-based symbol store that avoids materializing a *Symbol struct per symbol. This is what made the linker dramatically faster and lower-memory in the 1.15–1.16 era. Symbols are referred to by an integer Sym index; payloads are read lazily from the goobj data.
Object file format (goobj): the compiler writes packages as goobj blobs inside archives (.a). The linker reads these directly — it does not parse ELF .o for pure-Go input. goobj carries the symbol definitions, relocations, DWARF fragments, pcdata, and the metadata the linker needs.
2. Deadcode: reachability and method liveness¶
deadcode.go implements a worklist mark phase. The key data structure tracks, for each live symbol, the relocations to follow. The subtlety is methods reachable only through interfaces.
The interface/method problem¶
A method T.M may be called three ways:
- Directly:
t.M()on a concretet— a normal relocation; trivially reachable. - Through an interface:
var i I = t; i.M()— the call site references the interface methodI.M, notT.Mdirectly. The linker must connect "interface methods namedMare called" with "concrete typeT(with methodM) is converted to an interface." - Through reflection:
reflect.Value.MethodByName("M")— unprovable.
The deadcode pass keeps two sets while marking:
markableMethods— methods of types that have been converted to an interface (the type "went through" anitab/convT*), keyed by method name + signature.- The set of interface method names actually called at reachable call sites.
When a reachable call site uses interface method named M with signature S, the pass marks every queued method (M, S) from a converted type as live. This is why an unused method on a type that's never boxed into an interface gets dropped, but the same method survives once the type is used polymorphically.
Reflection escape hatch¶
The pass watches for the symbols reflect.Value.Method / reflect.Value.MethodByName becoming reachable. When they are, deadcode sets a flag (historically reflectMethod) that disables method pruning for reflectable types — it must keep full method sets because any method could be named at runtime. This is the linker-level reason reflection inflates binaries (see middle tier). You can observe it: a binary that calls MethodByName keeps methods that an otherwise-identical binary drops.
3. .gopclntab and DWARF — two different things¶
People conflate these. They are distinct, generated by different code, and removed by different flags.
.gopclntab (the pcln table)¶
Generated in src/cmd/link/internal/ld/pcln.go. It is a runtime data structure, not debug info. It maps any program counter to:
- the function it belongs to (name, entry),
- the source file and line (
pcfile,pclntables), - stack frame and GC pointer maps info (
pcsp, funcdata), - inlining tree (
pcinline).
The runtime requires this to:
- unwind the stack for panics and
runtime.Callers/runtime.Caller, - let the GC find pointers in stack frames,
- support profiling (
pprof),runtime.FuncForPC.
Because the runtime needs it, -s -w does NOT remove .gopclntab. That's why stripped Go binaries still produce readable stack traces with function names and line numbers. (You'd have to do something exotic to remove it, and the program would break.)
DWARF (.debug_info, .debug_line, ...)¶
Generated in src/cmd/link/internal/ld/dwarf.go. This is standard debugger format consumed by delve, gdb, lldb. It describes types, variables, scopes, and line mappings at full fidelity. It is not required at runtime.
-w removes DWARF. -s removes the Go symbol table and implies dropping DWARF too in practice. After -s -w you can still get stack traces (pclntab), but dlv can't do source-level debugging.
| Data | Generated by | Needed at runtime? | Removed by |
|---|---|---|---|
.gopclntab | pcln.go | Yes (panics, GC, profiling) | not by -s -w |
| Go symbol table | linker symtab | No (nice for tools) | -s |
DWARF .debug_* | dwarf.go | No | -w (and effectively -s) |
4. Internal vs external linkmode — exact triggers¶
-linkmode can be internal, external, or auto (default). In auto, the linker decides, and the decisive question is: do we have host (C) object code to link?
External linking is triggered when:
- The program uses cgo that pulls in non-trivial C objects, and the current platform requires the host linker to combine them. (Trivial cgo on some platforms can still link internally; the linker checks.)
- You select a
-buildmodethat needs the system linker:c-shared,c-archive,pie(on most platforms),plugin,shared. - You pass
-linkmode=externalexplicitly, or-extld/-extldflags. - Certain platforms/architectures only support external linking for some modes (e.g. some
-buildmode=pietargets).
Internal linking (default for pure Go exe):
cmd/linkwrites the final ELF/Mach-O/PE itself; nogcc/clang/ldrequired. This is why Go cross-compiles trivially for pure-Go programs:GOOS=linux GOARCH=arm64 go buildneeds no cross C toolchain.
When external linking kicks in, cmd/link produces a temporary object and invokes the host linker (-extld, default gcc/clang) with -extldflags. That's where -extldflags=-static (fully static cgo binaries) or -extldflags=-Wl,... options go.
# Force external linking and pass flags to the host linker:
go build -ldflags='-linkmode=external -extldflags=-static' ./...
# See what the linker decided / what host linker was called:
go build -ldflags='-v' ./... 2>&1 | grep -i 'host link\|linkmode'
5. Layout and relocations¶
Layout¶
After deadcode, every surviving symbol must get a final virtual address. The linker groups symbols into sections by kind (text, rodata, data, bss), orders them, and assigns addresses. Code goes in .text, immutable data in .rodata, pointer-free data in .noptrdata/.noptrbss so the GC can skip scanning, and so on. The Go-specific tables (.gopclntab, typelinks, itablinks, buildinfo) get their own sections.
Relocations¶
A compiled function references other symbols by placeholder. The compiler emits a relocation for each: "at offset X in this symbol, patch in the address of symbol Y, encoded as type Z." After addresses are assigned, the linker applies every relocation — writing the real address/offset into the machine code or data.
High-level relocation kinds you'll encounter:
| Kind (conceptual) | Meaning |
|---|---|
R_CALL / R_PCREL | PC-relative call/branch to a function |
R_ADDR | absolute address of a symbol (pointers in data) |
R_ADDROFF | offset of a symbol from a section base (Go uses many of these for compact tables) |
R_TLS_* | thread-local storage (the g pointer) |
R_GOTPCREL (external/PIE) | go-through-GOT for position independence |
Go's own relocation types live in src/cmd/internal/objabi/reloctype.go. When you build PIE or c-shared, more relocations become GOT/PLT-style so the loader can place the image anywhere.
6. Summary¶
- The pipeline: load goobj → resolve symbols (+ABI wrappers) → deadcode → type/itab gen → lay out sections → apply relocations → gen pclntab + DWARF + buildinfo → write ELF/Mach-O/PE.
- The modern
loader.Loaderuses integer symbol indices and lazy payloads for speed/memory. - Deadcode correlates types boxed into interfaces with interface methods called to keep the right methods; reflection disables method pruning.
.gopclntabis runtime-required (panics, GC, profiling) and survives-s -w; DWARF is debugger-only and is removed by-w.- External linking is triggered by cgo-with-C-objects and by pie/c-shared/c-archive/plugin/shared modes; pure-Go
exestays internal. - Layout assigns addresses by section kind; relocations are then applied to patch every cross-symbol reference.