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:
| M | src/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;