From f7aa7b75e7759b9751bb2df471571c9f4a51e946 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sun, 12 Jul 2026 12:51:29 -0400 Subject: [PATCH] doing dwarf injectiion/mods for the tape atoms. syncs with vscode cursor. --- .vscode/launch.json | 26 + code/gte_hello/hello_gte_tape.c | 40 +- scripts/build_psyq.ps1 | 49 ++ scripts/passes/dwarf_injection.lua | 777 +++++++++++++++++++++++++++++ scripts/ps1_meta.lua | 28 +- 5 files changed, 896 insertions(+), 24 deletions(-) create mode 100644 scripts/passes/dwarf_injection.lua diff --git a/.vscode/launch.json b/.vscode/launch.json index a47060a..defb6ef 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -76,6 +76,32 @@ "tbreak main", "continue" ] + }, + { + "name": "Debug: Hello GTE Psy-Q! (atoms debug — DWARF-injected)", + "type": "gdb", + "request": "attach", + "target": "localhost:3333", + "remote": true, + "cwd": "${workspaceRoot}", + "valuesFormatting": "parseText", + "stopAtConnect": true, + "gdbpath": "gdb-multiarch", + "windows": { + "gdbpath": "gdb-multiarch.exe" + }, + "osx": { + "gdbpath": "gdb" + }, + "executable": "${workspaceRoot}/build/hello_gte.dwarf-injected.elf", + "autorun": [ + "monitor reset shellhalt", + "load build/hello_gte.dwarf-injected.elf", + "source scripts/gdb/gdb_tape_atoms.gdb", + "tbreak main", + "continue" + ] } ] } + diff --git a/code/gte_hello/hello_gte_tape.c b/code/gte_hello/hello_gte_tape.c index 09c1584..d10ef2b 100644 --- a/code/gte_hello/hello_gte_tape.c +++ b/code/gte_hello/hello_gte_tape.c @@ -27,10 +27,10 @@ internal MipsAtom_(rbind_cube_g4_face) atom_info(atom_bind(Binds_CubeTri) , atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase) ){ /* Pop 4 arguments from the tape directly into the workspace registers */ - load_word(R_PrimCursor, R_TapePtr, O_(Binds_CubeTri,PrimCursor)), - load_word(R_FaceCursor, R_TapePtr, O_(Binds_CubeTri,FaceCursor)), - load_word(R_VertBase, R_TapePtr, O_(Binds_CubeTri,VertBase)), - load_word(R_OtBase, R_TapePtr, O_(Binds_CubeTri,OtBase)), + load_word(R_PrimCursor, R_TapePtr, O_(Binds_CubeTri,PrimCursor)), + load_word(R_FaceCursor, R_TapePtr, O_(Binds_CubeTri,FaceCursor)), + load_word(R_VertBase, R_TapePtr, O_(Binds_CubeTri,VertBase)), + load_word(R_OtBase, R_TapePtr, O_(Binds_CubeTri,OtBase)), add_ui_self( R_TapePtr, S_(Binds_CubeTri)), mac_yield() }; @@ -41,7 +41,7 @@ internal MipsAtom_(rbind_cube_g4_face) atom_info(atom_bind(Binds_CubeTri) * Reads 4 indices from R_FaceCur (V4_S2 = 8 bytes), loads 4 vertices into * the GTE, runs the PsyQ RotAverageNclip4 sequence, and renders a Poly_G4. */ -internal +internal MipsAtom_(cube_g4_face) atom_info( atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase), atom_writes(R_PrimCursor, R_FaceCursor) @@ -94,21 +94,21 @@ typedef Struct_(Binds_FloorTri) { V3_S2* VertBase; U4* OtBase; }; -internal +internal MipsAtom_(rbind_floor_f3_face) atom_info(atom_bind(Binds_FloorTri) , atom_reads(R_TapePtr) , atom_writes(R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase) ){ /* Pop 4 arguments from the tape directly into the workspace registers */ - load_word(R_PrimCursor, R_TapePtr, O_(Binds_FloorTri,PrimCursor)), - load_word(R_FaceCursor, R_TapePtr, O_(Binds_FloorTri,FaceCursor)), - load_word(R_VertBase, R_TapePtr, O_(Binds_FloorTri,VertBase)), - load_word(R_OtBase, R_TapePtr, O_(Binds_FloorTri,OtBase)), + load_word(R_PrimCursor, R_TapePtr, O_(Binds_FloorTri,PrimCursor)), + load_word(R_FaceCursor, R_TapePtr, O_(Binds_FloorTri,FaceCursor)), + load_word(R_VertBase, R_TapePtr, O_(Binds_FloorTri,VertBase)), + load_word(R_OtBase, R_TapePtr, O_(Binds_FloorTri,OtBase)), add_ui_self( R_TapePtr, S_(Binds_FloorTri)), mac_yield() }; -internal +internal MipsAtom_(floor_f3_face) atom_info( , atom_reads( R_PrimCursor, R_FaceCursor, R_VertBase, R_OtBase) , atom_writes(R_PrimCursor, R_FaceCursor) @@ -117,7 +117,7 @@ MipsAtom_(floor_f3_face) atom_info( mac_gte_load_tri_verts(R_T0, R_T1, R_T2), nop2, gte_cmdw_rotate_translate_perspective_triple, nop2, gte_cmdw_nclip, - + /* Culling (Branch forward if Backface) */ nop2, gte_mv_from_data_r(R_T0, C2_MAC0), nop, @@ -125,21 +125,21 @@ MipsAtom_(floor_f3_face) atom_info( /* Format Primitive */ mac_format_f3_color(0xFF, 0xFF, 0xFF), // RGB-form (R=FF, G=FF, B=FF = white) mac_gte_store_f3_post_rtpt(), - + /* Calculate Depth */ - nop2, gte_avg_sort_z3, - nop2, gte_mv_from_data_r(R_T1, C2_OTZ), + nop2, gte_avg_sort_z3, + nop2, gte_mv_from_data_r(R_T1, C2_OTZ), /* Bounds Check OTZ < 2048 (Branch forward to skip insertion) */ - add_ui( R_AT, R_0, OrderingTbl_Len), - set_lt_u( R_AT, R_T1, R_AT), - branch_equal(R_AT, R_0, atom_offset(bounds_chk, floor_f3_face_exit)), nop, + add_ui( R_AT, R_0, OrderingTbl_Len), + set_lt_u( R_AT, R_T1, R_AT), + branch_equal(R_AT, R_0, atom_offset(bounds_chk, floor_f3_face_exit)), nop, /* Insert into Ordering Table Linked List */ mac_insert_ot_tag_f3(), add_ui_self(R_PrimCursor, S_(Poly_F3)), /* Advance Prim Cursor (5 words) */ // Note(Ed): No bounds checking, should be checked before atom runs. /* Advance Input Cursor & Yield (Both branch targets land here) */ -atom_label(floor_f3_face_exit) +atom_label(floor_f3_face_exit) add_ui_self(R_FaceCursor, S_(S2) * 4), /* Advance Face Cursor (4 * S2 = 8 bytes) */ mac_yield() }; @@ -150,7 +150,7 @@ internal MipsAtom_(sync_primitive_arena) atom_info(atom_bind(Binds_SyncPrimitive , atom_writes(R_TapePtr) ){ load_word(R_AT, R_TapePtr, O_(Binds_SyncPrimitiveArena,used)), - load_word(R_T0, R_TapePtr, O_(Binds_SyncPrimitiveArena,cursor)), + load_word(R_T0, R_TapePtr, O_(Binds_SyncPrimitiveArena,cursor)), add_ui_self( R_TapePtr, S_(Binds_SyncPrimitiveArena)), /* Calculate byte offset and store directly back to RAM */ sub_u( R_T0, R_PrimCursor, R_T0), // R_T0 = R_PrimCursor - binds.cursor diff --git a/scripts/build_psyq.ps1 b/scripts/build_psyq.ps1 index 82ca13a..90941b6 100644 --- a/scripts/build_psyq.ps1 +++ b/scripts/build_psyq.ps1 @@ -394,6 +394,55 @@ function build-gte_hello { -out_root (join-path $path_build 'gen') ` -passes @('--gdb-runtime') ` -extra_args @('--elf', $elf) + ps1-meta -sources $atom_sources -metadata $path_atom_metadata ` + -out_root (join-path $path_build 'gen') ` + -passes @('--dwarf-injection') ` + -extra_args @('--elf', $elf) + + # F' track: post-link DWARF injection. The new Lua pass writes + # build/gen/.dwarf_*.bin blobs; we splice them into a COPY + # of the ELF via objcopy --update-section (works fine from PowerShell). + # The un-injected $elf + $exe are unchanged (shipping binary). + $dwarfLineBin = Join-Path (Join-Path $path_build 'gen') 'hello_gte.dwarf_line.bin' + $dwarfArangesBin = Join-Path (Join-Path $path_build 'gen') 'hello_gte.dwarf_aranges.bin' + $dwarfRnglistsBin = Join-Path (Join-Path $path_build 'gen') 'hello_gte.dwarf_rnglists.bin' + $injectElf = Join-Path $path_build 'hello_gte.dwarf-injected.elf' + if ((Test-Path $dwarfLineBin) -and (Test-Path $dwarfArangesBin) -and (Test-Path $dwarfRnglistsBin)) { + Write-Host "[build] DWARF-injecting $elf -> $injectElf" + Copy-Item -LiteralPath $elf -Destination $injectElf + & $Objcopy --update-section ".debug_line=$dwarfLineBin" $injectElf + if ($LASTEXITCODE -ne 0) { + Write-Warning "[build] objcopy .debug_line update failed (exit $LASTEXITCODE); removing $injectElf" + Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue + } else { + & $Objcopy --update-section ".debug_aranges=$dwarfArangesBin" $injectElf + if ($LASTEXITCODE -ne 0) { + Write-Warning "[build] objcopy .debug_aranges update failed (exit $LASTEXITCODE); removing $injectElf" + Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue + } else { + & $Objcopy --update-section ".debug_rnglists=$dwarfRnglistsBin" $injectElf + if ($LASTEXITCODE -ne 0) { + Write-Warning "[build] objcopy .debug_rnglists update failed (exit $LASTEXITCODE); removing $injectElf" + Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue + } else { + # Baked atoms execute from RAM but are emitted as C data arrays, so + # their ELF sections lack SHF_EXECINSTR. GDB discards line rows for + # non-code sections. Mark only the debug-copy sections executable; + # the shipping ELF and PS-EXE remain byte/flag unchanged. + & $Objcopy ` + --set-section-flags ".rodata=alloc,load,readonly,code,contents" ` + --set-section-flags ".data=alloc,load,data,code,contents" ` + $injectElf + if ($LASTEXITCODE -ne 0) { + Write-Warning "[build] objcopy atom-section flag update failed (exit $LASTEXITCODE); removing $injectElf" + Remove-Item -LiteralPath $injectElf -ErrorAction SilentlyContinue + } else { + Write-Host "[build] DWARF-injected ELF: $injectElf" + } + } + } + } + } } build-gte_hello diff --git a/scripts/passes/dwarf_injection.lua b/scripts/passes/dwarf_injection.lua new file mode 100644 index 0000000..4ff566b --- /dev/null +++ b/scripts/passes/dwarf_injection.lua @@ -0,0 +1,777 @@ +--- passes/dwarf_injection.lua — Per-atom DWARF injection for tape-atom step-debug (F'). +--- +--- Reads the post-link ELF directly (lfs + io.open; walks the ELF32 section +--- header table to find `.debug_line` + `.debug_aranges` + `.debug_rnglists`), +--- APPENDS synthetic DWARF line-program sequences for every `code_` atom, +--- EXTENDS the `.debug_aranges` and main-CU range tables with the atom ranges. +--- Writes the new section data to `/.dwarf_*.bin`. +--- +--- The `build_psyq.ps1` post-link hook then splices those `.bin` files into a copy of the ELF via: +--- mipsel-none-elf-objcopy --update-section .debug_line= +--- mipsel-none-elf-objcopy --update-section .debug_aranges= +--- mipsel-none-elf-objcopy --update-section .debug_rnglists= +--- (Splice step runs from PowerShell — no Lua subprocess; no cmd /c parsing issues. +--- objcopy's --update-section works fine in PowerShell even though Lua's `os.execute`/`io.popen` would mangle the `=` on Windows.) +--- +--- Result: VSCode's source gutter follows per-stepi inside atom bodies. +--- Native VSCode UX (gutter arrow + highlighted line + Run to Cursor + conditional BPs by source line). +--- No VSCode plugin, no Python, no pyelftools — pure Lua + objcopy. +--- +--- **Conventions:** tabs (1/level), EmmyLua annotations, no regex, +--- Lua 5.3 compatible. + +-- ════════════════════════════════════════════════════════════════════════════ +-- Bootstrap +-- ════════════════════════════════════════════════════════════════════════════ + +-- Load `duffle_paths.lua` via `debug.getinfo(1, "S").source` (works both +-- standalone + when require'd). Sets package.path + package.cpath then +-- returns duffle. +local _bootstrap_dir = debug.getinfo(1, "S").source:match("^@?(.*[/\\])") or "./" +local duffle = dofile(_bootstrap_dir .. "../duffle_paths.lua") + +-- Native `lfs` for file/directory operations. +local lfs = require("lfs") + +-- ════════════════════════════════════════════════════════════════════════════ +-- Constants +-- ════════════════════════════════════════════════════════════════════════════ + +-- DWARF line-program opcodes (per raddebugger's `dwarf_writer.c`). +-- Standard opcodes 1-12 are single-byte + payload. +local DW_LNS_copy = 1 +local DW_LNS_advance_pc = 2 +local DW_LNS_advance_line = 3 +local DW_LNS_set_file = 4 + +-- Byte 0 introduces every extended opcode. `opcode_base` is instead the first +-- special opcode and must never be used as the extended marker. +local DW_LNS_extended = 0 + +-- Extended sub-opcodes (encoded as a single byte AFTER the ULEB128 size). +local DW_LNE_end_sequence = 1 +local DW_LNE_set_address = 2 + +-- DWARF5 range-list entry encodings used by the main CU. +local DW_RLE_end_of_list = 0 +local DW_RLE_start_length = 7 + +-- File index 11 in the existing main line unit is hello_gte_tape.c. The +-- injector extends that unit rather than appending an unreferenced unit. +local ATOM_SOURCE_FILE_INDEX = 11 + +-- Path templates for the .bin outputs. +local function dwarf_line_path(out_root, basename) + return out_root .. "\\" .. basename .. ".dwarf_line.bin" +end +local function dwarf_aranges_path(out_root, basename) + return out_root .. "\\" .. basename .. ".dwarf_aranges.bin" +end +local function dwarf_rnglists_path(out_root, basename) + return out_root .. "\\" .. basename .. ".dwarf_rnglists.bin" +end + +-- Default basename if not provided via ctx. +local DEFAULT_BASENAME = "hello_gte" + +-- ════════════════════════════════════════════════════════════════════════════ +-- Type declarations +-- ════════════════════════════════════════════════════════════════════════════ + +--- @class DwarfInjectionCtx +--- @field flags table -- ctx.flags; reads flags.elf_path + flags.dwarf_injection +--- @field out_root string -- output root (e.g. "build/gen") +--- @field basename string -- input ELF basename (default "hello_gte") + +-- ════════════════════════════════════════════════════════════════════════════ +-- LEB128 encoders +-- ════════════════════════════════════════════════════════════════════════════ + +--- ULEB128 encoder. Returns the byte string. +--- @param n integer -- non-negative +--- @return string +local function uleb128(n) + assert(n >= 0, "uleb128 requires non-negative input") + local bytes = {} + repeat + local b = n % 128 + n = (n - b) / 128 + if n > 0 then b = b + 128 end + bytes[#bytes + 1] = string.char(b) + until n == 0 + return table.concat(bytes) +end + +--- SLEB128 encoder. Returns the byte string. +--- @param n integer +--- @return string +local function sleb128(n) + local bytes = {} + local more = true + while more do + local b = n % 128 + n = (n - b) / 128 + if n == 0 and (b % 128) < 64 then more = false end + if n == -1 and (b % 128) >= 64 then more = false end + if more then b = b + 128 end + bytes[#bytes + 1] = string.char(b) + end + return table.concat(bytes) +end + +-- ════════════════════════════════════════════════════════════════════════════ +-- DWARF line-program encoder +-- ════════════════════════════════════════════════════════════════════════════ + +--- Build the byte sequence for ONE atom's line program: +--- DW_LNE_set_address(addr) +--- DW_LNS_copy -- entry 1: addr, file, line=first.line +--- for each subsequent word: +--- DW_LNS_advance_pc(1 .word = 4 bytes) +--- DW_LNS_advance_line(line - prev_line) +--- DW_LNS_copy +--- DW_LNE_end_sequence +--- +--- Wire format reminder (DWARF5 §6.2.5): +--- - Standard opcodes: 1 byte opcode + payload (per opcode length). +--- - Extended opcode marker byte = 0. +--- - Extended opcodes: marker byte + ULEB128 size + sub_opcode + payload. +--- +--- @param atom table -- {name, addr, size_bytes, words, entries} +--- @return string +local function build_atom_sequence(atom) + local function set_address(addr) + -- Per DWARF5 §6.2.5.3: + -- marker(0) + size(ULEB128, includes sub_opcode byte) + sub_opcode + payload + -- For set_address: size = 1 (sub_opcode) + 4 (addr) = 5 + local addr_bytes = string.char( + addr % 256, + math.floor(addr / 256) % 256, + math.floor(addr / 65536) % 256, + math.floor(addr / 16777216) % 256) + local sub_size = string.char(DW_LNE_set_address) .. addr_bytes + return string.char(DW_LNS_extended) .. uleb128(#sub_size) .. sub_size + end + local function copy_op() + return string.char(DW_LNS_copy) + end + local function set_file(file_index) + return string.char(DW_LNS_set_file) .. uleb128(file_index) + end + local function advance_pc(bytes_delta) + return string.char(DW_LNS_advance_pc) .. uleb128(bytes_delta) + end + local function advance_line(line_delta) + return string.char(DW_LNS_advance_line) .. sleb128(line_delta) + end + local function end_sequence() + -- size = 1 (just the sub_opcode byte, no payload) + return string.char(DW_LNS_extended) .. string.char(1) .. string.char(DW_LNE_end_sequence) + end + + if not atom.entries or #atom.entries == 0 then + return set_address(atom.addr) .. end_sequence() + end + + -- set_address sets the address register to atom.addr. + -- line_state.line starts at 1 (per DWARF spec). + -- To land at entries[1].line for the FIRST emitted entry, we need to advance_line by (entries[1].line - 1) + then copy. + -- Then each subsequent entry uses the delta from the previous entry. + local parts = { + set_file(ATOM_SOURCE_FILE_INDEX), -- existing Unit 2 file table: hello_gte_tape.c + set_address(atom.addr), -- 7 bytes: marker + size + sub + addr + advance_line(atom.entries[1].line - 1), -- (entries[1].line - 1) bytes; line_state.line -> entries[1].line + copy_op(), -- emit entry 1: addr=atom.addr, line=entries[1].line + } + local prev_line = atom.entries[1].line + for idx = 2, #atom.entries do + local entry = atom.entries[idx] + parts[#parts + 1] = advance_pc(4) -- 1 .word = 4 bytes on MIPS + parts[#parts + 1] = advance_line(entry.line - prev_line) + parts[#parts + 1] = copy_op() + prev_line = entry.line + end + parts[#parts + 1] = end_sequence() + return table.concat(parts) +end + +-- ════════════════════════════════════════════════════════════════════════════ +-- ELF32 direct reader +-- ════════════════════════════════════════════════════════════════════════════ + +--- Read the existing `.debug_line` + `.debug_aranges` + `.debug_rnglists` bytes by walking the +--- ELF32 section-header table directly (no subprocess). +--- lfs is used only for the existence check; io.open + manual byte arithmetic does the rest. +--- +--- ELF32 header layout (52 bytes): +--- off 0: magic \x7fELF +--- off 4: class (1 = ELF32), endianness (1 = LE, 2 = BE) +--- ... +--- off 16: e_type (2), e_machine (2), e_version (4) +--- off 24: e_entry (4), e_phoff (4) +--- off 32: e_shoff (4) <-- section-header table file offset +--- off 36: e_flags (4), e_ehsize (2) +--- off 42: e_phentsize (2), e_phnum (2) +--- off 46: e_shentsize (2) <-- section-header entry size (40 for ELF32) +--- off 48: e_shnum (2) <-- number of section headers +--- off 50: e_shstrndx (2) <-- index of section-name string table +--- +--- Section header (40 bytes for ELF32): +--- off 0: sh_name (4) +--- off 4: sh_type (4) +--- off 8: sh_flags (4) +--- off 12: sh_addr (4) +--- off 16: sh_offset (4) <-- file offset of section data +--- off 20: sh_size (4) <-- size of section data +--- off 24: sh_link (4) +--- off 28: sh_info (4) +--- off 32: sh_addralign (4) +--- off 36: sh_entsize (4) +--- +--- Returns the three section byte strings (any may be empty if absent). +--- @param elf_path string +--- @return string, string, string +local function read_elf_dwarf_sections(elf_path) + -- Existence check (lfs.attributes avoids an io.open-vs-fail race). + if lfs.attributes(elf_path, "mode") ~= "file" then + io.stderr:write(string.format("[dwarf_injection] ELF not found: %s\n", elf_path)) + return "", "", "" + end + + local f = io.open(elf_path, "rb") + if not f then + io.stderr:write(string.format("[dwarf_injection] io.open failed: %s\n", elf_path)) + return "", "", "" + end + + -- Read the ELF32 header (52 bytes). + local header = f:read(52) + if not header or #header < 52 then + io.stderr:write("[dwarf_injection] ELF too small for ELF32 header\n") + f:close() + return "", "", "" + end + + -- Sanity-check magic + class + endianness. + if header:sub(1, 4) ~= "\127ELF" then + io.stderr:write("[dwarf_injection] not an ELF file\n") + f:close() + return "", "", "" + end + local class = header:byte(5) + local endian = header:byte(6) + if class ~= 1 then + io.stderr:write(string.format("[dwarf_injection] not ELF32 (class=%d)\n", class)) + f:close() + return "", "", "" + end + if endian ~= 1 then + io.stderr:write(string.format("[dwarf_injection] not little-endian (endian=%d); unsupported\n", endian)) + f:close() + return "", "", "" + end + + -- Parse e_shoff (off 32, 4 bytes LE), e_shentsize + -- (off 46, 2 bytes LE), e_shnum (off 48, 2 bytes LE), e_shstrndx (off 50, 2 bytes LE). + local function u32_le(buf, off) + return buf:byte(off) + buf:byte(off + 1) * 256 + buf:byte(off + 2) * 65536 + buf:byte(off + 3) * 16777216 + end + local function u16_le(buf, off) + return buf:byte(off) + buf:byte(off + 1) * 256 + end + + local e_shoff = u32_le(header, 33) + local e_shentsize = u16_le(header, 47) + local e_shnum = u16_le(header, 49) + local e_shstrndx = u16_le(header, 51) + + -- Read the section-header string table (.shstrtab). + -- Walk to section header index `e_shstrndx`, read its sh_offset + sh_size. + f:seek("set", e_shoff + e_shstrndx * e_shentsize) + local strtab_hdr = f:read(e_shentsize) + if not strtab_hdr or #strtab_hdr < e_shentsize then + io.stderr:write("[dwarf_injection] could not read .shstrtab header\n") + f:close() + return "", "", "" + end + local strtab_offset = u32_le(strtab_hdr, 17) + local strtab_size = u32_le(strtab_hdr, 21) + f:seek("set", strtab_offset) + local strtab = f:read(strtab_size) or "" + + -- Walk all section headers; collect names + (offset, size) for the + -- sections we care about. + local function read_section_bytes(sh_offset, sh_size) + f:seek("set", sh_offset) + return f:read(sh_size) or "" + end + + local debug_line_bytes = "" + local debug_aranges_bytes = "" + local debug_rnglists_bytes = "" + + for sh_idx = 0, e_shnum - 1 do + f:seek("set", e_shoff + sh_idx * e_shentsize) + local sh = f:read(e_shentsize) + if not sh or #sh < e_shentsize then break end + local sh_name = u32_le(sh, 1) + local sh_offset = u32_le(sh, 17) + local sh_size = u32_le(sh, 21) + + -- Extract the name (null-terminated C string in strtab). + local name_end = strtab:find("\0", sh_name + 1, true) or (sh_name + 1) + local name = strtab:sub(sh_name + 1, name_end - 1) + if name == ".debug_line" then debug_line_bytes = read_section_bytes(sh_offset, sh_size) + elseif name == ".debug_aranges" then debug_aranges_bytes = read_section_bytes(sh_offset, sh_size) + elseif name == ".debug_rnglists" then debug_rnglists_bytes = read_section_bytes(sh_offset, sh_size) + end + end + + f:close() + return debug_line_bytes, debug_aranges_bytes, debug_rnglists_bytes +end + +-- ════════════════════════════════════════════════════════════════════════════ +-- Helpers: nm + source-map.txt + atom table +-- ════════════════════════════════════════════════════════════════════════════ + +--- Read ELF symbol addresses via `mipsel-none-elf-nm -S`. +--- Returns `{name -> {addr, size_bytes}}` for every `code_` symbol. +--- Duplicated from passes/atoms_source_map.lua +--- (lift decision in scratch/phase1_helpers_audit.md: keep duplication until 3+ callers). +--- @param elf_path string +--- @return table +local function read_nm(elf_path) + local addrs = {} + local f = io.popen(string.format('mipsel-none-elf-nm -S "%s" 2>nul', elf_path)) + if not f then return addrs end + for line in f:lines() do + local addr_hex, size_hex, name = line:match("^(%x+)%s+(%x+)%s+%a%s+code_(%S+)") + if addr_hex and size_hex and name then + addrs[name] = { tonumber(addr_hex, 16), tonumber(size_hex, 16) } + end + end + f:close() + return addrs +end + +--- Parse FORMAT_VERSION 1 atoms.sourcemap.txt. +--- Returns `{name -> {total = N, words = {{n, line}, ...}}}`. +--- Matches the in-memory shape +--- (atoms_source_map.lua:142 uses `{pos, line, text}`; the `.txt` file uses `WORD ` so the parser maps `n` -> `pos`). +--- @param sm_path string +--- @return table +local function read_source_map(sm_path) + local out = {} + local cur_name, cur_total, cur_words = nil, 0, {} + for raw in io.lines(sm_path) do + local line = raw + if line:match("^#") then + if line:match("^# FORMAT_VERSION%s+(%d+)") then + local ver = tonumber(line:match("^# FORMAT_VERSION%s+(%d+)")) + if ver ~= 1 then + io.stderr:write(string.format( + "[dwarf_injection] source-map version mismatch (got %d, expected 1)\n", ver)) + return {} + end + end + -- skip other comments + elseif line:sub(1, 4) == "ATOM" then + -- ATOM "" + -- The path is quoted. Format per emission: string.format('ATOM %s "%s" 0', name, rel_path) + -- then patched: .gsub(" 0$", " " .. total). + -- So the wire format is: ATOM "" . + local _, _, name = line:find("ATOM%s+(%S+)%s+\"[^\"]*\"%s+(%d+)") + if name then + cur_name = name + cur_total = 0 + cur_words = {} + out[name] = { total = 0, words = cur_words } + end + elseif line == "ENDATOM" then + -- Update the recorded total from the entries count + -- (matches the `lines[1] = lines[1]:gsub(" 0$", " " .. total)` patch in atoms_source_map.lua:170). + if cur_name and out[cur_name] then + out[cur_name].total = #cur_words + end + cur_name, cur_total, cur_words = nil, 0, {} + elseif line:sub(1, 4) == "WORD" and cur_name then + -- WORD LINE TEXT + local _, n, _, src_line = line:find("WORD%s+(%d+)%s+LINE%s+(%d+)") + if n and src_line then + cur_words[#cur_words + 1] = { n = tonumber(n), line = tonumber(src_line) } + end + end + end + return out +end + +--- Build the atom table the section builders consume. +--- Cross-references nm symbols with source-map.txt entries; sorted by addr. +--- @param ctx DwarfInjectionCtx +--- @return table[] -- list of {name, addr, size_bytes, words, entries} +local function build_atom_table(ctx) + local basename = ctx.basename or DEFAULT_BASENAME + -- Source-map path: convention matches the α MVP's emission location. + -- writes `/.atoms.sourcemap.txt` (e.g. `build/gen/hello_gte_tape.atoms.sourcemap.txt`). + -- But ctx.out_root is `build/gen` (the per-build output root) and basename defaults to `hello_gte`. + -- The actual file emitted today is per-source; we look for any `*.atoms.sourcemap.txt` in out_root. + local sm_files = {} + if duffle and duffle.list_dir then + sm_files = duffle.list_dir(ctx.out_root, "%.atoms.sourcemap%.txt$") + elseif lfs then + -- Manual walk via lfs. + for entry in lfs.dir(ctx.out_root) do + if entry:match("%.atoms.sourcemap%.txt$") then + sm_files[#sm_files + 1] = ctx.out_root .. "\\" .. entry + end + end + end + if #sm_files == 0 then + io.stderr:write(string.format( + "[dwarf_injection] no *.atoms.sourcemap.txt in %s; need atoms-source-map pass first\n", + ctx.out_root)) + return {} + end + + -- Read nm + merge all source-map files. + local addrs = read_nm(ctx.flags.elf_path) + local merged = {} + for _, sm_path in ipairs(sm_files) do + local sm = read_source_map(sm_path) + for name, sm_data in pairs(sm) do + merged[name] = sm_data + end + end + + -- Cross-ref; keep atoms that exist in both. + local out = {} + for name, info in pairs(addrs) do + local sm = merged[name] + if sm then + out[#out + 1] = { + name = name, + addr = info[1], + size_bytes = info[2], + words = sm.total, + entries = sm.words, + } + end + end + table.sort(out, function(a, b) return a.addr < b.addr end) + return out +end + +-- ════════════════════════════════════════════════════════════════════════════ +-- Section builders (single dispatch table — see guide_metaprogram_ssdl.md §11) +-- ════════════════════════════════════════════════════════════════════════════ + +--- Append per-atom line-program sequences to the existing main .debug_line unit +--- (the final unit, referenced by the main CU's DW_AT_stmt_list). +--- +--- The old implementation appended a new Unit 3. +--- No compilation unit pointed at it through DW_AT_stmt_list, so gdb ignored it. +--- It also encoded byte 13 as the extended-opcode marker; byte 13 is actually the first special opcode. +--- The existing final unit already contains hello_gte_tape.c as file index 11 and ends with a valid end_sequence. +--- We preserve its bytes, append independent atom sequences, and increase only that unit's DWARF32 unit_length. +--- +--- @param existing string -- existing section bytes (verbatim) +--- @param atom_table table -- list of {name, addr, size_bytes, words, entries} +--- @return string +local function build_dwarf_line_section(existing, atom_table) + if #atom_table == 0 then return existing end + + -- Build the sequences. + local sequences = {} + for _, atom in ipairs(atom_table) do + sequences[#sequences + 1] = build_atom_sequence(atom) + end + local appended = table.concat(sequences) + + -- Walk DWARF32 line units and retain the final unit's bounds. + -- The main C CU points at this final unit (DW_AT_stmt_list = 0x5b in today's ELF). + local unit_pos, last_pos, last_length, last_end = 1, nil, nil, nil + while unit_pos <= #existing do + if unit_pos + 3 > #existing then return existing end + local unit_length = existing:byte(unit_pos) + + existing:byte(unit_pos + 1) * 256 + + existing:byte(unit_pos + 2) * 65536 + + existing:byte(unit_pos + 3) * 16777216 + if unit_length == 0xFFFFFFFF then return existing end + local unit_end = unit_pos + 3 + unit_length + if unit_end > #existing then return existing end + last_pos, last_length, last_end = unit_pos, unit_length, unit_end + unit_pos = unit_end + 1 + end + if unit_pos ~= #existing + 1 or not last_pos then return existing end + + local new_length = last_length + #appended + local new_length_bytes = string.char( + new_length % 256, + math.floor(new_length / 256) % 256, + math.floor(new_length / 65536) % 256, + math.floor(new_length / 16777216) % 256) + + return existing:sub(1, last_pos - 1) + .. new_length_bytes + .. existing:sub(last_pos + 4, last_end) + .. appended +end + +--- Extend .debug_aranges with one entry per atom, pointing at the same CU the existing entries point at +--- (read from the existing header's debug_info_offset field). +--- The 8-byte zero terminator is preserved. +--- +--- Existing .debug_aranges layout (DWARF4 §6.1.1, 32-bit): +--- unit_length (4 bytes) -- size of the rest +--- version (2 bytes) -- = 2 +--- debug_info_offset (4 bytes) -- CU DIE offset in .debug_info +--- address_size (1 byte) -- = 4 on MIPS +--- segment_size (1 byte) -- = 0 +--- [entries...] -- address(4) + length(4) per entry +--- terminator -- address=0 + length=0 (8 zero bytes) +--- +--- @param existing string +--- @param atom_table table +--- @return string +local function build_dwarf_aranges_section(existing, atom_table) + if #existing < 12 then return existing end -- header sanity + + -- .debug_aranges can contain multiple compilation units (CUs). + -- gcc-mips-elf emits one CU per .text section TU. + -- We extend the LAST unit by replacing its 8-byte terminator with our atom entries followed by a new 8-byte terminator. + -- We bump the unit's length field accordingly. + -- + -- Unit structure (DWARF4 §7.21): + -- unit_length (4) + -- version (2) + -- debug_info_offset (4) -- CU DIE offset in .debug_info + -- address_size (1) + -- segment_size (1) + -- entries... (4-byte addr + 4-byte length) + -- terminator (8 bytes: addr=0, length=0) + + -- Walk all units and emit each one (preserving existing structure). + -- For the LAST unit, replace the terminator with my entries + new term. + local result = {} + local i = 1 -- 1-indexed + local is_last_unit = false + + while i <= #existing do + -- Read this unit's length. + local ul = existing:byte(i) + + existing:byte(i + 1) * 256 + + existing:byte(i + 2) * 65536 + + existing:byte(i + 3) * 16777216 + if ul == 0xFFFFFFFF then + -- DWARF64 marker - not supported. + return existing + end + + local unit_start = i + local unit_end = i + 3 + ul -- last byte of unit content + is_last_unit = (unit_end == #existing) + + if is_last_unit then + -- The old terminator is replaced by entries + a new terminator, so + -- net section growth (and unit_length growth) is entries only. + local added_bytes = #atom_table * 8 + local new_ul = ul + added_bytes + local new_ul_bytes = string.char( + new_ul % 256, + math.floor(new_ul / 256) % 256, + math.floor(new_ul / 65536) % 256, + math.floor(new_ul / 16777216) % 256) + -- Emit everything EXCEPT the last 8 bytes (terminator). + result[#result + 1] = new_ul_bytes + .. existing:sub(i + 4, unit_end - 8) + -- Append my atom entries. + for _, atom in ipairs(atom_table) do + local a = atom.addr + local size = atom.size_bytes + result[#result + 1] = string.char( + a % 256, + math.floor(a / 256) % 256, + math.floor(a / 65536) % 256, + math.floor(a / 16777216) % 256, + size % 256, + math.floor(size / 256) % 256, + math.floor(size / 65536) % 256, + math.floor(size / 16777216) % 256) + end + -- Append a new terminator. + result[#result + 1] = string.rep("\0", 8) + else + -- Emit this unit unchanged. + result[#result + 1] = existing:sub(unit_start, unit_end) + end + + i = unit_end + 1 + end + + if not is_last_unit then + -- Malformed or no terminator found; append a new unit at the end. + -- For now, return existing unchanged to avoid making it worse. + return existing + end + + return table.concat(result) +end + +--- Extend the main CU's DWARF5 range list with one DW_RLE_start_length entry per atom. +--- GDB validates an address against DW_AT_ranges before consulting the CU's line program; +--- .debug_aranges alone is not sufficient. +--- +--- Current section shape (one DWARF32 table): +--- unit_length(4), version=5(2), address_size=4(1), segment_size=0(1), +--- offset_entry_count=0(4), start_length entries..., end_of_list(1). +--- +--- @param existing string +--- @param atom_table table +--- @return string +local function build_dwarf_rnglists_section(existing, atom_table) + if #existing < 13 or #atom_table == 0 then return existing end + + local unit_length = existing:byte(1) + + existing:byte(2) * 256 + + existing:byte(3) * 65536 + + existing:byte(4) * 16777216 + local version = existing:byte(5) + existing:byte(6) * 256 + local address_size = existing:byte(7) + local segment_size = existing:byte(8) + local offset_entry_count = existing:byte(9) + + existing:byte(10) * 256 + + existing:byte(11) * 65536 + + existing:byte(12) * 16777216 + + if unit_length + 4 ~= #existing + or version ~= 5 + or address_size ~= 4 + or segment_size ~= 0 + or offset_entry_count ~= 0 + or existing:byte(#existing) ~= DW_RLE_end_of_list then + return existing + end + + local entries = {} + for _, atom in ipairs(atom_table) do + local addr = atom.addr + entries[#entries + 1] = string.char(DW_RLE_start_length, + addr % 256, + math.floor(addr / 256) % 256, + math.floor(addr / 65536) % 256, + math.floor(addr / 16777216) % 256) + .. uleb128(atom.size_bytes) + end + local appended = table.concat(entries) + local new_length = unit_length + #appended + local new_length_bytes = string.char( + new_length % 256, + math.floor(new_length / 256) % 256, + math.floor(new_length / 65536) % 256, + math.floor(new_length / 16777216) % 256) + + return new_length_bytes + .. existing:sub(5, #existing - 1) + .. appended + .. string.char(DW_RLE_end_of_list) +end + +local SECTION_BUILDERS = { + debug_line = build_dwarf_line_section, + debug_aranges = build_dwarf_aranges_section, + debug_rnglists = build_dwarf_rnglists_section, +} + +-- ════════════════════════════════════════════════════════════════════════════ +-- Pass entry +-- ════════════════════════════════════════════════════════════════════════════ + +local M = {} + +--- M.run — orchestrator entry. Phase 1 Tasks 3-4 read + cross-ref. Phase 2 +--- Tasks 5-7 fill in the section builders + .bin emission. +--- @param ctx DwarfInjectionCtx +--- @return table +function M.run(ctx) + -- Guard: this pass is opt-in via --dwarf-injection (not run on --all). + if not (ctx.flags and ctx.flags.dwarf_injection) then + return { outputs = {}, errors = {}, warnings = {} } + end + + -- Guard: --elf is required. + local elf_path = ctx.flags and ctx.flags.elf_path + if not elf_path or elf_path == "" then + io.stderr:write("[dwarf_injection] --elf flag missing\n") + return { outputs = {}, errors = {}, warnings = {} } + end + + -- Resolve relative ELF path to absolute (io.open inherits the calling + -- process's CWD which is reliable, but absolute paths are safer when + -- this module is required from another directory). + if not elf_path:match("^[%a]:[\\/]") and not elf_path:match("^[/\\]") then + local cwd = lfs.currentdir() + elf_path = cwd:gsub("[\\/]$", "") .. "\\" .. elf_path + end + + -- Read the existing DWARF sections directly (no subprocess; lfs + io.open + manual ELF32 section-header walk). + local existing_line, existing_aranges, existing_rnglists = read_elf_dwarf_sections(elf_path) + io.stderr:write(string.format("[dwarf_injection] read .debug_line (%d) + .debug_aranges (%d) + .debug_rnglists (%d) bytes\n" + , #existing_line, #existing_aranges, #existing_rnglists)) + + -- Build the atom table (cross-ref nm symbols with source-map.txt entries). + local atom_table = build_atom_table(ctx) + io.stderr:write(string.format("[dwarf_injection] matched %d atoms between nm + source-map\n", #atom_table)) + + -- Build the new sections via the SECTION_BUILDERS dispatch. + local new_line = build_dwarf_line_section (existing_line, atom_table) + local new_aranges = build_dwarf_aranges_section (existing_aranges, atom_table) + local new_rnglists = build_dwarf_rnglists_section(existing_rnglists, atom_table) + io.stderr:write(string.format("[dwarf_injection] new .debug_line = %d bytes (was %d, +%d atoms)\n" + , #new_line, #existing_line, #atom_table)) + io.stderr:write(string.format("[dwarf_injection] new .debug_aranges = %d bytes (was %d, +%d atoms)\n" + , #new_aranges, #existing_aranges, #atom_table)) + io.stderr:write(string.format("[dwarf_injection] new .debug_rnglists = %d bytes (was %d, +%d atoms)\n" + , #new_rnglists, #existing_rnglists, #atom_table)) + + -- Write the .bin files. The build_psyq.ps1 post-link hook splices these into a copy of the ELF via objcopy --update-section. + local basename = ctx.basename or DEFAULT_BASENAME + if ctx.out_root and ctx.out_root ~= "" then + if lfs.attributes(ctx.out_root, "mode") ~= "directory" then + lfs.mkdir(ctx.out_root) + end + local line_path = dwarf_line_path (ctx.out_root, basename) + local aranges_path = dwarf_aranges_path (ctx.out_root, basename) + local rnglists_path = dwarf_rnglists_path(ctx.out_root, basename) + local f1 = io.open(line_path, "wb") + if not f1 then + io.stderr:write(string.format("[dwarf_injection] failed to open %s for write\n", line_path)) + else + f1:write(new_line); f1:close() + end + local f2 = io.open(aranges_path, "wb") + if not f2 then + io.stderr:write(string.format("[dwarf_injection] failed to open %s for write\n", aranges_path)) + else + f2:write(new_aranges); f2:close() + end + local f3 = io.open(rnglists_path, "wb") + if not f3 then + io.stderr:write(string.format("[dwarf_injection] failed to open %s for write\n", rnglists_path)) + else + f3:write(new_rnglists); f3:close() + end + io.stderr:write(string.format("[dwarf_injection] wrote %s + %s + %s\n", line_path, aranges_path, rnglists_path)) + return { + outputs = { + { dwarf_line_bin = line_path }, + { dwarf_aranges_bin = aranges_path }, + { dwarf_rnglists_bin = rnglists_path }, + }, + errors = {}, warnings = {}, + } + end + return { outputs = {}, errors = {}, warnings = {} } +end + +return M diff --git a/scripts/ps1_meta.lua b/scripts/ps1_meta.lua index 0a73f3a..5be671d 100644 --- a/scripts/ps1_meta.lua +++ b/scripts/ps1_meta.lua @@ -155,6 +155,17 @@ local PASSES = { desc = "Emit gen/.atoms.sourcemap.txt (per-.word C source line map for gdb debugging)", out = { { kind = "header", path_template = "/gen/.atoms.sourcemap.txt" } }, }, + ["dwarf-injection"] = { + module = "passes.dwarf_injection", + kind = "shared", + deps = {"atoms-source-map"}, + desc = "Inject per-atom .debug_line + .debug_aranges into the ELF (post-link; writes section .bin blobs for objcopy splice)", + out = { + { kind = "report", path_template = "/.dwarf_line.bin" }, + { kind = "report", path_template = "/.dwarf_aranges.bin" }, + { kind = "report", path_template = "/.dwarf_rnglists.bin" }, + }, + }, report = { module = "passes.report", kind = "report", @@ -180,6 +191,7 @@ local PASS_FLAG_TO_NAME = { ["--offsets"] = "offsets", ["--static-analysis"] = "static-analysis", ["--atoms-source-map"] = "atoms-source-map", + ["--dwarf-injection"] = "dwarf-injection", ["--report"] = "report", ["--scan-source"] = "scan-source", ["--all"] = ALL_PASSES_SENTINEL, @@ -187,7 +199,7 @@ local PASS_FLAG_TO_NAME = { local ALL_PASS_NAMES = { "scan-source", "word-counts", "components", "annotation", - "offsets", "static-analysis", "atoms-source-map", "report", + "offsets", "static-analysis", "atoms-source-map", "dwarf-injection", "report", } --- Append every pass name to args.requested_set. Used by --all and by the "default to --all if no pass flags were given" fallback. @@ -221,6 +233,7 @@ PASS_FLAGS (pick one or more, or use --all): --validate Run atom annotation DSL validation --offsets Generate /gen/.offsets.h --atoms-source-map Generate .atoms.sourcemap.txt per source + --dwarf-injection Inject per-atom .debug_line + .debug_aranges (post-link, requires --elf) --static-analysis [FUTURE] GTE pipeline-fill, mac_yield uniformity --report Render per-project summary --all Equivalent to all 6 flags above (default) @@ -231,7 +244,8 @@ COMMON_FLAGS: --out-root DIR Output root for reports (default: build/gen) --project-root DIR Project root for .macs.h scan (default: dirname(metadata)) --gdb-runtime Also emit /gdb_tape_atoms_runtime.gdb (post-link, requires --elf) - --elf PATH Path to linked .elf (for --gdb-runtime address resolution) + --dwarf-injection Opt in to DWARF injection (writes .dwarf_*.bin blobs for objcopy splice; requires --elf) + --elf PATH Path to linked .elf (for --gdb-runtime / --dwarf-injection) --dry-run Print dep order + ASCII graph; exit 0 without running --verbose Print per-pass debug output --help Show this help and exit @@ -269,6 +283,12 @@ FLAG_HANDLERS["--project-root"] = function(args, argv, arg_idx) args.project_roo -- mutates `args.flags` (which propagates into `ctx.flags`). FLAG_HANDLERS["--gdb-runtime"] = function(args) args.flags = args.flags or {}; args.flags.gdb_runtime = true end FLAG_HANDLERS["--elf"] = function(args, argv, arg_idx) args.flags = args.flags or {}; args.flags.elf_path = argv[arg_idx + 1]; return arg_idx + 1 end +-- F' track: enable DWARF injection (default OFF; opt-in via .vscode/launch.json or ps1_meta CLI). +FLAG_HANDLERS["--dwarf-injection"] = function(args) + args.flags = args.flags or {} + args.flags.dwarf_injection = true + args.requested_set[#args.requested_set + 1] = "dwarf-injection" +end -- Pass-flag handler. Reads the closed-set table, expands --all, appends to requested_set. Single-statement, no nesting. FLAG_HANDLERS[PASS_FLAG_DISPATCH_KEY] = function(args, a) @@ -582,8 +602,8 @@ local function render_dep_graph(passes, requested, closed) add(" |.macs.h | |.errors | |.offsets| |.static |") add(" | (header) | | .h | | .h | | _analysis |") add(" +------+-------+ | +annot.txt | | (header) | | .txt |") - add(" | +------+-------+ +--------------+ +------+--------+") - add(" v v v") + add(" | +--+-----------+ +--------------+ +------+--------+") + add(" v v v") add(" +------+----------------+ +------+-------+ |") add(" |offsets|static-analysis| |report| |<--------------------+") add(" | | | +------+-------+")