kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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:
Mdoc/IMAGE_INSPECT.md | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Adoc/LINK_DEBUG.md | 187+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/debug/debug_emit.c | 12+++++++++++-
Msrc/debug/dwarf_line.c | 42++++++++++++++++++++++++++++++------------
Msrc/debug/dwarf_open.c | 23++++++++++++++++++++++-
Msrc/link/link.c | 10++++++++++
Msrc/link/link.h | 8+++++++-
Msrc/link/link_internal.h | 21+++++++++++++++++++++
Msrc/link/link_layout.c | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/link/link_reloc_layout.c | 3++-
Msrc/obj/elf/link.c | 117++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/obj/macho/emit.c | 21+++++++++++++++------
Msrc/obj/macho/link.c | 138++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/obj/macho/read.c | 17+++++++++++++----
Msrc/obj/obj.h | 16++++++++++++++++
Msrc/obj/obj_secnames.c | 18++++++++++++++++++
Mtest/driver/run.sh | 59++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Atest/objdump/aarch64/cases/05-addr2line-linked.expected | 10++++++++++
Atest/objdump/aarch64/cases/05-addr2line-linked.sh | 47+++++++++++++++++++++++++++++++++++++++++++++++
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)"