kit

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

commit 152604e1ae971b6f30497fc904a7d9857d6968bb
parent 0cf3dff4e27ef007e400dc08a2216d98d85ba21a
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 15:24:07 -0700

link reports: fix --print-memory-usage alias double-count + high-half region, --cref imports

Three correctness fixes in the linker report writers (src/api/link.c):

- Bug 7 (--print-memory-usage): a section placed into multiple PT_LOAD phdrs
  appends ALIAS LinkSegments (nsections==0) describing the SAME bytes; the
  memory-usage loop charged each, double-counting an N-phdr section N times.
  Skip alias/phdr-only segments (nsections==0); real primaries and BSS/COMMON
  zero-fill carry nsections>=1, so genuine bytes stay counted exactly once.

- Bug 8 (--print-memory-usage): the region-window test computed
  origin+length as u64, which wraps to 0 for a region touching the top of the
  address space (e.g. high-half kernel ORIGIN=0xFFFFFFFF80000000
  LENGTH=0x80000000) → nothing attributed (used=0). Use overflow-safe
  subtract-based membership: base-origin < length.

- Bug 9 (--cref): the table omitted imported (DSO-resolved) externals — the
  cross-references a cref table exists to show — because it shared the nm-style
  symbol predicate that drops imports. Build the cref symbol set independently
  (defined-non-imported OR imported, excluding FILE/SECTION), dedup by name,
  sort by name; for an imported symbol the def line names the providing DSO
  (basename via dso_input_id) or '-'.

Tests in test/buildcmds/run.sh (red-then-green verified): dual-phdr used equals
single-phdr used (Bug 7); high-half region reports nonzero used (Bug 8, skips
under the pre-existing link_script.c hex-parser UBSan trap on the debug build);
--cref over an imported libc_marker lists the symbol with its DSO def (Bug 9).

Diffstat:
Msrc/api/link.c | 112++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mtest/buildcmds/run.sh | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 197 insertions(+), 27 deletions(-)

