qld 0.1.1

A fast, parallel linker compatible with GNU ld, gold, lld and mold
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
# Roadmap

Each milestone has a scope and **exit criteria**. A milestone is complete only
when all of its exit criteria pass in CI. The milestones are ordered so every
one produces a linker that is useful for something real. Later formats reuse
the infrastructure built for ELF.

Performance is a design constraint from M0 onwards, not a later phase. M5 is
about measuring and tuning, not about adding parallelism after the fact.

Legend: `[ ]` not started · `[~]` in progress · `[x]` done

---

## M0: Foundations

Set up the project skeleton and the infrastructure every later milestone needs.

- [x] Crate skeleton: one crate, module per pipeline stage (see
      [architecture.md]docs/architecture.md#module-layout), `rust-version = "1.89"`,
      edition 2024, shared types (IDs, errors, diagnostics, target) in place
- [x] Work split into parallel workstreams ([workstreams.md]docs/workstreams.md)
- [x] CI: build and test on stable and 1.89, `clippy -D warnings`, `rustfmt --check`,
      `cargo doc`, `cargo deny` (licenses/advisories), Linux/macOS/Windows hosts
- [x] Diagnostics framework: error/warning/note with input location, in
      lld's shape, held back and sorted by input position so a parallel link
      reads the same every time; `--error-limit`, `--color-diagnostics`,
      `--fatal-warnings` and `-w` behave as in lld
      `qld: error: ...` rendering, and structured form for library users
- [x] GNU flavor argument parser
  - [x] Complete option table covering every GNU ld 2.4x, gold, lld and mold
        option, each marked *implemented*, *accepted-ignored* or *unsupported*
  - [x] Single- and double-dash long options, joined/separate values, `-z` keywords
  - [x] Positional state: `--whole-archive`, `--as-needed`, `-Bstatic/-Bdynamic`,
        `--push-state/--pop-state`, `--start-group/--end-group`
  - [x] Response files (`@file`), `--sysroot` and `=`-prefixed paths
  - [x] `-v`/`--version` output that autoconf/libtool recognize as GNU-compatible
- [x] Flavor selection from `argv[0]` (`ld`, `ld.qld`, `ld64`, …) and `-flavor`
      (the ld64 parser itself is M8)
- [x] Concurrent symbol table with deterministic interning, and the
      order-independent archive extraction driver
- [x] Format-neutral GC, ICF and merge-section engines
- [x] Output writer: mapped output, parallel chunks, build-id
- [x] Input file loading: mmap, format detection by magic, `ar` archives
      (GNU, BSD, COFF and thin variants, symbol index), `-l` library search
- [x] Test harness: fixture compilation with the system gcc/clang,
      run-the-output tests, differential comparison against GNU ld
      ([testing.md]docs/testing.md)
- [x] Randomized corruption tests for every parser (`tests/corrupt.rs`:
      seeded mutation of every committed fixture, fed through the readers and
      the link drivers, with a time and memory watchdog). `cargo fuzz` itself
      stays deferred: it needs a second crate, and qld is one crate

**Exit criteria:** `qld --help` and `qld --version` work. All options on the
test corpus of real-world command lines (captured from gcc, clang and rustc
driver invocations) parse into the expected `LinkOptions`, and those options
serialize back. Archive symbol indexes are read correctly for GNU, BSD and thin
archives.

---

## M1: Static ELF on x86-64

The first end-to-end link: fully static x86-64 Linux executables.

- [x] ELF64 little-endian relocatable object parsing (zero-copy)
- [x] Symbol resolution: strong/weak/common/undefined; archive member extraction
      that does not depend on input order; COMDAT group deduplication
- [x] Section model: output section assignment from default rules, `SHF_MERGE`
      string/constant merging, `.init_array`/`.fini_array`/`.ctors` ordering
      (`SORT_BY_INIT_PRIORITY`)
- [x] `--gc-sections` with the usual roots (entry, `-u`, `KEEP`, init/fini arrays,
      `__start_`/`__stop_` references, `SHF_GNU_RETAIN`)
- [x] `.eh_frame` parsing, deduplicating CIEs, dropping FDEs of GC'ed sections,
      `.eh_frame_hdr` generation
- [x] x86-64 relocations for static links; GOT for `GOTPCREL`; `GOTPCRELX` relaxation
- [x] TLS: `PT_TLS`, local-exec, and IE/GD/LD/TLSDESC → LE relaxation
- [x] IFUNC in static binaries (`R_X86_64_IRELATIVE`, `__rela_iplt_start/end`)
- [x] Program headers: `PT_LOAD` (with `-z separate-code` default layout),
      `PT_TLS`, `PT_GNU_STACK`, `PT_GNU_PROPERTY`/`.note.gnu.property` (CET/IBT)
- [x] Linker-defined symbols (`_end`, `_etext`, `__bss_start`, `__ehdr_start`, …)
- [x] `--build-id` (parallel tree hash), `-s`/`-S`, `--strip-debug`
- [x] `--icf`, `-Map`, `--why-live`, `--print-gc-sections`
- [x] Compressed debug sections in inputs (`-gz`)
- [x] Musl C programs verified (static, static-PIE, PIE and dynamic, compared with GNU ld)
- [x] Parallel output writer: mapped output, disjoint slices per chunk,
      replacing an existing output atomically
- [x] Undefined-symbol diagnostics with `referenced by file.o:(.text+0x12)`

**Exit criteria:** Static C and C++ programs against glibc and musl link and run,
including exceptions, threads and TLS. The fixture suite passes. `readelf -a`
shows the output is well-formed. qld links a static build of itself and
the result passes the test suite.

---

## M2: Dynamic ELF on x86-64

Everything a typical Linux distribution build needs.

- [x] Shared object inputs: `.dynsym`, `DT_NEEDED`/`DT_SONAME`, symbol versions
      (`.gnu.version`, `.gnu.version_d`, `.gnu.version_r`)
- [x] Linker scripts as inputs (`libc.so` style `GROUP`/`AS_NEEDED`/`INPUT`)
- [x] Output kinds: PIE (`-pie`), non-PIE dynamic, shared objects (`-shared`)
- [x] PLT/GOT, lazy and `-z now` binding, `.plt.got`, IBT-enabled PLT
- [x] Copy relocations and canonical PLT entries for non-PIC executables
- [x] `.dynamic`, `.dynsym`, `.dynstr`, `.hash`/`.gnu.hash` (`--hash-style`)
- [x] `-z relro`, `-z now`, `PT_GNU_RELRO`, `PT_INTERP`, `--dynamic-linker`
- [x] `DT_RELR` (`-z pack-relative-relocs`), sorted `.rela.dyn`, `-z combreloc`
- [x] TLS in shared objects: GD/LD/IE with the dynamic TLS relocations
- [x] Symbol visibility, `--export-dynamic`, `--dynamic-list`, `--exclude-libs`,
      version scripts (`--version-script`), `-Bsymbolic`, `--no-undefined`,
      `--allow-shlib-undefined`
- [x] `--as-needed`, `-rpath`, `-rpath-link`, `--enable-new-dtags`, `$ORIGIN`
- [x] `--wrap`, `--defsym`, `--trace-symbol`, `-Map`, `--cref`,
      `--print-gc-sections`, `--why-live`
- [x] Relocatable output (`-r`) and `--emit-relocs` (`-r` limits: no non-COMDAT section groups, no `SHT_REL` inputs, no `--compress-debug-sections`)
- [x] Library symbol matching: suggest the missing `-l` flag, version mismatch
      hints, "did you mean" for near-miss names
      ([optimizations.md]docs/optimizations.md#intelligent-library-symbol-matching)

**Status: exit criteria met** (W16). coreutils 9.11, curl 8.22, OpenSSL 3.6,
Python 3.14 (shared and static), LLVM/clang/lld 23.1 (static and
`BUILD_SHARED_LIBS`), the rustc 1.98 stage-1 bootstrap with `tests/ui`, zlib,
and musl 1.2.5 shared-object tests all pass with qld as the linker; every
remaining test failure reproduces with GNU ld on the same machine. Build
scripts and notes are in `tests/projects/`.

**Exit criteria:** Used as the system linker (`-fuse-ld=qld`), qld builds and
passes the test suites of coreutils, curl, openssl, zlib, Python, Rust
(`rustc` bootstrap), and LLVM/clang. The shared objects it produces load in
glibc and musl `ld.so`. Differential tests against GNU ld show the same
dynamic symbol tables and `DT_*` entries, apart from documented differences.

---

## M3: Linker scripts, raw binary and embedded targets

- [x] Full GNU linker script language: `SECTIONS`, `MEMORY`, `PHDRS`, `ENTRY`,
      `PROVIDE`/`PROVIDE_HIDDEN`, `ASSERT`, `INCLUDE`, `INSERT AFTER/BEFORE`,
      `OVERWRITE_SECTIONS`, `/DISCARD/`, `KEEP`, `SORT_*`, `EXCLUDE_FILE`,
      `AT`/`AT>` load addresses, `>region`, `FILL`, `BYTE`/`LONG`/`QUAD`,
      location counter arithmetic and all built-in functions
- [x] `-T`, `--default-script`/`-dT` (`--verbose` does not dump the effective
      default script yet)
- [x] `-Ttext`/`-Tdata`/`-Tbss`/`--section-start`, `--image-base`
- [x] Raw binary output: `--oformat binary`, `OUTPUT_FORMAT(binary)`, gap fill
- [x] Binary input: `-b binary` / `--format=binary` with
      `_binary_<name>_start/_end/_size` symbols
- [x] Intel HEX and S-record output (`--oformat ihex`/`srec`)
- [x] `--no-relax`, `--nmagic`/`--omagic`, `-N`/`-n`
- [x] `-r` together with `-T`, and `--defsym` with full linker-script expressions

**Status: exit criteria met** (W19, verified by the integrator with
`tests/projects/kernel.sh` and `baremetal.sh`).

- **Kernel:** linux 7.2.5 `defconfig` builds with qld doing all six x86-64
  links (`vmlinux` through the kernel's own `SECTIONS` script with
  `--orphan-handling=error` and `--emit-relocs`, the vdso, and the compressed
  image); 43 allocated sections and all 238,897 symbol addresses match GNU
  ld's, and the bzImage boots in QEMU. 32-bit links (`-m elf_i386`) and `-r`
  still go to GNU ld: ELF32 is M4, and `-r` output of `vmlinux.o` makes
  objtool fail.
- **Bare metal:** a flash image (`MEMORY`, `AT>`, `KEEP`, `SORT`, fill, data
  commands, `NOLOAD`, `LOADADDR`, `ASSERT`, `/DISCARD/`) and a `PHDRS`
  executable match GNU ld's sections, segments and every symbol address, and
  the `binary`, `ihex` and `srec` outputs are byte-identical.

**Exit criteria:** The Linux kernel (x86-64, `vmlinux` and bzImage) builds and
boots in QEMU. A bare-metal x86-64 image built from a linker script matches
the section and symbol addresses GNU ld produces, and gives the same flat
binary.

---

## M4: More ELF architectures

In priority order. Each one needs its relocations, thunks and relaxations,
and TLS models.

- [x] **AArch64**: the static and dynamic relocation set, range extension
      thunks, PLT/GOT, all four TLS models with TLSDESC and the TLS
      relaxations, ADRP+LDR→ADRP+ADD and ADRP+ADD→NOP+ADR relaxations, BTI
      properties, `-z force-bti`, `-z pac-plt`, the Cortex-A53 843419 and
      835769 workarounds, `-r`, and script layout. TLSDESC is bound eagerly,
      as in lld: glibc and musl need no lazy TLSDESC PLT. Outstanding: erratum
      fixes under linker-script layout (the kernel uses them), a patch pool
      per 128 MiB of code
- [x] **RISC-V 64**: the full relocation set, PLT/GOT, TLS GD/IE/LE and
      TLSDESC with relaxation, linker relaxation with section shrinking (an
      architecture-neutral fixpoint in `src/elf/arch/shrink.rs`), `--relax-gp`,
      `__global_pointer$`, `.riscv.attributes` merging, `-r`, `--emit-relocs`;
      compared with lld symbolically, fixtures run under qemu in CI.
- [x] **RISC-V 32** (`elf32lriscv`, ILP32/ILP32F/ILP32D) on the same backend,
      with the word size as a parameter: 4-byte GOT and TLS entries, the `lw`
      PLT, `c.jal` relaxation, RV32 attribute merging; compared with lld and
      run under qemu-riscv32. Open for RISC-V: `DT_RISCV_VARIANT_CC`, and
      `PT_RISCV_ATTRIBUTES` under linker scripts
- [x] **i386**: GOT-relative relocations with GNU ld's GOT32X relaxations, `-z ibtplt`, the GNU TLS models and TLS descriptors with their relaxations, PLT and IFUNC; compared with GNU ld 2.46 and run natively
- [x] **ARM (32-bit)** (`armelf_linux_eabi`, ARMv7-A hard-float): the REL
      relocation set, ARM/Thumb interworking with `bl`↔`blx` rewriting,
      range-extension and interworking thunks, `.ARM.exidx` merging and
      `EXIDX_CANTUNWIND` synthesis with `PT_ARM_EXIDX`, build-attribute and
      float-ABI merging, PLT/GOT, all TLS models, IFUNC, `R_ARM_V4BX`;
      mapping symbols for the PLT and thunks, thunk pools spread through
      large sections, group relocations and the 12-bit GOT/TLS forms;
      compared with lld symbolically and with GNU ld where lld cannot link
      the form, fixtures run under qemu. Outstanding:
      BE8 (needs big-endian ELF32), GNU TLS descriptors, group relocations past
      G0
- [x] **x32** (`elf32_x86_64`): the x86-64 relocations and PLT in ELF32 with
      8-byte GOT entries, x32's TLS forms and `GOTPCRELX` relaxations, IFUNCs,
      copy relocations, IBT, `--emit-relocs`, `-z pack-relative-relocs`;
      compared with GNU ld 2.46 instruction for instruction, and with lld
      where lld can link x32
- [~] **PowerPC64 LE** (ELFv2): relocations including the Power10 prefixed
      forms, TOC and `.toc` relaxation, local entry points, `.plt.sec` call
      stubs that save r2, thunks (including TOC-saving and PC-relative
      ones), all TLS models with relaxations, `.glink`/PLT, IFUNCs, `-r`;
      compared with lld by meaning, fixtures run under qemu in CI.
      `.toc` in `.got` as GNU ld places it, `R_PPC64_REL16DX_HA`.
      Outstanding: `_savegpr*`/`_restgpr*`, inline PLT sequences
      (`-fno-plt`/`-mlongcall`), `R_PPC64_TOC`, `ADDR64_LOCAL`,
      `GOT_DTPREL*`, multi-TOC, `DT_PPC64_OPT`, thunks under linker
      scripts
- [~] **PowerPC64 BE** (ELFv1, `elf64ppc`): the relocation set with
      big-endian instruction fields, `.opd` function descriptors, `.TOC.`,
      TLS with its relaxations, IFUNC stubs, C++ exceptions, `-r`; static
      output is compared with GNU ld 2.42 and run under qemu. Outstanding:
      dynamic output (ELFv1's `.plt` of descriptors, refused with a clear
      error), `.opd` splitting for `--gc-sections`, taking an IFUNC's
      address, `.gnu.attributes` merging
- [x] **LoongArch64**: the relocation set (including the extreme code model
      and ADD/SUB/ULEB128), PLT/GOT, all TLS models with TLSDESC and IE/TLSDESC
      relaxation, size-preserving relaxation, `-r`; compared with lld by
      meaning. Outstanding: shrinking relaxation (deleting `nop`s,
      `R_LARCH_ALIGN`), B26 thunks, ALIGN synthesis in `-r`; the fixtures run
      under qemu in CI
- [x] **s390x** (big-endian): the relocation set including the
      halfword-counted PC-relative forms, `GOTENT`/`GOTPCDBL`, the 32-byte PLT
      with lazy binding, IFUNC, all TLS models through `__tls_get_offset` with
      the `GDCALL`/`LDCALL` markers and their relaxations, `lgrl`→`larl`,
      eight-byte `.hash`, `--s390-pgste`; compared with GNU ld 2.42 function
      by function and run under qemu. Outstanding: `-r`, `--gdb-index` and
      `R_390_PLTOFF*`
- [~] **m68k** (`EM_68K`, ELF32 big-endian): the `R_68K_*` relocation set,
      static executables and `-r`; the section→hunk mapping and range
      thunks are open, and dynamic output, TLS beyond local-exec and IFUNC
      are refused with a clear error
- [x] Big-endian ELF and ELF32 handled through the same generic code
      (monomorphized, no run-time endianness checks on hot paths): the
      pipeline is generic over `ElfFormat` and chosen once in `elf::link`.
      ELF64/ELF32 little-endian and ELF64 big-endian are instantiated, the
      all four class and byte-order combinations are instantiated. ELF64 BE
      is exercised end to end by s390x and PowerPC64 BE; `Elf32Be` is
      instantiated and decodes input, but no architecture selects it for
      output yet (32-bit Arm BE8 is next)

**Status:** AArch64 is the second architecture. Validation without an arm64
machine: every fixture links with qld and with `aarch64-unknown-linux-gnu-ld`,
and the relocated code is compared symbol by symbol from `objdump -d` (all
symbols identical except padding splits in the static glibc links). The
`arm64-linux` CI job runs the fixtures natively on an `ubuntu-24.04-arm`
runner.

**Exit criteria:** For each architecture, the fixture suite passes under
`qemu-user`, and a Debian or Alpine userland package set builds with qld as
its linker.

---

### AmigaOS Hunk output

- [x] `--oformat amigahunk` (and `BinaryFormat::Hunk`): `HUNK_HEADER`,
      `HUNK_CODE`/`DATA`/`BSS`, `HUNK_RELOC32`, `HUNK_SYMBOL`, `HUNK_END`,
      rendered from the finished image as `--oformat binary` is. The load
      file is byte-identical to vlink's on the whole reference corpus.
- [ ] Hunk object output (`HUNK_UNIT`/`HUNK_EXT`), overlays, Hunk input,
      chip/fast memory attributes, `HUNK_RELOC32SHORT` selection

---

## M5: Performance and optimization

- [~] Benchmark suite and dashboard: clang, chromium, rustc, the Linux kernel,
      and a large debug-info-heavy Rust binary. Compared against GNU ld, gold,
      lld, mold and wild on wall time, CPU time, peak RSS and output size.
      Capture/replay suite and results in
      [tests/projects/bench.md]tests/projects/bench.md (clang, clang with
      debug info, `libclang-cpp.so`, `vmlinux`, a Rust debug binary). Missing:
      chromium, gold, `librustc_driver`, per-commit publishing.
- [~] Profiling passes on every pipeline stage (done, 16 speedups merged).
      Thread scaling from 1 to 64+ cores: qld stops scaling at 12–16 threads.
- [x] Identical code folding: `--icf=safe` (using `.llvm_addrsig`) and `--icf=all`
- [x] String tail merging (`-O2`)
- [x] Section ordering: `--symbol-ordering-file`, `--call-graph-profile-sort` (hfsort/C3 and cdsort), `--call-graph-ordering-file`, `--print-symbol-order`
- [x] Compressed debug sections: read and write zlib and zstd
      (`--compress-debug-sections`), in-crate codecs, parallel
- [x] `--gdb-index` (version 8, DIE scan when pubnames are absent) and `--debug-names` generation
- [x] Unlinking a large old output file in the background
- [x] Optional separate-debug output (`--separate-debug-file`) with
      `.gnu_debuglink`, following mold (GNU ld has no such option)

**Exit criteria:** On the benchmark suite, qld's wall-clock time is at or below
mold's and wild's on x86-64, on both 8-core and 64-core machines, with peak
RSS no higher than lld's. Output is identical across 1, 2 and N threads for
every benchmark.

**Status (W42, 2026-09-20, 32-core Threadripper, machine loaded):** met
against mold at default and 64 threads, not at 8 threads or on one thread;
wild still leads on every large link. Overlapping the dynamic, symbol-table
and `.eh_frame` stages and cheaper GOT/PLT planning took clang from 150 to
131 ms and libclang-cpp from 92 to 85 ms at default threads (a quiet-machine
A/B of the same branch: clang 112 → 101 ms), with 1.3% fewer instructions and
identical output at 1, 2, 8 and 64 threads. What is left, in order: kernel
time (0.34–0.40 s of a clang link at 16 threads against 0.10 s at 1, from
page faults and 12k `mprotect` calls — a `mallopt` call in `main.rs` is worth
~20 ms but needs `unsafe` in a frozen file), the write at its backing's floor
(~35 ms), slimmer `ObjectInput`/`InputSection`, and single-threaded
relocation work. Full tables: [tests/projects/bench.md](tests/projects/bench.md).

---

## M6: Link-time optimization

- [x] GNU linker plugin API host (`-plugin`, `-plugin-opt`, `--plugin-opt=`),
      dynamically loading `liblto_plugin.so` (GCC 13–15) and `LLVMgold.so`
      (LLVM 18–22): standalone `plugin::Session` API
- [x] Claim-file handling, symbol resolution reporting (`LDPR_*`), adding
      compiled objects back into the link, archives containing IR members
      (including archives with no symbol index)
- [x] ThinLTO options passthrough: jobs, cache directory and pruning policy,
      `thinlto-index-only` (distributed ThinLTO verified end to end)
- [x] Clean fallback and a clear diagnostic when an IR input has no plugin

**Status:** exit criteria met for zlib, lua, curl (1629/1629), OpenSSL (4555
tests), coreutils and Python (same failures as their GNU ld builds), LLVM +
clang + lld built entirely with ThinLTO (`check-lld` 2063/2063), and a Rust
crate with `-C linker-plugin-lto`. Not tried with LTO: the rustc bootstrap and
musl. Replaying every `gcc -flto` link of those projects and diffing the
plugin's `-fresolution=` files against GNU ld's matched on 2.6M+ resolutions,
apart from the documented archive-order difference.

**Exit criteria:** `gcc -flto` and `clang -flto` / `-flto=thin` builds of the M2
project set link and pass their test suites. The linker plugin is behind the
`plugin` cargo feature, on by default (cargo features are per package, not per
target, so the library carries it too; building with `--no-default-features`
drops the FFI entirely).

---

## M7: PE/COFF (MinGW flavor)

- [x] COFF object and archive parsing, `.drectve` linker directives
- [x] Short import libraries (MSVC/LLVM style) and long import libraries
      (GNU dlltool `.idata$N` objects); linking directly against `.dll` files
- [x] PE32+ (x86-64), PE32 (i386) and ARM64 PE32+ (thunks, packed and unpacked `.pdata`); ARM64EC/ARM64X refused
- [x] EXE and DLL output, `--out-implib`, `.def` files, `--export-all-symbols`,
      export and import tables, base relocations, TLS directory
- [x] x86-64 SEH: `.pdata`/`.xdata` handling (sorted); i386 SafeSEH (`.sxdata` handler table)
- [x] Auto-import and runtime pseudo-relocations (`--enable-auto-import`,
      `__RUNTIME_PSEUDO_RELOC_LIST__`)
- [x] Resources (`.rsrc` from windres objects), subsystem and OS version fields,
      `--dynamicbase`, `--nxcompat`, `--high-entropy-va`, deterministic timestamps
- [x] DWARF in PE for MinGW debugging

**Status:** qld links MinGW x86-64 and i386 executables and DLLs (checked
against GNU ld) and ARM64 images (checked against lld). The `pe-windows` CI
job runs the x86-64 and i386 images on a Windows runner, including SafeSEH
enforcement, and `pe-windows-arm64` runs the ARM64 images on Windows on ARM.
PE links honour the library options for in-memory inputs, in-memory output
and cancellation.

**Exit criteria:** A MinGW-w64 GCC and a clang cross toolchain can use qld to
build and run a C/C++ test suite under Wine and on a Windows CI runner. qld
builds a working `x86_64-pc-windows-gnu` Rust binary, and it runs.

---

## M8: Mach-O (ld64 flavor)

- [x] ld64 argv flavor: `-arch`, `-platform_version`, `-syslibroot`,
      `-framework`, `-dylib`, `-bundle`, `-dead_strip`, `-undefined`, `-exported_symbols_list`
- [x] Mach-O object parsing including `.subsections_via_symbols` atomization (reader)
- [x] `.tbd` text stubs (v1–v5) and dylib inputs; two-level namespace and re-export resolution, including implicit public re-exports
- [x] arm64 and x86_64: stubs, GOT, thunks (arm64), TLV
- [x] `LC_DYLD_CHAINED_FIXUPS` and legacy `LC_DYLD_INFO_ONLY` output
- [x] Compact unwind (`__unwind_info`) synthesis, `__eh_frame`
- [x] Ad-hoc code signature (`LC_CODE_SIGNATURE`), which arm64 macOS requires
- [x] STABS debug map (`N_OSO`) so `dsymutil` can find DWARF in object files
- [x] Objective-C / Swift sections handled correctly: selector stubs, relative method lists (default from macOS 11), category merging (`-objc_category_merging`)
- [x] `-r` (relocatable output; DWARF sections not carried over yet), C string and literal merging, `-init`, `-alias`, `-bundle_loader`, `-flat_namespace`
- [x] LTO through `libLTO` (full and thin, `-object_path_lto`, `-cache_path_lto`)
- [x] arm64e (ptrauth chained fixups `DYLD_CHAINED_PTR_ARM64E`/`_USERLAND24`, `__auth_stubs`, `__auth_got`); checked structurally, since stock macOS does not run third-party arm64e code
- [x] **Universal (fat) binaries**: link each slice in parallel from multiple
      `-arch` values and write the `fat_header`; accept fat objects, archives
      and dylibs as inputs by selecting the matching slice

**Exit criteria:** clang on macOS builds and runs a C, C++ and Objective-C
test suite on arm64 and x86_64 CI runners with `-fuse-ld=qld`. A Rust
`aarch64-apple-darwin` binary runs. A universal binary passes `lipo -info`
and runs natively on both architectures.

**Status:** everything above except arm64e and LTO is implemented.
`tests/macho_link.rs` compares against `ld64.lld` on Linux, and the
`macho-macos` CI job links C, C++ (exceptions, TLV), Objective-C and Rust
(std: threads, unwinding, TLS) programs with Apple clang and rustc against
the real SDK and runs them on arm64 and, under Rosetta, x86_64, including a
universal binary. `-r` output is checked against Apple's `ld -r` there.
The broader `-fuse-ld` suite runs there too: 18 self-checking C, C++ and
Objective-C programs in three variants per arch, and zlib, Lua, SQLite and
{fmt} built with `-fuse-ld=qld` running their own tests, with every image
checked to be linked by qld. Mach-O LTO goes through Xcode's libLTO. Open:
arm64e.

---

## M9: Library API stabilization and 1.0

- [~] Public API review ([library-api.md]docs/library-api.md); semver guarantees.
      Review done ([api-review.md]tests/projects/api-review.md): the 1.0 surface is
      the crate root, `args`, `diag`, `error` and `target`; other modules are
      `#[doc(hidden)]`. W41 settled the blockers: `default()` is `new()`,
      options are enums, one input list for every format, no printing or
      environment reads in the library, and a caller's thread pool is used as
      it is. The smaller items left are listed at the end of
      [api-review.md]tests/projects/api-review.md.
- [~] In-memory inputs and outputs (`InputKind::bytes`, `MemoryFiles`,
      `link_to_memory`), caller-provided thread pool, cancellation
      (`CancelToken`, `Error::Cancelled`): done for ELF, PE and Mach-O
- [~] Complete rustdoc with examples (crate-level and `link` examples, five
      programs in `examples/`); published on crates.io
- [~] Packaging: prebuilt binaries, distribution packages, `ld.qld` and `ld64.qld` symlinks.
      Release workflow (`.github/workflows/release.yml`, tags only) and recipes in
      `packaging/` (Gentoo, Arch, Debian, Homebrew) are in; the first tagged release is pending

**Exit criteria:** M1–M8 exit criteria still pass. The API has had no breaking
changes for one release cycle.

---

## Later / under consideration

These are not scheduled. Each needs a concrete use case before it moves into
a milestone.

- MSVC `link.exe` / `lld-link` flavor with CodeView and PDB output
- WebAssembly (`wasm-ld` flavor)
- Incremental relinking (reusing layout from a previous link)
- Profile-guided function layout from perf/`propeller` data
- Mach-O Objective-C category merging, `-order_file`, chained-fixup-aware ICF

## Non-goals

- Being a runtime dynamic loader (`ld.so`/`dyld`); qld is a build-time linker
- Byte-identical output to GNU ld. qld aims for compatible *behavior*, and every
  intentional difference is documented in [compatibility.md]docs/compatibility.md.
- Legacy formats GNU ld supports through BFD (a.out, XCOFF, ECOFF, SOM, …)