Go: A Documentary

Linker and object files

Part I · 45 threads in 3 era slices, from Go 1.0 to Go 1.28. Each thread gives its design, then a per-release timeline of what landed (Done) and what was still open. Release labels in a timeline link to that release's page.

● = the thread had substantive commits in that cycle; hover for the stage summary, click to jump to it in the release page.

Thread map, Go 1.0 – 1.17

Thread1.01.11.21.31.41.51.61.71.81.91.101.111.121.131.141.151.161.17Status at go1.17 (or at the end of its era slice)
Linker and object files (Go 1.0–1.9)
Early loaders 5l/6l/8l and the shared src/cmd/ld (2008–2011)●replaced by the C-to-Go converted linker in 1.5 and by cmd/link thereafter → continues as Relocation-free pclntab and funcdata (offsets instead of relocations) and Per-platform object formats and internal linking (Mach-O, PE, XCOFF, wasm, ppc64, riscv64, loong64) (1.18+)
Go symbol table, pc/line table and function metadata (gosymtab, pclntab, findfunc)●●●●●●●●●●still the Go 1.2 pclntab design (contiguous per-function tables plus functab) with a findfunctab bucket index (1.5) and offsets-based types (1.7); the pclntab itself is rewritten in 1.18 → continues as Relocation-free pclntab and funcdata (offsets instead of relocations) (1.18+)
Stack splitting and the linker's nosplit overflow check●●●●●●nosplit chain checking is still done by the linker (stkcheck in cmd/link), reworked in 1.19 → continues as Nosplit stack checking and stack constants (1.19+)
Dead code and data elimination●●●●●●●reachability-flood plus method pruning conditioned on reflect.Value.Method/Call use, as designed in 1.7 → continues as Dead code elimination and reflection-aware pruning (1.18+)
DWARF generation in the linker●●●●●●●●●●function DWARF generated in the compiler (1.8) and assembled by the linker; types/globals still in the linker → continues as DWARF quality for debuggers (types, variables, trampolines) (1.18+)
External (host) linking and cgo●●●●●●●●●internal linking only for cgo-free or stdlib-cgo cases; external linking required in many cgo cases → continues as ELF/relro/hardening and external-link policy (1.18+)
Build modes: -shared, c-archive, c-shared, shared, PIE, plugin●●●●●●●all six build modes implemented; internal PIE on linux/amd64 exists but external is the default → continues as ELF/relro/hardening and external-link policy (1.18+)
liblink, the object file format and the move into the compilers●●●●●●cmd/internal/obj + goobj object format with symbol references, contiguous data and per-package indexes; replaced in the 1.15–1.17 era by the new object file format → continues as Relocation-free pclntab and funcdata (offsets instead of relocations) (1.18+)
From C to Go: cmd/link●●●●●●●cmd/link written in Go, still based on the 1.5 c2go output, being re-architected in the 1.15–1.17 era → continues as Linker performance and memory (1.18+)
Linker speed and memory (Go 1.7 focus)●●●●●incremental; the 1.17-era work moved to the new object format and loader → continues as Linker performance and memory (1.18+)
Runtime type data, GC metadata and sections laid out by the linker●●●●●●●●type descriptors with offsets (nameOff, typeOff, textOff) and GC bitmaps in dedicated sections → continues as Type, itab and typelink layout (.go.type section, relro rework) (1.18+)
Container writers: Windows PE, Mach-O and Plan 9●●●●●●●●●PE, Mach-O and ELF writers in cmd/link/internal/ld; Mach-O code-signing friendly and Windows external linking supported → continues as Per-platform object formats and internal linking (Mach-O, PE, XCOFF, wasm, ppc64, riscv64, loong64) (1.18+)
Architecture and OS ports in the linker (arm, power64, arm64, mips, s390x, nacl, BSDs, Solaris) and trampolines●●●●●●●●●●all ports in cmd/link/internal/<arch>; direct-branch trampolines for ARM and ppc64x → continues as Per-platform object formats and internal linking (Mach-O, PE, XCOFF, wasm, ppc64, riscv64, loong64) (1.18+)
Build IDs, ABI hashes, flags and determinism●●●●●●●●●-buildid, -X, -importcfg, -B, -linkmode, -extld all present → continues as Reproducible build IDs, buildinfo and symbol tables (1.18+)
Linker and object files (Go 1.10–1.17)
New object file format and the loader (dev.link)●●●●done; the linker reads only the indexed object format through cmd/link/internal/loader, sym.Symbol is gone, and the format gained a weak-reloc bit and 16-bit reloc types in 1.17 → continues as Relocation-free pclntab and funcdata (offsets instead of relocations) and Type, itab and typelink layout (.go.type section, relro rework) (1.18+)
Wavefront conversion of linker passes to the loader; sym.Symbol removal●●done (all passes run on loader.Sym; cmd/oldlink gone from the story) → continues as Relocation-free pclntab and funcdata (offsets instead of relocations) (1.18+)
Linker performance and memory engineering●●●●●●●●mmapped, parallel, streaming pipeline with generator symbols; ongoing → continues as Linker performance and memory (1.18+)
pclntab, funcdata and pc tables●●●●●●pclntab split into runtime.pcheader, funcnametab, pctab and generator-built tables; still relocation-based → continues as Relocation-free pclntab and funcdata (offsets instead of relocations) (1.18+)
Dead-code elimination and method pruning●●●●●●●index-based deadcode with UsedInIface/R_USEIFACE/R_USEIFACEMETHOD method pruning, weak itab edges and reflect-aware tracking → continues as Dead code elimination and reflection-aware pruning (1.18+)
DWARF generation in the linker●●●●●●●●DWARF 4 with per-compilation-unit DIE and line-table symbols generated in parallel; line tables produced by the compiler → continues as DWARF quality for debuggers (types, variables, trampolines) (1.18+)
DWARF compression and Mach-O dsymutil combining●●●●still the design at go1.17 (zlib ELF/Mach-O/PE compression; dsymutil combine on macOS/amd64 and arm64) → continues as Linker performance and memory (1.18+)
Symbol ABIs: stable ABI0 vs ABIInternal in the linker and object files●●●●wrapper-based ABIs with name-mangled shared linkage and ABI-aware cgo exports; ABI aliases only for code without wrappers → continues as Type, itab and typelink layout (.go.type section, relro rework) (1.18+)
Mach-O and Darwin support●●●●●●●●internal linking, PIE, plugins and c-shared on darwin/arm64 with code signing, __DATA_CONST, LC_BUILD_VERSION → continues as Per-platform object formats and internal linking (Mach-O, PE, XCOFF, wasm, ppc64, riscv64, loong64) (1.18+)
PE / Windows●●●●●●●●internal/external linking, PIE, ASLR, DEP, windows/arm and windows/arm64 → continues as Per-platform object formats and internal linking (Mach-O, PE, XCOFF, wasm, ppc64, riscv64, loong64) (1.18+)
AIX / XCOFF●●●done; aix/ppc64 supports internal and external linking, c-archive and PIE → continues as Per-platform object formats and internal linking (Mach-O, PE, XCOFF, wasm, ppc64, riscv64, loong64) (1.18+)
WebAssembly●●●●●still the design at go1.17 → continues as Per-platform object formats and internal linking (Mach-O, PE, XCOFF, wasm, ppc64, riscv64, loong64) (1.18+)
Build modes: PIE, plugin, c-shared, c-archive, shared libraries●●●●●●●internal-linking PIE on linux/amd64+arm64, windows and darwin; plugins on darwin/arm64 and freebsd/amd64, disabled without cgo → continues as ELF/relro/hardening and external-link policy (1.18+)
Host object loading and external linking●●●●●●●●host objects (ELF, Mach-O, PE, XCOFF) read with loader symbols; external linker flags probed and filtered; cgo directive encoding as JSON → continues as ELF/relro/hardening and external-link policy (1.18+)
Architecture ports and trampoline insertion●●●●●●two-pass trampoline pass for arm/arm64/ppc64; riscv64, mips64, windows/arm64 external linking supported → continues as Per-platform object formats and internal linking (Mach-O, PE, XCOFF, wasm, ppc64, riscv64, loong64) (1.18+)
GOEXPERIMENT plumbing and tool configuration●●●experiments are a struct in internal/goexperiment parsed by objabi/buildcfg → still the design at go1.17
Build identity, buildinfo and reproducibility●●●●still the design → continues as Reproducible build IDs, buildinfo and symbol tables (1.18+)
Linker restructuring (1.10 code health and error reporting)●●●done; later passes build on these package splits

Thread map, Go 1.18 – 1.28

Thread1.181.191.201.211.221.231.241.251.261.271.28Status at HEAD
Linker, object files and debug info (Go 1.18–1.28)
Relocation-free pclntab and funcdata (offsets instead of relocations)●●●●●done; pclntab has no relocations, lives read-only in .gopclntab together with funcdata and findfunctab; moduledata in its own section
Type, itab and typelink layout (.go.type section, relro rework)●●●●●type descriptors, itabs and GC masks have dedicated linker layout; typelinks and itablinks lists removed; alg/noalg descriptors deduplicated in 1.28 cycle
//go:linkname restrictions and the blocklist●●●●-checklinkname on by default; push-only linknames required for std symbols; blocklist extended each release
DWARF 5 migration●●enabled by default since the 1.25 cycle (ca19f987ca, 2024-12-19; experiment dwarf5 added bdb4676043); internal/buildcfg/exp.go keeps it off on macOS (dwarf5Supported)
DWARF quality for debuggers (types, variables, trampolines)●●●●●●●●●●ongoing, driven by Delve/gdb/lldb needs
Darwin split DWARF (dSYM)●default on Darwin
Nosplit stack checking and stack constants●●done
Dead code elimination and reflection-aware pruning●●●●done
Linker performance and memory●●●●●●incremental
Reproducible build IDs, buildinfo and symbol tables●●●●●GNU build ID and LC_UUID generated by default
FIPS 140 support in the linker●done
ELF/relro/hardening and external-link policy●●●●●●●
Per-platform object formats and internal linking (Mach-O, PE, XCOFF, wasm, ppc64, riscv64, loong64)●●●●●●●●●●●active

Linker and object files (Go 1.0–1.9)

Reader: Claude Sonnet. Cites 1,189 of 1,343 commits in this slice; every non-noise commit is cited, plus 13 larger commits the noise filter had set aside, which a separate gap reader described. Read the opening lines of the design-heavy messages and about 55 full messages, but no diffs. Grouped “Other” items for 1.0–1.8 are hash lists with short labels.