diff --git a/src/api/link.c b/src/api/link.c @@ -995,48 +995,96 @@ static KitStatus link_report_write_map_image(KitLinkSession* s, return link_report_write_symbols_nm(img, out); } -/* --cref: cross-reference table. For each global/weak defined or imported - * symbol (sorted by name for determinism) emit the defining file, then the - * set of distinct input files that reference it via a relocation. Input names - * are normalized to basenames. */ +/* --cref predicate: a symbol participates in the cross-reference table when it + * is a named non-FILE/non-SECTION global that is either locally defined OR + * imported from a DSO. Imported (DSO-resolved) externals are exactly the + * cross-references a cref table exists to show, so — unlike the nm-style + * "symbols" listing's link_report_symbol_include — they are NOT dropped. */ +static int link_report_cref_include(const LinkSymbol* s) { + if (!s || !s->name) return 0; + if (s->kind == SK_FILE || s->kind == SK_SECTION) return 0; + return (s->defined && !s->imported) || s->imported; +} + +/* --cref: cross-reference table. For each defined-or-imported global/weak + * symbol (deduped by name, sorted by name for determinism) emit the defining + * file — the providing DSO basename for an imported symbol, the defining object + * otherwise — then the set of distinct input files that reference it via a + * relocation. Input names are normalized to basenames. The symbol set is built + * INDEPENDENTLY of the nm-style "symbols" listing so imports are included. */ static KitStatus link_report_write_cref_image(KitLinkSession* s, KitWriter* out) { LinkImage* img = s->image; - LinkReportSymbols syms; + Heap* h; + LinkSymId* ids = NULL; + u32 n = 0, cap, i; KitStatus st; - u32 i; if (!img) return KIT_INVALID; + h = img->heap; st = link_report_cstr(out, "cross reference table\n"); if (st != KIT_OK) return st; - st = link_report_collect_symbols(img, &syms); - if (st != KIT_OK) return st; - for (i = 0; i < syms.n; ++i) { - const LinkSymbol* sym = LinkSyms_at(&img->syms, syms.ids[i] - 1u); + cap = LinkSyms_count(&img->syms); + if (!cap) return kit_writer_status(out); + ids = (LinkSymId*)h->alloc(h, sizeof(*ids) * cap, _Alignof(LinkSymId)); + if (!ids) return KIT_NOMEM; + /* Collect one canonical slot per qualifying name, deduped by Sym (the + * interned name handle): a name's first qualifying slot wins. Prefer a + * locally-defined slot over an imported one when both exist (so a symbol that + * is both defined and referenced through a DSO reports its real def). */ + for (i = 0; i < cap; ++i) { + const LinkSymbol* sy = LinkSyms_at(&img->syms, i); + u32 k; + int seen = 0; + if (!link_report_cref_include(sy)) continue; + for (k = 0; k < n; ++k) { + LinkSymbol* prev = LinkSyms_at(&img->syms, ids[k] - 1u); + if (prev->name != sy->name) continue; + seen = 1; + /* Upgrade a previously-recorded imported slot to a real definition. */ + if (sy->defined && !sy->imported && (!prev->defined || prev->imported)) + ids[k] = sy->id; + break; + } + if (!seen) ids[n++] = sy->id; + } + /* Insertion sort by name (small lists; deterministic). */ + for (i = 1; i < n; ++i) { + LinkSymId key = ids[i]; + KitSlice kn = + link_report_sym_name(img, LinkSyms_at(&img->syms, key - 1u)->name); + u32 j = i; + while (j > 0) { + KitSlice pn = link_report_sym_name( + img, LinkSyms_at(&img->syms, ids[j - 1u] - 1u)->name); + if (link_report_slice_cmp(pn, kn) <= 0) break; + ids[j] = ids[j - 1u]; + --j; + } + ids[j] = key; + } + for (i = 0; i < n; ++i) { + const LinkSymbol* sym = LinkSyms_at(&img->syms, ids[i] - 1u); KitSlice name = link_report_sym_name(img, sym->name); KitSlice deffile; LinkInputId defining_input = LINK_INPUT_NONE; u32 r, k; LinkInputId last = LINK_INPUT_NONE; - /* Aggregate by NAME, not by per-input slot id: collect_symbols includes a - * resolved slot per input, so a name can repeat (its sorted by vaddr then - * name → adjacent). Skip a name already emitted as the previous entry. */ - if (i > 0) { - const LinkSymbol* prev = LinkSyms_at(&img->syms, syms.ids[i - 1u] - 1u); - if (prev->name == sym->name) continue; - } - /* Find the defining file: the canonical global slot (img->globals) is the - * real definition; resolution copies its value into per-input reference - * slots, so scanning slots by `defined` alone would mis-attribute. Fall - * back to any defined same-name slot for locals (never in globals). */ - { + if (sym->imported) { + /* Imported symbol: the "definition" is the providing DSO input. */ + defining_input = sym->dso_input_id; + } else { + /* Find the defining file: the canonical global slot (img->globals) is the + * real definition; resolution copies its value into per-input reference + * slots, so scanning slots by `defined` alone would mis-attribute. Fall + * back to any defined same-name slot for locals (never in globals). */ LinkSymId canon = symhash_get(&img->globals, sym->name); if (canon != LINK_SYM_NONE) { const LinkSymbol* c = LinkSyms_at(&img->syms, canon - 1u); if (c->defined && !c->imported) defining_input = c->input_id; } if (defining_input == LINK_INPUT_NONE) { - for (k = 0; k < syms.n; ++k) { - const LinkSymbol* c = LinkSyms_at(&img->syms, syms.ids[k] - 1u); + for (k = 0; k < cap; ++k) { + const LinkSymbol* c = LinkSyms_at(&img->syms, k); if (c->name != sym->name) continue; if (c->defined && !c->imported && c->input_id != LINK_INPUT_NONE) { defining_input = c->input_id; @@ -1078,7 +1126,7 @@ static KitStatus link_report_write_cref_image(KitLinkSession* s, } if (st != KIT_OK) break; } - link_report_free_symbols(img, &syms); + h->free(h, ids, sizeof(*ids) * cap); return st == KIT_OK ? kit_writer_status(out) : st; } @@ -1106,9 +1154,19 @@ static KitStatus link_report_write_memory_usage_image(KitLinkSession* s, for (j = 0; j < img->nsegments; ++j) { const LinkSegment* seg = &img->segments[j]; u64 base = img->load_base + seg->vaddr; + /* Alias / program-header-only segments (nsections==0) describe the SAME + * bytes as a primary segment under an additional :phdr; charging them + * would double-count a section placed into multiple PT_LOADs. Real + * primaries (incl. BSS/COMMON zero-fill) carry nsections>=1, so genuine + * mem_size stays counted exactly once. */ + if (seg->nsections == 0) continue; /* A segment belongs to a region when its load address sits within the - * region window. Count memsz (covers BSS). */ - if (base >= rg->origin && base < rg->origin + rg->length) + * [origin, origin+length) window. Count memsz (covers BSS). The end of + * the window is computed overflow-safe via a subtract — a region that + * touches the top of the address space (origin+length wraps to 0) would + * otherwise mis-test `base < 0` and attribute nothing. */ + if (base >= rg->origin && + (rg->length == 0 ? 0 : base - rg->origin < rg->length)) used += seg->mem_size; } free_bytes = (rg->length > used) ? rg->length - used : 0; diff --git a/test/buildcmds/run.sh b/test/buildcmds/run.sh @@ -771,6 +771,118 @@ contains fx2-fs-hosted-cross-diag "$work/fx2-fs-hosted-cross.err" \ # Same-arch hosted freestanding link still succeeds (no false positive). run_ok fx2-fs-hosted-same "$KIT" build-exe -target x86_64-linux -ffreestanding \ -nostdlib -static -no-pie -e _start kstart.c fx2_fs_x64.o -o fx2_fs_same.elf +# Link report-writer correctness fixes (kernel-FY): --print-memory-usage alias +# double-count + high-half region overflow, and --cref imported-symbol coverage. +# =========================================================================== + +# Helper: extract a region's "used=0x..." token from a --print-memory-usage +# output file by region name (first match wins). +fy_memuse_used() { + awk -v rg="$1" '$1==rg { for (i=1;i<=NF;i++) if ($i ~ /^used=/) { print substr($i,6); exit } }' "$2" +} + +# ---- Bug 7: a section placed into N program headers (alias segments) must be +# charged to its MEMORY region ONCE, not N times. We compare the single-phdr +# `used` to the dual-phdr `used` for an otherwise-identical layout; they must be +# equal (no double-count). (Repro: dual-phdr was used=0x84 vs single 0x44.) +printf 'void _start(void){for(;;){}}\nint fydata=7;\n' > fymem.c +cat > fy_single.ld <<'LDEOF' +PHDRS { a PT_LOAD; } +MEMORY { RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 0x100000 } +SECTIONS { + . = 0x80000000; + .text : { *(.text*) } :a + .data : { *(.data*) } :a + .bss : { *(.bss*) } :a +} +LDEOF +cat > fy_dual.ld <<'LDEOF' +PHDRS { a PT_LOAD; b PT_LOAD; } +MEMORY { RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 0x100000 } +SECTIONS { + . = 0x80000000; + .text : { *(.text*) } :a :b + .data : { *(.data*) } :a + .bss : { *(.bss*) } :a +} +LDEOF +run_ok fy-memuse-single "$KIT" build-exe -target riscv64-none-elf -ffreestanding \ + -nostdlib -static -no-pie -T fy_single.ld --print-memory-usage fymem.c \ + -o fymem_single.elf +run_ok fy-memuse-dual "$KIT" build-exe -target riscv64-none-elf -ffreestanding \ + -nostdlib -static -no-pie -T fy_dual.ld --print-memory-usage fymem.c \ + -o fymem_dual.elf +fy_used_single=$(fy_memuse_used RAM "$work/fy-memuse-single.out") +fy_used_dual=$(fy_memuse_used RAM "$work/fy-memuse-dual.out") +if [ -n "$fy_used_single" ] && [ "$fy_used_single" = "$fy_used_dual" ]; then + ok fy-memuse-alias-not-doubled +else + { printf 'alias segment double-counts region usage\n'; + printf ' single-phdr used=%s\n' "$fy_used_single"; + printf ' dual-phdr used=%s (expected equal)\n' "$fy_used_dual"; + } > "$work/fy-memuse-alias-not-doubled.diag" + not_ok fy-memuse-alias-not-doubled "$work/fy-memuse-alias-not-doubled.diag" +fi + +# ---- Bug 8: a MEMORY region touching the TOP of the address space +# (ORIGIN=0xFFFFFFFF80000000, LENGTH=0x80000000 → origin+length wraps to 0) must +# still attribute the image that lives inside it (nonzero used / nonzero %age). +# Under an ASan/UBSan build the linker-script hex parser traps on the 16-digit +# ORIGIN (a separate, pre-existing defect in src/link/link_script.c, owned by +# another agent); in that case the high-half script can't even be parsed, so we +# skip rather than fail for an unrelated reason. +cat > fy_high.ld <<'LDEOF' +MEMORY { RAM (rwx) : ORIGIN = 0xFFFFFFFF80000000, LENGTH = 0x80000000 } +SECTIONS { + . = 0xFFFFFFFF80000000; + .text : { *(.text*) } + .data : { *(.data*) } + .bss : { *(.bss*) } +} +LDEOF +if "$KIT" build-exe -target riscv64-none-elf -ffreestanding -nostdlib -static \ + -no-pie -T fy_high.ld --print-memory-usage fymem.c -o fymem_high.elf \ + > "$work/fy-memuse-high.out" 2> "$work/fy-memuse-high.err"; then + fy_used_high=$(fy_memuse_used RAM "$work/fy-memuse-high.out") + if [ -n "$fy_used_high" ] && [ "$fy_used_high" != "0x0" ]; then + ok fy-memuse-high-half-nonzero + else + { printf 'high-half region reports zero usage (origin+length wrap)\n'; + sed 's/^/out: /' "$work/fy-memuse-high.out"; + } > "$work/fy-memuse-high-half-nonzero.diag" + not_ok fy-memuse-high-half-nonzero "$work/fy-memuse-high-half-nonzero.diag" + fi +elif grep -q 'link_script.c' "$work/fy-memuse-high.err"; then + skip_test fy-memuse-high-half-nonzero \ + "linker-script hex parser traps on 16-digit ORIGIN (pre-existing UBSan in link_script.c)" +else + not_ok fy-memuse-high-half-nonzero "$work/fy-memuse-high.err" +fi + +# ---- Bug 9: --cref must list imported (DSO-resolved) symbols and their +# referencing files. Two PIC objects reference libc_marker, resolved from the +# hand-built libc.so DSO fixture; libc_marker must appear in the cref table. +if [ -f "$repo_root/test/driver/fixtures/libc.so" ]; then + cp "$repo_root/test/driver/fixtures/libc.so" fy_libc.so + printf 'extern int libc_marker(void);\nint fy_use_a(void){return libc_marker();}\nvoid _start(void){fy_use_a();for(;;){}}\n' > fy_crefa.c + printf 'extern int libc_marker(void);\nint fy_use_b(void){return libc_marker()+1;}\n' > fy_crefb.c + run_ok fy-cref-obja "$KIT" build-obj -target x86_64-linux -fPIC fy_crefa.c -o fy_crefa.o + run_ok fy-cref-objb "$KIT" build-obj -target x86_64-linux -fPIC fy_crefb.c -o fy_crefb.o + run_ok fy-cref-link "$KIT" ld -pie --cref fy_cref.out \ + fy_crefa.o fy_crefb.o fy_libc.so -o fy_crefexe.elf + contains fy-cref-imported-symbol fy_cref.out "libc_marker" + # The imported symbol's def line should name the providing DSO (basename), + # never "-" and never an absolute host path. + if awk '/^libc_marker$/{f=1;next} f&&/^ def fy_libc.so$/{ok=1} f&&/^[^ ]/&&!/^libc_marker$/{f=0} END{exit !ok}' fy_cref.out; then + ok fy-cref-imported-def-dso + else + cp fy_cref.out "$work/fy-cref-imported-def-dso.diag" + not_ok fy-cref-imported-def-dso "$work/fy-cref-imported-def-dso.diag" + fi +else + skip_test fy-cref-imported-symbol "no libc.so fixture" + skip_test fy-cref-imported-def-dso "no libc.so fixture" +fi kit_summary build-driver kit_exit