commit e928b11c173e6c3466f548b35f45c5ff4265c6fb
parent a18ab3c7bd9b50e2b745a31773909c118525492f
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 27 May 2026 16:09:53 -0700
link/debug: retain DWARF in linked ELF/Mach-O images
Carry .debug_* sections through the linker into linked ELF images
(static / PIE / DSO) so `cfree addr2line -e <exe>` resolves file:line,
single- and multi-input. Debug sections are file-only (no PT_LOAD): a
new link_layout_debug pass appends one segment-less LinkSection per
.debug_* contribution at a per-name cumulative DWARF-relative base,
copies its bytes into a per-image registry, and maps it into the
InputMap so SK_SECTION symbols and relocations resolve. The ELF emitter
applies their relocs in place (no img_base, no dynamic relocs), excludes
them from the address shift, sorts/merges them after the loaded
segments, and writes them in the trailing non-alloc region. ELF-only;
the JIT path keeps its in-memory debug view, Mach-O/COFF emit untouched.
Two correctness fixes the concatenation exposed (cross-checked vs host
llvm-dwarfdump):
- producer: .debug_aranges debug_info_offset was a hardcoded 0 with no
reloc, so after concatenation every range unit pointed at the first
CU. Emit it as a section-relative R_ABS32 against .debug_info (also
fixes the JIT view's latent multi-input case).
- reader: cfree_dwarf_addr_to_line returned the first CU with any row
<= pc, ignoring end_sequence bounds, so multi-CU images mapped every
address to CU0. Respect sequence coverage, handling both the
degenerate zero-length final row and abutting CUs.
Mach-O DWARF reading + naming:
- obj_macho_debug_sectname() (shared, in obj_secnames.c): map .debug_*
to __debug_*, truncated to Mach-O's 16-byte sectname, which
reproduces Apple's spelling (.debug_str_offsets -> __debug_str_offs).
- macho emit: SEC_DEBUG sections now use it, producing __DWARF,__debug_*
(was debug_*, missing the __ prefix).
- dwarf_open: dw_find_section also matches the Mach-O __DWARF,__debug_*
(and __TEXT,__eh_frame) form, so addr2line reads Mach-O objects
instead of reporting "no debug info available".
Tests: new objdump golden 05-addr2line-linked (multi-input ELF
round-trip + merge + strip) and driver addr2line-macho. No regressions
in test-link (122/0), test-macho (80/0), test-dwarf, test-elf.
Linked Mach-O executables now carry .debug_* in a dedicated __DWARF
segment, so `cfree addr2line` / `cfree dbg` resolve file:line on
cfree-linked Mach-O images — single- and multi-input.
Reuses the format-agnostic file-only LinkSection machinery: ungate
link_layout_debug for the Mach-O lane (still skips JIT/COFF). The
Mach-O emitter gains a 6th segment slot (named constants MSEG_*;
__DWARF sits before __LINKEDIT so the ad-hoc code signature stays last):
- plan_layout skips file-only sections in the __TEXT/__DATA_CONST/
__DATA passes (debug sections have ABS64 low_pc relocs and were
leaking into __DATA_CONST) and places them in __DWARF, named via
the comma-form input name or obj_macho_debug_sectname.
- apply_relocs patches the debug registry buffer in place (no
stub/__got/chained-fixup; SK_SECTION → DWARF base, code sym →
absolute vaddr); shift_sections leaves file-only sections' DWARF
bases untouched since they bind no MSec.
- load-command sizing, segment emit, and ncmds account for __DWARF.
Also widen MSec.{seg,sect}name_buf to 17 so a full 16-char Mach-O
section name (e.g. __debug_line_str) survives C-string use — the old
16-byte buffers dropped the last char, which broke the reader's
section lookup.
Tests: driver addr2line-macho-linked (self-contained `cfree ld` link,
no SDK). No regressions: test-macho 80/0, test-link 122/0, test-dwarf,
test-elf, objdump goldens unchanged.
The Mach-O reader subtracted each defined symbol's section base from
n_value to recover a section-local offset. That's right for relocatable
MH_OBJECT inputs (the linker and obj model treat input symbol values as
section-local, and a .o's sections carry non-zero layout addrs), but
wrong for linked images: nm / objdump -t / size / addr2line then showed
section-relative values instead of real vaddrs, so `nm | addr2line`
didn't work on a Mach-O executable.
Gate the subtraction on filetype: MH_OBJECT stays section-local;
MH_EXECUTE / MH_DYLIB keep the absolute n_value — matching the ELF
reader, whose st_value is already absolute for images. The read_dso
export path already kept n_value, so the readers now agree.
`cfree nm prog` on a linked Mach-O reports absolute addresses and
feeds addr2line directly. Strengthened the addr2line-macho-linked driver
test to resolve file:line through that flow. No regressions: test-macho
80/0 (relocatable .o symbol values unchanged), test-elf, test-dwarf,
objdump goldens.
Diffstat:
19 files changed, 980 insertions(+), 58 deletions(-)
diff --git a/doc/IMAGE_INSPECT.md b/doc/IMAGE_INSPECT.md
@@ -215,8 +215,27 @@ the 64-bit family are skipped leniently.
binds + rebases; dynamic symbols from `LC_SYMTAB`. objdump `-f`/`-p`/`-T`/
`-R` for Mach-O. Committed golden on a cfree-linked aarch64 exec under
`test/objdump/aarch64-darwin/`.
-4. **Inherited tools.** Confirm nm `-D`, size, addr2line, strings behave on
- images; lift the relocatable-only assumptions where they block image input.
+4. **Inherited tools (landed).** nm, size, addr2line, strings open images via
+ `cfree_obj_open` now that it accepts `ET_EXEC`/`ET_DYN`. nm grew `-D`
+ (`.dynsym` via `cfree_obj_dynsymiter_new`); `CfreeObjSecInfo` grew a neutral
+ `addr` (load vaddr, 0 for relocatables, from `sh_addr`) so SysV `size -A`
+ reports an image's real layout. strings is format-agnostic and was already
+ fine. addr2line opens images correctly but reports "no debug info" — the
+ linker drops `.debug_*` (see phase 5). Golden:
+ `test/objdump/aarch64/cases/02-size-sysv-image`.
+5. **Debug-info retention in the linker (landed).** `.debug_*` sections are
+ carried through to linked images as file-only sections with their
+ relocations resolved in place, so `cfree addr2line` / `cfree dbg` resolve
+ file:line on cfree-linked executables, single- and multi-input. ELF
+ (static / PIE / DSO) places them in non-alloc sections; Mach-O in a
+ dedicated `__DWARF` segment (before `__LINKEDIT`, with `__debug_*`
+ section names matching Apple's spelling). See `doc/LINK_DEBUG.md` and the
+ dedicated section below. Goldens:
+ `test/objdump/aarch64/cases/05-addr2line-linked`, driver
+ `addr2line-macho` / `addr2line-macho-linked`. The Mach-O reader reports
+ absolute symbol vaddrs for linked images (MH_EXECUTE/MH_DYLIB) and
+ section-local offsets for relocatable MH_OBJECT inputs — matching the ELF
+ reader — so the `nm | addr2line` flow works the same across formats.
## Test strategy
@@ -253,3 +272,111 @@ dynsyms against what the linker emitted. Cross-check against the host
`LC_DYLD_EXPORTS_TRIE` (modern). Classic `LC_DYLD_INFO` opcode/trie reading
is not supported; reading older dylibs is out of scope. Confirm cfree's own
linker emits chained fixups when phase 3 begins.
+
+## Debug-info retention in the linker (phase 5, landed)
+
+Status: design proposal (2026-05-27). ELF / aarch64 first.
+
+### Goal
+
+Make `addr2line -e <cfree-linked-exe>` resolve `file:line`. Today it opens the
+image fine (phase 4) but finds nothing: the linker drops every `.debug_*`
+section, so the linked image carries no DWARF at all.
+
+### Why it's dropped
+
+`link_section_kept` keeps only allocatable progbits/nobits/array sections;
+`.debug_*` lack `SHF_ALLOC` and are cut at the first line:
+
+ /* src/link/link_layout.c:49 */
+ int link_section_kept(const Section* s) {
+ if (!(s->flags & SF_ALLOC)) return 0; /* .debug_* dies here */
+ if (s->sem == SSEM_PROGBITS || s->sem == SSEM_NOBITS) return 1;
+ if (s->sem == SSEM_INIT_ARRAY || s->sem == SSEM_FINI_ARRAY) return 1;
+ return 0;
+ }
+
+That predicate gates three things: layout (`link_layout.c:196,238`),
+section-header planning (`obj/elf/link.c:854`), and **relocation emission** —
+`link_emit_relocations` skips any reloc whose section isn't kept
+(`link_reloc_layout.c:1221`). So even the relocation records inside `.debug_*`
+are discarded along with the bytes.
+
+### What the debug sections need
+
+The DWARF producer (`src/debug/debug_emit.c`) writes zero placeholders and
+records relocations against them. Two classes, with different resolution:
+
+| Reloc | Sections | Target | Resolves to |
+|----------|-------------------------------------------------------------|------------------|-------------|
+| `R_ABS64`| `.debug_info` low_pc, `.debug_line` set_address, `.debug_aranges`, `.debug_rnglists` | function symbols | final code **vaddr** |
+| `R_ABS32`| `.debug_info`, `.debug_line`, `.debug_str_offsets` | **section symbols** (`.debug_abbrev`/`.debug_str`/…) | **offset within the output debug section** (DWARF sec-relative) |
+
+Self-contained, no relocs: `.debug_abbrev`, `.debug_str`, `.debug_line_str`.
+
+The reader applies no relocations — `dwarf_open.c` reads section bytes verbatim
+and trusts `DW_FORM_addr` to already hold the final address
+(`dwarf_open.c:453`, `dwarf_die.c:35`). So the linker must fully resolve the
+placeholders in place.
+
+### The relocation engine is already reusable
+
+Symbol resolution is generic:
+
+ /* src/link/link_reloc_layout.c:66 */
+ s->vaddr = ls->vaddr + (s->value - ls->obj_offset);
+
+Set a retained debug section's `LinkSection.vaddr` to its **section-relative
+base** (0 for a single input; cumulative across inputs) and the rest falls out:
+a section symbol pointing at it resolves to that base, so `R_ABS32 = base +
+addend` yields the correct DWARF sec-offset, while function symbols already
+resolve to real code vaddrs so `R_ABS64` is correct for free. No new
+relocation math — `link_reloc_apply` handles `R_ABS64`/`R_ABS32` as-is once the
+sections participate.
+
+### What has to be built
+
+The `.symtab`/`.strtab` survival path is **not** the model: those are
+*synthesized* file-only blobs (`obj/elf/link.c:956`), whereas `.debug_*` must be
+*carried and relocated*. The work is a new "retained, file-only input section"
+class:
+
+1. **Retention.** Split `link_section_kept` into alloc-kept vs a new file-only
+ predicate (debug sections, gated by a `--strip-debug` linker flag; default
+ keep when inputs carry debug). Route retained sections into `img->sections`
+ flagged `file_only`, with no segment.
+2. **Layout.** A file-only pass after the segments: concatenate same-named
+ debug sections across inputs, assign each output section a file offset and
+ `sh_addr = 0`, and record each input's contribution offset. Set
+ `LinkSection.vaddr` = contribution base so the resolver above does the right
+ thing. Sits beside the existing `.symtab` file placement
+ (`obj/elf/link.c:1032`).
+3. **Relocation routing.** Widen the gate at `link_reloc_layout.c:1221` to
+ include retained sections, and give `apply_all_relocs`
+ (`obj/elf/link.c:306`) a path that writes into the file-only section's own
+ byte buffer instead of `img->segment_bytes[...]` (keyed on a segment debug
+ sections won't have).
+4. **Output.** Emit `OutShdr` entries for the retained sections (`SHT_PROGBITS`,
+ `sh_addr=0`, no `SHF` bits) and write their relocated bytes at their file
+ offsets, next to `.symtab`/`.strtab`/`.shstrtab`.
+5. **Section symbols.** Verify local `SK_SECTION` symbols for debug sections
+ survive resolution as reloc targets (`link_reloc_layout.c:51-66` handles
+ defined section symbols generically; local-symbol retention needs a check).
+
+### Decisions / scope
+
+- **Single-input static exec first.** The common cfree case: merging is trivial
+ (one contribution at offset 0), collapsing steps 2/5. This is what unblocks
+ `addr2line` on a cfree-linked exe and is directly testable on aa64.
+- **Multi-input merge is the larger half.** Per-input contribution offsets and
+ section-symbol rebasing are the fiddly part; `.debug_line`/`.debug_aranges`
+ are per-CU so concatenation is naturally correct, but offsets must be exact.
+- **`--strip-debug` / `-S`** controls retention; default keeps debug when
+ present, matching the toolchain expectation that `-g` survives linking.
+- **Biggest risk:** silent corruption — a missed reloc class produces wrong
+ line numbers with no error, because the reader trusts the bytes. Mitigate
+ with a round-trip golden: link `-g`, `addr2line` a known function address,
+ assert `file:line`, cross-checked against host `addr2line` on the same bytes.
+
+This is a real linker feature (a debug-retention pass), not a flag flip —
+comparable in size to the image-inspection reader work.
diff --git a/doc/LINK_DEBUG.md b/doc/LINK_DEBUG.md
@@ -0,0 +1,187 @@
+# Debug-info retention in the linker (IMAGE_INSPECT phase 5)
+
+## Context
+
+`addr2line -e <cfree-linked-exe>` opens cfree-linked images fine (phase 4) but
+finds no DWARF: the linker drops every `.debug_*` section because
+`link_section_kept` keeps only `SF_ALLOC` progbits/nobits/array sections
+(`src/link/link_layout.c:49`). That predicate gates three things — layout,
+section-header planning, and relocation emission — so the debug *bytes* and the
+*relocation records* inside them are both discarded.
+
+Goal: carry `.debug_*` through to the linked image and fully resolve their
+relocations in place, so `addr2line` resolves `file:line` on a cfree-linked
+executable. The DWARF reader applies no relocations (`dwarf_open.c:453`,
+`dwarf_die.c:35` read `DW_FORM_addr` verbatim), so the linker must write final
+values into the placeholders.
+
+Scope (confirmed): **multi-input** concatenation supported and tested, across
+**all linked image types** — static ET_EXEC, PIE, and DSO (ET_DYN). The
+mechanism is image-type-agnostic: debug sections are **not loaded** (no
+PT_LOAD), so they carry link-time vaddrs and need **no dynamic relocations** —
+which is exactly what `addr2line -e` reads statically. For PIE/DSO `img_base`
+is 0, so the same `img_base + vaddr` formula yields link-time vaddrs that match
+the symbol table. Default keeps debug when present; existing `-S`/`--strip-debug`
+(already plumbed to `l->strip_debug`, `driver/ld.c:112,839`;
+`src/link/link.c:365`) drops it.
+
+## Design
+
+Debug sections are non-allocatable, so they get **no PT_LOAD segment**. But
+they must participate in symbol resolution and the relocation engine. The
+cleanest fit (and what the doc's "what has to be built" describes) is a new
+**file-only LinkSection** class that lives in `img->sections` (so section
+symbols resolve and the existing reloc engine applies) but carries its own byte
+buffer and `segment_id == LINK_SEG_NONE`.
+
+The relocation math is already reusable: symbol resolution does
+`s->vaddr = ls->vaddr + (s->value - ls->obj_offset)` (`link_reloc_layout.c:66`).
+Set a debug output section's `LinkSection.vaddr` to its **DWARF-section-relative
+base** (cumulative contribution offset, 0 for the first input). Then:
+- a `SK_SECTION` symbol pointing at a debug section resolves to that base, so
+ `R_ABS32 = base + addend` yields the right DWARF sec-offset;
+- function symbols already resolve to real code vaddrs, so `R_ABS64` low_pc /
+ set_address values come out correct for free.
+
+### Multi-input concatenation
+
+Each input's `.debug_X` becomes its own file-only `LinkSection`, laid out at a
+**contiguous, name-grouped** file region with `vaddr` = the running per-name
+cumulative size (0, size0, size0+size1, …). Because contributions of the same
+name are placed adjacently, the existing OutShdr planner
+(`src/obj/elf/link.c:896`, which merges adjacent same-`(segment_id,name)` runs)
+collapses them into **one** output `.debug_X` section spanning the whole run.
+Each input's `SK_SECTION` symbol carries its own per-input base via its own
+LinkSection's `vaddr`, so cross-section `R_ABS32` offsets land in the merged
+section correctly with no per-input bookkeeping beyond the vaddr. This reuses
+the same prefix-snapshot idea already proven in the JIT debug view
+(`src/link/link_jit.c:1487-1707`).
+
+## Changes
+
+### 1. Internal model (`src/link/link_internal.h`, `link.h`)
+- `LinkSection`: add a `u8 file_only` flag (reuse the existing `pad`/`u16 pad`
+ field) marking a non-segment, file-resident section.
+- `LinkImage`: add a small file-only-section registry so apply/emit can find a
+ debug section's byte buffer by id — `LinkSectionId dbg_first_lsid; u32
+ dbg_count; u8** dbg_bytes; u64* dbg_size;` plus a helper
+ `u8* link_fileonly_bytes(LinkImage*, LinkSectionId)` (returns NULL for
+ non-file-only ids). Debug LinkSections are appended as a contiguous id range,
+ so lookup is `dbg_bytes[lsid - dbg_first_lsid]`.
+
+### 2. Retention predicate (`src/link/link_layout.c`)
+- Keep `link_section_kept` (alloc) unchanged.
+- Add `int link_section_kept_fileonly(const Section* s)` → true for
+ `s->kind == SEC_DEBUG` (and not `removed`).
+
+### 3. New layout pass `link_layout_debug` (`src/link/link_layout.c`)
+Run after segment placement, before reloc emission (in the `link_resolve`
+orchestration, `src/link/link_layout.c:1040+`; gate on `!l->strip_debug`):
+1. Walk inputs; for each `SEC_DEBUG` section not discarded, record a
+ contribution `{input, obj_sec_id, name, size, align, bytes}`.
+2. Group contributions by name; assign per-name cumulative `vaddr` (the
+ DWARF-relative base) and a per-output-section running size.
+3. Append one `LinkSection` per contribution to `img->sections`:
+ `file_only=1`, `segment_id=LINK_SEG_NONE`, `vaddr`=per-name cumulative base,
+ `obj_offset=0`, `size`, `align`, `name`, `sem=SSEM_PROGBITS`,
+ input/obj ids set. Record the contiguous id range in the registry.
+4. Allocate a per-output-name byte buffer (`dbg_bytes`) and copy each
+ contribution's bytes at its cumulative offset (concatenation). File offsets
+ are *not* assigned here — that's ELF-specific (step 6).
+5. Populate the InputMap via `map_placed_unit(m, sid, OBJ_ATOM_NONE, lsid)` so
+ `link_input_symbol_section` / `link_input_reloc_section`
+ (`link_internal.h:87`) resolve debug section symbols and relocs to the new
+ LinkSections (today they return `LINK_SEC_NONE`).
+
+Result: the existing symbol-section assignment loop
+(`link_reloc_layout.c:44-54`) stamps `section_id` onto the local `SK_SECTION`
+debug symbols, and the vaddr resolver at `:66` gives them their sec-relative
+base — no new symbol code needed. (Section symbols stay out of `.symtab`: the
+`name==0` skip at `elf/link.c:991` already excludes them; they only need to
+exist as reloc targets.)
+
+### 4. Relocation emission gate (`src/link/link_reloc_layout.c:1221`)
+Widen the gate to also admit file-only debug sections:
+`if (!s || (!link_section_kept(s) && !link_section_kept_fileonly(s))) continue;`
+The existing record-building (`:1244-1262`) then produces `LinkRelocApply`s
+with `link_section_id` = the debug LinkSection and `offset = r->offset` (since
+`obj_offset==0`); `write_vaddr/file_offset` are computed but unused for
+file-only apply (see step 5).
+
+### 5. Relocation apply (`src/obj/elf/link.c:306` `apply_all_relocs`)
+Add a file-only branch keyed on `sec->segment_id == LINK_SEG_NONE`, placed
+**before** the `tgt->imported` / GLOB_DAT dynamic-reloc handling so debug
+sections never get dynamic relocs (they aren't loaded):
+- `P_bytes = link_fileonly_bytes(img, r->link_section_id) + r->offset`
+ (instead of `segment_bytes[...]`).
+- `S`: if `tgt->kind == SK_SECTION` → `S = tgt->vaddr` (DWARF sec-relative
+ base, **no `img_base`**), matching `R_ABS32` cross-section offsets; else
+ `S = img_base + tgt->vaddr` (link-time code vaddr) for `R_ABS64` low_pc /
+ set_address. `img_base` is the fixed base for ET_EXEC and **0** for PIE/DSO,
+ so debug bytes hold link-time vaddrs in every image type — matching the
+ symtab and what `addr2line -e` expects (the loader never relocates these
+ unloaded bytes). Then `link_reloc_apply(c, r->kind, P_bytes, S, addend, 0)`.
+
+This is the only place the function-vs-section reloc-class distinction lives,
+and it mirrors the JIT view's logic (`link_jit.c:1588-1615`).
+
+### 6. ELF output (`src/obj/elf/link.c`)
+- **OutShdr planning (`:854-920`):** debug LinkSections already flow through the
+ `img->sections` walk and merge by name. Add `int is_fileonly` to `OutShdr`
+ (set from `ls->file_only`). Adjust the sort comparator so file-only sections
+ sort **after** all real segments (treat `segment_id==0` as +inf) — keeps
+ debug shdrs at the end, matching `objdump` convention and leaving alloc shdr
+ indices stable.
+- **Trailing file offsets (`:1032-1050`):** after `end_of_segs`, assign debug
+ output sections a `file_offset` (ALIGN_UP) in the trailing non-alloc region,
+ *before* `.symtab`; bump `symtab_off` past them. Stash each merged OutShdr's
+ `file_offset` and propagate to its LinkSections (for the byte-write step).
+- **Shdr emit (`:1325-1372`):** for `is_fileonly` OutShdrs write
+ `sh_type=SHT_PROGBITS`, `sh_addr=0`, `sh_flags=0`, real `sh_offset`/`sh_size`.
+- **Byte write (`:1261-1278`):** before `.symtab`, write each debug output
+ section's buffer at its assigned `file_offset` (pad as needed), iterating the
+ registry in shdr order.
+- Bump the shdr count: debug shdrs are part of `noutshdr`, so `nshdr =
+ 1 + noutshdr + 4` already accounts for them; verify `shndx_*` arithmetic
+ still places build-id/symtab/strtab/shstrtab last.
+
+## Files touched
+- `src/link/link_internal.h`, `src/link/link.h` — `file_only` flag, registry,
+ helper decl.
+- `src/link/link_layout.c` — `link_section_kept_fileonly`, `link_layout_debug`,
+ registry alloc, orchestration call.
+- `src/link/link_reloc_layout.c` — widen reloc gate (`:1221`).
+- `src/obj/elf/link.c` — `OutShdr.is_fileonly`, sort tweak, apply branch,
+ trailing-offset + shdr-emit + byte-write for debug sections.
+
+No change to the DWARF producer/reader; relocation classes (`R_ABS32`/`R_ABS64`
+in `src/obj/reloc_apply.c:31-51`) are applied as-is.
+
+## Verification
+
+Red-green, aarch64-linux ELF (DWARF path that addr2line already exercises):
+
+1. **Round-trip golden — single input.** New objdump case
+ `test/objdump/aarch64/cases/05-addr2line-linked` (mirroring existing cases):
+ `cfree cc -g -target aarch64-linux ... -o prog.elf` (static exec), then
+ `cfree nm prog.elf` to get a function vaddr, then
+ `cfree addr2line -e prog.elf <vaddr>` asserting `file:line`. Compare against
+ the `.expected` golden. Cross-check with host `addr2line` on the same bytes
+ where available.
+2. **Multi-input.** Same flow linking two `-g` `.o`s into one static exec;
+ assert addr2line resolves a function from *each* input (exercises per-input
+ `SK_SECTION` rebasing and OutShdr concatenation). Inspect with
+ `cfree objdump -h prog.elf` to confirm one merged `.debug_info` etc.
+3. **PIE.** Same single-input round-trip linked `-pie` (ET_DYN): assert
+ addr2line resolves `file:line`, confirming the `img_base==0` path holds
+ link-time vaddrs and no spurious dynamic relocs were emitted for `.debug_*`
+ (`cfree objdump -R prog.elf` shows none against debug offsets).
+4. **Strip path.** `cfree ld -S` (or `cc ... -Wl,-S`) drops debug — assert no
+ `.debug_*` shdrs and addr2line reports no debug info.
+5. **No regressions.** `make test-link test-elf test-debug test-dwarf
+ test-driver` and the objdump goldens (`CFREE=build/cfree sh
+ test/objdump/run.sh`). Watch smoke tests that link `-g`.
+
+Risk (per doc): a missed reloc class silently produces wrong line numbers
+because the reader trusts the bytes — the addr2line round-trip cross-checked
+against host `addr2line` is the guard.
diff --git a/src/debug/debug_emit.c b/src/debug/debug_emit.c
@@ -194,6 +194,7 @@ typedef struct EmitCtx {
ObjSymId ssym_abbrev;
ObjSymId ssym_line;
ObjSymId ssym_rnglists;
+ ObjSymId ssym_info;
/* Body-relative offsets of the three CU-root-DIE attributes whose
* payloads are cross-section offsets. Captured at the call sites in
@@ -1075,7 +1076,7 @@ static void emit_section_aranges(EmitCtx* e) {
buf_init(&b, e->heap);
form_u32(&b, 0); /* unit_length placeholder */
form_u16(&b, 2); /* aranges version */
- form_u32(&b, 0); /* debug_info_offset = 0 */
+ form_u32(&b, 0); /* debug_info_offset — filled by R_ABS32 reloc below */
form_u8(&b, addr_size);
form_u8(&b, 0);
body_start = buf_pos(&b);
@@ -1121,6 +1122,14 @@ static void emit_section_aranges(EmitCtx* e) {
buf_patch(&b, 0, le, 4);
}
flatten_to_section(e, e->sec_aranges, &b);
+ /* debug_info_offset (header byte 6) points at this CU within
+ * .debug_info. Emit it as a section-relative R_ABS32 against the
+ * .debug_info section symbol (addend 0 — one CU per object at
+ * offset 0) so that when the linker / JIT view concatenate multiple
+ * inputs, each aranges unit is rebased to its CU's merged offset.
+ * Without this every unit would keep offset 0 and addr2line would
+ * map all addresses to the first input's CU. */
+ obj_reloc(e->ob, e->sec_aranges, 6u, R_ABS32, e->ssym_info, 0);
for (i = 0; i < e->naranges_relocs; ++i) {
obj_reloc(e->ob, e->sec_aranges, e->aranges_relocs[i].buf_offset, R_ABS64,
e->aranges_relocs[i].sym, 0);
@@ -1288,6 +1297,7 @@ void debug_emit(Debug* d) {
ec.ssym_str = mk_section_sym(&ec, ec.sec_str);
ec.ssym_line_str = mk_section_sym(&ec, ec.sec_line_str);
ec.ssym_str_off = mk_section_sym(&ec, ec.sec_str_off);
+ ec.ssym_info = mk_section_sym(&ec, ec.sec_info);
producer_sym = pool_intern_slice(pool, SLICE_LIT("cfree 0.1"));
/* Ensure the CU's primary source file occupies file-table slot 0 before
diff --git a/src/debug/dwarf_line.c b/src/debug/dwarf_line.c
@@ -458,20 +458,38 @@ CfreeStatus cfree_dwarf_addr_to_line(CfreeDebugInfo* d, uint64_t pc,
DwLineRow* best = NULL;
if (!d->lines_built[i]) dw_build_line(d, i);
lp = &d->lines_by_cu[i];
+ /* A line row covers [row.address, next_row.address); an end_sequence
+ * row terminates a sequence and is not itself a coverage row. `pc`
+ * is covered by `best` only if it falls before the end_sequence that
+ * closes best's sequence — otherwise best is stale and pc lies in a
+ * gap (or in another CU). Without this bound, a CU whose code sits
+ * before pc would always claim it, so multi-CU images (one CU per
+ * linked input) resolved every address to the first input's CU. */
for (j = 0; j < lp->nrows; ++j) {
DwLineRow* r = &lp->rows[j];
- if (r->end_sequence) continue;
- if (r->address > pc) break;
- best = r;
- }
- if (best) {
- const char* f = "";
- if (best->file_index < lp->nfile_norm && lp->file_norm)
- f = lp->file_norm[best->file_index];
- if (file_out) *file_out = cfree_slice_cstr(f);
- if (line_out) *line_out = best->line;
- if (col_out) *col_out = best->column;
- return CFREE_OK;
+ if (r->end_sequence) {
+ /* `best` covers pc when pc is strictly inside the sequence
+ * (pc < end_sequence.address) or sits exactly on the last row's
+ * address (a zero-length final row, which some producers emit with
+ * end_sequence at the same address). The second clause must test
+ * best->address, not r->address: when two CUs abut (one input's
+ * code ends exactly where the next begins, common with a single
+ * contiguous .text), end_sequence.address equals the next CU's
+ * first pc, and `pc == end_sequence.address` alone would let the
+ * earlier CU swallow an address that belongs to the later one. */
+ if (best && (pc < r->address || pc == best->address)) {
+ const char* f = "";
+ if (best->file_index < lp->nfile_norm && lp->file_norm)
+ f = lp->file_norm[best->file_index];
+ if (file_out) *file_out = cfree_slice_cstr(f);
+ if (line_out) *line_out = best->line;
+ if (col_out) *col_out = best->column;
+ return CFREE_OK;
+ }
+ best = NULL; /* sequence closed; pc not covered here */
+ continue;
+ }
+ if (r->address <= pc) best = r;
}
}
return CFREE_NOT_FOUND;
diff --git a/src/debug/dwarf_open.c b/src/debug/dwarf_open.c
@@ -18,20 +18,41 @@
#include "core/util.h"
#include "core/vec.h"
#include "debug/dwarf_internal.h"
+#include "obj/obj.h"
/* ---- section lookup --------------------------------------------------- */
void dw_find_section(CfreeDebugInfo* d, const char* name, DwSection* out) {
uint32_t i, n;
+ /* On Mach-O the obj layer reports DWARF sections as "__DWARF,__debug_*"
+ * (16-char-truncated) and .eh_frame as "__TEXT,__eh_frame", not the
+ * ELF ".debug_*"/".eh_frame" spelling. Precompute the Mach-O candidate
+ * for the requested section so one lookup spans both formats. */
+ char macho_full[8 + 17];
+ int have_macho = 0;
out->data = NULL;
out->size = 0;
out->sec_idx = UINT32_MAX;
if (!d->obj) return;
+ {
+ char sect[17];
+ size_t nl = (size_t)slice_from_cstr(name).len;
+ if (obj_macho_debug_sectname(name, nl, sect)) {
+ memcpy(macho_full, "__DWARF,", 8);
+ memcpy(macho_full + 8, sect, slice_from_cstr(sect).len + 1);
+ have_macho = 1;
+ } else if (nl == 9 && memcmp(name, ".eh_frame", 9) == 0) {
+ memcpy(macho_full, "__TEXT,__eh_frame", sizeof("__TEXT,__eh_frame"));
+ have_macho = 1;
+ }
+ }
n = cfree_obj_nsections(d->obj);
for (i = 0; i < n; ++i) {
CfreeObjSecInfo info;
if (cfree_obj_section(d->obj, i, &info) != CFREE_OK) continue;
- if (info.name.len && cfree_slice_eq_cstr(info.name, name)) {
+ if (info.name.len &&
+ (cfree_slice_eq_cstr(info.name, name) ||
+ (have_macho && cfree_slice_eq_cstr(info.name, macho_full)))) {
size_t len = 0;
const uint8_t* p = NULL;
if (cfree_obj_section_data(d->obj, i, &p, &len) != CFREE_OK) continue;
diff --git a/src/link/link.c b/src/link/link.c
@@ -575,6 +575,16 @@ static void link_image_release(LinkImage* img) {
img->heap->free(img->heap, img->dbg_objs_owned,
sizeof(*img->dbg_objs_owned) * img->dbg_objs_n);
}
+ if (img->dbg_bytes) {
+ for (i = 0; i < img->dbg_count; ++i)
+ if (img->dbg_bytes[i])
+ img->heap->free(img->heap, img->dbg_bytes[i], (size_t)img->dbg_size[i]);
+ img->heap->free(img->heap, img->dbg_bytes,
+ sizeof(*img->dbg_bytes) * img->dbg_count);
+ }
+ if (img->dbg_size)
+ img->heap->free(img->heap, img->dbg_size,
+ sizeof(*img->dbg_size) * img->dbg_count);
symhash_fini(&img->globals);
if (img->dyn) {
const ObjFormatImpl* fmt = obj_format_lookup(img->c->target.obj);
diff --git a/src/link/link.h b/src/link/link.h
@@ -117,7 +117,13 @@ typedef struct LinkSection {
u32 align;
Sym name; /* section name (interned); 0 if anon */
u16 sem; /* SecSem of the source obj section */
- u16 pad;
+ /* Non-segment, file-resident section (a .debug_* contribution). It
+ * lives in img->sections so its SK_SECTION symbol resolves and the
+ * reloc engine applies, but it has segment_id == LINK_SEG_NONE and
+ * carries its bytes in the LinkImage debug registry, not a segment
+ * buffer. See link_layout_debug / link_fileonly_bytes. */
+ u8 file_only;
+ u8 pad;
} LinkSection;
typedef struct LinkRelocApply {
diff --git a/src/link/link_internal.h b/src/link/link_internal.h
@@ -229,6 +229,9 @@ typedef enum SegBucket {
/* section_kept: 1 for allocatable progbits/nobits sections (link_layout.c). */
int link_section_kept(const Section* s);
+/* section_kept_fileonly: 1 for non-allocatable .debug_* sections that the
+ * AOT ELF path carries through as file-only sections (link_layout.c). */
+int link_section_kept_fileonly(const Section* s);
/* bucket_for: map section flags to SegBucket (link_layout.c). */
SegBucket link_bucket_for(u16 flags);
/* layout_page_size: page size for segment alignment (link_layout.c). */
@@ -275,6 +278,13 @@ void link_gc_drop_dead_globals(struct Linker*, LinkImage*, const GcLive*);
void link_layout_sections(struct Linker*, LinkImage*, const GcLive*);
void link_layout_commons(struct Linker*, LinkImage*);
void link_emit_segment_bytes(struct Linker*, LinkImage*);
+/* Carry .debug_* sections through as file-only sections + populate the
+ * debug registry (link_layout.c). AOT ELF path only; gated by the
+ * caller on !strip_debug / !jit_mode / ELF target. */
+void link_layout_debug(struct Linker*, LinkImage*);
+/* Byte buffer for a file-only debug LinkSection, or NULL if `id` is not
+ * a registered file-only section (link_layout.c). */
+u8* link_fileonly_bytes(LinkImage*, LinkSectionId);
/* ---- Public entries (link_reloc_layout.c) ---------------------------------
*/
@@ -495,6 +505,17 @@ struct LinkImage {
u8* dbg_objs_owned;
u32 dbg_objs_n;
+ /* File-only debug-section registry (AOT ELF path). link_layout_debug
+ * appends one file-only LinkSection per surviving .debug_* contribution
+ * as a contiguous id range [dbg_first_lsid, dbg_first_lsid+dbg_count).
+ * dbg_bytes[i] / dbg_size[i] hold that contribution's own byte buffer
+ * (relocs applied in place at reloc-offset), indexed by lsid -
+ * dbg_first_lsid. Empty on the JIT / Mach-O / COFF lanes. */
+ LinkSectionId dbg_first_lsid;
+ u32 dbg_count;
+ u8** dbg_bytes;
+ u64* dbg_size;
+
/* Dynamic-link state (Phase 4). NULL when emit_pie was not set on
* the Linker — i.e., the static-exe / JIT path. Owned by the image. */
LinkDynState* dyn;
diff --git a/src/link/link_layout.c b/src/link/link_layout.c
@@ -56,6 +56,13 @@ int link_section_kept(const Section* s) {
return 0;
}
+int link_section_kept_fileonly(const Section* s) {
+ /* Non-allocatable .debug_* sections. They get no PT_LOAD segment but
+ * are carried through to the file so addr2line / gdb resolve
+ * file:line on the linked image. */
+ return s && !s->removed && s->kind == SEC_DEBUG;
+}
+
SegBucket link_bucket_for(u16 flags) {
if (flags & SF_TLS) return SEG_TLS;
if (flags & SF_EXEC) return SEG_RX;
@@ -1035,6 +1042,147 @@ void link_emit_segment_bytes(Linker* l, LinkImage* img) {
}
}
+/* ---- pass 2c: file-only debug sections ----
+ *
+ * Carry every surviving .debug_* section through to the linked image as
+ * a non-segment, file-resident LinkSection so addr2line / gdb resolve
+ * file:line on the output. Contributions of the same name are assigned
+ * a per-name cumulative `vaddr` (the DWARF-section-relative base: 0 for
+ * the first input, size0 for the second, …); the ELF emitter merges
+ * same-name contributions into one output .debug_X section, and the
+ * per-input base makes SK_SECTION cross-section R_ABS32 offsets land in
+ * the merged section. Each contribution keeps its own byte buffer in
+ * the debug registry, with relocations applied in place at reloc-offset.
+ *
+ * Runs after link_emit_segment_bytes (so the segment-byte copy never
+ * sees these segment-less sections) and before link_assign_symbol_vaddrs
+ * (so the SK_SECTION debug symbols pick up their section_id + base). */
+
+/* Per-output-name cumulative base tracker. Debug section names are few
+ * (.debug_info/.debug_line/.debug_abbrev/.debug_str/...), so a linear
+ * scan is fine. */
+typedef struct DbgNameAcc {
+ Sym name;
+ u64 cum; /* running total size for this name */
+} DbgNameAcc;
+
+void link_layout_debug(Linker* l, LinkImage* img) {
+ Heap* h = img->heap;
+ u32 ii, j;
+ u32 ndbg = 0;
+
+ /* Pass 0: count surviving debug contributions. */
+ for (ii = 0; ii < LinkInputs_count(&l->inputs); ++ii) {
+ ObjBuilder* ob = LinkInputs_at(&l->inputs, ii)->obj;
+ InputMap* m = &img->input_maps[ii];
+ for (j = 1; j < obj_section_count(ob); ++j) {
+ const Section* s = obj_section_get(ob, j);
+ if (link_section_kept_fileonly(s) && !m->comdat_discarded[j] &&
+ section_size_for_link(s) > 0)
+ ++ndbg;
+ }
+ }
+ if (ndbg == 0) return;
+
+ /* Grow img->sections to hold the new file-only sections appended after
+ * the existing allocatable + common sections. */
+ {
+ u32 new_nsec = img->nsections + ndbg;
+ LinkSection* ns = (LinkSection*)h->realloc(
+ h, img->sections, sizeof(*img->sections) * img->nsections,
+ sizeof(*img->sections) * new_nsec, _Alignof(LinkSection));
+ if (!ns) compiler_panic(img->c, no_loc(), "link: oom on debug sections");
+ img->sections = ns;
+ }
+
+ img->dbg_bytes = (u8**)h->alloc(h, sizeof(*img->dbg_bytes) * ndbg, _Alignof(u8*));
+ img->dbg_size = (u64*)h->alloc(h, sizeof(*img->dbg_size) * ndbg, _Alignof(u64));
+ DbgNameAcc* acc =
+ (DbgNameAcc*)h->alloc(h, sizeof(*acc) * ndbg, _Alignof(DbgNameAcc));
+ if (!img->dbg_bytes || !img->dbg_size || !acc)
+ compiler_panic(img->c, no_loc(), "link: oom on debug registry");
+ memset(img->dbg_bytes, 0, sizeof(*img->dbg_bytes) * ndbg);
+ memset(img->dbg_size, 0, sizeof(*img->dbg_size) * ndbg);
+
+ img->dbg_first_lsid = (LinkSectionId)(img->nsections + 1u);
+ img->dbg_count = 0;
+ u32 nacc = 0;
+
+ /* Pass 1: append one file-only LinkSection per contribution, assign
+ * per-name cumulative base, and copy bytes into the registry. */
+ for (ii = 0; ii < LinkInputs_count(&l->inputs); ++ii) {
+ ObjBuilder* ob = LinkInputs_at(&l->inputs, ii)->obj;
+ InputMap* m = &img->input_maps[ii];
+ for (j = 1; j < obj_section_count(ob); ++j) {
+ const Section* s = obj_section_get(ob, j);
+ u32 size;
+ u64 base;
+ u32 ai, slot;
+ LinkSection* ls;
+ LinkSectionId lsid;
+ u8* buf;
+ if (!link_section_kept_fileonly(s) || m->comdat_discarded[j]) continue;
+ size = section_size_for_link(s);
+ if (size == 0) continue;
+
+ /* Per-name cumulative base. */
+ for (ai = 0; ai < nacc; ++ai)
+ if (acc[ai].name == s->name) break;
+ if (ai == nacc) {
+ acc[nacc].name = s->name;
+ acc[nacc].cum = 0;
+ ai = nacc++;
+ }
+ base = acc[ai].cum;
+ acc[ai].cum += size;
+
+ slot = img->dbg_count;
+ lsid = (LinkSectionId)(img->nsections + 1u);
+ ls = &img->sections[img->nsections++];
+ memset(ls, 0, sizeof(*ls));
+ ls->id = lsid;
+ ls->input_id = LinkInputs_at(&l->inputs, ii)->id;
+ ls->obj_section_id = j;
+ ls->obj_atom_id = OBJ_ATOM_NONE;
+ ls->segment_id = LINK_SEG_NONE;
+ ls->obj_offset = 0;
+ ls->input_offset = 0;
+ ls->file_offset = 0; /* assigned by the ELF emitter */
+ ls->vaddr = base; /* DWARF-section-relative base */
+ ls->size = size;
+ ls->flags = s->flags;
+ ls->align = s->align ? s->align : 1u;
+ ls->name = s->name;
+ ls->sem = SSEM_PROGBITS;
+ ls->file_only = 1u;
+
+ /* Copy this contribution's bytes into its own registry buffer. */
+ buf = (u8*)h->alloc(h, size, 1);
+ if (!buf) compiler_panic(img->c, no_loc(), "link: oom on debug bytes");
+ buf_read(&s->bytes, 0u, buf, (size_t)size);
+ img->dbg_bytes[slot] = buf;
+ img->dbg_size[slot] = size;
+ img->dbg_count++;
+
+ /* Map the input's section id to this LinkSection so the SK_SECTION
+ * debug symbol resolves (assign_symbol_vaddrs) and its relocations
+ * route here (emit_relocations). */
+ map_placed_unit(m, j, OBJ_ATOM_NONE, lsid);
+ }
+ }
+
+ h->free(h, acc, sizeof(*acc) * ndbg);
+}
+
+u8* link_fileonly_bytes(LinkImage* img, LinkSectionId id) {
+ if (!img || id == LINK_SEC_NONE || id < img->dbg_first_lsid) return NULL;
+ {
+ u32 idx = (u32)(id - img->dbg_first_lsid);
+ if (idx >= img->dbg_count) return NULL;
+ return img->dbg_bytes[idx];
+ }
+}
+
/* ---- public orchestration ---- */
LinkImage* link_resolve(Linker* l) {
@@ -1083,6 +1231,16 @@ LinkImage* link_resolve(Linker* l) {
metrics_scope_begin(l->c, "link.emit_segment_bytes");
if (!l->jit_mode) link_emit_segment_bytes(l, img);
metrics_scope_end(l->c, "link.emit_segment_bytes");
+ /* Carry .debug_* through as file-only sections. ELF places them in
+ * non-alloc sections; Mach-O in a __DWARF segment. The JIT path
+ * serves debug via cfree_jit_view instead, and COFF emit doesn't yet
+ * handle file-only sections. */
+ metrics_scope_begin(l->c, "link.layout_debug");
+ if (!l->strip_debug && !l->jit_mode &&
+ (l->c->target.obj == CFREE_OBJ_ELF ||
+ l->c->target.obj == CFREE_OBJ_MACHO))
+ link_layout_debug(l, img);
+ metrics_scope_end(l->c, "link.layout_debug");
metrics_scope_begin(l->c, "link.assign_vaddrs");
link_assign_symbol_vaddrs(l, img);
metrics_scope_end(l->c, "link.assign_vaddrs");
diff --git a/src/link/link_reloc_layout.c b/src/link/link_reloc_layout.c
@@ -1218,7 +1218,8 @@ void link_emit_relocations(Linker* l, LinkImage* img, const LinkSymId* got_map,
LinkSymId target;
LinkSection* ls;
LinkRelocApply rec;
- if (!s || !link_section_kept(s)) continue;
+ if (!s || (!link_section_kept(s) && !link_section_kept_fileonly(s)))
+ continue;
if (link_input_reloc_section(m, r, k) == LINK_SEC_NONE) continue;
if (r->kind == R_RV_RELAX || r->kind == R_RV_TPREL_ADD ||
r->kind == R_RV_ALIGN)
diff --git a/src/obj/elf/link.c b/src/obj/elf/link.c
@@ -164,6 +164,11 @@ static void shift_image_addresses(LinkImage* img, u64 delta) {
img->segments[i].vaddr += delta;
}
for (i = 0; i < img->nsections; ++i) {
+ /* File-only debug sections carry DWARF-section-relative bases, not
+ * load addresses — they live outside any PT_LOAD and must not shift
+ * with the loaded image. Their file_offset is assigned fresh by the
+ * trailing-offset pass below. */
+ if (img->sections[i].file_only) continue;
img->sections[i].file_offset += delta;
img->sections[i].vaddr += delta;
}
@@ -175,6 +180,13 @@ static void shift_image_addresses(LinkImage* img, u64 delta) {
LinkSymbol* s = LinkSyms_at(&img->syms, i);
if (s->kind == SK_ABS) continue;
if (!s->defined) continue;
+ /* A symbol resolved into a file-only debug section (e.g. the local
+ * SK_SECTION symbol a DWARF R_ABS32 targets) holds a sec-relative
+ * offset, not a load address — leave it unshifted so the apply pass
+ * writes the right DWARF offset. */
+ if (s->section_id != LINK_SEC_NONE && s->section_id <= img->nsections &&
+ img->sections[s->section_id - 1].file_only)
+ continue;
s->vaddr += delta;
}
/* tls_vaddr lives in the same image-relative coordinate system as
@@ -310,9 +322,27 @@ static void apply_all_relocs(LinkImage* img, u64 img_base) {
LinkRelocApply* r = LinkRelocs_at(&img->relocs, i);
const LinkSymbol* tgt = LinkSyms_at(&img->syms, r->target - 1);
const LinkSection* sec = &img->sections[r->link_section_id - 1];
- const LinkSegment* seg = &img->segments[sec->segment_id - 1];
+ const LinkSegment* seg;
u64 S, P;
u8* P_bytes;
+
+ /* File-only debug section: not loaded, so no dynamic reloc and no
+ * img_base. Write the final value straight into the registry buffer
+ * at the reloc offset. A SK_SECTION target resolves to its DWARF
+ * sec-relative base (tgt->vaddr, kept unshifted); a code/data symbol
+ * resolves to its link-time vaddr (img_base + vaddr) for low_pc /
+ * set_address. Mirrors the JIT debug view (link_jit.c). */
+ if (sec->segment_id == LINK_SEG_NONE) {
+ u8* dbg = link_fileonly_bytes(img, r->link_section_id);
+ if (!dbg) continue;
+ if (tgt->kind == SK_SECTION || tgt->kind == SK_ABS)
+ S = tgt->vaddr;
+ else
+ S = img_base + tgt->vaddr;
+ link_reloc_apply(img->c, r->kind, dbg + r->offset, S, r->addend, 0);
+ continue;
+ }
+ seg = &img->segments[sec->segment_id - 1];
if (reloc_is_tlsle(r->kind)) {
/* S is the target's TP-relative offset: distance from the
* TLS image start plus the 16-byte TCB. Both vaddrs are
@@ -575,6 +605,7 @@ typedef struct OutShdr {
u64 file_offset;
u64 size;
int is_nobits;
+ int is_fileonly; /* non-allocatable .debug_* section (no PT_LOAD) */
} OutShdr;
static u16 sym_shndx_for(const LinkSymbol* s, const OutShdr* outshdrs,
@@ -588,8 +619,14 @@ static u16 sym_shndx_for(const LinkSymbol* s, const OutShdr* outshdrs,
{
u32 i;
for (i = 0; i < noutshdr; ++i) {
- u64 lo = outshdrs[i].vaddr;
- u64 hi = lo + outshdrs[i].size;
+ u64 lo, hi;
+ /* File-only debug shdrs sit at sec-relative vaddrs (0-based) and
+ * never contain a loaded symbol — skip them so a low-vaddr code
+ * symbol (e.g. PIE, img_base 0) isn't mis-attributed to a
+ * .debug_* section whose [0,size) range happens to overlap. */
+ if (outshdrs[i].is_fileonly) continue;
+ lo = outshdrs[i].vaddr;
+ hi = lo + outshdrs[i].size;
if (s->vaddr >= lo && s->vaddr <= hi) return (u16)outshdrs[i].shdr_idx;
}
}
@@ -628,6 +665,21 @@ static u64 sec_flags_to_shf(u32 flags) {
return r;
}
+/* Output-shdr sort order: loaded sections first, by (segment_id, vaddr);
+ * then file-only debug sections after all segments, grouped by name (so
+ * same-name multi-input contributions are adjacent and merge into one
+ * output section) and ordered by their sec-relative base. Returns 1 if
+ * `a` should sort before `b`. */
+static int shdr_sort_less(const LinkSection* a, const LinkSection* b) {
+ if (a->file_only != b->file_only) return b->file_only; /* loaded first */
+ if (a->file_only) {
+ if (a->name != b->name) return a->name < b->name;
+ return a->vaddr < b->vaddr;
+ }
+ if (a->segment_id != b->segment_id) return a->segment_id < b->segment_id;
+ return a->vaddr < b->vaddr;
+}
+
void link_emit_elf(LinkImage* img, Writer* w) {
Heap* heap = img->heap;
Compiler* c = img->c;
@@ -882,9 +934,7 @@ void link_emit_elf(LinkImage* img, Writer* w) {
j = i;
while (j > 0) {
const LinkSection* b = &img->sections[order[j - 1]];
- if ((b->segment_id < a->segment_id) ||
- (b->segment_id == a->segment_id && b->vaddr <= a->vaddr))
- break;
+ if (!shdr_sort_less(a, b)) break; /* a not before b → stop (stable) */
order[j] = order[j - 1];
--j;
}
@@ -913,6 +963,7 @@ void link_emit_elf(LinkImage* img, Writer* w) {
o->file_offset = ls->file_offset;
o->size = ls->size;
o->is_nobits = (ls->sem == SSEM_NOBITS);
+ o->is_fileonly = ls->file_only;
noutshdr++;
}
}
@@ -1041,7 +1092,27 @@ void link_emit_elf(LinkImage* img, Writer* w) {
if (e > end_of_segs) end_of_segs = e;
}
}
- u64 symtab_off = ALIGN_UP(end_of_segs, (u64)8u);
+ /* File-only debug sections go in the trailing non-alloc region, after
+ * the loaded segments and before .symtab. Assign each merged debug
+ * OutShdr a file offset (and propagate it back to its constituent
+ * LinkSections for the byte-write pass). */
+ u64 dbg_cursor = end_of_segs;
+ if (img->dbg_count) {
+ u32 oi;
+ for (oi = 0; oi < noutshdr; ++oi) {
+ OutShdr* o = &outshdrs[oi];
+ u32 si;
+ if (!o->is_fileonly) continue;
+ o->file_offset = ALIGN_UP(dbg_cursor, o->align ? o->align : 1u);
+ for (si = 0; si < img->dbg_count; ++si) {
+ LinkSection* ls = &img->sections[img->dbg_first_lsid - 1 + si];
+ if (ls->name == o->name)
+ ls->file_offset = o->file_offset + ls->vaddr; /* base within run */
+ }
+ dbg_cursor = o->file_offset + o->size;
+ }
+ }
+ u64 symtab_off = ALIGN_UP(dbg_cursor, (u64)8u);
u64 symtab_size = (u64)ELF64_SYM_SIZE * nsyms_emit;
u64 strtab_off = symtab_off + symtab_size;
u64 strtab_size = strtab.len;
@@ -1258,6 +1329,31 @@ void link_emit_elf(LinkImage* img, Writer* w) {
}
}
+ /* ---- write file-only debug sections ---- *
+ *
+ * Emit each merged debug OutShdr at its assigned file offset by
+ * writing its constituent contributions in base order (the registry
+ * is per-name base-ascending). OutShdrs were placed at ascending file
+ * offsets, so cur_off advances monotonically. */
+ if (img->dbg_count) {
+ u32 oi;
+ for (oi = 0; oi < noutshdr; ++oi) {
+ const OutShdr* o = &outshdrs[oi];
+ u32 si;
+ if (!o->is_fileonly) continue;
+ if (cur_off < o->file_offset) {
+ write_zeroes(w, (size_t)(o->file_offset - cur_off));
+ cur_off = o->file_offset;
+ }
+ for (si = 0; si < img->dbg_count; ++si) {
+ const LinkSection* ls = &img->sections[img->dbg_first_lsid - 1 + si];
+ if (ls->name != o->name || img->dbg_size[si] == 0) continue;
+ write_bytes(w, img->dbg_bytes[si], (size_t)img->dbg_size[si]);
+ cur_off += img->dbg_size[si];
+ }
+ }
+ }
+
/* ---- write trailing non-alloc sections ---- */
if (cur_off < symtab_off) {
write_zeroes(w, (size_t)(symtab_off - cur_off));
@@ -1329,6 +1425,13 @@ void link_emit_elf(LinkImage* img, Writer* w) {
sh.sh_type = sec_sem_to_sht(o->sem);
sh.sh_flags = sec_flags_to_shf(o->flags);
sh.sh_addr = img_base + o->vaddr;
+ /* File-only debug sections aren't loaded: SHT_PROGBITS, no
+ * SHF_ALLOC, sh_addr 0. addr2line / gdb read them by file offset. */
+ if (o->is_fileonly) {
+ sh.sh_type = SHT_PROGBITS;
+ sh.sh_flags = 0;
+ sh.sh_addr = 0;
+ }
sh.sh_offset = o->file_offset;
sh.sh_size = o->size;
sh.sh_link = 0;
diff --git a/src/obj/macho/emit.c b/src/obj/macho/emit.c
@@ -118,16 +118,25 @@ static void name_to_seg_sect(const char* name, u32 nlen, u16 sec_kind,
seg = "__DATA";
sect = "__bss";
break;
- case SEC_DEBUG:
+ case SEC_DEBUG: {
+ /* ".debug_*" → "__DWARF,__debug_*" (truncated to Mach-O's 16-byte
+ * sectname, matching Apple's spelling). Shared with the DWARF
+ * reader so the names round-trip. Any non-".debug_*" SEC_DEBUG
+ * name falls back to the leading-dot strip. */
+ char ds[17];
seg = "__DWARF";
- /* Strip a leading `.` from the input name (".debug_info" →
- * "__debug_info") so the dwarf section names round-trip. */
- sect = (nlen && name[0] == '.') ? name + 1 : name;
copy_fixed16(out->segname, &out->seg_len, seg,
(u32)slice_from_cstr(seg).len);
- copy_fixed16(out->sectname, &out->sect_len, sect,
- (u32)((nlen && name[0] == '.') ? nlen - 1 : nlen));
+ if (obj_macho_debug_sectname(name, nlen, ds)) {
+ copy_fixed16(out->sectname, &out->sect_len, ds,
+ (u32)slice_from_cstr(ds).len);
+ } else {
+ sect = (nlen && name[0] == '.') ? name + 1 : name;
+ copy_fixed16(out->sectname, &out->sect_len, sect,
+ (u32)((nlen && name[0] == '.') ? nlen - 1 : nlen));
+ }
return;
+ }
default:
seg = "__DATA";
sect = "__data";
diff --git a/src/obj/macho/link.c b/src/obj/macho/link.c
@@ -210,9 +210,11 @@ typedef struct MSec {
/* Inline storage for segname/sectname when split from a Mach-O
* `__SEG,__sect`-form LinkSection name. Names from string literals
* (synth sections, derived-from-flags defaults) point at .rodata
- * and don't use these. 16 bytes matches the on-disk field width. */
- char segname_buf[16];
- char sectname_buf[16];
+ * and don't use these. 17 bytes: the on-disk field is a fixed 16
+ * (no NUL needed there), but these are read as C strings, so a full
+ * 16-char name (e.g. __debug_line_str) needs the extra NUL slot. */
+ char segname_buf[17];
+ char sectname_buf[17];
u64 vaddr;
u64 file_offset;
u64 size;
@@ -230,6 +232,19 @@ static void msec_repair_name_ptrs(MSec* m) {
if (m->sectname_buf[0]) m->sectname = m->sectname_buf;
}
+/* Segment slot indices in MCtx.segs[]. __DWARF carries the file-only
+ * .debug_* sections (debug-info retention); it sits before __LINKEDIT so
+ * the ad-hoc code signature stays the last bytes of the file. */
+enum {
+ MSEG_PAGEZERO = 0,
+ MSEG_TEXT = 1,
+ MSEG_DATA_CONST = 2,
+ MSEG_DATA = 3,
+ MSEG_DWARF = 4,
+ MSEG_LINKEDIT = 5,
+ MSEG_COUNT = 6,
+};
+
typedef struct MSeg {
const char* name;
u32 maxprot;
@@ -292,7 +307,7 @@ typedef struct MCtx {
u32 nsecs;
OutSec* outs;
u32 nouts;
- MSeg segs[5]; /* PAGEZERO, TEXT, DATA_CONST, DATA, LINKEDIT */
+ MSeg segs[MSEG_COUNT]; /* PAGEZERO, TEXT, DATA_CONST, DATA, DWARF, LINKEDIT */
u32 nsegs;
/* Synthetic byte buffers, owned. */
@@ -652,10 +667,10 @@ static void pick_macho_names(const LinkSection* ls, Compiler* c, MSec* m) {
/* Comma-form: "__SEG,__sect" round-tripped from a Mach-O input. */
for (size_t i = 0; i < nlen; ++i) {
if (nm[i] == ',') {
- u32 seg_n = (u32)(i > 15 ? 15 : i);
+ u32 seg_n = (u32)(i > 16 ? 16 : i);
memcpy(m->segname_buf, nm, seg_n);
m->segname_buf[seg_n] = 0;
- u32 sect_n = (u32)((nlen - i - 1) > 15 ? 15 : (nlen - i - 1));
+ u32 sect_n = (u32)((nlen - i - 1) > 16 ? 16 : (nlen - i - 1));
memcpy(m->sectname_buf, nm + i + 1, sect_n);
m->sectname_buf[sect_n] = 0;
m->segname = m->segname_buf;
@@ -697,8 +712,11 @@ static void plan_layout(MCtx* x) {
VM_PROT_READ | VM_PROT_WRITE);
seg_init(&x->segs[3], "__DATA", VM_PROT_READ | VM_PROT_WRITE,
VM_PROT_READ | VM_PROT_WRITE);
- seg_init(&x->segs[4], "__LINKEDIT", VM_PROT_READ, VM_PROT_READ);
- x->nsegs = 5;
+ /* __DWARF holds the file-only .debug_* sections; mapped R but never
+ * referenced at runtime. Empty (nsects 0) when there's no debug info. */
+ seg_init(&x->segs[MSEG_DWARF], "__DWARF", VM_PROT_READ, VM_PROT_READ);
+ seg_init(&x->segs[MSEG_LINKEDIT], "__LINKEDIT", VM_PROT_READ, VM_PROT_READ);
+ x->nsegs = MSEG_COUNT;
/* Pre-allocate MSec capacity: every LinkSection + 2 synth (__stubs,
* __got). (LinkSections from the dynamic-link layer — .dynsym / .plt
@@ -733,6 +751,7 @@ static void plan_layout(MCtx* x) {
for (u32 i = 0; i < img->nsections; ++i) {
LinkSection* ls = &img->sections[i];
if (!ls->size) continue;
+ if (ls->file_only) continue; /* .debug_* → __DWARF segment below */
if (sec_is_writable(ls)) continue;
if (sec_is_zerofill(ls)) continue; /* placed in __DATA */
if (sec_needs_data_const(img, ls)) continue;
@@ -795,6 +814,7 @@ static void plan_layout(MCtx* x) {
}
for (u32 i = 0; i < img->nsections; ++i) {
LinkSection* ls = &img->sections[i];
+ if (ls->file_only) continue; /* .debug_* → __DWARF (has ABS64 relocs) */
if (!sec_needs_data_const(img, ls)) continue;
MSec* m = &x->secs[x->nsecs++];
memset(m, 0, sizeof(*m));
@@ -813,6 +833,7 @@ static void plan_layout(MCtx* x) {
u32 first_d = x->nsecs;
for (u32 i = 0; i < img->nsections; ++i) {
LinkSection* ls = &img->sections[i];
+ if (ls->file_only) continue; /* .debug_* → __DWARF */
if (!ls->size && !sec_is_zerofill(ls)) continue;
if (!sec_is_writable(ls)) continue;
MSec* m = &x->secs[x->nsecs++];
@@ -880,6 +901,60 @@ static void plan_layout(MCtx* x) {
x->segs[3].nsects = x->nsecs - first_d;
x->segs[3].first_sec = first_d;
+ /* __DWARF: file-only .debug_* sections (debug-info retention). Each
+ * contribution becomes a synth MSec whose bytes are the per-image
+ * debug registry buffer (relocs applied in place at apply_relocs).
+ * Iterated in registry order (= input order) so same-name runs land
+ * adjacent and the per-input DWARF-relative bases line up with the
+ * coalesced section's byte layout. */
+ u32 first_dw = x->nsecs;
+ for (u32 i = 0; i < img->nsections; ++i) {
+ LinkSection* ls = &img->sections[i];
+ u8* dbg;
+ if (!ls->file_only || !ls->size) continue;
+ dbg = link_fileonly_bytes(img, ls->id);
+ if (!dbg) continue;
+ MSec* m = &x->secs[x->nsecs++];
+ memset(m, 0, sizeof(*m));
+ m->synth_data = dbg;
+ m->synth_size = (u32)ls->size;
+ /* Section name: a Mach-O input already carries "__DWARF,__debug_*";
+ * an in-process .debug_* maps via obj_macho_debug_sectname. */
+ {
+ Slice nm = pool_slice(x->c->global, ls->name);
+ const char* comma = nm.s ? memchr(nm.s, ',', nm.len) : NULL;
+ char sect[17];
+ if (comma) {
+ u32 sgn = (u32)(comma - nm.s);
+ if (sgn > 16u) sgn = 16u;
+ memcpy(m->segname_buf, nm.s, sgn);
+ m->segname_buf[sgn] = 0;
+ u32 stn = (u32)(nm.len - (comma - nm.s) - 1);
+ if (stn > 16u) stn = 16u;
+ memcpy(m->sectname_buf, comma + 1, stn);
+ m->sectname_buf[stn] = 0;
+ } else if (obj_macho_debug_sectname(nm.s, nm.len, sect)) {
+ memcpy(m->segname_buf, "__DWARF", 8);
+ memcpy(m->sectname_buf, sect, slice_from_cstr(sect).len + 1);
+ } else {
+ memcpy(m->segname_buf, "__DWARF", 8);
+ u32 stn = nm.len > 16u ? 16u : (u32)nm.len;
+ memcpy(m->sectname_buf, nm.s, stn);
+ m->sectname_buf[stn] = 0;
+ }
+ m->segname = m->segname_buf;
+ m->sectname = m->sectname_buf;
+ }
+ /* align 1: contributions concatenate gap-free so the DWARF-relative
+ * bases (assigned without padding in link_layout_debug) stay valid. */
+ m->align = 1u;
+ m->size = ls->size;
+ m->segidx = MSEG_DWARF;
+ m->flags = 0; /* S_REGULAR */
+ }
+ x->segs[MSEG_DWARF].nsects = x->nsecs - first_dw;
+ x->segs[MSEG_DWARF].first_sec = first_dw;
+
/* Group MSecs by (segname, sectname) within each segment so vaddr
* placement keeps same-named runs contiguous. Otherwise Phase B's
* adjacency-based coalescing splits a single Mach-O section into
@@ -945,17 +1020,17 @@ static void plan_layout(MCtx* x) {
++nseg_real;
continue;
} /* PAGEZERO */
- if (i == 4) {
+ if (i == MSEG_LINKEDIT) {
++nseg_real;
continue;
} /* LINKEDIT always */
- if (x->segs[i].nsects > 0) ++nseg_real;
+ if (x->segs[i].nsects > 0) ++nseg_real; /* incl. __DWARF when present */
}
/* Each LC_SEGMENT_64 carries 72 + 80*nouts bytes (one section_64
* record per coalesced (segname,sectname), not per MSec). */
u32 sizeofcmds = 0;
for (u32 i = 0; i < x->nsegs; ++i) {
- if (i == 0 || i == 4) {
+ if (i == 0 || i == MSEG_LINKEDIT) {
sizeofcmds += MACHO_SEGCMD64_SIZE; /* no sections */
continue;
}
@@ -990,7 +1065,7 @@ static void plan_layout(MCtx* x) {
u64 fileoff = x->headers_size;
/* Pad __TEXT sections to natural alignment. */
for (u32 i = 0; i < x->nsegs; ++i) {
- if (i == 0 || i == 4) continue;
+ if (i == 0 || i == MSEG_LINKEDIT) continue; /* DWARF placed here too */
MSeg* sg = &x->segs[i];
if (i > 1) {
/* page-align the start of __DATA_CONST and __DATA */
@@ -1087,8 +1162,8 @@ static void plan_layout(MCtx* x) {
/* LINKEDIT placeholder; size is filled after blob assembly. */
vaddr = ALIGN_UP(vaddr, MZ_PAGE);
fileoff = ALIGN_UP(fileoff, MZ_PAGE);
- x->segs[4].vmaddr = vaddr;
- x->segs[4].fileoff = fileoff;
+ x->segs[MSEG_LINKEDIT].vmaddr = vaddr;
+ x->segs[MSEG_LINKEDIT].fileoff = fileoff;
x->linkedit_vaddr = vaddr;
x->linkedit_fileoff = fileoff;
@@ -1173,7 +1248,7 @@ static void plan_layout(MCtx* x) {
for (u32 i = 0; i < x->nsegs; ++i) {
x->segs[i].first_out = 0;
}
- u32 prev_nouts[5];
+ u32 prev_nouts[MSEG_COUNT];
for (u32 i = 0; i < x->nsegs; ++i) prev_nouts[i] = x->segs[i].nouts;
for (u32 i = 0; i < x->nsegs; ++i) x->segs[i].nouts = 0;
for (u32 i = 0; i < x->nouts; ++i) {
@@ -1380,6 +1455,22 @@ static void apply_relocs(MCtx* x, FixList* fl) {
for (u32 i = 0; i < LinkRelocs_count(&img->relocs); ++i) {
LinkRelocApply* r = LinkRelocs_at(&img->relocs, i);
if (r->target == LINK_SYM_NONE) continue;
+ /* File-only .debug_* section: patch the registry buffer in place (no
+ * __got/stub/chained-fixup — debug bytes aren't loaded or slid). A
+ * SK_SECTION target resolves to its DWARF-section-relative base; a
+ * code/data symbol to its final (absolute) vaddr for low_pc. Mach-O
+ * vaddrs are already absolute, so there's no extra image base. */
+ {
+ const LinkSection* sec = &img->sections[r->link_section_id - 1u];
+ if (sec->file_only) {
+ u8* dbg = link_fileonly_bytes(img, r->link_section_id);
+ const LinkSymbol* tgt = sym_at(img, r->target);
+ if (dbg && tgt)
+ link_reloc_apply(x->c, r->kind, dbg + r->offset, tgt->vaddr,
+ r->addend, 0);
+ continue;
+ }
+ }
MSec* msec = NULL;
u8* P_bytes = patch_ptr(x, img, r, &msec);
if (!P_bytes) continue;
@@ -2099,9 +2190,9 @@ static void layout_linkedit(MCtx* x) {
* including) codesig. Codesig is computed below. */
u64 le_size = cur - x->linkedit_fileoff;
/* Set linkedit segment size; will be increased after codesig. */
- x->segs[4].filesize = le_size;
- x->segs[4].vmsize = ALIGN_UP(le_size, MZ_PAGE);
- if (!x->segs[4].vmsize) x->segs[4].vmsize = MZ_PAGE;
+ x->segs[MSEG_LINKEDIT].filesize = le_size;
+ x->segs[MSEG_LINKEDIT].vmsize = ALIGN_UP(le_size, MZ_PAGE);
+ if (!x->segs[MSEG_LINKEDIT].vmsize) x->segs[MSEG_LINKEDIT].vmsize = MZ_PAGE;
}
/* ---- ad-hoc code signature (CodeDirectory + SuperBlob) ----
@@ -2342,8 +2433,8 @@ void link_emit_macho(LinkImage* img, Writer* w) {
build_codesig_skeleton(&x, code_limit, "a.out");
/* Now extend linkedit segment to include codesig. */
u64 le_size = (u64)x.codesig_off + (u64)x.codesig_size - x.linkedit_fileoff;
- x.segs[4].filesize = le_size;
- x.segs[4].vmsize = ALIGN_UP(le_size, MZ_PAGE);
+ x.segs[MSEG_LINKEDIT].filesize = le_size;
+ x.segs[MSEG_LINKEDIT].vmsize = ALIGN_UP(le_size, MZ_PAGE);
/* Build load commands buffer. */
MByte lc;
@@ -2355,7 +2446,9 @@ void link_emit_macho(LinkImage* img, Writer* w) {
if (x.segs[2].nsects > 0)
emit_load_command_segment(&lc, &x, 2); /* DATA_CONST */
if (x.segs[3].nsects > 0) emit_load_command_segment(&lc, &x, 3); /* DATA */
- emit_load_command_segment(&lc, &x, 4); /* LINKEDIT */
+ if (x.segs[MSEG_DWARF].nsects > 0)
+ emit_load_command_segment(&lc, &x, MSEG_DWARF); /* DWARF (debug info) */
+ emit_load_command_segment(&lc, &x, MSEG_LINKEDIT); /* LINKEDIT */
/* LC_DYLD_CHAINED_FIXUPS (linkedit_data_command: 16B) */
mbuf_u32(&lc, LC_DYLD_CHAINED_FIXUPS);
@@ -2507,7 +2600,8 @@ void link_emit_macho(LinkImage* img, Writer* w) {
ncmds += 2; /* PAGEZERO + TEXT */
if (x.segs[2].nsects > 0) ncmds++;
if (x.segs[3].nsects > 0) ncmds++;
- ncmds++; /* LINKEDIT */
+ if (x.segs[MSEG_DWARF].nsects > 0) ncmds++; /* __DWARF (debug info) */
+ ncmds++; /* LINKEDIT */
ncmds += 11 + x.ndylibs;
/* (chained, exports_trie, symtab, dysymtab, dyld, uuid, build_version,
* main, fn_starts, data_in_code, codesig) = 11 */
diff --git a/src/obj/macho/read.c b/src/obj/macho/read.c
@@ -599,10 +599,19 @@ ObjBuilder* read_macho(Compiler* c, const char* name, const u8* data,
kind = SK_NOTYPE;
} else {
sec_id = msecs[n_sect - 1].obj_sec;
- /* Mach-O n_value for defined symbols is segment-relative addr;
- * convert back to a section-local offset. */
- u64 base = msecs[n_sect - 1].addr;
- value = (n_value >= base) ? (n_value - base) : 0;
+ /* MH_OBJECT: the obj model and the linker treat an input
+ * symbol's value as a section-local offset, and a relocatable
+ * .o's sections carry non-zero layout addrs, so subtract the
+ * section base. Linked images (MH_EXECUTE/MH_DYLIB) keep the
+ * absolute n_value so nm / objdump -t / size / addr2line report
+ * real vaddrs — matching the ELF reader, whose st_value is
+ * already absolute for images. */
+ if (filetype == MH_OBJECT) {
+ u64 base = msecs[n_sect - 1].addr;
+ value = (n_value >= base) ? (n_value - base) : 0;
+ } else {
+ value = n_value;
+ }
kind = (msecs[n_sect - 1].flags & S_ATTR_PURE_INSTRUCTIONS) ? SK_FUNC
: SK_OBJ;
}
diff --git a/src/obj/obj.h b/src/obj/obj.h
@@ -603,6 +603,22 @@ Sym obj_secname_preinit_array(Compiler*);
Sym obj_secname_tdata(Compiler*);
Sym obj_secname_tbss(Compiler*);
+/* DWARF debug-section name translation for Mach-O.
+ *
+ * cfree carries DWARF sections under their ELF spelling (".debug_info")
+ * internally; on Mach-O they live in the __DWARF segment with "__"-
+ * prefixed section names ("__debug_info"). The transform drops the
+ * leading '.', prepends "__", and truncates to Mach-O's 16-byte
+ * `sectname` field — which reproduces the names Apple's toolchain uses
+ * (e.g. ".debug_str_offsets" -> "__debug_str_offs").
+ *
+ * Writes the bare Mach-O section name (NUL-terminated, <=16 chars) into
+ * `out` (>=17 bytes) and returns 1 when (`name`,`len`) is a ".debug_*"
+ * section; returns 0 otherwise, leaving `out` untouched. Shared by the
+ * Mach-O writer (emit) and the DWARF reader (section lookup) so the two
+ * agree on the truncated spelling. */
+int obj_macho_debug_sectname(const char* name, size_t len, char out[17]);
+
/* ---- thread-local storage emission ---------------------------------
*
* The frontend collects a `_Thread_local` definition's bytes (or marks
diff --git a/src/obj/obj_secnames.c b/src/obj/obj_secnames.c
@@ -26,6 +26,24 @@
#include "obj/format.h"
#include "obj/obj.h"
+int obj_macho_debug_sectname(const char* name, size_t len, char out[17]) {
+ /* Only ".debug_*" sections translate here; ".eh_frame" lives in __TEXT
+ * and is handled by the writer's generic SecKind path and the reader's
+ * own candidate list, not this helper. */
+ static const char kPrefix[] = ".debug_";
+ const size_t plen = sizeof(kPrefix) - 1; /* 7 */
+ size_t i;
+ if (!name || len < plen || memcmp(name, kPrefix, plen) != 0) return 0;
+ /* out = "__" + name-without-dot, capped at Mach-O's 16-byte sectname.
+ * The cap yields Apple's spelling for the one overlong DWARF5 name
+ * (".debug_str_offsets" -> "__debug_str_offs"). */
+ out[0] = '_';
+ out[1] = '_';
+ for (i = 0; i + 1 < len && i < 14u; ++i) out[2 + i] = name[1 + i];
+ out[2 + i] = '\0';
+ return 1;
+}
+
static Sym secname_panic_unimpl(Compiler* c, const char* which) {
SrcLoc l = {0, 0, 0};
compiler_panic(c, l,
diff --git a/test/driver/run.sh b/test/driver/run.sh
@@ -788,7 +788,6 @@ else
fi
# ---- addr2line ----
-# Use ELF target so DWARF reader works (Mach-O debug sections not supported).
cat > "$work/a2l.c" <<'SRC'
int calc(int x) { return x * 2; }
int main(void) { return calc(42); }
@@ -827,6 +826,64 @@ else
printf 'FAIL addr2line-nonzero (setup)\n'; fail=$((fail + 1))
fi
+# addr2line over Mach-O DWARF: the producer emits .debug_* into the
+# __DWARF segment as __debug_* (16-char-truncated, Apple spelling) and
+# the reader maps the requested ELF name onto that form, so addr2line
+# resolves file:line on a Mach-O object too (not just ELF).
+if "$CFREE" cc -g -target arm64-apple-macos -c "$work/a2l.c" \
+ -o "$work/a2l.macho.o" > "$work/a2l-m-cc.out" 2> "$work/a2l-m-cc.err"; then
+ m_addr=$("$CFREE" nm "$work/a2l.macho.o" 2>/dev/null | \
+ grep " _\{0,1\}calc$" | awk '{print $1}')
+ if [ -n "$m_addr" ] &&
+ "$CFREE" addr2line -e "$work/a2l.macho.o" "$m_addr" \
+ > "$work/a2l-m.out" 2> "$work/a2l-m.err" &&
+ grep -q "a2l.c" "$work/a2l-m.out"; then
+ printf 'PASS %s\n' "addr2line-macho"
+ pass=$((pass + 1))
+ else
+ printf 'FAIL %s (m_addr=%s)\n' "addr2line-macho" "$m_addr"
+ sed 's/^/ | /' "$work/a2l-m.out" "$work/a2l-m.err"
+ fail=$((fail + 1))
+ fi
+else
+ printf 'FAIL addr2line-macho (setup compile failed)\n'; fail=$((fail + 1))
+fi
+
+# Debug-info retention through the Mach-O linker: link a self-contained
+# image (no SDK/libSystem needed), then resolve file:line via the same
+# `nm | addr2line` flow as the ELF case — exercising the __DWARF segment
+# the linker carried plus the reader on a linked image. nm reports
+# absolute vaddrs for linked Mach-O images, so its value feeds addr2line
+# directly.
+cat > "$work/a2lm.c" <<'SRC'
+int helper(int x) { return x * 3; }
+int compute(int n) { return helper(n) + n; }
+void mainx(void) { compute(5); }
+SRC
+if "$CFREE" cc -g -target arm64-apple-macos -c "$work/a2lm.c" \
+ -o "$work/a2lm.o" > "$work/a2lm-cc.out" 2> "$work/a2lm-cc.err" &&
+ "$CFREE" ld -o "$work/a2lm.exe" "$work/a2lm.o" -e mainx \
+ > "$work/a2lm-ld.out" 2> "$work/a2lm-ld.err"; then
+ cm_addr=$("$CFREE" nm "$work/a2lm.exe" 2>/dev/null | \
+ grep " _\{0,1\}compute$" | awk '{print $1}')
+ if "$CFREE" objdump -h "$work/a2lm.exe" 2>/dev/null \
+ | grep -q '__DWARF,__debug_info' &&
+ [ -n "$cm_addr" ] &&
+ "$CFREE" addr2line -e "$work/a2lm.exe" "0x$cm_addr" \
+ > "$work/a2lm-a2l.out" 2> "$work/a2lm-a2l.err" &&
+ grep -q "a2lm.c" "$work/a2lm-a2l.out"; then
+ printf 'PASS %s\n' "addr2line-macho-linked"
+ pass=$((pass + 1))
+ else
+ printf 'FAIL %s (compute=%s: %s)\n' "addr2line-macho-linked" \
+ "$cm_addr" "$(cat "$work/a2lm-a2l.out" 2>/dev/null)"
+ fail=$((fail + 1))
+ fi
+else
+ printf 'FAIL addr2line-macho-linked (link failed)\n'; fail=$((fail + 1))
+ sed 's/^/ | /' "$work/a2lm-ld.err"
+fi
+
# addr2line -h (help)
if "$CFREE" addr2line --help > "$work/a2l-help.out" 2> "$work/a2l-help.err" &&
grep -q "USAGE" "$work/a2l-help.out"; then
diff --git a/test/objdump/aarch64/cases/05-addr2line-linked.expected b/test/objdump/aarch64/cases/05-addr2line-linked.expected
@@ -0,0 +1,10 @@
+merged debug sections:
+ .debug_info
+ .debug_line
+addr2line:
+ _start a.c:3:6
+ helper a.c:2:5
+ compute b.c:2:5
+ main b.c:7:5
+stripped debug shdrs: 0
+stripped addr2line: addr2line: stripped.elf: no debug info available
diff --git a/test/objdump/aarch64/cases/05-addr2line-linked.sh b/test/objdump/aarch64/cases/05-addr2line-linked.sh
@@ -0,0 +1,47 @@
+# Golden: debug-info retention through the linker (IMAGE_INSPECT phase 5).
+# Links two `-g` objects into a static aarch64 ELF executable, then asserts
+# `cfree addr2line` resolves file:line for a function from *each* input —
+# exercising multi-input .debug_* concatenation, per-input SK_SECTION
+# rebasing, and the .debug_aranges debug_info_offset reloc. Also confirms
+# the merged image carries a single .debug_info/.debug_line and that
+# `ld -S` drops debug entirely. Built fresh (not a committed binary) so the
+# file:line mapping tracks the C front-end + debug emitter.
+set -e
+CC="cc -g -c -target aarch64-linux"
+
+cat > a.c <<'EOF'
+extern int compute(int);
+int helper(int x) { return x * 3; }
+void _start(void) { compute(5); }
+EOF
+cat > b.c <<'EOF'
+extern int helper(int);
+int compute(int n) {
+ int acc = 0;
+ for (int i = 0; i < n; i++) acc += helper(i);
+ return acc;
+}
+int main(void) { return compute(5); }
+EOF
+
+$CFREE $CC -o a.o a.c
+$CFREE $CC -o b.o b.c
+$CFREE ld -o prog.elf a.o b.o -e _start
+
+# Merged debug sections: one .debug_info / .debug_line spanning both inputs.
+echo "merged debug sections:"
+$CFREE objdump -h prog.elf | awk '$2 ~ /^\.debug_(info|line)$/ {print " " $2}' | sort
+
+# addr2line resolves a function defined in each input (helper/_start from
+# a.c, compute/main from b.c) — wrong rebasing would map them all to one CU.
+echo "addr2line:"
+for fn in _start helper compute main; do
+ addr=$($CFREE nm prog.elf | awk -v f=" $fn\$" '$0 ~ f {print $1}')
+ printf ' %-8s %s\n' "$fn" "$($CFREE addr2line -e prog.elf "0x$addr")"
+done
+
+# -S strips debug: no .debug_* shdrs, addr2line reports no info.
+$CFREE ld -S -o stripped.elf a.o b.o -e _start
+echo "stripped debug shdrs: $($CFREE objdump -h stripped.elf | grep -c '\.debug_')"
+caddr=$($CFREE nm stripped.elf | awk '/ compute$/{print $1}')
+echo "stripped addr2line: $($CFREE addr2line -e stripped.elf "0x$caddr" 2>&1)"