Release digest for this slice
  • Go 1.0 — Everything from the first 6l/8l/5l through Go 1: ELF, Mach-O and PE writers, the shared src/cmd/ld, dead-code elimination, symbol/pclntab as rodata, DWARF, NOSPLIT checking, cgo dynamic imports, and ports to OpenBSD/NetBSD/Windows/NaCl.
  • Go 1.1 — External (host) linking is born (-hostobj then -linkmode, -extld), -shared PIC output, symbol table repacked host-endian, args/locals info, race/fieldtrack hooks, and typelinks for reflect.ArrayOf.
  • Go 1.2 — New in-memory symbol table and contiguous pclntab with FUNCDATA GC metadata and argument bitmaps; ARM external linking and TLS merge.
  • Go 1.3 — liblink and the object-file reader/writer move instruction selection out of the linker; a Go skeleton cmd/link appears; Solaris and NaCl land.
  • Go 1.4 — Go-specific DWARF type info and fixed frame tables, linker-defined symbols moved into package runtime, Android TLS, and a quiet cycle in the linker core.
  • Go 1.5 — The linker is converted from C to Go via c2go and moved to cmd/link; -buildmode c-archive/c-shared/shared/-linkshared appear; power64, arm64 and darwin/arm ports; external linking on Windows and arm64; findfunc lookup table.
  • Go 1.6 — PIE and many shared/c-shared architecture additions, -msan, -libgcc/-extar, mips64, ppc64 external linking.
  • Go 1.7 — Linker speed/memory campaign, new object format with reference indexes and contiguous data, string merging and method pruning, typelink offsets, cmd/newlink removed, deterministic builds, s390x.
  • Go 1.8 — Plugins and plugin versioning, rel.ro segment and internal PIE groundwork, DWARF for functions generated in the compiler, ARM/ppc64x trampolines, mips, global state removed from cmd/link.
  • Go 1.9 — PIE linked externally by default, Windows external linking and DWARF fixes, Mach-O DWARF for dsymutil, -importcfg, object magic go19ld, objabi split and symbol-kind trimming.

Early loaders 5l/6l/8l and the shared src/cmd/ld (2008–2011)

Span: Go 1.0 → Go 1.0 · Status at go1.17: replaced by the C-to-Go converted linker in 1.5 and by cmd/link thereafter → continues as Relocation-free pclntab and funcdata (offsets instead of relocations) and Per-platform object formats and internal linking (Mach-O, PE, XCOFF, wasm, ppc64, riscv64, loong64) (1.18+)

Design. The Go linkers started in June 2008 as one C program per architecture (6l for amd64, then 8l for 386 in 7d443bb67a, 5l for ARM imported from the Inferno distribution in db3a21d7c6). Each had its own obj.c (object reader), asm.c (container writer), span.c (instruction encoding and span-dependent jumps), pass.c (stack-split insertion, branch following) and optab.c. The object format was the Plan 9 style .6 file: text-ish records with a $$ delimited export-data section (4958d41d3b changed the delimiter to \n$$) followed by instruction records. Libraries were read through libbio (a570eaabcd, bfc70d0f0a).

Over 2009–2011 the portable parts were progressively hoisted out of the three copies into src/cmd/ld: Go-specific loader code (b87e3e8b7f, “ldbody”), portable object and library bits (c2874976e4), the XputY writers (fb55941539), asmlc (30edda690f), symbol table and ELF generation (d37c572ad5, 1.1: one asmbelf in elf.c replacing three nearly identical copies, plus a single genasmsym). A new flag parser matching package flag semantics replaced the C-style clustered flags in the linkers and compilers (cbbc6a102d, 1.1); -V (b4fb00b69b), -L (48974f553d, e6bc5bf5af), -r/rpath (e6443f6255), -I interpreter (9040da9e40), -B build ID (32316bba5b), -X string-variable setting (267f56e10b parsed, e3755434b8 implemented), -w (64b497c62a), -s eliding the Go symbol table (10d0dffd76) and -H as an enum with symbolic names (0cf6f8c096) appear in this period.

Layout changed in several steps. Initially the linker laid out the whole program as one instruction list; 9c20485268 (6l) and eb6f683691 (8l) switched to function-at-a-time layout and reworked span-dependent jump resolution: forward jumps resolved at their targets via a comefrom list, jumps marked small or big with only small to big transitions, so most functions need two passes and the maximum dropped from 5 to 3. 01df088f8d split address assignment into a separate pass (code laid out before data) and e5e9211071 introduced sub-symbols, symbols laid out inside a larger one but addressable directly (first used for the Mach-O pointer array). Archive loading changed from symbol-driven to loading whole archives (49084db386), earlier having been tuned with a heuristic (a3c4faf83f) that was dropped again in favour of letting the dead-code pass discard unused objects (fc8dca9dac). Duplicate loading of the same object was stopped (69e244a104), only one object file (package main) is accepted on the command line and the rest are pulled in via the package path (3b1a0355b6, 00f4c6a1b5), and the link fails if the top-level package is not main (11e254e62c).

Timeline.

Go symbol table, pc/line table and function metadata (gosymtab, pclntab, findfunc)

Span: Go 1.0 → Go 1.5 · Status at go1.17: still the Go 1.2 pclntab design (contiguous per-function tables plus functab) with a findfunctab bucket index (1.5) and offsets-based types (1.7); the pclntab itself is rewritten in 1.18 → continues as Relocation-free pclntab and funcdata (offsets instead of relocations) (1.18+)

Continues in: pclntab, funcdata and pc tables (Go 1.12 → Go 1.17)

Design docs & discussion

Design. The binaries carried two tables the runtime and debug/gosym read: gosymtab (Plan 9 style nm symbols, used for names) and gopclntab (per-function pc-to-line and pc-to-SP-delta tables). Early on they were loaded by a special mapping to a high address (0x99000000, “pad Go symbol table out to page boundary” fe43325b30, “move mapped symbol table lower in memory” 8ec9ffc742). 19fd5c787f (2010) turned both into ordinary rodata symbols so the information is available on systems that cannot do the magic load (NaCl, ARM QEMU, Linux without highmem) and removed Windows special cases. In 2011 the ELF overlap of sections that owned the same bytes was fixed so strip was happy (cf143e9dbf, ba2e3af177).

A large share of binary size was symbol names for compiler-generated data. 7a09a88274 (2011) deleted the names of type.*, string.* and go.string.* symbols from the symbol table, aggregating them into contiguous blocks, shrinking godoc from 9.15 MB to 4.29 MB (3.40 MB without DWARF). Go 1.1 changed encoding for host-linking: 4e2aa9bff0 stored symbol values as native-endian (the table begins with a magic saying which) so a host linker can fill them via relocations, and c8dcaeb25d repacked the table with pointer-sized host-endian addresses and varint-encoded other values.

Go 1.2 is the structural change: “new in-memory symbol table format” (5d363c6357, design go12symtab) and a “contiguous pcln table” (c3de91bb15). pclntab became a single table with a header, a sorted functab of (entry pc, funcoff) pairs, a Func record per function containing offsets to pcsp/pcfile/pcln/pcdata tables, a file table and a funcdata array. GC information moved from pseudo-instructions in the instruction stream to FUNCDATA (48769bf546), args pointer bitmaps (4e0a51c210) and accurate args/locals sizes in bytes (f466617a62) were added. In 1.3 liblink’s new object reader/writer introduced a pcdata iterator and removed the linker’s “line number stack” and “path fragments” (a9f6db58ce). In 1.5, 63116de558 added a build-time lookup table (the later findfunctab) to find the function containing a pc, 13.35% faster stack copy and ~0.5% of text size, with a 16-byte minimum function size; 6dd31660b0 and 214c7a2c43 stopped putting container symbols in functab/symtab.

Timeline.

Stack splitting and the linker’s nosplit overflow check

Span: Go 1.0 → Go 1.5 · Status at go1.17: nosplit chain checking is still done by the linker (stkcheck in cmd/link), reworked in 1.19 → continues as Nosplit stack checking and stack constants (1.19+)

Design. In the earliest linkers, pass.c inserted the prologue that calls morestack for segmented stacks (“segmented stack” 1f6828bcdf, b987f7a757, then 5l support 83632c7359, 1ac2cfc720 with argsize passed). The comparison was made unsigned (c2eb89af92), large frames were supported (9e99383df5, “frame size > StackBig”), the frame size is passed to morestack when needed (1fa4173444), and -K checks stack underflow (f74ec00278, fixed e642503d6f). A NOSPLIT function skips the prologue, so a chain of nosplit calls must fit in the guaranteed red zone; d9fd11443c (2011) added the linker check (“nosplit stack overflow”, printing the chain with the assumed bytes on entry), which immediately found runtime functions (assertI2T, selectgo, newselect) on amd64 with unexpected frames and ARM functions wrongly tagged NOSPLIT (e.g. recursive printpanics).

Timeline.

Dead code and data elimination

Span: Go 1.0 → Go 1.8 · Status at go1.17: reachability-flood plus method pruning conditioned on reflect.Value.Method/Call use, as designed in 1.7 → continues as Dead code elimination and reflection-aware pruning (1.18+)

Continues in: Dead-code elimination and method pruning (Go 1.10 → Go 1.17)

Design docs & discussion

Design. The first linkers tracked reachability with a pass that discarded unreferenced symbols and then (after 9cba9c8890, “eliminate dead code, not just the symbols”) really removed their code from the instruction lists; 8d8225d529 turned GC of unused data/code back on. 7b29dbb866 stopped emitting unreachable data symbols. fc8dca9dac replaced the heuristic of not pulling objects from archives purely for their init function with “pick up everything and let the dead code/data eliminator throw away the parts that weren’t useful”. Line numbers survive removal of dead functions (47e27758db, fda0e78b23). The Go 1.3 skeleton of the new Go linker had a worklist-based dead.go (7dcc652f10).

The substantive 1.7 change is 862b9ddda7: previously all methods of reachable types were kept to support reflect.Value.Call. The linker now looks for use of reflect.Value.Call (and Method, e2836935bb) and, if absent, prunes methods: a method is kept if it is directly called or matches the signature of a method of some used interface. Whether a method body is kept is decided by the relocation from the receiver’s *rtype to the method’s *rtype; d06b0db5bd also prunes the funcType, and 44d3f89e99/f2772a4935 removed some method type data (second method type computed at runtime). The go.plugin.tabs symbol is marked reachable only in plugins (67b2927990) and DWARF function symbols marked reachable (aad06da2b9). 6f31abd23a (1.8) added R_WEAKADDROFF, a weak variant of R_ADDROFF used for the ptrToThis field so the pointer-to type is kept only if otherwise reachable.

Timeline.

DWARF generation in the linker

Span: Go 1.0 → Go 1.9 · Status at go1.17: function DWARF generated in the compiler (1.8) and assembled by the linker; types/globals still in the linker → continues as DWARF quality for debuggers (types, variables, trampolines) (1.18+)

Continues in: DWARF generation in the linker (Go 1.10 → Go 1.17)

Design. DWARF began in 2010 with ca9098ee94 (“Dwarf output for 6l. Part 1, general scaffolding and pc/lc sections”), then b433552ebc (8l in ELF), 728003e340 (Mach-O), later PE (f1751e7de5, 34ac4ec30c, b256358008), frame info (eb572e0174), global/local variables and type info (4228e62259), types of builtin structured types with gdb pretty-printing (9a71bb00bb), gdb support for interfaces and goroutines (7a4ce23d65) and runtime reflection type decoding in dwarf (2aa6cf3e7e, 63544c4754). A language code reserved for Go (d2d650bdc9) and -w to disable (64b497c62a).

