Files
pikuma_ps1/scripts/passes/dwarf_injection.lua
T

1519 lines
76 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
--- passes/dwarf_injection.lua — Per-atom DWARF injection for tape-atom step-debug (F' + G').
---
--- Reads the post-link ELF directly
--- (lfs + io.open; walks the ELF32 section header table to find
--- `.debug_info`
--- + `.debug_abbrev`
--- + `.debug_str`
--- + `.debug_line`
--- + `.debug_aranges`
--- + `.debug_rnglists`),
--- APPENDS synthetic DWARF line-program sequences for every `code_<name>` atom, EXTENDS the `.debug_aranges` and main-CU range tables with the atom ranges,
--- APPENDS a new compilation unit to `.debug_info` with per-atom `DW_TAG_subprogram` + per-wave-context-reg `DW_TAG_variable` entries
--- (so `R_PrimCursor` etc. appear as atom-scoped locals in VSCode's Variables pane).
--- Writes the new section data to `<out_root>/<basename>.dwarf_*.bin` (7 blobs total).
---
--- 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_info=<bin> <elf>
--- mipsel-none-elf-objcopy --update-section .debug_abbrev=<bin> <elf>
--- mipsel-none-elf-objcopy --update-section .debug_str=<bin> <elf>
--- mipsel-none-elf-objcopy --update-section .debug_line=<bin> <elf>
--- mipsel-none-elf-objcopy --update-section .debug_aranges=<bin> <elf>
--- mipsel-none-elf-objcopy --update-section .debug_rnglists=<bin> <elf>
--- mipsel-none-elf-objcopy --add-section .debug_loc=<bin> <elf>
--- (.debug_loc doesn't exist in the source ELF; --add-section creates it.
--- 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 (F'),
--- AND the Variables pane shows the wave-context regs as atom-scoped locals (G').
--- Native VSCode UX (gutter arrow + highlighted line + Run to Cursor + conditional BPs by source line + per-atom locals).
--- 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")
-- ELF32 / DWARF / atoms-source-map utilities (post-link debug-info injection).
-- Sister module to duffle.lua — contains the format-constant tables (ELF32 byte offsets, DWARF opcodes, etc.) and the I/O helpers
-- (read_elf_sections, nm, source-map parser, LE byte r/w). `list_dir` lives in duffle.lua as a general I/O primitive (lifted out during F'').
local elf_dwarf = require("elf_dwarf")
local lfs = require("lfs")
-- ════════════════════════════════════════════════════════════════════════════
-- Constants
-- ════════════════════════════════════════════════════════════════════════════
-- DWARF line-program opcodes + range-list entry encodings (per DWARF5 spec).
-- All values lifted from `elf_dwarf.DWARF_LINE_OPS` + `elf_dwarf.DWARF5_RNGLISTS`.
-- Local aliases preserve the F' code's readability
-- (e.g. `DW_LNS_copy` reads better than `elf_dwarf.DWARF_LINE_OPS.DW_LNS_copy` in an emitter body).
local DWARF_LINE_OPS = elf_dwarf.DWARF_LINE_OPS
local DWARF5_RNGLISTS = elf_dwarf.DWARF5_RNGLISTS
local MIPS_BYTES_PER_WORD = elf_dwarf.MIPS_BYTES_PER_WORD
local DW_LNS_copy = DWARF_LINE_OPS.DW_LNS_copy
local DW_LNS_advance_pc = DWARF_LINE_OPS.DW_LNS_advance_pc
local DW_LNS_advance_line = DWARF_LINE_OPS.DW_LNS_advance_line
local DW_LNS_set_file = DWARF_LINE_OPS.DW_LNS_set_file
local DW_LNS_extended = DWARF_LINE_OPS.DW_LNS_extended
local DW_LNE_end_sequence = DWARF_LINE_OPS.DW_LNE_end_sequence
local DW_LNE_set_address = DWARF_LINE_OPS.DW_LNE_set_address
local DW_RLE_end_of_list = DWARF5_RNGLISTS.end_of_list
local DW_RLE_start_length = DWARF5_RNGLISTS.start_length
-- 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
-- Wave-context registers (per duffle.lua :: WAVE_CONTEXT_REGS). The MIPS
-- register number is what DW_OP_regN uses as N.
--
-- R_PrimCursor = R_T7 = $15 → DW_OP_reg15
-- R_FaceCursor = R_T4 = $12 → DW_OP_reg12
-- R_VertBase = R_T5 = $13 → DW_OP_reg13
-- R_OtBase = R_T6 = $14 → DW_OP_reg14
-- R_TapePtr = R_T8 = $24 → DW_OP_reg24
-- R_AtomJmp = R_T9 = $25 → DW_OP_reg25
--
-- A register-resident variable uses DW_OP_regN (opcode 0x50 + N).
-- DW_OP_bregN instead describes a memory location addressed from a register;
-- it makes GDB dereference the atom register value rather than display it.
local WAVE_CONTEXT_LOCATIONS = {
-- Names prefixed with `RR` to avoid collision with the C-level enum `{R_AtomJmp = 25, R_TapePtr = 24, R_InCursor = 12, R_PrimCursor = 15, ...}`
-- declared in the duffle headers (code/duffle/dsl.h or atom_dsl.h).
-- Without the prefix, gdb's `print R_PrimCursor` resolves to the enum constant (= 15), not to this DWARF variable.
-- With `set print enum on` (gdb default for enum values), gdb displays the enum identifier, so the Watch panel shows
-- `R_PrimCursor = R_PrimCursor` instead of the register value.
{ name = "RR_PrimCursor", reg = 0x0F }, -- $15
{ name = "RR_FaceCursor", reg = 0x0C }, -- $12
{ name = "RR_VertBase", reg = 0x0D }, -- $13
{ name = "RR_OtBase", reg = 0x0E }, -- $14
{ name = "RR_TapePtr", reg = 0x18 }, -- $24
{ name = "RR_AtomJmp", reg = 0x19 }, -- $25
}
-- New abbreviation codes (100+ to avoid collision with gcc's existing
-- 1-60+ codes).
local ABBREV_CU = 0x64 -- 100: DW_TAG_compile_unit
local ABBREV_SUBPROGRAM = 0x65 -- 101: DW_TAG_subprogram
local ABBREV_VARIABLE = 0x66 -- 102: DW_TAG_variable (DW_AT_type = ref4 to U4; was missing pre-2026-07-13 → gdb resolved R_PrimCursor against the C-level enum, not the register)
-- Task 8 (rbind composite via DW_OP_piece): 4 new abbreviations.
local ABBREV_STRUCT_TYPE = 0x67 -- 103: DW_TAG_structure_type with children (Binds_X mirror)
local ABBREV_MEMBER = 0x68 -- 104: DW_TAG_member no children (DW_AT_type = ref4 to U4 base)
local ABBREV_BIND_VAR = 0x69 -- 105: DW_TAG_variable no children + DW_AT_type = ref4 (the bind_args variable)
local ABBREV_BASE_TYPE = 0x6A -- 106: DW_TAG_base_type no children (U4)
-- DWARF3/4/5 opcodes / attributes / forms (for the .debug_info synth).
-- DW_TAG values are stable across DWARF3-5 per the standard's Table 7.1;
-- gcc emits DW_TAG_structure_type=0x13 + DW_TAG_base_type=0x24 even in
-- DWARF5-versioned CUs, so we match those exact byte values.
local DW_TAG_compile_unit = 0x11
local DW_TAG_subprogram = 0x2E
local DW_TAG_variable = 0x34
local DW_TAG_structure_type = 0x13
local DW_TAG_member = 0x0D
local DW_TAG_base_type = 0x24
local DW_AT_name = 0x03
local DW_AT_low_pc = 0x11
local DW_AT_high_pc = 0x12
local DW_AT_language = 0x13
local DW_AT_location = 0x02
local DW_AT_comp_dir = 0x1B
local DW_AT_byte_size = 0x0B
local DW_AT_encoding = 0x3E -- DWARF5 §7.7.1: DW_AT_encoding (for DW_ATE_unsigned base type; was 0x13 = DW_AT_language in prior slice — semantically wrong)
local DW_AT_data_member_location = 0x38
local DW_AT_type = 0x49
local DW_AT_linkage_name = 0x6E -- DWARF5 §7.7.1: DW_AT_linkage_name (standard form; 0x200027 was the GNU extension form — wrong vs DW_FORM_string abbrev)
local DW_AT_external = 0x3F -- marks a variable/function as externally visible
local DW_FORM_addr = 0x01
local DW_FORM_data1 = 0x0B
local DW_FORM_string = 0x08 -- inline null-terminated
local DW_FORM_strp = 0x0E -- 4-byte offset into .debug_str
local DW_FORM_exprloc = 0x18 -- length-prefixed (ULEB128) DW_OP bytes
local DW_FORM_ref4 = 0x13 -- 4-byte offset within the same .debug_info CU
local DW_FORM_udata = 0x0F -- ULEB128 (DW_AT_byte_size for struct_type, DW_AT_data_member_location for member)
local DW_FORM_implicit_const = 0x21 -- DWARF5 §7.5.6: abbrev declaration carries a SLEB constant (used by the abbrev-table walker)
local DW_OP_reg0 = 0x50
-- DW_OP_regN = 0x50 + N; the variable's value resides in that register.
local DW_OP_piece = 0x93 -- followed by ULEB128 byte count (per DWARF5 §7.7.5)
local DW_ATE_unsigned = 0x07 -- DWARF5 §7.8.1: DW_ATE_unsigned (used for U4 base type)
local DW_LANG_C99 = 0x0C -- = 12 (C99); use as a safe "C-like" placeholder
-- (DW_LANG_Mips_Assembler = 0x8001 was used in the F' Task 5 test, but we want
-- this CU to look like a C TU so VSCode's Variables pane treats it as code.)
-- R_ alias → MIPS register number.
-- Subset covering every R_ name used in the source atoms.
-- Used by Task 8 to build the piece-chain location for bind_args (which field comes from which reg).
-- The wave-context entries match WAVE_CONTEXT_LOCATIONS above; the rest are the standard MIPS C-ABI register names.
local R_NAME_TO_INDEX = {
["R_0"] = 0, ["R_AT"] = 1, ["R_V0"] = 2, ["R_V1"] = 3,
["R_A0"] = 4, ["R_A1"] = 5, ["R_A2"] = 6, ["R_A3"] = 7,
["R_T0"] = 8, ["R_T1"] = 9, ["R_T2"] = 10, ["R_T3"] = 11,
["R_T4"] = 12, ["R_T5"] = 13, ["R_T6"] = 14, ["R_T7"] = 15,
["R_S0"] = 16, ["R_S1"] = 17,
["R_T8"] = 24, ["R_T9"] = 25,
-- Wave-context aliases (same encoding as R_T4..R_T7, R_T8..R_T9).
["R_PrimCursor"] = 15, ["R_FaceCursor"] = 12,
["R_VertBase"] = 13, ["R_OtBase"] = 14,
["R_TapePtr"] = 24, ["R_AtomJmp"] = 25,
}
-- Default name for the synthetic CU (so VSCode lists it as a known source).
local DEFAULT_CU_NAME = "tape_atom_locals"
local DEFAULT_CU_COMP_DIR = "."
-- Path templates for the .bin outputs are now in SECTION_WRITERS (see below).
-- 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
-- ════════════════════════════════════════════════════════════════════════════
--
-- Lifted to `elf_dwarf.uleb128` + `elf_dwarf.sleb128` (F'' refactor).
-- See those helpers for the bit-layout documentation + named constants
-- (LEB_CONT_BIT, LEB_DATA_MASK, SLEB_SIGN_BIT).
-- Local aliases so the line-program encoder (below) can keep its short names.
local uleb128 = elf_dwarf.uleb128
local sleb128 = elf_dwarf.sleb128
-- ════════════════════════════════════════════════════════════════════════════
-- 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 = elf_dwarf.write_u32_le(addr)
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(DWARF_LINE_OPS.end_sequence_payload_size)
.. 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
copy_op(), -- same PC/line: GDB 12 zero-instruction-prologue marker
}
local prev_line = atom.entries[1].line
for idx = 2, #atom.entries do
local entry = atom.entries[idx]
parts[#parts + 1] = advance_pc(MIPS_BYTES_PER_WORD) -- 1 .word = MIPS_BYTES_PER_WORD 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
-- ════════════════════════════════════════════════════════════════════════════
-- Helpers: nm + source-map.txt + atom table
-- ════════════════════════════════════════════════════════════════════════════
--- 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 `<out_root>/<basename>.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 = duffle.list_dir(ctx.out_root, "%.atoms.sourcemap%.txt$")
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 = elf_dwarf.read_nm(ctx.flags.elf_path)
local merged = {}
for _, sm_path in ipairs(sm_files) do
local sm = elf_dwarf.parse_source_map_file(sm_path, 1)
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
-- ════════════════════════════════════════════════════════════════════════════
-- G' Phase 2 Task 8: rbind atom detection + (reg, field) pairing
-- ════════════════════════════════════════════════════════════════════════════
--
-- An "rbind atom" has `atom_bind(Binds_X)` in its `atom_info(...)` sub-call.
-- The atom body pops one field per `load_word(R_<reg>, R_TapePtr, O_(Binds_X,Field))`
-- and the order of these load_word calls defines the (reg, field) pairing for the piece-chain DWARF location expression.
--
-- We use the SourceScan payload populated by `passes/scan_source.lua` (the dep-closed upstream pass).
-- That pass walks each source once and populates `src.scan.atom_infos` (the atom_info sub-call parse) and `src.scan.binds` (the Binds_X struct field parse).
-- We re-walk the body for the (reg, field) pairing because the SourceScan pass doesn't preserve which R_<reg> receives which Binds_X.<field>.
--
-- **Local Binds_X field re-parse**: the source-scan pass's `scan_binds_fields` only recognizes U4 fields;
-- The current source uses pointer types too (`V4_S2* FaceCursor`, `U4* OtBase`).
-- To keep Task 8 contained (don't change the shared scan-source pass), we re-parse the Binds_X typedef body locally with a more permissive rule:
-- U4 field = 4 bytes; any pointer type (`*`-suffixed) = 4 bytes (MIPS32 pointer). Everything else = skip + advance.
--- Re-parse a Binds_X typedef body, returning {fields = {{name, offset}, ...}, bytes = N}.
--- Walks one ident at a time. A field declaration looks like:
--- U4 PrimCursor; | M3_S2* transform; | V4_S2* FaceCursor; | U4* OtBase;
--- which tokenizes as: TYPE [WS+ *] WS+ FIELD SEMI
--- Where TYPE is `U4` (recognized), or TYPE + optional `*` (pointer, recognized).
--- Other type tokens (e.g. user-defined `Struct_(Foo)`) are skipped + we move on.
--- @param body string -- the typedef body (between `{` and `}`)
--- @return table, integer -- (fields, byte_count)
local function reparse_binds_body(body)
local fields = {}
local byte_off = 0
local pos = 1
local body_len = #body
while pos <= body_len do
pos = duffle.skip_ws_and_cmt(body, pos)
if pos > body_len then break end
local type_ident, type_end = duffle.read_ident(body, pos)
if not type_ident then
pos = pos + 1
else
-- Scan past optional whitespace + `*` + whitespace.
-- Recognized field types: U4 (no *) OR any TYPE* (pointer, 4 bytes on MIPS32).
local p = duffle.skip_ws_and_cmt(body, type_end)
local is_pointer = false
if p <= body_len and body:sub(p, p) == "*" then
is_pointer = true
p = p + 1
end
local field_pos = duffle.skip_ws_and_cmt(body, p)
local field_ident, field_end = duffle.read_ident(body, field_pos)
-- A field is "recognized" if type is `U4` (no *) OR if it's a pointer.
if field_ident and (type_ident == "U4" or is_pointer) then
fields[#fields + 1] = { name = field_ident, offset = byte_off }
byte_off = byte_off + 0x04 -- 4 bytes per field on MIPS32
end
-- Advance past the field ident (or the type ident if no field was found).
pos = field_end or (type_end + 1)
end
end
return fields, byte_off
end
--- Find every `load_word(R_<reg>, R_TapePtr, O_(Binds_X, FieldName))` call in the
--- atom body and return ordered (reg_index, field_name) pairs.
---
--- Source pattern (single-line, comma-separated at top level):
--- load_word(R_PrimCursor, R_TapePtr, O_(Binds_CubeTri,PrimCursor)),
--- load_word(R_FaceCursor, R_TapePtr, O_(Binds_CubeTri,FaceCursor)),
--- ...
---
--- @param body string -- the atom body text (between { ... })
--- @param binds_name string -- expected Binds_X name (skip pairs with mismatching binds)
--- @return table[] -- list of {reg = <MIPS index>, field = <field name>}
local function parse_body_load_pairs(body, binds_name)
local pairs = {}
local pos = 1
local body_len = #body
while pos <= body_len do
-- Find the next "load_word" identifier.
pos = duffle.skip_ws_and_cmt(body, pos)
if pos > body_len then break end
local ident, ident_end = duffle.read_ident(body, pos)
if not ident or ident ~= "load_word" then
-- Either no ident here OR a non-load_word ident (e.g. add_ui_self).
-- Either way, advance by at least 1 byte to avoid infinite loops.
-- ident_end is `pos` when no ident matches (read_ident returns nil, pos).
pos = (ident_end and ident_end > pos) and ident_end or (pos + 1)
else
-- Read the argument list.
local p_open = duffle.skip_ws_and_cmt(body, ident_end)
if body:sub(p_open, p_open) ~= "(" then
pos = p_open + 1
else
local inner, p_after = duffle.read_parens(body, p_open)
-- Split top-level commas.
local args = duffle.split_top_level_commas(inner)
-- Expected shape: (R_<reg>, R_TapePtr, O_(Binds_<X>, FieldName))
if #args >= 3 then
local reg_name = duffle.trim(args[1])
local third_arg = duffle.trim(args[3])
-- Match O_(Binds_<X>, FieldName)
local b, f = third_arg:match("^O_%((Binds_[%w_]+)%s*,%s*(.-)%s*%)$")
if b and b == binds_name and R_NAME_TO_INDEX[reg_name] then
pairs[#pairs + 1] = {
reg = R_NAME_TO_INDEX[reg_name],
field = duffle.trim(f),
}
end
end
pos = p_after
end
end
end
return pairs
end
--- Collect every rbind atom + the matching Binds_X struct + (reg, field) pairs.
---
--- Inputs come from the dep-closed `scan-source` pass:
--- ctx.sources[i].scan.atom_infos -- list of {atom_name, binds, reads, writes, info_line}
--- ctx.sources[i].scan.binds -- list of {line, name, fields, bytes}
---
--- Returns:
--- rbind_atoms = {[atom_name] = {binds, fields, regs, byte_size, info_line}}
--- rbind_structs = {[binds_name] = {byte_size, fields, atom_names}}
---
--- The `regs` list per atom is ordered: each entry is the MIPS reg index
--- that holds the matching field in the source-order pop sequence.
--- The piece chain uses (DW_OP_regN, DW_OP_piece, ULEB128(field_size)).
---
--- @param ctx DwarfInjectionCtx
--- @param atom_table table[] -- the cross-ref'd atom table from build_atom_table
--- @return table, table -- (rbind_atoms, rbind_structs)
local function parse_rbind_atoms(ctx, atom_table)
local rbind_atoms = {}
local rbind_structs = {}
-- Index binds by struct name; RE-PARSE the body locally (Task 8's local parser
-- handles pointer types too; the scan-source pass's parser only handles U4).
local binds_body_by_name = {}
for _, src in ipairs(ctx.sources or {}) do
local scan = src.scan
if scan then
for _, b in ipairs(scan.binds or {}) do
binds_body_by_name[b.name] = b -- need `b.body`; falls through to local reparse below
end
end
end
-- Re-parse each Binds_X body via the local reparser (U4 + pointer support).
-- The scan-source pass preserves the original body text under each entry's `line` field but doesn't re-emit the body;
-- so we re-walk the source file directly for each Binds_X typedef. (Future: ask scan_source to preserve body.)
for binds_name in pairs(binds_body_by_name) do
local body_text = nil
for _, src in ipairs(ctx.sources or {}) do
local text = src.text
if text then
-- Look for `typedef Struct_(<binds_name>)` in the source.
-- Plain search (4th arg = true) avoids Lua pattern metachar interpretation.
local needle = "typedef Struct_(" .. binds_name .. ")"
local s, e = text:find(needle, 1, true)
if s then
-- Find the body between `{` and matching `}` after `e`.
local brace = text:find("{", e, true)
if brace then
local inner, _after = duffle.read_braces(text, brace)
body_text = inner
break
end
end
end
end
if body_text then
local fields, byte_count = reparse_binds_body(body_text)
rbind_structs[binds_name] = {
bytes = byte_count,
fields = fields,
atom_names = {},
}
end
end
-- Walk every atom_info; if `binds` is set, find the atom body + parse load_word pairs.
local body_by_atom = {}
for _, src in ipairs(ctx.sources or {}) do
local scan = src.scan
if scan then
for _, atom in ipairs(scan.atoms or {}) do
body_by_atom[atom.name] = atom.body
end
end
end
local ai_by_atom = {}
for _, src in ipairs(ctx.sources or {}) do
local scan = src.scan
if scan then
for _, ai in ipairs(scan.atom_infos or {}) do
ai_by_atom[ai.atom_name] = ai
end
end
end
for atom_name, ai in pairs(ai_by_atom) do
if ai.binds then
local struct = rbind_structs[ai.binds]
local body = body_by_atom[atom_name]
if struct and body then
local pairs = parse_body_load_pairs(body, ai.binds)
if #pairs > 0 then
rbind_atoms[atom_name] = {
binds = ai.binds,
fields = struct.fields, -- {name, offset} from local reparse
bytes = struct.bytes,
regs = pairs, -- ordered list of {reg, field}
info_line = ai.info_line,
}
table.insert(struct.atom_names, atom_name)
end
end
end
end
-- Mark rbind atoms in the main atom_table.
for _, atom in ipairs(atom_table) do
if rbind_atoms[atom.name] then
atom.rbind = rbind_atoms[atom.name]
end
end
return rbind_atoms, rbind_structs
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 = elf_dwarf.read_u32_le(existing, unit_pos)
if unit_length == elf_dwarf.ELF32.dw_dwarf32_terminator 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 = elf_dwarf.write_u32_le(new_length)
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 = elf_dwarf.read_u32_le(existing, i)
if ul == elf_dwarf.ELF32.dw_dwarf32_terminator 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 * elf_dwarf.DWARF4_ARANGES.entry_size
local new_ul = ul + added_bytes
local new_ul_bytes = elf_dwarf.write_u32_le(new_ul)
-- Emit everything EXCEPT the last 8 bytes (terminator).
result[#result + 1] = new_ul_bytes
.. existing:sub(i + 4, unit_end - elf_dwarf.DWARF4_ARANGES.terminator_size)
-- Append my atom entries.
for _, atom in ipairs(atom_table) do
local a = atom.addr
local size = atom.size_bytes
result[#result + 1] = elf_dwarf.write_u32_le(a) .. elf_dwarf.write_u32_le(size)
end
-- Append a new terminator.
result[#result + 1] = string.rep("\0", elf_dwarf.DWARF4_ARANGES.terminator_size)
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 < elf_dwarf.DWARF5_RNGLISTS.first_entry_offset or #atom_table == 0 then return existing end
local unit_length = elf_dwarf.read_u32_le(existing, elf_dwarf.DWARF5_RNGLISTS.unit_length_offset)
local version = elf_dwarf.read_u16_le(existing, elf_dwarf.DWARF5_RNGLISTS.version_offset)
local address_size = existing:byte(elf_dwarf.DWARF5_RNGLISTS.addr_size_offset)
local segment_size = existing:byte(elf_dwarf.DWARF5_RNGLISTS.seg_size_offset)
local offset_entry_count = elf_dwarf.read_u32_le(existing, elf_dwarf.DWARF5_RNGLISTS.offset_count_offset)
if unit_length + 4 ~= #existing
or version ~= elf_dwarf.DWARF5_RNGLISTS.version_expected
or address_size ~= elf_dwarf.DWARF5_RNGLISTS.addr_size_expected
or segment_size ~= elf_dwarf.DWARF5_RNGLISTS.seg_size_expected
or offset_entry_count ~= elf_dwarf.DWARF5_RNGLISTS.offset_count_expected
or existing:byte(#existing) ~= DW_RLE_end_of_list then
return existing
end
local entries = {}
for _, atom in ipairs(atom_table) do
entries[#entries + 1] = string.char(DW_RLE_start_length)
.. elf_dwarf.write_u32_le(atom.addr)
.. uleb128(atom.size_bytes)
end
local appended = table.concat(entries)
local new_length = unit_length + #appended
local new_length_bytes = elf_dwarf.write_u32_le(new_length)
return new_length_bytes
.. existing:sub(5, #existing - 1)
.. appended
.. string.char(DW_RLE_end_of_list)
end
-- ════════════════════════════════════════════════════════════════════════════
-- G' helpers: per-atom .debug_info synthesis (DW_TAG_subprogram + DW_TAG_variable)
-- ════════════════════════════════════════════════════════════════════════════
--- Build the location bytes (DW_FORM_exprloc) for a register-resident value.
--- DW_OP_reg0..reg31 occupy opcodes 0x50..0x6f. DW_OP_reg15 is 0x5f;
--- 0x90 is DW_OP_regx, not the base of the compact register opcode range.
--- @param reg_n integer -- 0..31 (MIPS register number)
--- @return string -- the exprloc byte sequence (length-prefixed)
local function reg_exprloc(reg_n)
local op = string.char(DW_OP_reg0 + reg_n)
return uleb128(#op) .. op
end
--- Build the piece-chain location bytes (DW_FORM_exprloc) for an rbind atom's bind_args.
---
--- The location expression is a sequence of (DW_OP_regN, DW_OP_piece, ULEB128(size)) tuples, one per field.
--- GDB composites the pieces into a struct-shaped value matching the Binds_X layout.
---
--- Order matters: the i-th piece corresponds to the i-th byte range in the composite.
--- For Binds_CubeTri (4 fields × 4 bytes), the chain is:
--- reg15 piece(4) -- R_PrimCursor → PrimCursor @ offset 0
--- reg12 piece(4) -- R_FaceCursor → FaceCursor @ offset 4
--- reg13 piece(4) -- R_VertBase → VertBase @ offset 8
--- reg14 piece(4) -- R_OtBase → OtBase @ offset 12
---
--- Each piece's size = byte offset of next field - byte offset of this field (or struct.byte_size for the last piece).
--- Since all fields are U4/pointer (4 bytes each) in the current source, each piece is 4 bytes.
---
--- @param rbind table -- {regs = {{reg, field}, ...}, fields = {{name, offset}, ...}, bytes = N}
--- @return string -- the exprloc byte sequence (length-prefixed)
local function piece_chain_exprloc(rbind)
local op_bytes = {}
local field_offset_by_name = {}
for _, f in ipairs(rbind.fields) do
field_offset_by_name[f.name] = f.offset
end
local next_offset = rbind.bytes
for i = #rbind.regs, 1, -1 do -- walk backwards to know each piece's size
local pair = rbind.regs[i]
local off = field_offset_by_name[pair.field] or 0
local size
if i == #rbind.regs then
size = next_offset - off
else
local next_off = field_offset_by_name[rbind.regs[i + 1].field]
if not next_off then
-- Defensive: a load_word references a field not in the Binds_X struct.
-- Use struct.bytes as a fallback so the piece chain is still well-formed.
next_off = rbind.bytes
end
size = next_off - off
end
-- DW_OP_regN, DW_OP_piece, ULEB128(size)
op_bytes[#op_bytes + 1] = string.char(DW_OP_reg0 + pair.reg, DW_OP_piece) .. uleb128(size)
next_offset = off
end
-- We built it back-to-front; reverse it.
local rev = {}
for i = #op_bytes, 1, -1 do rev[#rev + 1] = op_bytes[i] end
local op = table.concat(rev)
return uleb128(#op) .. op
end
-- ════════════════════════════════════════════════════════════════════════════
-- ULEB/SLEB decoders + .debug_abbrev walker + main-CU locator
-- ════════════════════════════════════════════════════════════════════════════
--
-- Per-atom DWARF DIE synthesis from a DETACHED synthetic CU (which GDB ignored at PC lookup) into the MAIN CU as inserted children.
-- That requires us to:
-- 1. Locate the main CU in .debug_info (walk DWARF32 unit lengths).
-- 2. Locate the main CU's abbrev table in .debug_abbrev (from the CU header).
-- 3. Duplicate that table to a new offset + append codes 100..106.
-- 4. Patch the main CU's debug_abbrev_offset to the duplicate.
-- 5. Insert our DIEs as children of the main CU (just before its root terminator).
--
-- All offsets in this section are 0-based (matching the user's "section offset 0x24" convention).
-- When reading via elf_dwarf.read_u32_le (1-indexed), call with `pos + 1`.
--
-- Pure-Lua 5.3 LEB decoders (no `bit` library; `2^shift` arithmetic matches elf_dwarf.lua's
-- "math.floor(/16) instead of bit.rshift for LuaJIT 2.1 compat" comment at line 352).
--- Read a ULEB128 value starting at 0-based byte offset `pos` in `buf`.
--- Returns (value, next_pos) where `next_pos` is the 0-based offset of the byte AFTER the last byte consumed.
--- Returns (nil, pos) on truncation.
--- @param buf string
--- @param pos integer -- 0-based
--- @return integer|nil, integer
local function read_uleb128_at(buf, pos)
local value = 0
local shift = 0
local buf_len = #buf
while pos < buf_len do
local byte = buf:byte(pos + 1)
value = value + (byte % 0x80) * (2 ^ shift) -- extract low 7 bits, shift left
shift = shift + 7
pos = pos + 1
if byte < 0x80 then return value, pos end -- continuation bit clear → final byte
end
return nil, pos
end
--- Read a SLEB128 value starting at 0-based byte offset `pos` in `buf`.
--- Returns (value, next_pos). Returns (nil, pos) on truncation.
--- @param buf string
--- @param pos integer -- 0-based
--- @return integer|nil, integer
local function read_sleb128_at(buf, pos)
local value = 0
local shift = 0
local buf_len = #buf
while pos < buf_len do
local byte = buf:byte(pos + 1)
value = value + (byte % 0x80) * (2 ^ shift)
shift = shift + 7
pos = pos + 1
if byte < 0x80 then
-- Sign-extend if the sign bit (bit 6) of the last byte is set.
if byte >= 0x40 then
value = value - (2 ^ shift)
end
return value, pos
end
end
return nil, pos
end
--- Walk a .debug_abbrev table starting at 0-based offset `table_start` and return the 0-based offset of the table-terminator byte
--- (a single 0 byte marking the end of declarations in this table per DWARF5 §7.5.3).
---
--- The walker consumes each declaration's abbrev code (ULEB) + tag (ULEB) + has_children byte + (attr, form)
--- attr-spec pairs (each pair = ULEB + ULEB, with an extra SLEB constant for DW_FORM_implicit_const per DWARF5 §7.5.6).
--- The declaration's attr-spec list ends at the (0, 0) pair.
---
--- Returns nil on truncated or malformed input.
--- @param existing string -- the .debug_abbrev section bytes
--- @param table_start integer -- 0-based offset of the table's first byte
--- @return integer|nil -- 0-based offset of the table-terminator byte
local function find_abbrev_table_end(existing, table_start)
local pos = table_start
local buf_len = #existing
-- Empty table = terminator byte at table_start.
if pos >= buf_len or existing:byte(pos + 1) == 0 then return pos end
while pos < buf_len do
-- Each declaration: ULEB code, ULEB tag, 1 byte has_children.
local _code, code_end = read_uleb128_at(existing, pos)
if not _code then return nil end
pos = code_end
local _tag, tag_end = read_uleb128_at(existing, pos)
if not _tag then return nil end
pos = tag_end
-- has_children: 1 byte (DW_CHILDREN_yes=1 / DW_CHILDREN_no=0).
if pos >= buf_len then return nil end
pos = pos + 1
-- attr/form loop until (0, 0) terminator pair.
while pos < buf_len do
local attr, attr_end = read_uleb128_at(existing, pos)
if not attr then return nil end
pos = attr_end
local form, form_end = read_uleb128_at(existing, pos)
if not form then return nil end
pos = form_end
if attr == 0 and form == 0 then break end
-- DW_FORM_implicit_const: declaration carries a SLEB constant.
if form == DW_FORM_implicit_const then
local _const, const_end = read_sleb128_at(existing, pos)
if not _const then return nil end
pos = const_end
end
end
-- Next declaration starts here; if next byte is 0, the table is done.
if pos >= buf_len then return nil end
if existing:byte(pos + 1) == 0 then return pos end
end
return nil
end
-- DWARF5 compile-unit header constants.
local DW_VERSION_5 = 5
local DW_UT_compile = 0x01
local DWARF32_TERMINATOR = 0xFFFFFFFF -- sentinel for DWARF64 marker
local CU_HEADER_SIZE = 12 -- 4 + 2 + 1 + 1 + 4
--- Walk .debug_info to find the FINAL compilation unit, validate it as a DWARF5 32-bit compile-unit, and extract its bounds + abbrev-table offset.
--- Returns nil on any layout mismatch. Callers fall back to existing sections.
---
--- Returns (cu_start, cu_end_excl, abbrev_offset), all 0-based:
--- cu_start -- byte offset of the unit_length field
--- cu_end_excl -- byte offset of the first byte AFTER the main CU
--- abbrev_offset -- the main CU's debug_abbrev_offset field value (0-based)
---
--- Validation:
--- - Section is non-empty.
--- - All units are DWARF32 (unit_length != 0xFFFFFFFF).
--- - No truncated units (each unit's end <= section length).
--- - The section ends exactly on a unit boundary.
--- - At least 2 units present (crt CU + main CU).
--- - The FINAL unit is DWARF5 32-bit compile-unit (version=5, unit_type=0x01, address_size=4).
--- - The final byte of the main CU is 0x00 (the CU's root children-terminator).
---
--- @param existing string -- the .debug_info section bytes
--- @return integer|nil, integer|nil, integer|nil
local function find_main_cu_layout(existing)
local buf_len = #existing
if buf_len < CU_HEADER_SIZE then return nil end
local pos = 0
local main_cu_start = nil
local main_cu_end_excl = nil
while pos + 4 <= buf_len do
local unit_length = elf_dwarf.read_u32_le(existing, pos + 1)
if unit_length == DWARF32_TERMINATOR then return nil end
local unit_end_excl = pos + 4 + unit_length
if unit_end_excl > buf_len then return nil end
main_cu_start = pos
main_cu_end_excl = unit_end_excl
pos = unit_end_excl
end
if pos ~= buf_len or main_cu_start == nil then return nil end
if main_cu_start == 0 then return nil end -- need at least 2 units (crt + main)
-- Validate the main CU header.
-- Bytes relative to main_cu_start (0-based):
-- [0..3] unit_length
-- [4..5] version
-- [6] unit_type
-- [7] address_size
-- [8..11] debug_abbrev_offset
local hdr = main_cu_start + 4
local version = elf_dwarf.read_u16_le(existing, hdr + 1)
local unit_type = existing:byte(hdr + 3)
local address_size = existing:byte(hdr + 4)
local abbrev_off = elf_dwarf.read_u32_le(existing, hdr + 5)
if version ~= DW_VERSION_5 or unit_type ~= DW_UT_compile or address_size ~= 4 then
return nil
end
-- Final byte of the main CU must be the root children-terminator (0).
local final_pos = main_cu_end_excl - 1
if final_pos >= buf_len or existing:byte(final_pos + 1) ~= 0 then return nil end
return main_cu_start, main_cu_end_excl, abbrev_off
end
--- Build the new abbreviation declarations that go AFTER the existing gcc-generated ones. We use codes 100+ to avoid collision.
---
--- The new abbreviations define a 4-deep tree (Task 8 addition):
--- Abbrev 100 (DW_TAG_compile_unit, with children):
--- DW_AT_name (DW_FORM_strp) -- CU name
--- DW_AT_comp_dir (DW_FORM_strp) -- compilation dir
--- DW_AT_language (DW_FORM_data1) -- DW_LANG_C99
--- Abbrev 101 (DW_TAG_subprogram, with children):
--- DW_AT_name (DW_FORM_string) -- atom function name
--- DW_AT_low_pc (DW_FORM_addr) -- atom.addr
--- DW_AT_high_pc (DW_FORM_addr) -- atom.addr + size
--- Abbrev 102 (DW_TAG_variable, no children):
--- DW_AT_name (DW_FORM_string) -- "R_PrimCursor" etc.
--- DW_AT_location (DW_FORM_exprloc) -- DW_OP_regN
--- Abbrev 103 (DW_TAG_structure_type, with children): -- NEW Task 8
--- DW_AT_name (DW_FORM_string) -- "Binds_CubeTri" etc.
--- DW_AT_byte_size (DW_FORM_udata) -- struct byte size
--- Abbrev 104 (DW_TAG_member, no children): -- NEW Task 8
--- DW_AT_name (DW_FORM_string) -- "PrimCursor" etc.
--- DW_AT_data_member_location (DW_FORM_udata) -- byte offset in struct
--- DW_AT_type (DW_FORM_ref4) -- → U4 base type
--- Abbrev 105 (DW_TAG_variable, no children): -- NEW Task 8
--- DW_AT_name (DW_FORM_string) -- "bind_args"
--- DW_AT_location (DW_FORM_exprloc) -- DW_OP_regN, piece, regN, piece, ...
--- DW_AT_type (DW_FORM_ref4) -- → DW_TAG_structure_type DIE
--- Abbrev 106 (DW_TAG_base_type, no children): -- NEW Task 8
--- DW_AT_name (DW_FORM_string) -- "unsigned int"
--- DW_AT_byte_size (DW_FORM_data1) -- 4
--- DW_AT_encoding (DW_FORM_data1) -- DW_ATE_unsigned
---
--- Returns the 7 abbrev declarations + a trailing `\0` byte (the table terminator per DWARF5 §7.5.3).
--- When APPENDED to the existing .debug_abbrev (which has its own terminator), the result is:
--- [existing declarations] [existing \0] [new declarations] [\0]
--- which is a valid (extended) abbrev table.
--- @return string
local function build_new_abbrev()
local function attr(name, form) return uleb128(name) .. uleb128(form) end
local function abbrev(code, tag, has_children, attrs)
local children = has_children and 0x01 or 0x00 -- DW_CHILDREN_yes / no
return uleb128(code)
.. uleb128(tag)
.. string.char(children)
.. attrs
.. string.char(0x00, 0x00) -- end of attr list (2 zeros)
end
local abbrev_cu = abbrev(ABBREV_CU, DW_TAG_compile_unit, true, -- DW_CHILDREN_yes
attr( DW_AT_name, DW_FORM_strp)
.. attr(DW_AT_comp_dir, DW_FORM_strp)
.. attr(DW_AT_language, DW_FORM_data1))
local abbrev_subprogram = abbrev(ABBREV_SUBPROGRAM, DW_TAG_subprogram, true, -- DW_CHILDREN_yes
attr( DW_AT_name, DW_FORM_string)
.. attr(DW_AT_low_pc, DW_FORM_addr)
.. attr(DW_AT_high_pc, DW_FORM_addr)
.. attr(DW_AT_linkage_name, DW_FORM_string)) -- equals DW_AT_name; lets gdb's symbol-table lookup resolve to our subprogram (not the gcc global `code_<name>` const U4 array)
local abbrev_variable = abbrev(ABBREV_VARIABLE, DW_TAG_variable, false, -- DW_CHILDREN_no
attr( DW_AT_name, DW_FORM_string)
.. attr(DW_AT_location, DW_FORM_exprloc)
.. attr(DW_AT_type, DW_FORM_ref4)
.. attr(DW_AT_external, DW_FORM_data1)) -- DW_AT_external=1: visible at CU scope (gdb's `info locals` + Variables pane shows these for the current frame even if the scope lookup misses)
-- Task 8: rbind composite.
-- DW_FORM_udata (0x0F, ULEB128) is declared at module scope. For small values (struct byte_size, member offsets) 1 byte is enough;
-- we emit ULEB128 anyway for spec compliance.
local abbrev_struct_type = abbrev(ABBREV_STRUCT_TYPE, DW_TAG_structure_type, true, -- DW_CHILDREN_yes
attr( DW_AT_name, DW_FORM_string)
.. attr(DW_AT_byte_size, DW_FORM_udata))
local abbrev_member = abbrev(ABBREV_MEMBER, DW_TAG_member, false, -- DW_CHILDREN_no
attr( DW_AT_name, DW_FORM_string)
.. attr(DW_AT_data_member_location, DW_FORM_udata)
.. attr(DW_AT_type, DW_FORM_ref4))
local abbrev_bind_var = abbrev(ABBREV_BIND_VAR, DW_TAG_variable, false, -- DW_CHILDREN_no
attr( DW_AT_name, DW_FORM_string)
.. attr(DW_AT_location, DW_FORM_exprloc)
.. attr(DW_AT_type, DW_FORM_ref4))
local abbrev_base_type = abbrev(ABBREV_BASE_TYPE, DW_TAG_base_type, false, -- DW_CHILDREN_no
attr( DW_AT_name, DW_FORM_string)
.. attr(DW_AT_byte_size, DW_FORM_data1)
.. attr(DW_AT_encoding, DW_FORM_data1))
return abbrev_cu .. abbrev_subprogram .. abbrev_variable
.. abbrev_struct_type .. abbrev_member .. abbrev_bind_var .. abbrev_base_type
.. string.char(0x00)
end
--- Build the new strings to append to .debug_str. Each string is null-terminated.
--- The CU name + comp_dir + 6 per-atom register names all go into one new blob.
--- @param atom_table table[] -- list of {name, addr, size_bytes, ...}
--- @return string, table -- (new_strings_blob, map_of_name_to_offset_in_new_blob)
local function build_new_strings(atom_table)
-- The CU name + comp_dir are the first two strings (offsets 0 and N1).
-- Then each unique atom name + each register name follows.
local strings = {}
local map = {}
-- CU name at offset 0 in the new blob
strings[#strings + 1] = DEFAULT_CU_NAME .. "\0"
map["__cu_name__"] = 0
local cu_name_len = #strings[#strings]
-- comp_dir at offset cu_name_len
strings[#strings + 1] = DEFAULT_CU_COMP_DIR .. "\0"
map["__comp_dir__"] = cu_name_len
local comp_dir_len = #strings[#strings]
-- Atom names (one per unique atom)
for _, atom in ipairs(atom_table) do
local name = atom.name
if not map[name] then
map[name] = #table.concat(strings)
strings[#strings + 1] = name .. "\0"
end
end
-- Register names (one per unique wave-context reg)
for _, loc in ipairs(WAVE_CONTEXT_LOCATIONS) do
if not map[loc.name] then
map[loc.name] = #table.concat(strings)
strings[#strings + 1] = loc.name .. "\0"
end
end
return table.concat(strings), map
end
--- Build the DWARF DIE bytes to insert into the MAIN CU as children, immediately
--- before the main CU's root children-terminator (the final 0 byte of the CU).
---
--- The same content was emitted as a DETACHED synthetic CU appended after the main CU.
--- GDB's PC lookup selects the main CU, so the synthetic CU was out of scope and `RR_PrimCursor` + `bind_args` never appeared in the current frame.
--- Inserting the DIEs as children of the main CU puts them in scope for every PC the main CU owns;
--- including every atom PC (since `.debug_aranges` + `.debug_rnglists` already assign atom PCs to it).
---
--- Layout (matches the pre-build_new_cu exactly; only the insertion point and ref4 basis change):
--- 1) DW_TAG_base_type "unsigned int" (abbrev 106)
--- DW_AT_name = "unsigned int" (DW_FORM_string)
--- DW_AT_byte_size = 4 (DW_FORM_data1)
--- DW_AT_encoding = DW_ATE_unsigned (DW_FORM_data1)
--- 2) Per Binds_X: DW_TAG_structure_type (abbrev 103, children=yes)
--- DW_AT_name = "Binds_CubeTri" etc. (DW_FORM_string)
--- DW_AT_byte_size = struct bytes (DW_FORM_udata)
--- (children: one DW_TAG_member per field)
--- 3) Per field: DW_TAG_member (abbrev 104)
--- DW_AT_name = "PrimCursor" etc.
--- DW_AT_data_member_location = byte offset (DW_FORM_udata)
--- DW_AT_type = ref4 → base_type DIE
--- 4) Per atom: DW_TAG_subprogram (abbrev 101, children=yes)
--- DW_AT_name = atom.name (DW_FORM_string)
--- DW_AT_low_pc = atom.addr
--- DW_AT_high_pc = atom.addr + size_bytes
--- DW_AT_linkage_name = atom.name (DW_FORM_string; same as DW_AT_name)
--- (children: 6 wave-context vars + 1 bind_args var if rbind)
--- 5) Per wave-context reg: DW_TAG_variable (abbrev 102)
--- DW_AT_name = "RR_<reg>" (DW_FORM_string)
--- DW_AT_location = DW_OP_regN (DW_FORM_exprloc)
--- DW_AT_type = ref4 → base_type DIE
--- DW_AT_external = 1
--- 6) For rbind atoms: DW_TAG_variable "bind_args" (abbrev 105)
--- DW_AT_name = "bind_args"
--- DW_AT_location = piece-chain (DW_FORM_exprloc)
--- DW_AT_type = ref4 → structure_type DIE
---
--- **DOES NOT** emit the final 0 byte (root terminator).
--- build_debug_info_section splices our bytes between the existing DIE bytes and that terminator, which is preserved verbatim.
---
--- **ref4 basis**: DW_FORM_ref4 is CU-relative (offset from the first byte of the CU header).
--- Our inserted DIEs live in the main CU, so every ref4 = (target section offset) - main_cu_offset.
--- Per-die section offsets are tracked via the running `next_offset` cursor (= section offset of the NEXT byte to emit).
---
--- @param main_cu_offset integer -- 0-based section offset of the main CU's unit_length field
--- @param main_cu_end_excl integer -- 0-based section offset of the first byte AFTER the main CU
--- @param atom_table table[] -- atoms (with atom.rbind set if rbind)
--- @param rbind_structs table -- {[binds_name] = {bytes, fields, atom_names}}
--- @return string -- bytes to splice into the main CU just before its root terminator
local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_table, rbind_structs)
rbind_structs = rbind_structs or {}
-- We insert IMMEDIATELY BEFORE the main CU's root children-terminator (the last byte of the main CU).
-- The first emitted byte lives at section offset (main_cu_end_excl - 1).
local insertion_start = main_cu_end_excl - 1
local bytes = {}
local next_offset = insertion_start -- 0-based section offset of the NEXT byte to emit
local function emit(s)
bytes[#bytes + 1] = s
next_offset = next_offset + #s
end
local function ref4_of(section_offset)
return section_offset - main_cu_offset
end
-- 1) Emit the base_type DIE first (member ref4s reference it).
local base_type_section_offset = next_offset
emit(uleb128(ABBREV_BASE_TYPE))
emit("unsigned int\0") -- DW_FORM_string (DW_AT_name)
emit(string.char(4)) -- DW_FORM_data1 (DW_AT_byte_size)
emit(string.char(DW_ATE_unsigned)) -- DW_FORM_data1 (DW_AT_encoding)
-- 2) Emit one DW_TAG_structure_type per unique Binds_X.
local struct_section_offsets = {}
local sorted_struct_names = {}
for k in pairs(rbind_structs) do sorted_struct_names[#sorted_struct_names + 1] = k end
table.sort(sorted_struct_names)
for _, binds_name in ipairs(sorted_struct_names) do
local struct = rbind_structs[binds_name]
struct_section_offsets[binds_name] = next_offset
emit(uleb128(ABBREV_STRUCT_TYPE))
emit(binds_name .. "\0") -- DW_FORM_string (DW_AT_name)
emit(uleb128(struct.bytes)) -- DW_FORM_udata (DW_AT_byte_size)
-- Emit DW_TAG_member children (one per field).
for _, field in ipairs(struct.fields) do
emit(uleb128(ABBREV_MEMBER))
emit(field.name .. "\0") -- DW_FORM_string (DW_AT_name)
emit(uleb128(field.offset)) -- DW_FORM_udata (DW_AT_data_member_location)
emit(elf_dwarf.write_u32_le(ref4_of(base_type_section_offset))) -- DW_FORM_ref4 → base_type
end
emit(string.char(0x00)) -- end of structure_type's children
end
-- 3) Emit per-atom DW_TAG_subprograms (children of main CU).
-- Subprograms are named `<name>` (matching the nm symbol; the `code_` prefix was removed from the MipsAtom_ macro in code/duffle/lottes_tape.h).
-- The gcc global `<name>[]` is a DW_TAG_variable without children; our subprogram has the wave-context var children.
-- gdb's symbol resolution picks our subprogram (it has low_pc/high_pc + children) over the gcc global for function-context lookups.
for _, atom in ipairs(atom_table) do
emit(uleb128(ABBREV_SUBPROGRAM))
emit(atom.name .. "\0") -- DW_FORM_string (DW_AT_name)
emit(elf_dwarf.write_u32_le(atom.addr))
emit(elf_dwarf.write_u32_le(atom.addr + atom.size_bytes))
emit(atom.name .. "\0") -- DW_FORM_string (DW_AT_linkage_name; same as DW_AT_name for non-mangled C)
-- Per wave-context reg: DW_TAG_variable (type = U4 via ref4 to base_type DIE)
for _, loc in ipairs(WAVE_CONTEXT_LOCATIONS) do
emit(uleb128(ABBREV_VARIABLE))
emit(loc.name .. "\0") -- DW_FORM_string
emit(reg_exprloc(loc.reg)) -- DW_FORM_exprloc (DW_OP_regN)
emit(elf_dwarf.write_u32_le(ref4_of(base_type_section_offset))) -- DW_FORM_ref4 → base_type
emit(string.char(0x01)) -- DW_AT_external=1 (visible at CU scope)
end
-- If rbind, emit bind_args variable with piece-chain location.
if atom.rbind then
local binds_name = atom.rbind.binds
emit(uleb128(ABBREV_BIND_VAR))
emit("bind_args\0") -- DW_FORM_string (DW_AT_name)
emit(piece_chain_exprloc(atom.rbind)) -- DW_FORM_exprloc
emit(elf_dwarf.write_u32_le(ref4_of(struct_section_offsets[binds_name]))) -- DW_FORM_ref4 → struct_type
end
emit(string.char(0x00)) -- end of subprogram's children
end
-- DO NOT emit a final 0 here — that's the main CU's root terminator, which
-- build_debug_info_section preserves verbatim.
return table.concat(bytes)
end
--- Build the new .debug_abbrev: existing + duplicate of the main CU's table + the new declarations 100..106 (with their terminating 0).
---
--- **Why duplicate the main table**: a CU can name only one abbrev table via its `debug_abbrev_offset` field.
--- The main CU currently points at the gcc-generated table (codes 1..60+);
--- codes 100..106 are NEW, so they live in a different table.
--- To keep all main-CU abbreviation codes in one table we DUPLICATE
--- the gcc-generated codes at a new offset and append our new declarations 100..106 immediately after (with a single shared terminator 0).
---
--- Result layout (0-based section offsets):
--- [0 .. #existing-1] existing .debug_abbrev (unchanged)
--- [#existing .. +#main_dup-1] duplicate of the main table (codes 1..60+, NO terminator)
--- [+#main_dup .. +#new_abbrevs-1] codes 100..106 + final 0
---
--- The MAIN CU's debug_abbrev_offset is patched to `#existing` (start of the duplicate table).
--- Codes 100..106 live inside the same table immediately after the duplicated gcc codes, so a single abbrev-table pointer is enough.
---
--- Fails safely by returning existing sections unchanged if the table walker can't find the table terminator (malformed input).
---
--- @param existing string -- existing .debug_abbrev bytes (verbatim)
--- @param main_abbrev_offset integer -- 0-based offset into `existing` of the main CU's abbrev table
--- @return string, integer -- (new_abbrev_bytes, offset_where_duplicate_table_starts = #existing)
local function build_debug_abbrev_section(existing, main_abbrev_offset)
local table_end = find_abbrev_table_end(existing, main_abbrev_offset)
if not table_end then return existing, nil end
-- Duplicate the main table declarations EXCLUDING its terminating 0 byte.
-- (1-indexed sub: existing:sub(main_abbrev_offset + 1, table_end) reads bytes
-- from 0-based [main_abbrev_offset .. table_end - 1].)
local main_table_dup = existing:sub(main_abbrev_offset + 1, table_end)
local new_abbrevs = build_new_abbrev() -- includes its own terminating 0
-- The MAIN CU's debug_abbrev_offset points to the duplicate's start (= #existing).
-- Codes 100..106 follow the duplicate's declarations inside that same table.
return existing .. main_table_dup .. new_abbrevs, #existing
end
--- Build the new .debug_str: existing strings + new strings appended.
--- @param existing string -- existing .debug_str bytes (verbatim)
--- @param atom_table table[]
--- @return string, integer, table -- (new_str_bytes, new_strings_offset, string_map)
local function build_debug_str_section(existing, atom_table)
local new_strings, string_map = build_new_strings(atom_table)
return existing .. new_strings, #existing, string_map
end
--- Build the new .debug_info: SPLICE inserted DIEs into the MAIN CU as children.
---
--- This function appended a DETACHED synthetic CU to the end of .debug_info.
--- That put every atom DIE in a separate CU from the one GDB selected for PC lookup, so `RR_PrimCursor` + `bind_args` never appeared in scope.
--- This implementation instead:
--- 1. Builds the inserted-children bytes (base_type, struct_types, subprograms with their RR_* + bind_args children) via build_inserted_children.
--- 2. Patches the main CU's `unit_length` field to account for the inserted bytes.
--- 3. Patches the main CU's `debug_abbrev_offset` field to point at the duplicate abbrev table (offset = #existing_abbrev_before_append).
--- 4. Preserves all original main CU DIE bytes verbatim.
--- 5. Replaces the main CU's final byte (the root children-terminator, 0) with: inserted_children_bytes + a single 0 byte (root terminator preserved).
---
--- The crt CU (everything before main_cu_start) is preserved verbatim.
---
--- **No detached synthetic CU is appended.**
---
--- @param existing string -- existing .debug_info bytes (verbatim)
--- @param main_cu_start integer -- 0-based offset of the main CU's unit_length field
--- @param main_cu_end_excl integer -- 0-based offset of the first byte AFTER the main CU
--- @param new_abbrev_offset integer -- 0-based offset into the new .debug_abbrev of the duplicate main table
--- @param atom_table table[]
--- @param rbind_structs table -- {[binds_name] = {bytes, fields, atom_names}} (Task 8)
--- @return string -- the rebuilt .debug_info bytes
local function build_debug_info_section(existing, main_cu_start, main_cu_end_excl, new_abbrev_offset, atom_table, rbind_structs)
-- 1) Build the inserted children bytes (just before the main CU's root terminator).
local inserted = build_inserted_children(main_cu_start, main_cu_end_excl, atom_table, rbind_structs)
local inserted_len = #inserted
-- 2) Patch main CU's unit_length += inserted_len.
local old_unit_length = elf_dwarf.read_u32_le(existing, main_cu_start + 1)
local new_unit_length = old_unit_length + inserted_len
local new_unit_length_bytes = elf_dwarf.write_u32_le(new_unit_length)
-- 3) Patch main CU's debug_abbrev_offset (bytes [main_cu_start + 8 .. + 11]).
local new_abbrev_offset_bytes = elf_dwarf.write_u32_le(new_abbrev_offset)
-- 4) Splice. All offsets below are 0-based; existing:sub is 1-indexed inclusive.
-- Byte ranges (0-based, inclusive):
-- [0 .. main_cu_start - 1] crt CU (verbatim)
-- [main_cu_start + 0 .. + 3] unit_length (PATCHED)
-- [main_cu_start + 4 .. + 7] version + unit_type + address_size (verbatim)
-- [main_cu_start + 8 .. + 11] debug_abbrev_offset (PATCHED)
-- [main_cu_start + 12 .. main_cu_end_excl - 2] existing DIE bytes (verbatim)
-- [main_cu_end_excl - 1] root children-terminator (verbatim 0)
local pre_end = main_cu_end_excl - 2 -- 0-based end of existing DIE bytes (inclusive)
local root_terminator = main_cu_end_excl - 1 -- 0-based position of the final 0 byte
return existing:sub(1, main_cu_start) -- crt CU
.. new_unit_length_bytes -- patched unit_length (4 bytes)
.. existing:sub(main_cu_start + 5, main_cu_start + 8) -- version(2) + unit_type(1) + address_size(1) verbatim
.. new_abbrev_offset_bytes -- patched debug_abbrev_offset (4 bytes)
.. existing:sub(main_cu_start + 13, pre_end + 1) -- existing DIE bytes verbatim
.. inserted -- our inserted children
.. existing:sub(root_terminator + 1, main_cu_end_excl) -- root children-terminator (verbatim 0)
end
--- Build the .debug_loc: just a terminator.
--- Atoms don't have stack frames (the .debug_loc section describes per-instruction location adjustments for call-frame-based variables;
--- we use DW_OP_regN which is register-based and doesn't need .debug_loc entries).
--- The section itself must not be empty OR gdb may complain; the DW_LLE_end_of_list marker (per DWARF5 §7.7) is a single byte 0x00.
--- @return string
local function build_debug_loc_section()
return string.char(0x00)
end
local SECTION_BUILDERS = {
debug_line = build_dwarf_line_section,
debug_aranges = build_dwarf_aranges_section,
debug_rnglists = build_dwarf_rnglists_section,
debug_abbrev = build_debug_abbrev_section,
debug_info = build_debug_info_section,
debug_str = build_debug_str_section,
debug_loc = function() return build_debug_loc_section() end,
}
-- Per-section output path resolver (mirrors SECTION_BUILDERS).
-- Returns the on-disk path for the section's `.bin` blob.
local SECTION_WRITERS = {
debug_line = function(out_root, basename) return out_root .. "\\" .. basename .. ".dwarf_line.bin" end,
debug_aranges = function(out_root, basename) return out_root .. "\\" .. basename .. ".dwarf_aranges.bin" end,
debug_rnglists = function(out_root, basename) return out_root .. "\\" .. basename .. ".dwarf_rnglists.bin" end,
debug_abbrev = function(out_root, basename) return out_root .. "\\" .. basename .. ".dwarf_abbrev.bin" end,
debug_info = function(out_root, basename) return out_root .. "\\" .. basename .. ".dwarf_info.bin" end,
debug_str = function(out_root, basename) return out_root .. "\\" .. basename .. ".dwarf_str.bin" end,
debug_loc = function(out_root, basename) return out_root .. "\\" .. basename .. ".dwarf_loc.bin" end,
}
-- ════════════════════════════════════════════════════════════════════════════
-- 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 via the canonical duffle helper.
elf_path = duffle.to_absolute_path(elf_path)
-- Read the existing DWARF sections directly (no subprocess; lfs + io.open + manual ELF32 section-header walk).
-- We need ALL 7 sections since the F' group (.debug_line, .debug_aranges, .debug_rnglists)
-- extends the existing sections + the G' group (.debug_info, .debug_abbrev, .debug_str, .debug_loc) appends a new compile unit.
-- The dispatch (SECTION_BUILDERS) handles each.
local existing_sections = elf_dwarf.read_elf_sections(elf_path, {
".debug_line", ".debug_aranges", ".debug_rnglists",
".debug_info", ".debug_abbrev", ".debug_str",
-- .debug_loc doesn't exist in the source ELF (we --add-section it on splice);
-- reading it just returns "" which is the "missing" case the builder handles.
".debug_loc",
})
io.stderr:write(string.format(
"[dwarf_injection] read %d DWARF sections: .debug_line=%d .debug_aranges=%d .debug_rnglists=%d .debug_info=%d .debug_abbrev=%d .debug_str=%d .debug_loc=%d\n",
7,
#(existing_sections[".debug_line"] or ""),
#(existing_sections[".debug_aranges"] or ""),
#(existing_sections[".debug_rnglists"] or ""),
#(existing_sections[".debug_info"] or ""),
#(existing_sections[".debug_abbrev"] or ""),
#(existing_sections[".debug_str"] or ""),
#(existing_sections[".debug_loc"] or "")))
-- 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))
-- Task 8: detect rbind atoms + index Binds_* struct fields (from ctx.sources[i].scan, populated by scan-source pass).
local _rbind_atoms, rbind_structs = parse_rbind_atoms(ctx, atom_table)
local rbind_count = 0
for _ in pairs(_rbind_atoms) do rbind_count = rbind_count + 1 end
io.stderr:write(string.format("[dwarf_injection] matched %d rbind atoms across %d Binds_* structs\n",
rbind_count, (function() local n = 0; for _ in pairs(rbind_structs) do n = n + 1 end; return n end)()))
-- Write the .bin files. The build_psyq.ps1 post-link hook splices these into a copy of the ELF via objcopy --update-section.
--
-- Build order (Task 2 GREEN: scope ownership):
-- 0. Validate .debug_info layout (crT CU + DWARF5 main CU + final 0 root terminator).
-- If validation fails, FAIL SAFELY by writing the existing sections verbatim (no malformed output, no synthetic CU append, no header patch).
-- 1. Build new .debug_abbrev using the main CU's abbrev offset → returns the offset of the duplicate main table (= #existing_abbrev).
-- 2. Build new .debug_info by splicing inserted children into the main CU (patches main CU's unit_length + debug_abbrev_offset;
-- preserves all original DIE bytes; does NOT append a synthetic CU).
-- 3. Build .debug_line + .debug_aranges + .debug_rnglists (independent, F' tasks).
-- 4. Build .debug_loc (just a terminator).
--
-- .debug_str is left untouched in this slice: the inserted children use DW_FORM_string (inline) for all DW_AT_name values,
-- so no new strings are required. The existing strings table is preserved verbatim.
-- (The pre-Task 2 synthetic-CU build used .debug_str for CU name + comp_dir; that path is gone.)
local basename = ctx.basename or DEFAULT_BASENAME
if ctx.out_root and ctx.out_root ~= "" then
duffle.ensure_dir(ctx.out_root)
-- Step 0: layout validation. Bail out safely if the .debug_info layout doesn't match what we expect (crt CU + DWARF5 main CU + final 0 byte).
local existing_info = existing_sections[".debug_info"] or ""
local existing_abbrev = existing_sections[".debug_abbrev"] or ""
local main_cu_start, main_cu_end_excl, main_abbrev_offset = find_main_cu_layout(existing_info)
if not main_cu_start then
io.stderr:write("[dwarf_injection] layout validation failed; writing existing sections unchanged\n")
local results_safe = {
{ name = "debug_info", data = existing_info, was = #existing_info },
{ name = "debug_abbrev", data = existing_abbrev, was = #existing_abbrev },
{ name = "debug_str", data = existing_sections[".debug_str"] or "", was = #(existing_sections[".debug_str"] or "") },
{ name = "debug_line", data = build_dwarf_line_section (existing_sections[".debug_line"] or "", atom_table), was = #(existing_sections[".debug_line"] or "") },
{ name = "debug_aranges", data = build_dwarf_aranges_section (existing_sections[".debug_aranges"] or "", atom_table), was = #(existing_sections[".debug_aranges"] or "") },
{ name = "debug_rnglists", data = build_dwarf_rnglists_section(existing_sections[".debug_rnglists"] or "", atom_table), was = #(existing_sections[".debug_rnglists"] or "") },
{ name = "debug_loc", data = build_debug_loc_section(), was = #(existing_sections[".debug_loc"] or "") },
}
local outputs_safe = {}
for _, r in ipairs(results_safe) do
local path = SECTION_WRITERS[r.name](ctx.out_root, basename)
local f = io.open(path, "wb")
if not f then
io.stderr:write(string.format("[dwarf_injection] failed to open %s for write\n", path))
else
f:write(r.data); f:close()
outputs_safe[#outputs_safe + 1] = { [r.name .. "_bin"] = path }
end
end
return { outputs = outputs_safe, errors = {}, warnings = {} }
end
-- Step 1: duplicate main abbrev table + append new codes 100..106.
local new_abbrev, new_abbrev_offset = build_debug_abbrev_section(existing_abbrev, main_abbrev_offset)
if not new_abbrev_offset then
io.stderr:write("[dwarf_injection] main abbrev-table validation failed; refusing to emit malformed DWARF\n")
return { outputs = {}, errors = {}, warnings = {"main abbrev-table validation failed"} }
end
-- Step 2: splice inserted children into the main CU (patches unit_length + abbrev_offset; no synthetic CU).
local new_info = build_debug_info_section(existing_info, main_cu_start, main_cu_end_excl, new_abbrev_offset, atom_table, rbind_structs)
-- Step 3-4: independent sections (.debug_str left untouched — see comment above).
local results = {
{ name = "debug_abbrev", data = new_abbrev, was = #existing_abbrev },
{ name = "debug_info", data = new_info, was = #existing_info },
{ name = "debug_str", data = existing_sections[".debug_str"] or "", was = #(existing_sections[".debug_str"] or "") },
{ name = "debug_line", data = build_dwarf_line_section (existing_sections[".debug_line"] or "", atom_table), was = #(existing_sections[".debug_line"] or "") },
{ name = "debug_aranges", data = build_dwarf_aranges_section (existing_sections[".debug_aranges"] or "", atom_table), was = #(existing_sections[".debug_aranges"] or "") },
{ name = "debug_rnglists", data = build_dwarf_rnglists_section(existing_sections[".debug_rnglists"] or "", atom_table), was = #(existing_sections[".debug_rnglists"] or "") },
{ name = "debug_loc", data = build_debug_loc_section(), was = #(existing_sections[".debug_loc"] or "") },
}
local outputs = {}
for _, r in ipairs(results) do
local path = SECTION_WRITERS[r.name](ctx.out_root, basename)
local f = io.open(path, "wb")
if not f then
io.stderr:write(string.format("[dwarf_injection] failed to open %s for write\n", path))
else
f:write(r.data); f:close()
io.stderr:write(string.format("[dwarf_injection] new .%s = %d bytes (was %d, +%d atoms)\n"
, r.name, #r.data, r.was, #atom_table))
outputs[#outputs + 1] = { [r.name .. "_bin"] = path }
end
end
return {
outputs = outputs,
errors = {},
warnings = {},
}
end
return { outputs = {}, errors = {}, warnings = {} }
end
return M