commit 1b5a5963a73fae9898dc2301f2cafd0a5a91d70f
parent 0463b6ab0c2e8e7f51c0aabca3a7730b2b3d2be7
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 11 May 2026 10:45:18 -0700
dbg: implement cfree_jit_view + image/runtime PC translation
Unblocks every DWARF-dependent REPL feature (b file:line, bt, p NAME,
info locals/args, source-level resume modes) by surfacing the JIT's
.debug_* sections through cfree_jit_view and translating PCs at every
DWARF call boundary.
LinkImage captures the input ObjBuilders at the tail of link_resolve so
they outlive link_free; cfree_jit_view lazily builds a private CfreeObjFile
that copies debug-section bytes and applies relocations against final
image vaddrs. cfree_jit_runtime_to_image / cfree_jit_image_to_runtime
expose the segment-table translation the driver and session-side step
loops need. Single-input v1; multi-input degrades to NULL pending
cross-CU offset adjustment.
Diffstat:
8 files changed, 464 insertions(+), 14 deletions(-)
diff --git a/driver/dbg.c b/driver/dbg.c
@@ -349,6 +349,47 @@ static void dbg_on_sigint(void* user) {
if (s && s->session) cfree_jit_session_interrupt(s->session);
}
+/* PC-space translation between the JIT runtime address space (where
+ * SIGTRAP fires and where the debugger installs breakpoints) and the
+ * image-relative vaddr space DWARF was authored in. Every DWARF call
+ * that takes a PC consumes an image vaddr, and every DWARF result
+ * that names a code address is image-relative — translate at the
+ * boundary. Fallback is pass-through so out-of-image PCs (e.g.
+ * stops inside libc on a future multi-input setup) don't return 0
+ * and silently degrade lookups. */
+static uint64_t dbg_pc_rt_to_img(DbgState* s, uint64_t rt) {
+ uint64_t v = cfree_jit_runtime_to_image(s->jit, rt);
+ return v ? v : rt;
+}
+static uint64_t dbg_pc_img_to_rt(DbgState* s, uint64_t img) {
+ uint64_t v = cfree_jit_image_to_runtime(s->jit, img);
+ return v ? v : img;
+}
+
+/* Build a frame view in image-PC space for DWARF queries that key off
+ * frame->pc (subprogram_at, param_iter_new, vars_at_new, unwind_step).
+ * The register snapshot and CFA stay in their original (runtime) form
+ * because dw_eval_expr / loc_read interpret them as live host values. */
+static CfreeUnwindFrame dbg_frame_for_dwarf(DbgState* s,
+ const CfreeUnwindFrame* rt) {
+ CfreeUnwindFrame out = *rt;
+ out.pc = dbg_pc_rt_to_img(s, rt->pc);
+ return out;
+}
+
+/* Translate any image-vaddr fields stored on a CfreeDwarfVarLoc back
+ * to runtime addresses before the loc is handed to a memory accessor
+ * (session_read_mem / session_write_mem operate in runtime space).
+ * Only DLOC_GLOBAL carries an absolute address straight from
+ * .debug_info; DLOC_REG / DLOC_FRAME_OFS / DLOC_EXPR derive their
+ * effective address from live register state or are evaluated against
+ * the frame, both of which are already in runtime space. */
+static void dbg_translate_loc(DbgState* s, CfreeDwarfVarLoc* loc) {
+ if (!loc) return;
+ if (loc->kind == CFREE_DLOC_GLOBAL)
+ loc->v.global = dbg_pc_img_to_rt(s, loc->v.global);
+}
+
/* ============================================================
* Tiny driver-local string utilities
* ============================================================
@@ -539,7 +580,7 @@ static int dbg_resolve_loc(DbgState* s, const char* spec, BpKind* kind_out,
}
driver_free(s->env, file, file_size);
*kind_out = BP_LINE;
- *addr_out = pc;
+ *addr_out = dbg_pc_img_to_rt(s, pc);
return 0;
}
@@ -626,7 +667,9 @@ static void dbg_print_pc(DbgState* s, uint64_t pc) {
driver_printf(" <%s>", sym);
}
if (s->dwarf &&
- cfree_dwarf_addr_to_line(s->dwarf, pc, &file, &line, &col) == 0 && file) {
+ cfree_dwarf_addr_to_line(s->dwarf, dbg_pc_rt_to_img(s, pc), &file, &line,
+ &col) == 0 &&
+ file) {
driver_printf(" at %s:%u", file, line);
if (col) driver_printf(":%u", col);
}
@@ -778,6 +821,7 @@ static void dbg_cmd_bt(DbgState* s) {
frame = s->last_stop.regs;
for (;;) {
CfreeDwarfSubprogram sp;
+ CfreeUnwindFrame img_frame;
int have_sp;
int step;
@@ -795,13 +839,14 @@ static void dbg_cmd_bt(DbgState* s) {
}
}
- have_sp = (cfree_dwarf_subprogram_at(s->dwarf, frame.pc, &sp) == 0);
+ img_frame = dbg_frame_for_dwarf(s, &frame);
+ have_sp = (cfree_dwarf_subprogram_at(s->dwarf, img_frame.pc, &sp) == 0);
if (have_sp && sp.name) {
CfreeDwarfParamIter* it;
CfreeDwarfVar p;
int first = 1;
driver_printf(" in %s%s (", sp.name, sp.inlined ? " [inlined]" : "");
- it = cfree_dwarf_param_iter_new(s->dwarf, frame.pc);
+ it = cfree_dwarf_param_iter_new(s->dwarf, img_frame.pc);
if (it) {
while (cfree_dwarf_param_iter_next(it, &p)) {
uint8_t stack_buf[64];
@@ -810,6 +855,7 @@ static void dbg_cmd_bt(DbgState* s) {
size_t got;
if (!first) driver_printf(", ");
driver_printf("%s=", p.name ? p.name : "?");
+ dbg_translate_loc(s, &p.loc);
if (dbg_read_value(s, &p.loc, &frame, stack_buf, sizeof(stack_buf),
&buf, &alloc, &got) == 0) {
dbg_print_value(s, p.loc.type, buf, got, 0);
@@ -828,8 +874,8 @@ static void dbg_cmd_bt(DbgState* s) {
const char* file = NULL;
uint32_t line = 0;
uint32_t col = 0;
- if (cfree_dwarf_addr_to_line(s->dwarf, frame.pc, &file, &line, &col) ==
- 0 &&
+ if (cfree_dwarf_addr_to_line(s->dwarf, img_frame.pc, &file, &line,
+ &col) == 0 &&
file) {
driver_printf(" at %s:%u", file, line);
if (col) driver_printf(":%u", col);
@@ -837,12 +883,19 @@ static void dbg_cmd_bt(DbgState* s) {
}
driver_printf("\n");
- step = cfree_dwarf_unwind_step(s->dwarf, &frame);
+ /* unwind_step keys off frame.pc as an image vaddr (FDE lookup) and
+ * writes the caller's return PC — read straight from registers or
+ * the stack — which is already a runtime address. Feed the image
+ * frame in, then copy back to `frame` so the next iteration's
+ * lookups translate from the new runtime PC. */
+ step = cfree_dwarf_unwind_step(s->dwarf, &img_frame);
if (step == 1) break; /* bottom of stack */
if (step != 0) {
driver_errf(DBG_TOOL, "unwind step failed");
break;
}
+ frame.cfa = img_frame.cfa;
+ frame.pc = img_frame.pc; /* runtime PC from the stack */
if (++level > 256) {
driver_errf(DBG_TOOL, "backtrace truncated at 256 frames");
break;
@@ -1092,7 +1145,9 @@ static void dbg_cmd_print(DbgState* s, const char* name) {
}
if (s->dwarf &&
- cfree_dwarf_var_at(s->dwarf, s->last_stop.regs.pc, name, &loc) == 0) {
+ cfree_dwarf_var_at(s->dwarf, dbg_pc_rt_to_img(s, s->last_stop.regs.pc),
+ name, &loc) == 0) {
+ dbg_translate_loc(s, &loc);
if (dbg_read_value(s, &loc, &s->last_stop.regs, stack_buf,
sizeof(stack_buf), &buf, &alloc, &got) != 0) {
driver_errf(DBG_TOOL, "could not read %s", name);
@@ -1141,10 +1196,12 @@ static void dbg_cmd_set(DbgState* s, const char* name, uint64_t value) {
return;
}
if (!s->dwarf ||
- cfree_dwarf_var_at(s->dwarf, s->last_stop.regs.pc, name, &loc) != 0) {
+ cfree_dwarf_var_at(s->dwarf, dbg_pc_rt_to_img(s, s->last_stop.regs.pc),
+ name, &loc) != 0) {
driver_errf(DBG_TOOL, "no variable named '%s'", name);
return;
}
+ dbg_translate_loc(s, &loc);
sz = (loc.byte_size == 0 || loc.byte_size > 8) ? 8 : loc.byte_size;
for (i = 0; i < sz; ++i) buf[i] = (uint8_t)(value >> (8 * i));
@@ -1226,7 +1283,8 @@ static void dbg_cmd_info_vars(DbgState* s, uint32_t mask, const char* label) {
return;
}
- it = cfree_dwarf_vars_at_new(s->dwarf, s->last_stop.regs.pc, mask);
+ it = cfree_dwarf_vars_at_new(s->dwarf,
+ dbg_pc_rt_to_img(s, s->last_stop.regs.pc), mask);
if (!it) {
driver_printf("No %s.\n", label);
return;
@@ -1237,6 +1295,7 @@ static void dbg_cmd_info_vars(DbgState* s, uint32_t mask, const char* label) {
size_t alloc;
size_t got;
printed = 1;
+ dbg_translate_loc(s, &v.loc);
if (dbg_read_value(s, &v.loc, &s->last_stop.regs, stack_buf,
sizeof(stack_buf), &buf, &alloc, &got) != 0) {
driver_printf(" %s = <unreadable>\n", v.name);
diff --git a/include/cfree.h b/include/cfree.h
@@ -433,6 +433,25 @@ const CfreeObjFile* cfree_jit_view(CfreeJit*);
int cfree_jit_addr_to_sym(CfreeJit*, uint64_t addr, const char** name_out,
uint64_t* off_out);
+/* PC-space translation between the JIT's runtime address space (where
+ * executable code actually lives) and the image-relative vaddr space
+ * (the coordinate system the linked image — and any DWARF emitted at
+ * compile time — was authored in).
+ *
+ * The DWARF consumer (cfree_dwarf_addr_to_line, cfree_dwarf_line_to_addr,
+ * cfree_dwarf_unwind_step, etc.) operates entirely in image-relative
+ * vaddrs; the debugger, host signal handlers, and breakpoint installer
+ * work in runtime addresses. Callers translate at every boundary.
+ *
+ * Both functions return 0 if the input is not contained in any mapped
+ * segment. Identity maps for the JIT's iplt / abs-symbol cases are out
+ * of scope here — those addresses don't participate in source-level
+ * stepping.
+ *
+ * Stable for the JIT's lifetime; constant-time over jit segment count. */
+uint64_t cfree_jit_runtime_to_image(CfreeJit*, uint64_t runtime_pc);
+uint64_t cfree_jit_image_to_runtime(CfreeJit*, uint64_t image_vaddr);
+
/* Enumerate every globally visible symbol in the resolved JIT image.
* Drives `info functions` / `info variables` and tab completion in dbg.
* `name` is interned and valid until cfree_jit_free; CfreeSymKind is the
diff --git a/src/api/pipeline.c b/src/api/pipeline.c
@@ -807,6 +807,40 @@ CfreeObjFile* cfree_obj_open(const CfreeEnv* env,
return f;
}
+/* Internal: allocate an empty CfreeObjFile with a freshly initialized
+ * private Compiler and an empty ObjBuilder. The caller populates the
+ * builder via the normal obj_section/obj_write/obj_reloc/obj_finalize
+ * surface and the returned CfreeObjFile is closed via cfree_obj_close
+ * (which compiler_fini's the private compiler and obj_free's the
+ * builder), exactly like a file produced by cfree_obj_open.
+ *
+ * Used by cfree_jit_view to surface the JIT's debug sections to the
+ * DWARF consumer without re-emitting the image as an on-disk ELF/Mach-O
+ * blob. Format (ELF / Mach-O / etc.) is the caller's choice — DWARF
+ * lookup is name-based, so ELF is the natural pick. */
+CfreeObjFile* cfree_objfile_empty_new(const CfreeEnv* env, CfreeTarget target,
+ CfreeObjFmt fmt) {
+ Heap* h;
+ CfreeObjFile* f;
+ if (!env || !env->heap) return NULL;
+ h = (Heap*)env->heap;
+ f = (CfreeObjFile*)h->alloc(h, sizeof(*f), _Alignof(CfreeObjFile));
+ if (!f) return NULL;
+ compiler_init(&f->compiler, target, env);
+ if (setjmp(f->compiler.panic)) {
+ compiler_run_cleanups(&f->compiler);
+ compiler_fini(&f->compiler);
+ h->free(h, f, sizeof(*f));
+ return NULL;
+ }
+ f->fmt = fmt;
+ f->sec_data_cache = NULL;
+ f->sec_data_size = NULL;
+ f->sec_data_n = 0;
+ f->ob = obj_new(&f->compiler);
+ return f;
+}
+
void cfree_obj_close(CfreeObjFile* f) {
Heap* h;
if (!f) return;
diff --git a/src/dbg/step.c b/src/dbg/step.c
@@ -13,6 +13,16 @@
#define DBG_AA64_BL_MASK 0xFC000000u
#define DBG_AA64_BL_OP 0x94000000u
+/* DWARF line/CFI tables are authored in image-relative vaddrs (cfree's
+ * debug emitter writes them, the JIT view applies relocs against final
+ * image vaddrs). Stop PCs and the values dropped onto the stack by
+ * BL/RET, on the other hand, live in runtime address space. Every
+ * DWARF call from the session translates at the boundary. */
+static uint64_t step_rt_to_img(CfreeJitSession* s, uint64_t pc) {
+ uint64_t v = cfree_jit_runtime_to_image(s->jit, pc);
+ return v ? v : pc;
+}
+
static int prepare_step_insn(CfreeJitSession* s) {
uint64_t pc = s->stop.regs.pc;
uint64_t scratch_entry = 0;
@@ -44,13 +54,14 @@ static int dwarf_line_for(CfreeJitSession* s, uint64_t pc, const char** file,
uint32_t col = 0;
*file = NULL;
*line = 0;
- return cfree_dwarf_addr_to_line(s->dwarf, pc, file, line, &col);
+ return cfree_dwarf_addr_to_line(s->dwarf, step_rt_to_img(s, pc), file, line,
+ &col);
}
static int dwarf_sub_for(CfreeJitSession* s, uint64_t pc,
CfreeDwarfSubprogram* out) {
memset(out, 0, sizeof(*out));
- return cfree_dwarf_subprogram_at(s->dwarf, pc, out);
+ return cfree_dwarf_subprogram_at(s->dwarf, step_rt_to_img(s, pc), out);
}
static int line_changed(const char* base_file, uint32_t base_line,
@@ -125,7 +136,11 @@ static int run_step_out(CfreeJitSession* s) {
CfreeUnwindFrame frame;
u32 bp_id = 0;
frame = s->stop.regs;
+ frame.pc = step_rt_to_img(s, frame.pc); /* CFI lookup is in image space */
if (cfree_dwarf_unwind_step(s->dwarf, &frame) != 0) return 1;
+ /* On success unwind_step writes frame.pc from the saved return-address
+ * register / stack slot — already a runtime PC, no inverse translation
+ * needed before the internal bp install. */
if (frame.pc == 0) return 1;
if (dbg_bp_set_internal(s, frame.pc, &bp_id) != 0) return 1;
if (dbg_session_signal_resume(s) != 0) return 1;
@@ -150,10 +165,12 @@ static int run_next_line(CfreeJitSession* s) {
{
CfreeUnwindFrame frame = s->stop.regs;
u32 bp_id = 0;
+ frame.pc = step_rt_to_img(s, frame.pc);
if (cfree_dwarf_unwind_step(s->dwarf, &frame) != 0 || frame.pc == 0) {
/* Fall back to stepping into the call. */
return run_step_line_loop(s);
}
+ /* frame.pc is now a runtime return-address (from the stack). */
if (dbg_bp_set_internal(s, frame.pc, &bp_id) != 0) {
return run_step_line_loop(s);
}
diff --git a/src/link/link.c b/src/link/link.c
@@ -346,6 +346,62 @@ void link_set_interp_path(Linker* l, const char* path) {
l->interp_path = (path && path[0]) ? pool_intern_cstr(l->c->global, path) : 0;
}
+/* ---- debug-input capture ----
+ *
+ * Called once at the tail of link_resolve. For each LinkInput, record
+ * its ObjBuilder on the LinkImage so the JIT debug view (cfree_jit_view)
+ * can read .debug_* sections after the Linker is freed. Two ownership
+ * regimes:
+ *
+ * LINK_INPUT_OBJ_BYTES: linker owns; transfer to the image and null
+ * the LinkInput's obj so linker_release doesn't double-free.
+ * LINK_INPUT_OBJ: caller owns; borrow the pointer (do not free
+ * at image teardown). Caller is responsible for keeping the
+ * builder alive at least as long as the JIT.
+ *
+ * DSO / archive-only inputs carry no source-level debug info worth
+ * surfacing through cfree_jit_view, so their slot stays NULL. */
+void link_capture_debug_inputs(Linker* l, LinkImage* img) {
+ u32 n;
+ u32 i;
+ Heap* h;
+ if (!l || !img) return;
+ n = LinkInputs_count(&l->inputs);
+ img->dbg_objs_n = n;
+ if (n == 0) {
+ img->dbg_objs = NULL;
+ img->dbg_objs_owned = NULL;
+ return;
+ }
+ h = img->heap;
+ img->dbg_objs = (ObjBuilder**)h->alloc(h, sizeof(*img->dbg_objs) * n,
+ _Alignof(ObjBuilder*));
+ img->dbg_objs_owned = (u8*)h->alloc(h, sizeof(*img->dbg_objs_owned) * n, 1u);
+ if (!img->dbg_objs || !img->dbg_objs_owned)
+ compiler_panic(img->c, no_loc(),
+ "link_capture_debug_inputs: oom on dbg arrays");
+ memset(img->dbg_objs, 0, sizeof(*img->dbg_objs) * n);
+ memset(img->dbg_objs_owned, 0, sizeof(*img->dbg_objs_owned) * n);
+ for (i = 0; i < n; ++i) {
+ LinkInput* in = LinkInputs_at(&l->inputs, i);
+ if (!in || !in->obj) continue;
+ switch (in->kind) {
+ case LINK_INPUT_OBJ_BYTES:
+ img->dbg_objs[i] = in->obj;
+ img->dbg_objs_owned[i] = 1u;
+ in->obj = NULL; /* transfer: linker_release must not free it */
+ break;
+ case LINK_INPUT_OBJ:
+ img->dbg_objs[i] = in->obj;
+ img->dbg_objs_owned[i] = 0u; /* borrowed; caller still owns */
+ break;
+ default:
+ /* DSO / TBD: skip — no user-level debug sections we expose. */
+ break;
+ }
+ }
+}
+
/* ---- LinkImage accessors ---- */
const LinkSymbol* link_symbol(LinkImage* img, LinkSymId id) {
@@ -428,6 +484,17 @@ static void link_image_release(LinkImage* img) {
img->heap->free(img->heap, img->input_maps,
sizeof(*img->input_maps) * img->ninput_maps);
}
+ if (img->dbg_objs) {
+ for (i = 0; i < img->dbg_objs_n; ++i) {
+ if (img->dbg_objs[i] && img->dbg_objs_owned && img->dbg_objs_owned[i])
+ obj_free(img->dbg_objs[i]);
+ }
+ img->heap->free(img->heap, img->dbg_objs,
+ sizeof(*img->dbg_objs) * img->dbg_objs_n);
+ if (img->dbg_objs_owned)
+ img->heap->free(img->heap, img->dbg_objs_owned,
+ sizeof(*img->dbg_objs_owned) * img->dbg_objs_n);
+ }
symhash_fini(&img->globals);
if (img->dyn) link_dyn_state_free(img);
img->heap->free(img->heap, img, sizeof(*img));
diff --git a/src/link/link_internal.h b/src/link/link_internal.h
@@ -112,6 +112,14 @@ struct Linker {
/* Defined in link_layout.c. */
void link_ingest_archives(struct Linker*);
+/* Defined in link.c. Walks the Linker's inputs and records each input's
+ * ObjBuilder on the LinkImage so the JIT debug view can reach its
+ * .debug_* sections after link_free runs. LINK_INPUT_OBJ_BYTES
+ * builders are moved (the LinkInput's obj pointer is nulled so
+ * linker_release skips them); LINK_INPUT_OBJ builders are borrowed
+ * (the caller still owns). DSO / TBD inputs are skipped. */
+void link_capture_debug_inputs(struct Linker*, LinkImage*);
+
/* Defined in link_dyn.c. Phase 4: synthesize .interp/.dynsym/.dynstr/
* .gnu.hash/.rela.dyn/.rela.plt/.plt/.got.plt/.dynamic when the link
* is producing a PIE / ET_DYN exe. No-op when there are zero imports
@@ -294,6 +302,22 @@ struct LinkImage {
InputMap* input_maps; /* one per input; indexed by input_id-1 */
u32 ninput_maps;
+ /* Debug-capture state for the JIT path. Populated by
+ * link_capture_debug_inputs at the tail of link_resolve so the input
+ * ObjBuilders (which carry .debug_* sections + their per-section
+ * relocations — neither consumed nor mutated by layout) survive the
+ * Linker's teardown and become reachable from cfree_jit_view.
+ *
+ * Parallel to input_maps: dbg_objs[i] is the ObjBuilder for input
+ * (i+1), or NULL when no debug info is present / the input kind isn't
+ * relevant (DSO/TBD). dbg_objs_owned[i] is 1 when the image must
+ * obj_free the builder at link_image_free (transferred from
+ * LINK_INPUT_OBJ_BYTES), 0 when borrowed (LINK_INPUT_OBJ — caller
+ * still owns). */
+ ObjBuilder** dbg_objs;
+ u8* dbg_objs_owned;
+ u32 dbg_objs_n;
+
/* 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_jit.c b/src/link/link_jit.c
@@ -12,11 +12,18 @@
#include <string.h>
#include "core/bytes.h"
+#include "core/buf.h"
#include "core/heap.h"
#include "core/pool.h"
#include "core/util.h"
#include "link/link.h"
#include "link/link_internal.h"
+#include "obj/obj.h"
+
+/* Defined in src/api/pipeline.c — allocates an empty CfreeObjFile with
+ * a private Compiler and an empty ObjBuilder for caller population. */
+CfreeObjFile* cfree_objfile_empty_new(const CfreeEnv* env, CfreeTarget target,
+ CfreeObjFmt fmt);
static SrcLoc no_loc(void) {
SrcLoc l = {0, 0, 0};
@@ -48,6 +55,14 @@ struct CfreeJit {
LinkImage* image;
CfreeExecMemRegion* segs; /* one per image->nsegments */
u32 nsegs;
+ /* DWARF view, lazily constructed on first cfree_jit_view call. Built
+ * over a private Compiler so its string pools and the new ObjBuilder
+ * are owned end-to-end by the view; freed in cfree_jit_free. NULL
+ * means "not yet built"; view_built distinguishes "tried and gave up"
+ * (multi-input v1, etc.) from "untried". */
+ CfreeObjFile* view;
+ u8 view_built;
+ u8 pad[7];
};
/* AArch64 ELF ABI: TP points 16 bytes before the TLS image; TLSLE
@@ -269,6 +284,8 @@ CfreeJit* cfree_jit_from_image(LinkImage* img) {
jit->image = img;
jit->segs = segs;
jit->nsegs = img->nsegments;
+ jit->view = NULL;
+ jit->view_built = 0u;
/* Take ownership of the image: undefer it from the compiler so a
* future panic doesn't reap something we still hold. */
@@ -300,6 +317,13 @@ void cfree_jit_free(CfreeJit* jit) {
if (!jit) return;
heap = (Heap*)jit->c->env->heap;
mem = jit->c->env->execmem;
+ /* The debug view (if built) is closed first — it owns a private
+ * Compiler whose pools must be released before the image's
+ * referenced builders are freed in link_image_free. */
+ if (jit->view) {
+ cfree_obj_close(jit->view);
+ jit->view = NULL;
+ }
if (jit->segs && mem && mem->release) {
for (i = 0; i < jit->nsegs; ++i) {
if (jit->segs[i].size) mem->release(mem->user, &jit->segs[i]);
@@ -334,9 +358,171 @@ void* cfree_jit_lookup(CfreeJit* jit, const char* name) {
/* ---- inspector entries ---- */
+/* True if `name` (NUL-terminated) is a debug section the DWARF consumer
+ * (src/dwarf/dwarf_open.c) might read. Everything else is skipped. */
+static int jit_view_is_debug_name(const char* name) {
+ if (!name) return 0;
+ if (name[0] == '.' && name[1] == 'd' && name[2] == 'e' && name[3] == 'b' &&
+ name[4] == 'u' && name[5] == 'g' && name[6] == '_')
+ return 1; /* .debug_* */
+ /* .eh_frame is consulted by cfree_dwarf for CFI unwinding when
+ * available; cfree itself doesn't currently emit it, but inputs read
+ * from external .o files may carry it. */
+ if (name[0] == '.' && name[1] == 'e' && name[2] == 'h' && name[3] == '_' &&
+ name[4] == 'f' && name[5] == 'r' && name[6] == 'a' && name[7] == 'm' &&
+ name[8] == 'e' && name[9] == '\0')
+ return 1;
+ return 0;
+}
+
+/* True if input `ii` carries any debug section that's worth surfacing.
+ * Cheap walk over the input's section table. */
+static int jit_view_input_has_debug(CfreeJit* jit, u32 ii) {
+ ObjBuilder* ob;
+ u32 nsec, k;
+ if (ii >= jit->image->dbg_objs_n) return 0;
+ ob = jit->image->dbg_objs[ii];
+ if (!ob) return 0;
+ nsec = obj_section_count(ob);
+ for (k = 0; k < nsec; ++k) {
+ const Section* s = obj_section_get(ob, (ObjSecId)(k + 1));
+ const char* nm;
+ if (!s || !s->name) continue;
+ nm = pool_str(jit->c->global, s->name, NULL);
+ if (jit_view_is_debug_name(nm)) return 1;
+ }
+ return 0;
+}
+
+/* Resolve an input-local ObjSymId to the final image vaddr of the
+ * defining LinkSymbol, going through the per-input InputMap and the
+ * image's LinkSyms table. Returns 0 if the symbol is undefined or the
+ * mapping is missing — debug relocations against unresolved symbols
+ * collapse to zero, which is the DWARF "absent" convention. */
+static u64 jit_view_sym_vaddr(CfreeJit* jit, u32 ii, ObjSymId obj_sym) {
+ const InputMap* m;
+ LinkSymId lid;
+ const LinkSymbol* s;
+ if (obj_sym == OBJ_SYM_NONE) return 0;
+ if (ii >= jit->image->ninput_maps) return 0;
+ m = &jit->image->input_maps[ii];
+ if (!m->sym || obj_sym >= m->nsym) return 0;
+ lid = m->sym[obj_sym];
+ if (lid == LINK_SYM_NONE || lid > LinkSyms_count(&jit->image->syms)) return 0;
+ s = LinkSyms_at(&jit->image->syms, lid - 1);
+ if (!s || !s->defined) return 0;
+ return s->vaddr; /* image-relative — what DWARF was emitted in */
+}
+
+/* Copy one debug section from input `ii` into `view_ob` with its
+ * relocations applied against final image vaddrs. Relocations in
+ * .debug_* are almost universally R_ABS{32,64} against a code symbol;
+ * link_reloc_apply ignores its P argument for those kinds, so we pass
+ * P=0. */
+static void jit_view_copy_debug_section(CfreeJit* jit, u32 ii,
+ ObjSecId in_sec_id,
+ ObjBuilder* view_ob) {
+ ObjBuilder* in_ob = jit->image->dbg_objs[ii];
+ const Section* in_sec = obj_section_get(in_ob, in_sec_id);
+ Heap* h;
+ u32 nbytes, k, total_relocs;
+ const char* nm;
+ Sym view_name;
+ ObjSecId out_id;
+ u8* bytes;
+ if (!in_sec) return;
+ nbytes = in_sec->bytes.total;
+ if (nbytes == 0) return;
+ h = (Heap*)jit->c->env->heap;
+
+ nm = pool_str(jit->c->global, in_sec->name, NULL);
+ if (!nm) return;
+ view_name = pool_intern_cstr(obj_compiler(view_ob)->global, nm);
+ out_id = obj_section_ex(view_ob, view_name, SEC_DEBUG, SSEM_PROGBITS,
+ in_sec->flags, in_sec->align ? in_sec->align : 1u,
+ in_sec->entsize, 0, 0);
+
+ bytes = (u8*)h->alloc(h, nbytes, 1);
+ if (!bytes) return;
+ buf_flatten(&in_sec->bytes, bytes);
+
+ /* Apply this section's relocations in place. obj_reloc_at returns
+ * all relocations across the input; filter by section_id. */
+ total_relocs = obj_reloc_total(in_ob);
+ for (k = 0; k < total_relocs; ++k) {
+ const Reloc* r = obj_reloc_at(in_ob, k);
+ u64 S;
+ if (!r || r->section_id != in_sec_id) continue;
+ if (r->offset >= nbytes) continue; /* malformed; skip */
+ S = jit_view_sym_vaddr(jit, ii, r->sym);
+ /* P is unused by ABS kinds; PC-relative debug-section relocs are
+ * not produced by cfree's debug emitter, but if some external .o
+ * carried one against a debug section, P=0 would give a
+ * non-meaningful offset — acceptable for a viewer that only reads
+ * absolute address fields. */
+ link_reloc_apply(jit->c, (RelocKind)r->kind, bytes + r->offset, S,
+ r->addend, 0);
+ }
+
+ obj_write(view_ob, out_id, bytes, nbytes);
+ h->free(h, bytes, nbytes);
+}
+
+/* Build the view on first call. Returns NULL if no input carries
+ * debug info, or if more than one does (v1 doesn't concatenate cross-
+ * CU references; see doc/DBG.md §12). */
+static CfreeObjFile* jit_view_build(CfreeJit* jit) {
+ u32 i, dbg_input_ii = UINT32_MAX, n_with_debug = 0;
+ CfreeObjFile* view;
+ ObjBuilder* view_ob;
+ u32 nsec, k;
+
+ if (!jit->image || jit->image->dbg_objs_n == 0) return NULL;
+
+ for (i = 0; i < jit->image->dbg_objs_n; ++i) {
+ if (jit_view_input_has_debug(jit, i)) {
+ dbg_input_ii = i;
+ ++n_with_debug;
+ }
+ }
+ if (n_with_debug == 0) return NULL;
+ if (n_with_debug > 1) {
+ /* v1 limitation: cross-CU offset adjustment for concatenated
+ * .debug_abbrev / .debug_str / .debug_str_offsets isn't wired up
+ * yet. Single-TU dbg sessions are the supported shape. */
+ return NULL;
+ }
+
+ view =
+ cfree_objfile_empty_new(jit->c->env, jit->c->target, jit->c->target.obj);
+ if (!view) return NULL;
+ view_ob = cfree_obj_builder(view);
+ if (!view_ob) {
+ cfree_obj_close(view);
+ return NULL;
+ }
+
+ nsec = obj_section_count(jit->image->dbg_objs[dbg_input_ii]);
+ for (k = 0; k < nsec; ++k) {
+ const Section* s = obj_section_get(jit->image->dbg_objs[dbg_input_ii],
+ (ObjSecId)(k + 1));
+ const char* nm;
+ if (!s || !s->name) continue;
+ nm = pool_str(jit->c->global, s->name, NULL);
+ if (!jit_view_is_debug_name(nm)) continue;
+ jit_view_copy_debug_section(jit, dbg_input_ii, (ObjSecId)(k + 1), view_ob);
+ }
+
+ obj_finalize(view_ob);
+ return view;
+}
+
const CfreeObjFile* cfree_jit_view(CfreeJit* jit) {
- (void)jit;
- return NULL;
+ if (!jit) return NULL;
+ if (jit->view_built) return jit->view;
+ jit->view = jit_view_build(jit);
+ jit->view_built = 1u;
+ return jit->view;
}
/* True for symbol kinds the user-facing JIT inspector surfaces. Mapping
@@ -448,6 +634,43 @@ int cfree_jit_image_contains(CfreeJit* jit, uint64_t runtime_addr) {
return 0;
}
+/* runtime <-> image vaddr translation. The pair is symmetric with
+ * vaddr_to_runtime above; exposed here so dwarf consumers and the
+ * driver can cross the boundary at every DWARF call. Walks the
+ * segment table (at most a handful of entries) so callers can do this
+ * per stop without measurable cost. */
+uint64_t cfree_jit_runtime_to_image(CfreeJit* jit, uint64_t runtime_pc) {
+ u32 i;
+ uintptr_t a;
+ if (!jit || !jit->segs) return 0;
+ a = (uintptr_t)runtime_pc;
+ for (i = 0; i < jit->nsegs; ++i) {
+ uintptr_t lo = (uintptr_t)jit->segs[i].runtime;
+ uintptr_t hi = lo + (uintptr_t)jit->segs[i].size;
+ if (a >= lo && a < hi) {
+ const LinkSegment* s = &jit->image->segments[i];
+ return s->vaddr + (uint64_t)(a - lo);
+ }
+ }
+ /* One-past-end: lets a return-address that sits exactly at a
+ * segment's end-boundary still round-trip. */
+ for (i = 0; i < jit->nsegs; ++i) {
+ uintptr_t hi = (uintptr_t)jit->segs[i].runtime + (uintptr_t)jit->segs[i].size;
+ if (a == hi) {
+ const LinkSegment* s = &jit->image->segments[i];
+ return s->vaddr + s->mem_size;
+ }
+ }
+ return 0;
+}
+
+uint64_t cfree_jit_image_to_runtime(CfreeJit* jit, uint64_t image_vaddr) {
+ uintptr_t rt;
+ if (!jit || !jit->segs) return 0;
+ rt = vaddr_to_runtime(jit->image, jit->segs, image_vaddr);
+ return (uint64_t)rt;
+}
+
CfreeArchKind cfree_jit_image_arch(CfreeJit* jit) {
return jit->c->target.arch;
}
diff --git a/src/link/link_layout.c b/src/link/link_layout.c
@@ -2970,5 +2970,12 @@ LinkImage* link_resolve(Linker* l) {
gc_live_free(&g, h);
}
+ /* Hand the input ObjBuilders to the image so cfree_jit_view can
+ * surface .debug_* sections after link_free runs (layout/reloc are
+ * complete, so the builders are otherwise idle). Must be the last
+ * step before returning — any pass that walks LinkInputs.obj
+ * expecting a value would break otherwise. */
+ link_capture_debug_inputs(l, img);
+
return img;
}