Go 1.4 made the output closer to what debug/dwarf parses and added Go-specific type info (125d1e9269, e1821692ca), array types as count rather than upper bound with varint encoding (cddad8affe, 74c9cc8394, 79b106ec62), and fixed .debug_frame off-by-ones (7b0ee53429, 060b24006a). In 1.3, liblink’s rewrite made the linker write a single compilation unit instead of one per package (a9f6db58ce).

The big design change was in 1.7–1.8: a037c73ccf (1.7) generated DWARF via symbols instead of direct buffer writes; 795ad07b3b (1.8, a copy of an earlier CL by Ryan Brown) moved per-function DWARF (DW_TAG_subprogram, variables, location lists) into cmd/compile through the new cmd/internal/dwarf package, costing roughly 0.1s on go build cmd/go (2.25s to 2.36s) while the linker only stitches the compiler-provided symbols together. Other 1.8 steps: more efficient line table encoding (8f65379f94), types referenced in ptabs/debug_info (9f5673d930), no truncated fieldname locations (d80e8de54e). In 1.9: external linking writes DWARF relocations and sections on Windows (aada49038c, 29f061960d), lexical blocks for locals (2ad41a3090), DW_AT_producer (41d0bbdc16), DW_AT_go_embedded_field (b1868cf107), specialized runtime._type variants exported (073247f11f), and a Mach-O DWARF segment that dsymutil accepts (18e77673d8, b28f2f7399, 23dc3633ca).

Timeline.

External (host) linking and cgo

Span: Go 1.1 → Go 1.9 · Status at go1.17: internal linking only for cgo-free or stdlib-cgo cases; external linking required in many cgo cases → continues as ELF/relro/hardening and external-link policy (1.18+)

Continues in: Host object loading and external linking (Go 1.10 → Go 1.17)

Design docs & discussion

Design. Go 1.1 added the ability to produce a host object file and let the system linker finish the link (issue 4069). The design stages: 4e2aa9bff0 (native-endian symtab), d37c572ad5 (portable ELF writer), 0cb0f6d090 (-hostobj: write a host object file instead of an executable, so 6l -hostobj -o x.o x.6 followed by ld -e _rt0_amd64_linux x.o works), 60f783d92b (host linking on linux/amd64, go test -ldflags -hostobj misc/cgo/test passes), 30e29ee9b6 (TLS relocations in the .o so gcc picks m and g offsets), b4f3533c92 (-hostobj replaced by -linkmode), then extension to darwin (b83d4af330, df6072b41c, 3b85b724c5), 386 (7663ffcae6, cd94cabad6), FreeBSD amd64 (18f926aab3), and 3197be4807 (GO_EXTLINK_ENABLED=0 defaults to -linkmode=internal). 9182c364aa added -extld and -extldflags, e982ecacd1 -tmpdir, 696901204f always uses external for -linkmode=external. Relocation reading from external objects: ELF/Mach-O/PE object readers (4d8d6d5cda, cf42a45587, f5690004c2) existed since 2010 and were extended for GOT relocs and scattered relocations (e607380ff6, 9de61e7c8c, 65b782e951).

Later stages: 1.2 ARM external linking (45233734e2, merging runtime.g/m into a runtime.tlsgm TLS symbol and using MRC), 1.3 FreeBSD/ARM (d31d19765b), TLS relocations for ELF in external mode (6f8b120869) and importing runtime/cgo for external link (f85600859d, 1683dab725); 1.5 external linking on windows 386/amd64 (04642e92ac, e7df053977, stdcall decoration 6112e6e404), arm64 (f55b2a11f4, 7a96ecde4d), darwin/arm64 (4fd9a3fdbb) and the policy that cgo forces external link on darwin/arm (1083715b7f) and linux/arm64; 1.6 ppc64 (c155e59062); 1.7 mips64 (073d292c45, dcd613862b); 1.8 mips (a3b670e333) and trampolines for external ARM (c7e855658d); 1.9 PIE uses external by default (ab067cde34).

Timeline.

Build modes: -shared, c-archive, c-shared, shared, PIE, plugin

Span: Go 1.1 → Go 1.9 · Status at go1.17: all six build modes implemented; internal PIE on linux/amd64 exists but external is the default → continues as ELF/relro/hardening and external-link policy (1.18+)

Continues in: Build modes: PIE, plugin, c-shared, c-archive, shared libraries (Go 1.10 → Go 1.17)

Design docs & discussion

Design. All position-independent modes rest on generating PIC code and dynamic relocations. 3bdeaf2a64 (1.1, Elias Naur) added -shared to 5l/6l: PIC output with RIP-relative addressing and dynamic relocations. 45233734e2 (1.2) implemented -shared in terms of external linking. In 1.5 the linker gained -buildmode: 2a5f88d850 c-shared as an alternative to -shared; 8da54a4eec finds _rt0_<GOARCH>_<GOOS>_lib and adds it to .init_array so the runtime initializes at dlopen; f616af23e0 calls runtime.addmoduledata from .init_array so shared objects register their moduledata (arm64 4e3a1e409a, arm 1b4d28f8cf, ppc64le 1ccefcd1b8, 386 09d7de8d61 in 1.6); 449969a4ac and ced7ffe95b c-archive on linux and darwin, using ar (-extar in 1.6, c9778ec302 table of contents in output); b2f95a167a c-archive forces external link; 00bc19e996 -buildmode=shared and 75c0566b55 -linkshared on amd64, with 7556948ebf/de6d5b0801 writing the list of packages in the shared library to an ELF note, bcc1870fcf the libraries linked against and 65518032b9 an ABI hash note used by 77fc03f4cd to abort on a shared library ABI mismatch at start.

In 1.6 the modes spread: PIE on linux/amd64 (b7e4de4b1e), ppc64le (2ffdbd7ce4), android/arm (26205cb3c0); shared on 386 (3ae4dc8a75), arm64 (51586aadfb), ppc64le (9a476028c0); c-shared on 386 (cbc55971e2), arm64 (ab7e82ef3b), android/386 (bda885511d), darwin/386 (9efc46f1cb); .data.rel.ro for read-only relocated data (2c2cbb69c8, 5cbca8d84b); local.moduledata naming (7f9e443d9c); lazy PLT resolution disabled (69a99ccc62) and functions resolved locally (c34fb3cfc6).

1.7 delivered deterministic builds and PIE fixes, c-archive on windows and darwin text relocations (ed8f0e5c33, aea4222f67), itablinks as a slice in moduledata (79688ca58f), types kept together when dynamic linking (2d5688952d). In 1.8: eced6754c2 plugin support on linux (collect exported plugin symbols from the compiler into moduledata, treat any binary that imports plugin as dynamically linked), 73e7a569b4 darwin/amd64 plugins, 03da2690c9 plugin versioning (hash of every imported package, local and exported versions; the first exported symbol per package becomes canonical), 9da7058466/eb4e17b73b full plugin path in symbols and visibility analysis, d0e408334b no-export plugins, 5923df1af9 compiler table of main symbol types, 8eb9fdaa01 type symbols for ptabs; internal PIE: rel.ro segment (276803d611) with PT_GNU_RELRO (d9d1399eea), dynamic relocs (81ee93679d, 5940a007c1), TLS IE-to-LE (44ee2b00db), ET_DYN (04b4dbe1f0), enabling (1a42d8fbd6) but then d185cc3b64 disabled internal PIE “for now” and ab067cde34 (1.9) made external the default for PIE. d92a3606f5 (1.8) builds c-archive position independent on ELF; 361d2738d5 removed the -shared flag in favour of buildmode.

Timeline.

liblink, the object file format and the move into the compilers

Span: Go 1.3 → Go 1.9 · Status at go1.17: cmd/internal/obj + goobj object format with symbol references, contiguous data and per-package indexes; replaced in the 1.15–1.17 era by the new object file format → continues as Relocation-free pclntab and funcdata (offsets instead of relocations) (1.18+)

Continues in: New object file format and the loader (dev.link) (Go 1.14 → Go 1.16)

Design docs & discussion

Design. 7d507dc6e6 (Dec 2013) created liblink, “a new library based on linker code” with include/link.h defining a Link structure that holds all linker-relevant state replacing global variables, with the architecture-independent parts of the old linker moved in. The next step, a9f6db58ce, moved instruction selection and layout into the compilers and assemblers: “new object file reader/writer (liblink/objfile.c)”, removal of the old writing routines, a pcdata iterator, removal of line-number stack and path fragments, dwarf writing a single compilation unit; the linker’s nosplit check was temporarily disabled. Linker input became already-assembled machine code with relocations (8d39e55c65 removed arch-specific constants from the file format, cc08d9232c added a leaf bit, 468cf82780 fixed a hash lookup keyed on name+version that compared only the name). ca85d572d6 (1.4) used pc-relative addressing for all amd64 memory references so code is position-independent.

In 1.5 the C code was converted to Go with c2go (see next thread) and cmd/internal/obj became the Go incarnation of liblink. In 1.7 the format was reworked for size and speed: strings no longer NUL-terminated or aligned (735e5a483c, 08c2cd8469), string data deduplicated for long strings and merged into one symbol (afd835434c, a0232ea0dd, 0c84c4f19c, then replaced by compiler-side dedup 21af2d39c2), edde955d7f arbitrarily sized string data, symbol references as an index table (3648d2d4cd: pkg/linux_amd64 shrank 61MB to 43MB; LinkCmdGo -15.22%, LinkJuju -10.16%), all symbol data in one contiguous section (36d5650a1c: -3.3%/-4.3%), lengths recorded (7f067c87d8), static data written directly into LSyms instead of ADATA (7c18f8cdc3), __.(GO)?SYMDEF entries dropped (67dbde0d71), object version bump (6b019e216b), -linkobj two-part objects (feb6131b1a), common architecture representation (c6e11fe037). The 1.9 bumps the magic to go19ld (7c84dc79fd) after another format change, moves LSym type fixing to the compiler (18fb670e5e), adc80c0665 refactors Linksym, and 1e3570ac86/be2ee2a4b4 create cmd/internal/objabi with linker-only symkinds moved to the linker. 3e7d4f11c0 (1.5) moved debug/goobj to cmd/internal/goobj; a99f812cba (1.8) taught cmd/objdump to read .o files.

Timeline.

From C to Go: cmd/link

Span: Go 1.3 → Go 1.8 · Status at go1.17: cmd/link written in Go, still based on the 1.5 c2go output, being re-architected in the 1.15–1.17 era → continues as Linker performance and memory (1.18+)

Design docs & discussion

Design. Two parallel Go linkers existed in 1.3–1.5. 146897b031 (Jan 2014) was a “skeleton of linker written in Go” (cmd/link, with prog.go, layout.go, load.go, dead.go), followed by Mach-O formatter (8449863d31), dead code removal (7dcc652f10) and pclntab generation (a453f28c70). That effort was an independent rewrite (cmd/link) that never completed; 2a141dedc4 (1.5) renamed it to cmd/newlink, and fb880b8a03 deleted it in 1.7 together with removing weak symbol support (9f4a359fc2).

