commit ef28221b5e51646d11fd54686a23349b93007d02
parent df67c1c69c18c95b176eaf6dc0c256e3e1046428
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 27 May 2026 11:20:43 -0700
aa64: wire DWARF debug emission through the native_direct path
The semantic-CgTarget cutover dropped the -g wiring on the aa64 backend
(the only compiled native target), so cc -g produced no .debug_* sections:
- CgTarget lost its debug field and begin_obj hardcoded g->debug=NULL;
restore the field and read it back from the backend-made target.
- aa64 make() discarded the Debug object; keep it on the target.
- aa_emit32 now records a line row per instruction (as x64/rv64 do).
- aa_func_end now reports the function PC range, so emit_section_line
no longer skips every function.
- The producer seeds the CU's primary source file before emitting the
root DIE, so DW_AT_name/comp_dir are populated.
addr2line now resolves cc -g objects; verified via test/driver addr2line
cases and CFREE_OPT_LEVELS=0 CFREE_TEST_PATHS=R toy run (254/0).
Diffstat:
5 files changed, 40 insertions(+), 2 deletions(-)
diff --git a/src/arch/aa64/arch.c b/src/arch/aa64/arch.c
@@ -118,13 +118,13 @@ static CgTarget* aa64_backend_make(Compiler* c, ObjBuilder* o,
NativeTarget* native;
NativeDirectTargetConfig cfg;
if (cg_mc_debug_new(c, o, opts, &mc, &debug) != CFREE_OK) return NULL;
- (void)debug;
native = aa64_native_target_new(c, o, mc);
if (!native) return NULL;
memset(&cfg, 0, sizeof cfg);
cfg.native = native;
cfg.ops = aa64_native_direct_ops();
t = native_direct_target_new(c, o, &cfg);
+ if (t) t->debug = debug;
return t;
}
diff --git a/src/arch/aa64/native.c b/src/arch/aa64/native.c
@@ -129,10 +129,20 @@ static void aa_panic(AANativeTarget* a, const char* msg) {
compiler_panic(a->base.c, a->loc, "aarch64 native target: %s", msg);
}
+/* Declared locally rather than pulling in debug/debug.h, keeping the
+ * backend's dependency on the Debug producer to this one entry point —
+ * same pattern as the x64/rv64 emit TUs (see arch/mc.h). */
+extern void debug_emit_row(Debug*, ObjSecId text_section, u32 offset, SrcLoc);
+extern void debug_func_pc_range(Debug*, ObjSecId text_section, u32 begin_ofs,
+ u32 end_ofs);
+
static void aa_emit32(MCEmitter* mc, u32 word) {
u8 b[4];
+ u32 ofs = obj_pos(mc->obj, mc->section_id);
wr_u32_le(b, word);
mc->emit_bytes(mc, b, sizeof b);
+ /* Record one line-table row per instruction start (no-op when not -g). */
+ if (mc->debug) debug_emit_row(mc->debug, mc->section_id, ofs, mc->loc);
}
static void aa_patch32(ObjBuilder* obj, ObjSecId sec, u32 off, u32 word) {
@@ -1054,6 +1064,12 @@ static void aa_func_end(NativeTarget* t) {
obj_atom_define(t->obj, a->func->text_section_id, a->func_start,
mc->pos(mc) - a->func_start, a->func->sym, 0);
}
+ /* Hand the function's PC range to the Debug producer so its line program
+ * (and DW_AT_low_pc/high_pc) cover this function — emit_section_line skips
+ * functions without a recorded range. */
+ if (mc->debug)
+ debug_func_pc_range(mc->debug, a->func->text_section_id, a->func_start,
+ mc->pos(mc));
if (mc->cfi_endproc) mc->cfi_endproc(mc);
mc_end_function(mc);
a->func = NULL;
diff --git a/src/cg/cgtarget.h b/src/cg/cgtarget.h
@@ -380,12 +380,22 @@ typedef struct CGLocalStaticDataDesc {
u32 align;
} CGLocalStaticDataDesc;
+/* Forward-declared (same as arch/mc.h) so a CgTarget can carry an optional
+ * Debug producer without this header depending on debug/debug.h. */
+typedef struct Debug Debug;
+
typedef struct CgTarget CgTarget;
struct CgTarget {
/* Typed IR lowering context. Subclasses extend. */
Compiler* c;
ObjBuilder* obj;
+ /* Optional DWARF producer, created by the backend's `make` when
+ * opts->debug_info is set (else NULL). The session reads this back into
+ * its own g->debug to drive func/line/emit; the backend's MCEmitter
+ * shares the same object for line-row emission. */
+ Debug* debug;
+
/* ---- function lifecycle ---- */
void (*func_begin)(CgTarget*, const CGFuncDesc*);
void (*func_end)(CgTarget*);
diff --git a/src/cg/session.c b/src/cg/session.c
@@ -106,6 +106,11 @@ CfreeStatus cfree_cg_begin_obj(CfreeCg* g, CfreeObjBuilder* out,
if (!backend) return CFREE_UNSUPPORTED;
target = backend->make((Compiler*)c, (ObjBuilder*)out, opts);
if (!target) return CFREE_UNSUPPORTED;
+ /* The backend's make() creates the DWARF producer when opts->debug_info
+ * is set (NULL otherwise); capture it before any optimizer wrapper so the
+ * session can drive func/line/emit. The backend's MCEmitter holds the
+ * same Debug for line-row emission. */
+ g->debug = target->debug;
#if CFREE_OPT_ENABLED
if (opt_level > 0) {
target = opt_cgtarget_new((Compiler*)c, target, opt_level);
@@ -114,7 +119,6 @@ CfreeStatus cfree_cg_begin_obj(CfreeCg* g, CfreeObjBuilder* out,
#endif
g->obj = (ObjBuilder*)out;
g->target = target;
- g->debug = NULL;
g->opt_level = opt_level;
g->check_only = (opts && opts->check_only) ? 1u : 0u;
g->function_sections = (opts && opts->function_sections) ? 1u : 0u;
diff --git a/src/debug/debug_emit.c b/src/debug/debug_emit.c
@@ -1290,6 +1290,14 @@ void debug_emit(Debug* d) {
ec.ssym_str_off = mk_section_sym(&ec, ec.sec_str_off);
producer_sym = pool_intern_slice(pool, SLICE_LIT("cfree 0.1"));
+ /* Ensure the CU's primary source file occupies file-table slot 0 before
+ * we read it for DW_AT_name/comp_dir. debug_file() is otherwise first
+ * invoked later (child-DIE decl_file / line program), so without this the
+ * CU name and comp_dir come out empty. Seed it from the first function's
+ * declaration site. */
+ if (d->nfiles == 0 && d->nfuncs > 0) {
+ (void)debug_file(d, d->funcs[0].decl.file_id);
+ }
if (d->nfiles > 0) {
primary_dir = d->files[0].dir;
primary_base = d->files[0].base;