kit

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

commit ef340b4cc5bea1a6a715058fc4d15559549af74c
parent 295a93eeb9452f75958744a51bf6edd4271a2127
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 14:41:14 -0700

link: map completeness + cross-reference + memory-usage reports

- Map writer (link_report_write_map_image): add per-section LMA alongside VMA,
  a discarded-sections list (GC-dropped / COMDAT-discarded / unplaced kept
  candidates), and an unresolved-symbols list. Normalize input paths to
  basenames (link_report_normalize_path) so absolute host paths never leak and
  the map stays byte-identical across build dirs.
- kit_link_session_write_cref: deterministic --cref table (per global symbol:
  defining file + distinct referencing files via reloc attribution), aggregated
  by name and using normalized basenames.
- kit_link_session_write_memory_usage: GNU-ld-style per-MEMORY-region
  used/free/total summary driven by the linker script's regions.

Diffstat:
Msrc/api/link.c | 337+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 332 insertions(+), 5 deletions(-)

diff --git a/src/api/link.c b/src/api/link.c @@ -218,6 +218,17 @@ KitStatus kit_link_session_new(KitCompiler* c, } if (opts->linker_script) link_set_script(l, opts->linker_script); if (opts->text_base_set) link_set_text_base(l, opts->text_base); + /* Linker-flags/policy plumbing (kernel-C work). The Linker struct lives in + * link_internal.h; these fields have no dedicated setter, so the public API + * composition layer assigns them directly. The arrays are borrowed from the + * caller (the driver keeps them alive for the session's lifetime). */ + l->defsyms = opts->defsyms; + l->ndefsyms = opts->ndefsyms; + l->section_starts = opts->section_starts; + l->nsection_starts = opts->nsection_starts; + l->orphan_handling = opts->orphan_handling; + l->fatal_warnings = opts->fatal_warnings ? 1 : 0; + l->freestanding_strict = opts->freestanding_strict ? 1 : 0; if (opts->entry.s && opts->entry.len) { link_set_entry(l, opts->entry); } else if (opts->pe_subsystem == KIT_PE_SUBSYSTEM_WINDOWS_GUI && @@ -746,13 +757,130 @@ static KitStatus link_report_write_symbols_nm(LinkImage* img, KitWriter* out) { return st == KIT_OK ? kit_writer_status(out) : st; } +/* Normalize a (possibly absolute) input path to its basename so map/side files + * stay byte-identical regardless of where the inputs live on disk (an absolute + * host path would leak the build directory and break determinism). */ +static KitSlice link_report_normalize_path(KitSlice raw) { + size_t i; + size_t base = 0; + if (!raw.s || raw.len == 0) return raw; + for (i = 0; i < raw.len; ++i) + if (raw.s[i] == '/' || raw.s[i] == '\\') base = i + 1u; + if (base >= raw.len) return raw; /* trailing separator: keep as-is */ + raw.s += base; + raw.len -= base; + return raw; +} + static KitSlice link_report_input_name(LinkImage* img, LinkInputId id) { LinkInput* in; + KitSlice raw; if (!img || !img->linker || id == LINK_INPUT_NONE || id > LinkInputs_count(&img->linker->inputs)) return KIT_SLICE_NULL; in = LinkInputs_at(&img->linker->inputs, id - 1u); - return in && in->name ? pool_slice(img->c->global, in->name) : KIT_SLICE_NULL; + if (!in || !in->name) return KIT_SLICE_NULL; + raw = pool_slice(img->c->global, in->name); + return link_report_normalize_path(raw); +} + +/* Discarded sections: input sections that are layout candidates + * (link_section_kept) but did not land in the final image — dropped by + * --gc-sections, retired as a COMDAT duplicate, or otherwise unplaced. Walks + * the per-input maps in input/section order for determinism, and reports the + * normalized input name so no absolute host path leaks. */ +static KitStatus link_report_write_discarded(LinkImage* img, KitWriter* out) { + KitStatus st; + u32 ii; + if (!img || !img->linker) return KIT_OK; + st = link_report_cstr(out, "\ndiscarded\n"); + if (st != KIT_OK) return st; + for (ii = 0; ii < LinkInputs_count(&img->linker->inputs); ++ii) { + LinkInput* in = LinkInputs_at(&img->linker->inputs, ii); + InputMap* m = (ii < img->ninput_maps) ? &img->input_maps[ii] : NULL; + ObjBuilder* ob = in ? in->obj : NULL; + KitSlice input; + u32 j, n; + if (!ob || !m) continue; + if (in->kind == LINK_INPUT_DSO_BYTES) continue; + input = link_report_input_name(img, in->id); + n = obj_section_count(ob); + for (j = 1; j < n; ++j) { + const Section* sec = obj_section_get(ob, j); + int dropped; + if (!sec || sec->removed || !link_section_kept(sec)) continue; + dropped = (m->comdat_discarded && j < m->nsection && m->comdat_discarded[j]) || + (m->section && j < m->nsection && m->section[j] == LINK_SEC_NONE); + if (!dropped) continue; + st = link_report_cstr(out, " "); + if (st != KIT_OK) return st; + { + KitSlice nm = sec->name ? link_report_sym_name(img, sec->name) + : KIT_SLICE_NULL; + st = nm.s ? link_report_slice(out, nm) : link_report_cstr(out, "-"); + if (st != KIT_OK) return st; + } + if (input.s && input.len) { + st = link_report_cstr(out, " input="); + if (st != KIT_OK) return st; + st = link_report_slice(out, input); + if (st != KIT_OK) return st; + } + st = link_report_cstr(out, "\n"); + if (st != KIT_OK) return st; + } + } + return KIT_OK; +} + +/* Unresolved symbols: image symbols that remain undefined and unimported + * after resolution (only reachable when the link is permissive, e.g. + * --allow-undefined; a strict link panics instead). Sorted by name for a + * deterministic listing. */ +static KitStatus link_report_write_unresolved(LinkImage* img, KitWriter* out) { + KitStatus st; + Heap* h = img->heap; + LinkSymId* ids = NULL; + u32 n = 0, cap, i; + if (!img) return KIT_OK; + st = link_report_cstr(out, "\nunresolved\n"); + if (st != KIT_OK) return st; + cap = LinkSyms_count(&img->syms); + if (!cap) return KIT_OK; + ids = (LinkSymId*)h->alloc(h, sizeof(*ids) * cap, _Alignof(LinkSymId)); + if (!ids) return KIT_NOMEM; + for (i = 0; i < cap; ++i) { + const LinkSymbol* s = LinkSyms_at(&img->syms, i); + if (!s || s->name == 0) continue; + if (s->defined || s->imported) continue; + if (s->kind == SK_FILE || s->kind == SK_SECTION) continue; + ids[n++] = s->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* s = LinkSyms_at(&img->syms, ids[i] - 1u); + KitSlice nm = link_report_sym_name(img, s->name); + st = link_report_cstr(out, " "); + if (st == KIT_OK) st = link_report_slice(out, nm); + if (st == KIT_OK) st = link_report_cstr(out, "\n"); + if (st != KIT_OK) break; + } + h->free(h, ids, sizeof(*ids) * cap); + return st; } static KitStatus link_report_write_map_image(KitLinkSession* s, @@ -819,22 +947,32 @@ static KitStatus link_report_write_map_image(KitLinkSession* s, const LinkSection* sec = &img->sections[i]; KitSlice name = link_report_sym_name(img, sec->name); KitSlice input = link_report_input_name(img, sec->input_id); + int loaded = !(sec->file_only || sec->segment_id == LINK_SEG_NONE); /* Loaded sections report their final runtime address; file-only debug * sections aren't mapped (sh_addr 0), so leave their image-relative * bookkeeping vaddr untouched rather than biasing it by the load base. */ - u64 sec_vaddr = (sec->file_only || sec->segment_id == LINK_SEG_NONE) - ? sec->vaddr - : img->load_base + sec->vaddr; + u64 sec_vaddr = loaded ? img->load_base + sec->vaddr : sec->vaddr; + /* LMA (load-memory address): where the section's initialized bytes load. + * Equals the VMA for the common case; differs only when the section's + * segment declares a distinct load region (a script `AT>` clause, which + * sets the segment paddr apart from its vaddr). */ + u64 sec_lma = sec_vaddr; + if (loaded && sec->segment_id <= img->nsegments) { + const LinkSegment* seg = &img->segments[sec->segment_id - 1u]; + if (seg->paddr != seg->vaddr) + sec_lma = img->load_base + seg->paddr + (sec->vaddr - seg->vaddr); + } st = link_reportf(out, " [%u] ", sec->id); if (st != KIT_OK) return st; st = name.s ? link_report_slice(out, name) : link_report_cstr(out, "-"); if (st != KIT_OK) return st; st = link_reportf(out, - " seg=%u off=0x%llx vaddr=0x%llx size=0x%llx " + " seg=%u off=0x%llx vaddr=0x%llx lma=0x%llx size=0x%llx " "align=0x%x", sec->segment_id, (unsigned long long)sec->file_offset, (unsigned long long)sec_vaddr, + (unsigned long long)sec_lma, (unsigned long long)sec->size, sec->align); if (st != KIT_OK) return st; if (input.s && input.len) { @@ -847,11 +985,151 @@ static KitStatus link_report_write_map_image(KitLinkSession* s, if (st != KIT_OK) return st; } + st = link_report_write_discarded(img, out); + if (st != KIT_OK) return st; + st = link_report_write_unresolved(img, out); + if (st != KIT_OK) return st; + st = link_report_cstr(out, "\nsymbols\n"); if (st != KIT_OK) return st; 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. */ +static KitStatus link_report_write_cref_image(KitLinkSession* s, + KitWriter* out) { + LinkImage* img = s->image; + LinkReportSymbols syms; + KitStatus st; + u32 i; + if (!img) return KIT_INVALID; + 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); + 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). */ + { + 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); + if (c->name != sym->name) continue; + if (c->defined && !c->imported && c->input_id != LINK_INPUT_NONE) { + defining_input = c->input_id; + break; + } + } + } + } + deffile = link_report_input_name(img, defining_input); + st = link_report_slice(out, name); + if (st == KIT_OK) st = link_report_cstr(out, "\n"); + if (st != KIT_OK) break; + /* Defining file. */ + st = link_report_cstr(out, " def "); + if (st == KIT_OK) + st = (deffile.s && deffile.len) ? link_report_slice(out, deffile) + : link_report_cstr(out, "-"); + if (st == KIT_OK) st = link_report_cstr(out, "\n"); + if (st != KIT_OK) break; + /* Referencing files: scan relocs targeting any same-name slot. The reloc + * list is already in a deterministic input/section/offset order, so + * consecutive dedup of input ids gives a stable per-symbol reference set. */ + for (r = 0; r < LinkRelocs_count(&img->relocs); ++r) { + const LinkRelocApply* ra = LinkRelocs_at(&img->relocs, r); + const LinkSymbol* tsym; + KitSlice reffile; + if (!ra || ra->target == LINK_SYM_NONE) continue; + tsym = LinkSyms_at(&img->syms, ra->target - 1u); + if (!tsym || tsym->name != sym->name) continue; + if (ra->input_id == last) continue; + last = ra->input_id; + reffile = link_report_input_name(img, ra->input_id); + st = link_report_cstr(out, " ref "); + if (st == KIT_OK) + st = (reffile.s && reffile.len) ? link_report_slice(out, reffile) + : link_report_cstr(out, "-"); + if (st == KIT_OK) st = link_report_cstr(out, "\n"); + if (st != KIT_OK) break; + } + if (st != KIT_OK) break; + } + link_report_free_symbols(img, &syms); + return st == KIT_OK ? kit_writer_status(out) : st; +} + +/* --print-memory-usage: GNU-ld-style per-MEMORY-region used/free/total. The + * regions come from the linker script; usage is the total size of placed + * sections (and BSS zero-fill) whose runtime address falls inside a region's + * [origin, origin+length) window. */ +static KitStatus link_report_write_memory_usage_image(KitLinkSession* s, + KitWriter* out) { + LinkImage* img = s->image; + const KitLinkScript* script = s->opts.linker_script; + KitStatus st; + u32 ri; + if (!img) return KIT_INVALID; + st = link_report_cstr(out, "Memory region Used Size Region Size " + "%age Used\n"); + if (st != KIT_OK) return st; + if (!script || script->nregions == 0) return kit_writer_status(out); + for (ri = 0; ri < script->nregions; ++ri) { + const KitLinkRegion* rg = &script->regions[ri]; + u64 used = 0; + u64 free_bytes; + unsigned pct_int = 0, pct_frac = 0; + u32 j; + for (j = 0; j < img->nsegments; ++j) { + const LinkSegment* seg = &img->segments[j]; + u64 base = img->load_base + seg->vaddr; + /* 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) + used += seg->mem_size; + } + free_bytes = (rg->length > used) ? rg->length - used : 0; + if (rg->length) { + /* Two-decimal percentage without floating point: used*10000/length. */ + u64 scaled = (used * 10000ull) / rg->length; + pct_int = (unsigned)(scaled / 100ull); + pct_frac = (unsigned)(scaled % 100ull); + } + st = link_report_slice(out, + rg->name.s ? rg->name : KIT_SLICE_LIT("<unnamed>")); + if (st == KIT_OK) + st = link_reportf(out, " used=0x%llx free=0x%llx total=0x%llx %u.%02u%%\n", + (unsigned long long)used, + (unsigned long long)free_bytes, + (unsigned long long)rg->length, pct_int, pct_frac); + if (st != KIT_OK) return st; + } + return kit_writer_status(out); +} + typedef struct LinkReportArg { KitWriter* out; uint8_t format; @@ -911,6 +1189,55 @@ KitStatus kit_link_session_write_symbols(KitLinkSession* s, uint8_t format, return arg.st == KIT_OK ? kit_writer_status(out) : arg.st; } +static void link_session_cref_inner(KitLinkSession* s, void* arg) { + LinkReportArg* a = (LinkReportArg*)arg; + a->st = link_report_write_cref_image(s, a->out); +} + +static void link_session_memusage_inner(KitLinkSession* s, void* arg) { + LinkReportArg* a = (LinkReportArg*)arg; + a->st = link_report_write_memory_usage_image(s, a->out); +} + +KitStatus kit_link_session_write_cref(KitLinkSession* s, KitWriter* out) { + LinkReportArg arg; + KitStatus st; + if (!s || !out) return KIT_INVALID; + if ((KitLinkOutputKind)s->opts.output_kind == KIT_LINK_OUTPUT_RELOCATABLE || + (KitLinkOutputKind)s->opts.output_kind == KIT_LINK_OUTPUT_JIT) + return KIT_INVALID; + if (!s->resolved) { + st = kit_link_session_resolve(s); + if (st != KIT_OK) return st; + } + arg.out = out; + arg.format = KIT_LINK_SYMBOLS_NM; + arg.st = KIT_OK; + st = link_session_guard(s, link_session_cref_inner, &arg); + if (st != KIT_OK) return st; + return arg.st == KIT_OK ? kit_writer_status(out) : arg.st; +} + +KitStatus kit_link_session_write_memory_usage(KitLinkSession* s, + KitWriter* out) { + LinkReportArg arg; + KitStatus st; + if (!s || !out) return KIT_INVALID; + if ((KitLinkOutputKind)s->opts.output_kind == KIT_LINK_OUTPUT_RELOCATABLE || + (KitLinkOutputKind)s->opts.output_kind == KIT_LINK_OUTPUT_JIT) + return KIT_INVALID; + if (!s->resolved) { + st = kit_link_session_resolve(s); + if (st != KIT_OK) return st; + } + arg.out = out; + arg.format = KIT_LINK_SYMBOLS_NM; + arg.st = KIT_OK; + st = link_session_guard(s, link_session_memusage_inner, &arg); + if (st != KIT_OK) return st; + return arg.st == KIT_OK ? kit_writer_status(out) : arg.st; +} + typedef struct LinkEmitArg { KitWriter* out; } LinkEmitArg;