The linker that shipped was the C one converted mechanically: 5f1efe738b made cmd/ld “a real library”, 1f9dbb60ef (“convert from C to Go using rsc.io/c2go’s run.ld script”: 25 files, 17,636 insertions in cmd/internal/ld) and dca5f2e9b3 (“replace C code with Go code”: 56 files, 4,256 insertions vs 19,460 deletions) moved cmd/new5l etc. into place on 2015-02-28. Subsequent 1.5 commits tidied the generated Go (717cb74907, 665c3da33a, 8c3cc58d0c, d7b10060b6, 2692f48330, c776592a4f, 91938fd1ca putting type decoding in decodesym.go), consolidated adddynlib/adddynsym (004706099d, 4cfff271c2) and then cmd/internal/ld was moved to cmd/link/internal/ld with per-arch directories (cf932cd897, the “retire architecture letter” commit, and flags: 12795c02f3 -X name=value, aefa6cd1f9 dead flags, a2c50ece2b -h, ac1f48e2f6 -buildid, 9ae3c560b9 for non-ELF).

The 1.7 and 1.8 releases were an internal Go cleanup: Allsym as a slice (4b92cd4ec1), LSym attributes packed (a3c258a567), Autom linked list to slice (5be961a352), textp slice (7d56215bcb), Minalign in ld.Arch (90cb1fdaf2), the removal of the global Ctxt (4338e5a891, 2f783c3458), Bso (64f5023e6f), Cursym (1d3fae461c), Linklookup (25d094034b, 324f6ab48c), Symbols split from Link (d284d4ff92, d41a7f77c5, b6324ef5ff), standard flag package (0a15d95091, fa8a28d559), LSym renamed Symbol (8e90b9026b), a SymKind type (3877f820a6), Headtype type (428c349552), ld.Errorf (5a597e2d29), ctxt.Logf (d61c07ffd8), and debug/pe replaced the hand-written ldpe reader (d08c3d1329, 237d7e34bc). Many of these are the groundwork for the 1.9+ cleanups.

Timeline.

Linker speed and memory (Go 1.7 focus)

Span: Go 1.7 → Go 1.9 · Status at go1.17: incremental; the 1.17-era work moved to the new object format and loader → continues as Linker performance and memory (1.18+)

Paths not taken: Linker speed experiments of 2016: mmap input, section-oriented objects (superseded)

Continues in: Linker performance and memory engineering (Go 1.10 → Go 1.17)

Design. In the 1.7 cycle (driven by Shahar Kohanim, Michael Hudson-Doyle, David Crawshaw, Brad Fitzpatrick and others) the Go linker was profiled with LinkCmdGo and LinkJuju benchmarks. Techniques: batch allocation of Lsym (8096881927), preallocated Lsym map (e0b04fa508), allocate small []byte reads together (36f25a7e7b), track offset instead of seeking (6d0b551931), batched byte writes (d3b00a8cc4), integer parsing (3504945081: -1.5%), encbuf writes (d6d33f678d), bufio.Reader based readSymName (73edd7b208, -1.1% allocs), replacing the custom DWARF hashmap (c0f7195d62), string map for single-version symbols (eb57a1dd75), patching up symbols once per object (93098de0cc), deleting unreachable collision checks (70ef564e79, 49be1ebab4), reusing duplicate symbol objects (b6fe2c2c20), stdlib sort in dodata (90a59d448e), symbol generation optimizations (4f12cc0813), concurrent section alignment (5a0881a1d1) and typelink generation moved into the linker (4140da7b57, d8d445280a, up to -6.9% time / -4.9% RSS). Earlier, 1.6 had cf73357e37 concurrent object copy for external linking, and 1.0 had 1bc84b7e18 (“25% faster”), 8a6da2f6a0 (n^2 to n log n in data initialization) and 9c7535432a/505fd760a2 (6l/5l speedups). 1.5’s 1082e2390e rejected data size over 2 GB (6f80e5edf4).

Timeline.

Runtime type data, GC metadata and sections laid out by the linker

Span: Go 1.0 → Go 1.9 · Status at go1.17: type descriptors with offsets (nameOff, typeOff, textOff) and GC bitmaps in dedicated sections → continues as Type, itab and typelink layout (.go.type section, relro rework) (1.18+)

Design docs & discussion

Design. From 2009 the linker generated type strings (0532f89b1f “invent gotypestrings if needed”, 47caf6424c gotypesigs on demand) until the compiler emitted them (7c534e1bd2 makes ld-supplied typestrings obsolete). In 1.0 4e3f8e915f tagged pointer-free data and bss and allocated them in separate noptr sections (5bcad92f07 NOPTRBSS and #pragma textflag) so the GC need not scan them; 1.1 added .gcdata/.gcbss (032e5bfb30, fixed by 251baea1af in 1.2), hostobj sections marked noptr in 1.4 (65e2b6f847). 1120982590 (1.1) introduced a linker-generated typelinks section listing array/chan/map/slice types to support reflect.ArrayOf etc., while removing the interface{} header from rtype: in godoc about 10,000 types lose two words each and about 500 types regain one. 44fd7573aa supports reflect.PtrTo and f76f120324 aligns large objects to 64 bits.

Go 1.5’s 512f75e8df replaced GC programs with a simpler encoding (data layout for large arrays) and the linker’s c949cff6a7 reads the gcdata without relying on local symbols; 0acecb7164 elides gcbits symbols; e8f2eb4349 lets the linker drop string headers. In 1.7: offsets replace pointers in read-only data (f028b9f9e2, the first of a series to cut dynamic relocations in PIE/c-shared: cmd/go -86KB (0.7%) on linux/amd64 PIE), name data not aligned (bb52ceafea), go.string dedup, typelinks sorted in the linker (4140da7b57). In 1.8: typelink generation via a flag set by the compiler (d8d445280a), a ptrToThis weak relocation (6f31abd23a), and a rel.ro segment (276803d611).

Timeline.

Container writers: Windows PE, Mach-O and Plan 9

Span: Go 1.0 → Go 1.9 · Status at go1.17: PE, Mach-O and ELF writers in cmd/link/internal/ld; Mach-O code-signing friendly and Windows external linking supported → continues as Per-platform object formats and internal linking (Mach-O, PE, XCOFF, wasm, ppc64, riscv64, loong64) (1.18+)

Continues in: Mach-O and Darwin support (Go 1.10 → Go 1.17) · PE / Windows (Go 1.10 → Go 1.17)

Design. Windows support began with 977e19d69f (8l PE output, 2010), then an import table for kernel32.dll (3b1a71862e), a DOS stub (30f981d980), //dynimport and dynexport (70deac67cf, a3120f67ca), DWARF in PE (f1751e7de5, 34ac4ec30c, b256358008, 2d99974ec5), resources (2ad58d48fb), -Hwindowsgui (75fb2f6c82), windows/amd64 (3aec5516ed, cgo 9f636598ba, 1aa2d88739), reading PE objects (ldpe: fa249cae38 redo pe writing, 4e5e12e305, 7ce1a4bdc0, 3199a6ca8d, 9e2ffc315f for dllimport). The PE header link version was bumped to allow code signing (c9bf048337), stack size raised for cgo (428062da4e), thread count raised (074354c2ea), timestamp zeroed in 1.5 (98376204e8), and gcc 4.9 long section names via the string table are read (d0ee959ab7, d704bb0dc9). In 1.7 the PE symbol table writer was rewritten (bb48b864b1): the old one walked all symbols twice, allocated a global array of 32-byte COFFSym structs and walked again; the new one writes each record in a single walk. In 1.8 ldpe.go was rewritten on top of debug/pe (d08c3d1329) and in 1.9: all PE section names in the symbol table (b660a4b04d), section reordering (d0a978f5b5), shNames (e9abf1a716), peemitreloc simplification (b32170abdf), IMAGE_SYM_CLASS_STATIC for locals (d3f5e36917), file parameters computed after linkmode is known (0ad247c6f0), DWARF sections and relocations on external windows linking (29f061960d, aada49038c), .bss not read in ldpe (507f4d5f48).

Mach-O: 073486c3ed (debug support), 2157025ef0 factoring out macho code, 133a158bd8 (8c, 8l dynamic loading, cgo on darwin 386 and amd64), 0f6926474f (LC_RPATH), exported dynamic symbols (882f9d6d7a, 3a43ff1a77, ac1c09458c, fa563ae82e sorting), code signing (78ad19f214 string table to end of __LINKEDIT, 280d85a80b), c5444a8937 zero-fill sections, b83d4af330 Mach-O sections matching internal sections, df6072b41c full symbol table, 1.2 a2c30fe648 sizes to Mach-O symbols, 1.3 a4a1fadfa2 lldb addresses for DWARF sections, 1.5 47746f10fe .init_array, 1.6 4a0eee2faa LC_VERSION_MIN_MACOSX, 1.8 9d1efba28d (text at address 0x1000000 on darwin/amd64) and 7ee7936523 R_GOTPCREL. 1.9 reworked the DWARF segment for dsymutil and -s/-w handling (18e77673d8, b28f2f7399, 23dc3633ca, 371c83b594).

Timeline.

Architecture and OS ports in the linker (arm, power64, arm64, mips, s390x, nacl, BSDs, Solaris) and trampolines

Span: Go 1.0 → Go 1.9 · Status at go1.17: all ports in cmd/link/internal/<arch>; direct-branch trampolines for ARM and ppc64x → continues as Per-platform object formats and internal linking (Mach-O, PE, XCOFF, wasm, ppc64, riscv64, loong64) (1.18+)

Continues in: Architecture ports and trampoline insertion (Go 1.10 → Go 1.17)

Design. Each port added a linker directory with asm.c/obj.c/l.h (later asm.go, obj.go) holding archreloc, adddynrel, elfreloc1, machoreloc1 and the Arch description. ARM: 4e1896a1b5 ELF in 5l, softfloat and VFP (9c6df3ca13, ae60526848), GOARM embedded as runtime.goarm (88ba4de152, 77e42e2108), aborting when VFPv3 is missing (55ca5ab0be, GOARM default resets to 6), CLZ/PLD/BSWAP style instructions (d186d07eda, 2031252fb8, ed480128a6), dynamic linking (452a9e452b, dac4c3eee9 auto detection of the dynamic linker), FreeBSD/NetBSD ELF (d901808869, 2ebfaa36b5), 64k alignment on linux/arm (f64b7d301d), BLX for BL (Rx) (35e1deaebf), and in 1.8 trampolines for too-far calls (7c431cb7f9: 24-bit offset only reaches +/-32M; direct jump relocs resolved during address assignment and packages laid down in dependency order).

Power: 1a1d450712 (dev.power64 9l), db923390a0 internal linking, 15c67e21da ELF ABI version, e32fe2049d local entry offsets, c155e59062 external linking on ppc64 (1.6), 1ccefcd1b8 addmoduledata on ppc64le, split of large text sections (b4efd09d18, 1.8), trampolines for internal (d26b0661c2, 1.8) and external linking (738739f565, 1.9: the earlier approach of splitting text so the GNU linker adds stubs failed when a plt_branch was created), function alignment 16 (09b71d563a), st_other (5331e7e9df), PLT stubs at start of Textp (15b37655bc).

ARM64: 3d1ce27ba5 (7l, 1.5), external linking linux/arm64 and darwin/arm64 (f55b2a11f4, 4fd9a3fdbb, 7a96ecde4d), two-instruction loads (3a9bc571b0), absolute addressing removed in .text (6e3a2a3f9f), arm64 GOT workaround (4bae454d68), gold on arm64 (a3c92c9db1). MIPS: mips64 (053c75411f, 1.6), external (073d292c45, 1.7), mips32 (76d8e60451, 585a0b03b2, 5b147122d6, a3b670e333, 1.8). s390x (163ff700b8, 29a6149e67, 78715cebcf, 1.7). NaCl: 4a4ec23870 .. eae0906bed, 7390aa0102 (8l, 2010), d9c6ae6ae8 (1.3 final merge, 32-byte “halt sled”, a9b2651ade fix in 1.4). Solaris: 6ebf59b953, 4687b54147 (1.3), e481aac0e1 (PLT instead of GOT, 1.5), non-exec stack (f373bf1eb9, 1.8), e79fef8e55. Others: OpenBSD (ffa14e849c, 31758b2c1a signatures, e6ca125f14 no PT_TLS, 9417c022c6 PT_TLS in 1.9), NetBSD (420fe22921, 37e195cf72 note size), DragonFly (71dc91db0f, fd0af9b56c, d0206101c8), Android TLS emulation (12b990ba7d), amd64p32 (e09ac3cf2a).

Timeline.

Build IDs, ABI hashes, flags and determinism

Span: Go 1.0 → Go 1.9 · Status at go1.17: -buildid, -X, -importcfg, -B, -linkmode, -extld all present → continues as Reproducible build IDs, buildinfo and symbol tables (1.18+)

Continues in: Build identity, buildinfo and reproducibility (Go 1.10 → Go 1.17)

Design. 32316bba5b (1.1) added -B to set a build ID (an ELF note, with NetBSD/OpenBSD identification notes 5842f7e46a, 31758b2c1a, and e69b9ddd42). In 1.5 ac1f48e2f6 added -buildid to write the Go build ID to ELF output, 9ae3c560b9 for non-ELF binaries, and the ELF note identifier was made “Go” (7e27625e25). -X: parsed in 2011, implemented as -X name=value (12795c02f3), checked for non-string variables (69f0d4c6be), f6fc14094a stopped marking symbols as reachable, full path escaping fixed in 1.8 (1773cdd081). Determinism: 5bbb98df09 (1.7), 83746fd55a (1.6, “$GOROOT” in object paths substituted at link time with GOROOT_FINAL) and 6e3a7930eb (1.0, GOROOT_FINAL rewrite). 533ed967c6 restricts the ABI hash to version info and export data (1.9), 4f2269edbd adds -importcfg. 3d40062c68 (1.1) is the fieldtrack experiment mentioned under dead code.

Timeline.

Changes in this slice that are not part of a thread are listed on the release pages: 1.0 · 1.1 · 1.2 · 1.3 · 1.4 · 1.5 · 1.6 · 1.7 · 1.8 · 1.9

Linker and object files (Go 1.10–1.17)

Reader: Claude Sonnet. Cites 1,101 of 1,267 commits in this slice; every non-noise commit is cited, plus 11 larger commits the noise filter had set aside, which a separate gap reader described. Read the design-heavy messages and subject lines. About 300 small commits (PE refactor steps, loader API additions) are cited in grouped items.

Release digest for this slice
  • Go 1.10 — Code-health release for cmd/link: globals moved into Link, packages split out (object reader, sym, loadelf/loadmacho/loadpe), the PE writer rewritten around peFile/peSection, DWARF given one compilation unit per package, content-based build IDs, plus PIE/plugin/c-shared enablement across arches.
  • Go 1.11 — DWARF compression (ELF, then Windows and Darwin) and separation of VA from file layout, is_stmt line tables, dsymutil workarounds, DWARF on darwin/arm*, WebAssembly port, GOROOT default at link time, JSON-encoded cgo directives.
  • Go 1.12 — AIX/XCOFF port, stable ABI0 vs ABIInternal separation and ABI aliases in the object file, linker memory slimming (cold Symbol fields), deadcode-before-mangling.
  • Go 1.13 — mmapped output and object data, Reloc shrunk, pctab deduplication in pclntab, -strictdups, runtime version info symbol, AIX external linking and PIE, riscv64 groundwork, GOWASM.
  • Go 1.14 — Compiler-generated DWARF line tables with multi-CU packages, open-coded defer funcdata, libFuzzer counters, and the first shipped pieces of the new object file format/loader (default via -newobj) plus initial riscv64 and PIE work.
  • Go 1.15 — The dev.link conversion: old object format deleted, all passes moved to loader.Sym, new DWARF on loader, parallel relocation/asmb/dwarfcompress, internal PIE and Windows PIE.
  • Go 1.16 — sym.Symbol deleted; pclntab split into pcheader/funcnametab/pctab with generator symbols and hashed symbols; streaming external relocations; UsedInIface/R_USEIFACE/R_USEIFACEMETHOD method pruning; darwin/arm64 internal linking and code signing.
  • Go 1.17 — Register ABI plumbing (ABI-mangled shared/PE/Mach-O names, cgo export ABIs, argument-printing metadata, DWARF location expressions), weak itab references, two-pass trampolines, windows/arm64, centralised GOEXPERIMENT.

New object file format and the loader (dev.link)

Span: Go 1.14 → Go 1.16 · Status at go1.17: done; the linker reads only the indexed object format through cmd/link/internal/loader, sym.Symbol is gone, and the format gained a weak-reloc bit and 16-bit reloc types in 1.17 → continues as Relocation-free pclntab and funcdata (offsets instead of relocations) and Type, itab and typelink layout (.go.type section, relro rework) (1.18+)

Paths not taken: Linker speed experiments of 2016: mmap input, section-oriented objects (superseded)

Earlier history: liblink, the object file format and the move into the compilers (Go 1.3 → Go 1.9)

Design docs & discussion

Design. Through Go 1.13 the compiler wrote an object file whose symbols were identified by name strings and which the linker (objfile.Read) expanded eagerly into one heap-allocated sym.Symbol (with []sym.Reloc, auxiliary fields, DWARF autos, etc.) per symbol. The dev.link branch replaced this with a new object file (“goobj2” in cmd/internal/goobj2, renamed back to cmd/internal/goobj once the old one was deleted) in which every symbol is addressed by an index: a defined symbol is (package index, local index), referenced non-package symbols and builtins use pseudo-package indices (PkgIdxNone, PkgIdxBuiltin, later PkgIdxHashed), and the file is laid out in blocks (header, string table, per-symbol definition array, reloc array, aux array, data) so the linker can mmap it and read only what a pass needs. The reader, NewReaderFromBytes, works directly on the mapped bytes.

The linker side is the loader.Loader. It assigns each symbol a global loader.Sym index in object order (package DAG order), keeps a local-to-global array per object, and tracks overwrite (a dupok or contentless definition replaced by a real one) and ABI aliases. The first design gave every appearance its own global index with a redirect map; 8e2b5d3e71 made the index unique per symbol (N-to-one local-to-global mapping, a global-to-primary-local array, no overwrite map) and 2f22143cd5 stopped reserving index space for dups so there are no holes. Package-defined symbols were added first so they need no name lookup (b0275bfbf4). Symbol metadata (type, value, size, alignment, section, attributes, ELF type, dynimport, plt/got, Gotype, compilation unit, outer/sub relationship) lives in loader side arrays and bitmaps instead of struct fields; 24814e2147 enforced a single level of “outer” symbol. Symbols created by the linker itself are “external” loader.Syms (07914eda40, b720014743 SymbolBuilder, later MakeSymbolUpdater) that carry a payload (extSymPayload) until the end of loading.

Costs of the old design were the motivation: memory (a full sym.Symbol for every symbol even if dead), startup time, and GC pressure. The new design lets dead-code elimination run on indices before any contents are loaded, and every later pass materialize only live symbols. Further format changes tuned locality and self-description: StringRef was changed to {len, offset} so a string header is two adjacent uint32s (ff29f97437), referenced symbol names were written to the file so nm/objdump are self-contained (73dd74a9fe), content-addressable “hashed” symbols were added (see the pclntab thread) (27342e5bd9, 4f217d5aaa), and in 1.17 the relocation type became uint16 with a separate weak bit (f901ea701d, 414fa8c35e).

Timeline.

Wavefront conversion of linker passes to the loader; sym.Symbol removal

Span: Go 1.15 → Go 1.16 · Status at go1.17: done (all passes run on loader.Sym; cmd/oldlink gone from the story) → continues as Relocation-free pclntab and funcdata (offsets instead of relocations) (1.18+)

Design docs & discussion

Design. The linker is a sequence of phases (load, deadcode, symtab/dodata, address, reloc, asmb, DWARF, …). Rather than convert everything at once, the dev.link team moved a “wavefront”: everything before it works on loader.Sym, everything after still on *sym.Symbol, with LoadFull / PropagateLoaderChangesToSymbols shims (3fa98a0436, 7055f01e12) copying state across. Each CL pushed the wavefront one pass later (“move the wavefront past addexport()”, 84111acd35; “past address”, dc83274c95), and removed *Link dependencies from architecture hooks by introducing Target and ArchSyms (e9056a6a73, b9642cbb8d, 4ab7ab54b0, cb4c84bd9a, bf5dc7af0f) so that relocation can be made thread-safe. The dodata (data layout and section assignment) was split up and rewritten on the loader (a6a8974a5a, 817bd10cae, 941de9760b) and enabled per architecture, from darwin/amd64 (b2fde1098a) via plan9/wasm (f9ed846a46), ELF arm/arm64 (d5c9327628), MIPS/RISCV64 (aa74fce005), s390x (5ea431fb63), ppc64 (8bd7c01417) and darwin/arm64 (512a0219ef), until the old dodata could be deleted (98e3fdab3e). Later the relocation pass (76c6cce116, external linking 095d2a4532), asmb2 (1adae7fe76, then per-OS in 1.16) and the architecture archreloc functions were converted, one architecture per commit.

Timeline.

Linker performance and memory engineering

Span: Go 1.10 → Go 1.17 · Status at go1.17: mmapped, parallel, streaming pipeline with generator symbols; ongoing → continues as Linker performance and memory (1.18+)

Earlier history: Linker speed and memory (Go 1.7 focus) (Go 1.7 → Go 1.9)

Design docs & discussion

Design. Three families of work. (1) Avoid memory: shrink hot structs (Reloc moved cold fields to a lazily initialised side struct, 8ef45cf60b: kube-apiserver inuse_space 1240MB to 1123MB, max RSS under GOGC=5 2.05G to 1.83G; Symbol fields moved to cold sections in 1.12: 09df9b06a1, 2975914a1a, f78cc1324a, dd9e81f678; Reachparent as side table 32bc0976f9; Dynimp strings split f00bfb385b), read-only mmap backing for symbol-name strings and object data (9ac471a87d, dbe32284ff), then the loader’s lazy loading. (2) Avoid I/O and copies: the output file is mmapped (b2f94d3e88, Windows af880809d8 with MapViewOfFile), relocation is applied while writing symbols (37682f7a79) instead of in a pre-pass (earlier f957a7e357 “apply relocations later”, 15a31bd9c8, 248444d5eb apply DWARF relocations during compression), OutData removed (ee8541e5b8: Asmb alloc 3.78MB to 0.14MB), fallocate the output and drop msync (8ab37b1baf: Munmap 16.0ms to 0.8ms), a heap-side OutBuf flushed at 10MB (7466cad9c4), buffered file IO removed (b2def42d9e, 93c9a3bd38). (3) Parallelism: per-phase CPU/mem profiles (416561c9e2, 105053fa19, 6def07cf1e), parallel relocsym (491615e3f7: Reloc 13.9ms to 9.0ms; made possible by Target e9056a6a73), parallel dwarfcompress (a3873ab49f: 181ms to 62ms; TotalTime -14%), parallel asmb with a parallel OutBuf (c46632a2e0, -78% Asmb on amd64, then all platforms 6506cf1981), parallel second-stage DWARF (d1a186d29c), parallel ELF relocation writing (36939aef77), and parallel generator functions (e674b7703e, 06839e3c9a).

Streaming external relocations were a memory trade: instead of converting Go relocations into an in-memory external format during relocsym and writing them in asmb2, the writer emits ELF/Mach-O records straight from the Go relocations into the mmapped output (987ce93824: Asmb2 alloc 66.8MB to 8.2MB; 59a702aa6a; 88382a9f97 amd64 where Asmb2 became 23% slower but Asmb -84% alloc; then 386, arm64, MIPS, s390x, arm, windows and ppc64 with 936a2d6966, bf1816c7b7, ed6b8af509, 71e2133304, e4a3e57f47, 9d22325681, and the old path deleted 4b3cfcee58). External relocations were first stored in Reloc2 format (6e3bde5f30) then as an index into the original reloc (cdfff4d25a). Generator symbols (f033e23414) are real symbols with size but no data; a function fills the bytes during Asmb, so very large tables never occupy heap (used for pclntab pieces, see that thread).

Timeline.

pclntab, funcdata and pc tables

Span: Go 1.12 → Go 1.17 · Status at go1.17: pclntab split into runtime.pcheader, funcnametab, pctab and generator-built tables; still relocation-based → continues as Relocation-free pclntab and funcdata (offsets instead of relocations) (1.18+)

Earlier history: Go symbol table, pc/line table and function metadata (gosymtab, pclntab, findfunc) (Go 1.0 → Go 1.5)

Design. Up to Go 1.15 the linker built a single runtime.pclntab symbol in pcln.go: header, function table (ftab, pc to func offset), each _func record, function names, file table and the pc-value tables (pcsp, pcfile, pcln, pcdata) concatenated into one big heap symbol, with strings and file names deduplicated but renumbered per link. Memory and time were dominated by this symbol. Jeremy Faller’s 2020 plan (documented in ba9c639470 as an “improved format” doc) broke the table into independent pieces that no longer need rewriting: header, function names, file names, pc tables, and per-function _func data, each emitted late in the link from generator symbols and addressed by offsets rather than copied.

Mechanisms used: (a) deduplicate pc tables - first by a cache keyed by content inside the linker (d79aea6b96, 33% hit rate during make.bash, binaries about 1% smaller), then by making pcdata content-addressable symbols in the object file (5387cdcb24); (b) hashed/content-addressable symbols and PkgIdxHashed (27342e5bd9) to dedupe across packages; (c) moving per-function derived facts into the compiler/assembler so the linker does not need to load names (89cf569a45 FuncID, e4e1c6a7af compilation-unit index in Func); (d) per-package file tables used directly as file indices to avoid renumbering (9bdaf99966, 9ae8f71c94); (e) generator symbols filling bytes during Asmb (f033e23414).

Timeline.

Dead-code elimination and method pruning

Span: Go 1.10 → Go 1.17 · Status at go1.17: index-based deadcode with UsedInIface/R_USEIFACE/R_USEIFACEMETHOD method pruning, weak itab edges and reflect-aware tracking → continues as Dead code elimination and reflection-aware pruning (1.18+)

Earlier history: Dead code and data elimination (Go 1.0 → Go 1.8)

Design. deadcode.go does a flood from roots (_rt0_*, runtime.main, init tasks, exported cgo symbols) over relocations. Methods get special treatment: a method of a reachable type is kept only if it can be called indirectly. The original rule matched type methods against all methods of every reachable interface type (by name+signature string), plus R_METHODOFF-based tracking for reflect.Type.Method/MethodByName (which disable pruning entirely when reachable).

The dev.link rewrite ran on loader indices (8a9be4921a, method tracking 7d6f79e617, reflect.Type.Method d6535415fb, relocation hooks 376ef734a7, field tracking 219922e95b, -dumpdep restored c6621d9241) with a work queue ordered by a min-heap because global indices follow package DAG order (36dbdbe9bd; hand-written heap 7179e426e2). In 1.16 Cherry Zhang tightened the semantics in several steps: compare the method name and type descriptor rather than strings (e4d6bfdbdf); compile marks type descriptors converted to interfaces and the linker only considers methods of such UsedInIface types, propagating the mark to “child” types reachable by reflection (95848fc5c6: cmd/go -5.1% bytes) and through method descriptors (789d77a87e); the mark is applied only when the converting code is reachable via a new marker relocation R_USEIFACE (af18bce87c: cmd/go 13.6MB to 13.47MB); used interface methods are marked by R_USEIFACEMETHOD, so only called interface methods keep matching concrete methods alive (39dde09126); itabs reference methods weakly so unused ones can be pruned (e8700f1ce6, 1.17); unreachable methods get offset -1 (0ab72ed020) so a bug fails fast.

Timeline.

DWARF generation in the linker

Span: Go 1.10 → Go 1.17 · Status at go1.17: DWARF 4 with per-compilation-unit DIE and line-table symbols generated in parallel; line tables produced by the compiler → continues as DWARF quality for debuggers (types, variables, trampolines) (1.18+)

Earlier history: DWARF generation in the linker (Go 1.0 → Go 1.9)

Design. The linker builds .debug_info, .debug_line, .debug_ranges, .debug_frame, .debug_loc and (until 1.16) pubnames/pubtypes from go.info.* DIE fragments emitted by the compiler plus type DIEs it synthesizes from runtime type descriptors (dwarf.go). Key transitions: one compilation unit per package with its own line table, instead of one giant CU (d4dda76b5f; motivation: sequential line-table scanning, CU PC ranges 97920373fa, per-package producer flags 2c1d2e06af, and moving more generation into the compiler); .debug_aranges eliminated (77c27c3102); DW_AT_go_runtime_type to link a DWARF type to its runtime type for decoding interface contents (393f84a125); constants exported (9daee93121, 7830a19a4f); function-type size (f4abbc0e61); sudog fix (e70fae8a64); location lists and compact encodings (84188296e9 then reverted 4410900a3c, 31ddd8a39d, cd702b171c, 38bd725bf1); args vs results distinguished (3ba818c894); DW_AT_decl_line (4a1be1e1da); is_stmt in line tables (619679a397, dead03b794: +2614 bytes of .debug_line on go1.test, 3636d53c61, 8dba66dfeb); regabi location expressions (162d4f9c92, 1.17).

The big structural change is moving line-table generation from linker to compiler (Jeremy Faller, 1.14): the compiler records a debug-lines symbol per function (376fc48338, f7f85bdc2c, ee3dded36d: a state machine reset at the end of every function so blocks can be concatenated after dead code), a compilation unit can no longer be assumed 1:1 with a package since assembly files only know their own file names (78a3734714), and the linker switches to simply pasting these fragments (8506b7d42f: +52/-200 lines; 7defbffcda removes the old isStmt symbol). In 1.16 the linker emits include directories in the line prologue (fb184a383e: kubelet .zdebug_line ~30% smaller) and changes line generation to cut heap (d08bab99b7), DWARF pub tables are dropped (bc1dbe58cf). Under dev.link, DWARF was converted to loader APIs in two phases (types 25f1b093fe; funcs/CUs 0c41a37c6c), SDWARFINFO split into six subtypes (96ec09da48), DWARF subsymbols demoted to aux (8634642f9a), file renumbering removed (0dff5b0b9e), and second-stage generation parallelised (d1a186d29c).

Timeline.

DWARF compression and Mach-O dsymutil combining

Span: Go 1.11 → Go 1.16 · Status at go1.17: still the design at go1.17 (zlib ELF/Mach-O/PE compression; dsymutil combine on macOS/amd64 and arm64) → continues as Linker performance and memory (1.18+)

Design docs & discussion

Design. Compressing DWARF requires relocations to be applied first and breaks the constant offset between file position and virtual address; the linker therefore separates VA layout from file layout (bd83774593), then compresses DWARF sections at the end of the file and recomputes their addresses from the compressed size (594eae5ad0: hello 1.60MB to 1.05MB, -34%; StdCmd +5.4% time). External-link mode compresses debug sections too (b397f633ca), then Windows (83515df3f3) and Darwin (726a2d04ea) in 1.11; BestSpeed (12ed0ddec1). In 1.13 DWARF relocations are applied during compression (248444d5eb), in 1.15 compression is parallel (a3873ab49f) and converted to the loader (3e8975172f), 0bbc19760b frees compressed symbols.

On Mach-O, dsymutil cannot handle base-address selectors in location lists/ranges, so the linker rewrites them to CU-relative entries only when the external link will go through dsymutil (230b0bad1f, eca41af012). Binaries are post-processed by machoCombineDwarf (0fe1986a72 extracts load-command parsing and disables combining for non-macOS Apple platforms), STAB symbols stripped after dsymutil for reproducibility (e8f918535e, using -Wl,-S when DWARF is off), xcode strip used (7e25bdba5e).

Timeline.

Symbol ABIs: stable ABI0 vs ABIInternal in the linker and object files

Span: Go 1.12 → Go 1.17 · Status at go1.17: wrapper-based ABIs with name-mangled shared linkage and ABI-aware cgo exports; ABI aliases only for code without wrappers → continues as Type, itab and typelink layout (.go.type section, relro rework) (1.18+)

Design docs & discussion

Design. The compiler/linker split of calling conventions (685aca45dc) prepared for the register ABI: the assembler produces symabis, the compiler generates ABI wrappers for body-less Go declarations and for Go functions referenced from assembly, and the linker represents the two ABIs as symbol “versions” (c5718b6b26 repurposes the reference version field as an ABI number, -1 for static, and adds a list of ABI alias symbols). While the ABIs were identical, wrappers were “ABI alias” symbols resolved by the linker (63815923fc resolves aliases for external symbols; 1794ee6829 nice error message on ABI mismatch). In 1.17, with GOEXPERIMENT=regabi the aliases stop (59703d53e2), shared linkage mangles ABIInternal names with .abiinternal/.abi0 suffixes so the ABI of a shared-library symbol is known (a21de9ec73, d7473fd907), PE and Mach-O external linking also mangle names with ABI (fd5e0bd385, dcf85b30ba), and cgo export directives carry definition ABIs so host objects bind to the right Go entry point (69262d4871; before that, host references were assumed ABI0 and crashed through function values).

Timeline.

Mach-O and Darwin support

Span: Go 1.10 → Go 1.17 · Status at go1.17: internal linking, PIE, plugins and c-shared on darwin/arm64 with code signing, __DATA_CONST, LC_BUILD_VERSION → continues as Per-platform object formats and internal linking (Mach-O, PE, XCOFF, wasm, ppc64, riscv64, loong64) (1.18+)

Earlier history: Container writers: Windows PE, Mach-O and Plan 9 (Go 1.0 → Go 1.9)

Design docs & discussion

Design. macho.go writes segments __TEXT, __DATA, __DWARF and a LINKEDIT with symbol and dynamic-link info; external linking hands the Go object (go.o) to the system ld; internal linking needs dynamic imports and stubs (macho PLT, MACHOPLT).

Timeline.

PE / Windows

Span: Go 1.10 → Go 1.17 · Status at go1.17: internal/external linking, PIE, ASLR, DEP, windows/arm and windows/arm64 → continues as Per-platform object formats and internal linking (Mach-O, PE, XCOFF, wasm, ppc64, riscv64, loong64) (1.18+)

Earlier history: Container writers: Windows PE, Mach-O and Plan 9 (Go 1.0 → Go 1.9)

Design docs & discussion

Design. In 1.10 pe.go was refactored from procedural code with global section tables into peFile, peSection, peStringTable types (20832e6d4b … 3bd79713b7, about 30 small commits: addDWARFSection, checkOffset/checkSegment/pad, emitRelocations, writeSymbols, writeOptionalHeader, etc.). Subsequent work is feature-driven.

Timeline.

AIX / XCOFF

Span: Go 1.12 → Go 1.14 · Status at go1.17: done; aix/ppc64 supports internal and external linking, c-archive and PIE → continues as Per-platform object formats and internal linking (Mach-O, PE, XCOFF, wasm, ppc64, riscv64, loong64) (1.18+)

Design docs & discussion

Design. AIX uses XCOFF, big-TOC addressing and a host linker with an export file. The linker gained an internal/xcoff reader (b171b28138, later moved to internal/xcoff, e256afff51), an XCOFF writer (269e531917, 1315 new lines), DWARF64 (75a0b9dbf3), and TOC-relative data access (cebf9d47cf: addi instead of load for .data/.bss symbols).

Timeline.

WebAssembly

Span: Go 1.11 → Go 1.14 · Status at go1.17: still the design at go1.17 → continues as Per-platform object formats and internal linking (Mach-O, PE, XCOFF, wasm, ppc64, riscv64, loong64) (1.18+)

Design docs & discussion

Design. The wasm port has no ELF-style layout; cmd/link/internal/wasm writes the type, import, function, table, memory, global, export, element, code and data sections plus custom sections directly, with the Go heap/data placed in wasm linear memory.

Timeline.

Build modes: PIE, plugin, c-shared, c-archive, shared libraries

Span: Go 1.10 → Go 1.17 · Status at go1.17: internal-linking PIE on linux/amd64+arm64, windows and darwin; plugins on darwin/arm64 and freebsd/amd64, disabled without cgo → continues as ELF/relro/hardening and external-link policy (1.18+)

Earlier history: Build modes: -shared, c-archive, c-shared, shared, PIE, plugin (Go 1.1 → Go 1.9)

Design. Each build mode changes the relocation strategy. In PIE and c-shared, absolute R_ADDR relocations in data have to be dynamic relocations; internal-link PIE requires the linker to emit them itself (and in 1.15 to emit cheap R_X86_64_RELATIVE instead of symbol-lookup relocations, because a simple HTTP server needed over 1s of dynamic linking, 675a2e7078). Plugins and shared libraries need type-symbol name mangling (24e4a128c9, 6355d6c7e2, 273b657b4e for -X main.*), no coalescing of type descriptors when dynamically linking Go (d6a27e8edc) and a linker-visible plugin.Open marker (b1a20253fe caches the check). Which modes work on which OS/arch is mostly down to each architecture’s adddynrel.

Timeline.

Host object loading and external linking

Span: Go 1.10 → Go 1.17 · Status at go1.17: host objects (ELF, Mach-O, PE, XCOFF) read with loader symbols; external linker flags probed and filtered; cgo directive encoding as JSON → continues as ELF/relro/hardening and external-link policy (1.18+)

Earlier history: External (host) linking and cgo (Go 1.1 → Go 1.9)

Design. For cgo the linker either links C objects itself (“internal” mode, using ldelf/ldmacho/ldpe/ldxcoff readers) or writes go.o and invokes the system linker (“external”). A 1.10 change stopped go build from pre-linking all cgo objects with ld -r (8e5ac83d43, fixing a libgcc multiple-definition problem), which required the Mach-O loader to apply unsigned relocations relative to symbol values. Host-object readers were moved into packages loadelf, loadmacho, loadpe (and loadxcoff), and eventually converted to the loader (ELF fa284af720, 3fa9500dd1; Mach-O 4ad94a5db9; XCOFF 8896a6b8aa; PE 83ba044be6), with read-only mmapped data for ELF (9d673bf1db) and Mach-O (b4245ea544). Flag probing (linkerFlagSupported) was made robust: pass provided ldflags, not all args (6417e91962, 023dbb188d), consider alternative linkers (bcda68447b), pass arch flags (a318d56c1e), use -Qunused-arguments only if supported (565e70fcef).

Timeline.

Architecture ports and trampoline insertion

Span: Go 1.10 → Go 1.17 · Status at go1.17: two-pass trampoline pass for arm/arm64/ppc64; riscv64, mips64, windows/arm64 external linking supported → continues as Per-platform object formats and internal linking (Mach-O, PE, XCOFF, wasm, ppc64, riscv64, loong64) (1.18+)

Earlier history: Architecture and OS ports in the linker (arm, power64, arm64, mips, s390x, nacl, BSDs, Solaris) and trampolines (Go 1.0 → Go 1.9)

Design docs & discussion

Design. On RISC targets a direct call has limited range, so trampoline passes insert veneers. The 1.17 redesign by Cherry Zhang replaced a single dependency-ordered pass (which fails with linkname cycles and with PLT stubs placed late) by a two-pass approach: pass one assigns addresses assuming the program is small; only if some call is out of range does it start over inserting trampolines for far targets and targets with unknown addresses (18852e8372). ARM64 trampolines (f12dfeac89), PLT-call trampolines on ARM64 and ARM (657f58d845, 12eaefead4), R12 as ARM scratch (11052d77a3), ppc64 split text sections forced to 64-byte alignment (6edd573218), and 098504c73f (1.16) conservative trampolines for inter-dependent packages.

Timeline.

GOEXPERIMENT plumbing and tool configuration

Span: Go 1.16 → Go 1.17 · Status at go1.17: experiments are a struct in internal/goexperiment parsed by objabi/buildcfg → still the design at go1.17

Design docs & discussion

Design. Until 1.16 experiments (fieldtrack, framepointer, staticlockranking…) were scattered GOEXPERIMENT string lookups in cmd/internal/objabi and the runtime. The 1.17 work centralises them: parsing in one place (06ca809410), build-time default plus runtime override handled in cmd/go (7fc638d6f1; bootstrap built without any GOEXPERIMENT, 6461d74bf2), boolean flags (af4388aee1), consolidation into internal/goexperiment.Flags (6304b401e4), representation as a diff from the baseline (a8e55538af, cf2396c70e), GOEXPERIMENT=none meaning no flags (756e2b1529), objabi.Experiment holding all flags (0c93b16d01), simpler runtime side (095ba22597, aeaa4519b5). 2b8b06ebbf (1.16) adds the regabi experiment as the toggle for the register ABI, and c847932804 (1.17) uses regabidefer to replace reflectcall of defers with direct calls.

Timeline.

Build identity, buildinfo and reproducibility

Span: Go 1.10 → Go 1.17 · Status at go1.17: still the design → continues as Reproducible build IDs, buildinfo and symbol tables (1.18+)

Earlier history: Build IDs, ABI hashes, flags and determinism (Go 1.0 → Go 1.9)

Design. The go command computes a content-based build ID (7dea509703, 1.10) that includes compiler and linker flags; the linker records it in an ELF note / Mach-O UUID or text-segment magic. d6b06f0254 (1.13) adds a writable-data symbol starting with a magic number and pointers to runtime.buildVersion and runtime.modinfo, so go version <binary> can find them without disassembly (runtime.buildVersion/modinfo always marked reachable by 3a34395363, 1.16). Paths: -trimpath accepts a semicolon-separated list with =>replacement (4891a3b66c), the compilation directory is not stored in DWARF (2f170520aa, 4a3d408d18), STABs stripped on darwin for reproducibility (e8f918535e), e4665da9bc reproducible PIE toolchain builds.

Timeline.

Linker restructuring (1.10 code health and error reporting)

Span: Go 1.10 → Go 1.12 · Status at go1.17: done; later passes build on these package splits

Design. Go 1.10 turned a C-translated linker with many package-level variables into a library-shaped one: ld.Thearch, SysArch, Headtype, Iself, FlagLinkshared, coutbuf and build/link-mode globals moved onto *Link (5d95de2072, 2e8545531e, 6738c494ad, d05f82a11a, 38cfeb9cb5, 5fe9bbcf63; da02dcda02 unexports Thearch in 1.11), the object reader got its own package without ctxt (c80338accb, 9f9bb97420), and symbol types moved to cmd/link/internal/sym.

Timeline.

Changes in this slice that are not part of a thread are listed on the release pages: 1.10 · 1.11 · 1.12 · 1.13 · 1.14 · 1.15 · 1.16 · 1.17

Linker, object files and debug info (Go 1.18–1.28)

Reader: Claude Sonnet. Cites 529 of 693 commits; 49 small platform and test fixes are uncited.

Relocation-free pclntab and funcdata (offsets instead of relocations)

Span: Go 1.18 → Go 1.26 · Status at HEAD: done; pclntab has no relocations, lives read-only in .gopclntab together with funcdata and findfunctab; moduledata in its own section

Earlier history: Early loaders 5l/6l/8l and the shared src/cmd/ld (2008–2011) (Go 1.0 → Go 1.0) · Go symbol table, pc/line table and function metadata (gosymtab, pclntab, findfunc) (Go 1.0 → Go 1.5) · liblink, the object file format and the move into the compilers (Go 1.3 → Go 1.9) · New object file format and the loader (dev.link) (Go 1.14 → Go 1.16) · Wavefront conversion of linker passes to the loader; sym.Symbol removal (Go 1.15 → Go 1.16) · pclntab, funcdata and pc tables (Go 1.12 → Go 1.17)

Design docs & discussion

Design. The pclntab (cmd/link/internal/ld/pcln.go) is the linker-generated function table that the runtime and debug/gosym read. Historically every function entry, funcdata pointer and stack-object pointer was a relocation, which cost linker time and, for externally linked or PIE binaries, dynamic relocations (and forced the table into relro). The thread converted each such pointer into a 32-bit offset from a well-known base symbol (runtime.text, go.func.*, go.funcrel.*) so the data becomes position independent and relocation-free.

On the object side the same logic removed per-function side blocks: PCDATA moved from a separate object-file block to ordinary aux symbols (SymPcdata-style aux in cmd/internal/goobj), funcdata offsets were dropped from the object file and replaced by aux symbol references, and symbol alignment moved into the object writer so the linker no longer computes it. The result is a smaller goobj format and a linker that only emits plain bytes for these tables.

By Go 1.26 the process was completed for the header and the surrounding runtime tables: the only remaining relocation (textStart in pcHeader) was dropped, the pclntab magic constants moved to internal/abi (shared by linker, runtime and debug/gosym), and funcdata symbols, runtime.findfunctab and the pclntab all share one .gopclntab section.

Timeline.

Type, itab and typelink layout (.go.type section, relro rework)

Span: Go 1.18 → Go 1.28 · Status at HEAD: type descriptors, itabs and GC masks have dedicated linker layout; typelinks and itablinks lists removed; alg/noalg descriptors deduplicated in 1.28 cycle

Earlier history: Runtime type data, GC metadata and sections laid out by the linker (Go 1.0 → Go 1.9) · New object file format and the loader (dev.link) (Go 1.14 → Go 1.16) · Symbol ABIs: stable ABI0 vs ABIInternal in the linker and object files (Go 1.12 → Go 1.17)

Design docs & discussion

Design. The linker previously kept a family of per-kind relro symbol kinds (STYPERELRO, SITABLINK, typelink section, etc.) and shifted symbol kinds around to make relro data writable until relocation. The 1.27 cycle (issue #76038/#6853) rewrote this. Possible relro data now lives in its own sections that become relro when appropriate: type descriptors and their data go into a new .go.type section (runtime.etypes is now the end of that section instead of end of rodata), function descriptors into .go.func, ordinary relro rodata stays in .data.rel.ro. The typerel:* and go:funcdescrel symbols are dropped.

With types contiguous, the runtime can walk them: typelinks was removed in favour of marking the start and end of typelinked descriptors, and the runtime steps through them lazily (cmd/go shrank 15K). Itabs were treated the same (itablinks removed, itabs live in .go.type, and their size is recorded because the zero-sized runtime.etypes can float on AIX external linking). The type-section length is stored rather than the end address, saving a relocation and fixing Darwin dynamic linking. GC masks are grouped under runtime.gcmask.* in noptrbss and removed from the symbol table; earlier (1.24) data/BSS pointer masks were already built from types instead of GC programs.

Timeline.

//go:linkname restrictions and the blocklist

Span: Go 1.23 → Go 1.26 · Status at HEAD: -checklinkname on by default; push-only linknames required for std symbols; blocklist extended each release

Design docs & discussion

Design. Linkname was an unintended hole in API integrity. Go 1.23 added an attribute set by the compiler on linknamed symbols declared in the current package (so inlining or instantiation from another package is not conflated) and the linker checks it against rules. “Push” linknames (definition side opts in) are always allowed; “pull-only” references to standard-library symbols are rejected unless the definition carries a linkname (possibly to itself). References to external non-Go (C) symbols stay allowed; assembly references are treated like linknames. First a hard-coded blocklist of symbols newly added in 1.23 was introduced, then the pull-only rule gated by -checklinkname, then the flag was defaulted to on. To avoid breaking widely used libraries, push linknames were added to std for legacy symbols found in a large code corpus, in badlinkname.go files.

Timeline.

DWARF 5 migration

Span: Go 1.25 → Go 1.28 · Status at HEAD: enabled by default since the 1.25 cycle (ca19f987ca, 2024-12-19; experiment dwarf5 added bdb4676043); internal/buildcfg/exp.go keeps it off on macOS (dwarf5Supported)

Design docs & discussion

Design. Issue #26379. The work moves compiler (cmd/internal/dwarf, ssa/debug.go) and linker (ld/dwarf.go) to DWARF 5 in stages, all gated by the dwarf5 GOEXPERIMENT. Line tables first: DWARF 5 drops the implicit zeroth directory/file entries so files and dirs become 0-based and the prologue changes 767e7680ec. Then .debug_info goes to version 5 and subprogram high/low PC use DW_FORM_addrx: the compiler emits new R_DWTXTADDR_* relocations (sized to the largest function index in the package) targeting the function, and the linker, in the parallel DWARF phase, assigns a slot in .debug_addr (new SDWARFADDR symbol kind) and later sets each CU’s DW_AT_addr_base. Range lists move to .debug_rnglists, location lists to .debug_loclists, both more compact and able to reference .debug_addr. Lexical scope hi/lo cannot use addrx+constant, so scopes and inlined subroutines always use DW_AT_ranges.

Timeline.

DWARF quality for debuggers (types, variables, trampolines)

Span: Go 1.18 → Go 1.28 · Status at HEAD: ongoing, driven by Delve/gdb/lldb needs

Earlier history: DWARF generation in the linker (Go 1.0 → Go 1.9) · DWARF generation in the linker (Go 1.10 → Go 1.17)

Design. Compiler dwarfgen and linker ld/dwarf.go generate type DIEs from the runtime types (DW_AT_go_kind, DW_AT_go_runtime_type), variable DIEs with location lists, inline trees and scopes. Changes here are mostly additive attributes that debuggers consume.

Timeline.

Darwin split DWARF (dSYM)

Span: Go 1.28 → Go 1.28 · Status at HEAD: default on Darwin

Design docs & discussion

Design. With -splitdwarf the linker writes DWARF into <out>.dSYM/Contents/Resources/DWARF instead of combining it into the executable (external mode used to run dsymutil then merge). Because LLDB loads the symbol table from the DWARF file and, for MH_DSYM, relies solely on .debug_aranges, the linker also emits a symbol table and brings .debug_aranges back. LLDB cannot read compressed DWARF so -compressdwarf=0 is needed.

Timeline.

Nosplit stack checking and stack constants

Span: Go 1.19 → Go 1.21 · Status at HEAD: done

Earlier history: Stack splitting and the linker’s nosplit overflow check (Go 1.0 → Go 1.5)

Design. The linker’s nosplit check was a top-down walk from each nosplit function, revisiting functions at different depths and reporting only one chain. The rewrite is bottom-up: it computes maximum stack height for each function’s call tree, visiting each function once. Errors trigger a slower top-down re-analysis that prints all over-limit paths as a tree.

Timeline.

Dead code elimination and reflection-aware pruning

Span: Go 1.18 → Go 1.23 · Status at HEAD: done

Earlier history: Dead code and data elimination (Go 1.0 → Go 1.8) · Dead-code elimination and method pruning (Go 1.10 → Go 1.17)

Design. The deadcode pass keeps method sets alive only for types converted to interfaces whose called method names appear. Two hazards were handled: generic interface calls and reflection by name. For generics, a new relocation (R_USEGENERICIFACEMETHOD, later R_USENAMEDMETHOD b4c889b561) records that a method was called on a generic interface so implementations are kept. The compiler learned to flag Type.Method/Value.Method users as ReflectMethod, letting the linker drop its special cases for Method() and MethodByName(). Large unreferenced map variables are removed by the compiler making their init functions weak, with a linker pass after deadcode that redirects weak R_CALL relocations to unreachable map.init.* helpers at a no-op (mapinitnoop).

Timeline.

Linker performance and memory

Span: Go 1.18 → Go 1.28 · Status at HEAD: incremental

Paths not taken: PGO beyond inlining: block layout, edge profiles, function ordering (dormant)

Earlier history: From C to Go: cmd/link (Go 1.3 → Go 1.8) · Linker speed and memory (Go 1.7 focus) (Go 1.7 → Go 1.9) · Linker performance and memory engineering (Go 1.10 → Go 1.17) · DWARF compression and Mach-O dsymutil combining (Go 1.11 → Go 1.16)

Design. The loader (cmd/link/internal/loader) indexes symbols across mmap’d object files. Optimizations replace maps with slices/bitmaps and narrow types.

Timeline.

Reproducible build IDs, buildinfo and symbol tables

Span: Go 1.18 → Go 1.24 · Status at HEAD: GNU build ID and LC_UUID generated by default

Earlier history: Build IDs, ABI hashes, flags and determinism (Go 1.0 → Go 1.9) · Build identity, buildinfo and reproducibility (Go 1.10 → Go 1.17)

Design docs & discussion

Design. The linker embeds a Go build ID; -B gobuildid derives a GNU build ID from it deterministically, and since 1.24 both ELF and Mach-O get IDs by default and -B applies to external linking.

Timeline.

FIPS 140 support in the linker

Span: Go 1.24 → Go 1.24 · Status at HEAD: done

Design docs & discussion

Design. For init-time self-verification, FIPS code and data get distinguished symbol kinds (cmd/internal/obj/fips.go), the linker places them in contiguous regions (ld/fips.go), records the section bounds and expected checksum for the runtime.

Timeline.

ELF/relro/hardening and external-link policy

Span: Go 1.18 → Go 1.28

Earlier history: External (host) linking and cgo (Go 1.1 → Go 1.9) · Build modes: -shared, c-archive, c-shared, shared, PIE, plugin (Go 1.1 → Go 1.9) · Build modes: PIE, plugin, c-shared, c-archive, shared libraries (Go 1.10 → Go 1.17) · Host object loading and external linking (Go 1.10 → Go 1.17)

Design. Policy for when to link externally and how ELF is laid out.

Timeline.

Per-platform object formats and internal linking (Mach-O, PE, XCOFF, wasm, ppc64, riscv64, loong64)

Span: Go 1.18 → Go 1.28 · Status at HEAD: active

Earlier history: Early loaders 5l/6l/8l and the shared src/cmd/ld (2008–2011) (Go 1.0 → Go 1.0) · Container writers: Windows PE, Mach-O and Plan 9 (Go 1.0 → Go 1.9) · Architecture and OS ports in the linker (arm, power64, arm64, mips, s390x, nacl, BSDs, Solaris) and trampolines (Go 1.0 → Go 1.9) · Mach-O and Darwin support (Go 1.10 → Go 1.17) · PE / Windows (Go 1.10 → Go 1.17) · AIX / XCOFF (Go 1.12 → Go 1.14) · WebAssembly (Go 1.11 → Go 1.14) · Architecture ports and trampoline insertion (Go 1.10 → Go 1.17)

Design. Each platform needs a host-object loader (loadelf, loadmacho, loadpe, loadxcoff), a relocation handler in cmd/link/internal/<arch>, trampolines and format writers (ld/elf.go, macho.go, pe.go, xcoff.go, wasm/asm.go).

Timeline.

Changes in this slice that are not part of a thread are listed on the release pages: 1.18 · 1.19 · 1.20 · 1.21 · 1.22 · 1.23 · 1.24 · 1.25 · 1.26 · 1.27 · 1.28