mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-07-23 16:07:50 +00:00
2456 lines
130 KiB
Lua
2456 lines
130 KiB
Lua
--- 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, and 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) plus deterministic `<out_root>/<basename>.gdbinit` skip commands.
|
||
---
|
||
--- 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")
|
||
|
||
-- word-counting helper shared with passes/atoms_source_map.lua.
|
||
-- Used here to walk a component's body_tokens in lockstep with their word-count allocation
|
||
-- when we propagate per-word body lines into each invocation's `body_lines` array.
|
||
local word_count_eval = require("word_count_eval")
|
||
local count_token_words = word_count_eval.count_token_words
|
||
|
||
local lfs = require("lfs")
|
||
|
||
-- File-scope LEB128 aliases; the helpers are available at module-load time so no forward-reference table is needed.
|
||
local uleb128 = elf_dwarf.uleb128
|
||
local sleb128 = elf_dwarf.sleb128
|
||
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- 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_negate_stmt = DWARF_LINE_OPS.DW_LNS_negate_stmt
|
||
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
|
||
|
||
-- RR_<R_Name> debug-visible variables come from the merged register_alias_registry filtered to aliases whose code is a valid MIPS GPR 0..31
|
||
-- (see collect_per_source_registries + by_alias in build_inserted_children).
|
||
-- Names are prefixed with `RR` to avoid collision with the C-level enum (`{R_AtomJmp = 25, R_TapePtr = 24, R_PrimCursor = 15, ...}` declared in duffle headers);
|
||
-- without the prefix, gdb's `print R_PrimCursor` resolves to the enum constant, not to this DWARF variable.
|
||
-- With `set print enum on`, gdb displays the enum identifier, so the Watch panel would show `R_PrimCursor = R_PrimCursor` instead of the register value.
|
||
-- Each RR_<R_Name>'s DW_OP_regN uses the alias's `code` as N.
|
||
-- DW_OP_bregN would describe a memory location addressed from a register;
|
||
-- the breg form would make gdb dereference the atom register value rather than display it.
|
||
|
||
-- 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)
|
||
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)
|
||
-- Component step-into (DW_TAG_inlined_subroutine + abstract DW_TAG_subprogram).
|
||
local ABBREV_ABSTRACT_SUBPROGRAM = 0x6B -- 107: DW_TAG_subprogram (abstract — no low_pc/high_pc); for each unique mac_X component
|
||
local ABBREV_INLINED_SUBROUTINE = 0x6C -- 108: DW_TAG_inlined_subroutine with children (per-component invocation range)
|
||
-- Bind_args uses DW_FORM_sec_offset → .debug_loclists for PC-ranged liveness
|
||
-- (each field transitions from tape memory to GPR at load_pc + 8 = MIPS I load-delay slot boundary).
|
||
local ABBREV_BIND_VAR_LOCLIST = 0x6D -- 109: DW_TAG_variable no children + DW_AT_type = ref4 + DW_AT_location = sec_offset
|
||
|
||
-- DWARF5 §7.7.3 loclist opcodes.
|
||
local DW_LLE_end_of_list = 0x00
|
||
local DW_LLE_start_length = 0x08
|
||
local DW_OP_reg0 = 0x50 -- base reg op; regN = 0x50 + N
|
||
local DW_OP_breg0 = 0x70 -- base breg op; bregN = 0x70 + N (SLEB offset)
|
||
local MIPS_LOAD_DELAY_BYTES = 0x08 -- 1 load word + 1 BD-slot word
|
||
|
||
-- 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
|
||
-- Component step-into.
|
||
local DW_TAG_inlined_subroutine = 0x1D
|
||
|
||
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
|
||
-- Inlined_subroutine + abstract_origin attributes.
|
||
local DW_AT_abstract_origin = 0x31
|
||
local DW_AT_call_file = 0x58
|
||
local DW_AT_call_line = 0x59
|
||
local DW_AT_inline = 0x20 -- DWARF5 §7.7.1: DW_AT_inline (used by abstract subprogram for the mac_X() components)
|
||
-- decl_file + decl_line on the abstract subprogram so consumers can resolve an abstract origin back to its definition site even when no inlined_subroutine instance currently maps to it.
|
||
local DW_AT_decl_file = 0x3A -- DWARF5 §7.7.1: DW_AT_decl_file (1-based file index into the CU's file table)
|
||
local DW_AT_decl_line = 0x3B -- DWARF5 §7.7.1: DW_AT_decl_line
|
||
|
||
-- File index lookup table for the existing main line unit (Unit 2).
|
||
-- Provenance paths come back with mixed slashes; we normalize to basename and look up against the line unit's actual file table.
|
||
-- Current scope has two provenance basenames: hello_gte_tape.c (the atom's call site) and lottes_tape.h (the component definition).
|
||
-- Both live in the existing gcc-generated line unit;
|
||
-- their 1-based indices are stable across rebuilds because the include order in code/gte_hello/hello_gte.c determines the unit's file table.
|
||
local PROVENANCE_BASENAME_TO_FILE_INDEX = {
|
||
["hello_gte_tape.c"] = ATOM_SOURCE_FILE_INDEX, -- = 11
|
||
["lottes_tape.h"] = 4,
|
||
}
|
||
|
||
--- Resolve an absolute provenance path to the line-unit file index used by the emitting line program.
|
||
--- Normalizes mixed `/` and `\` separators to a basename and looks it up against the known file table.
|
||
---
|
||
--- Fails loudly on an unknown provenance basename: adding a new component source file requires extending
|
||
--- `PROVENANCE_BASENAME_TO_FILE_INDEX` so the line-program emission contract stays explicit.
|
||
--- Silent fallback to ATOM_SOURCE_FILE_INDEX would mask the new-file case by misattributing component rows to the atom's source file.
|
||
--- @param path string -- absolute provenance path (e.g. "C:/.../lottes_thttps://www.youtube.com/watch?v=ORM4yLkdKx8ape.h" or "C:\\...\\lottes_tape.h")
|
||
--- @return integer -- 1-based line-unit file index
|
||
local function resolve_provenance_file_index(path)
|
||
if path == nil or path == "" then
|
||
error("[dwarf_injection] resolve_provenance_file_index: empty path")
|
||
end
|
||
-- Normalize backslashes → forward slashes (paths arrive with mixed separators from the provenance file: forward slashes from Lua's io.lines;
|
||
-- backslashes if the input ever round-trips through Windows shell expansion).
|
||
local normalized = path:gsub("\\", "/")
|
||
-- Take the last path component (the basename).
|
||
local basename = normalized:match("([^/]+)$") or normalized
|
||
local idx = PROVENANCE_BASENAME_TO_FILE_INDEX[basename]
|
||
if idx == nil then
|
||
error(string.format(
|
||
"[dwarf_injection] resolve_provenance_file_index: unknown provenance basename '%s' (from '%s'). "
|
||
.. "Extend PROVENANCE_BASENAME_TO_FILE_INDEX in passes/dwarf_injection.lua.",
|
||
basename, path))
|
||
end
|
||
return idx
|
||
end
|
||
|
||
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_FORM_sec_offset = 0x17 -- 4-byte section-relative offset (into .debug_loclists / .debug_rnglists)
|
||
|
||
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', but we want this CU to look like a C TU so VSCode's Variables pane treats it as code.)
|
||
|
||
-- R_<name> → MIPS GPR lookups go through the merged register_alias_registry
|
||
-- (collected by collect_per_source_registries from ctx.sources[*].scan.register_alias_registry).
|
||
-- Aliases without an `atom_reg` opt-in are absent from the registry, which means they are NOT debug-visible (no fallback GPR).
|
||
-- See the precedence chain in build_inserted_children for the per-alias type resolution.
|
||
|
||
-- Build the .debug_loclists section for every rbind atom.
|
||
-- Returns the section bytes (DWARF5 layout: unit_length(4) + version=5(2) + address_size=4(1) + segment_size=0(1) + offset_entry_count=0(4) + DW_LLE entries;
|
||
-- each atom contributes one loclist terminated by DW_LLE_end_of_list).
|
||
--
|
||
-- Per rbind atom we emit 2 loclist entries (tape → GPR split) using the last field's transition as the boundary.
|
||
-- This is a conservative approximation that always reads the correct GPR value once the first load has retired (load_pc + 8).
|
||
-- A future task can refine to per-field transitions.
|
||
--
|
||
-- `tape_alias` (default: "R_TapePtr") names the wave-runtime pointer register whose value is the tape address.
|
||
-- The GPR integer comes from the merged registry; if the alias is absent, this function fails loud
|
||
-- (the build was misconfigured; rbind atoms depend on R_TapePtr being in the registry for their piece-chain DW_OP_breg<N> location).
|
||
-- @param atom_table table[] -- list of atoms with .rbind set
|
||
-- @param registries table -- merged registries from collect_per_source_registries
|
||
-- @return string -- section bytes
|
||
local function build_debug_loclists_section(atom_table, registries)
|
||
registries = registries or {}
|
||
-- R_TapePtr comes from the merged register_alias_registry (only present if the user opted it in via `#define atom_reg` in lottes_tape.h).
|
||
-- When absent we emit just the section terminator (a single DW_LLE_end_of_list byte);
|
||
-- the .debug_loclists section MUST not be empty for the linker, and `bind_args` will be emitted with no loclist PC range
|
||
-- (readelf will display it as having no .debug_loclists entries).
|
||
local tape_alias_entry = registries.register_alias_registry and registries.register_alias_registry["R_TapePtr"]
|
||
local tape_reg = tape_alias_entry and tape_alias_entry.code
|
||
local parts = {}
|
||
for _, atom in ipairs(atom_table) do
|
||
if atom.rbind and tape_reg then
|
||
local fields = atom.rbind.fields or {}
|
||
local regs = atom.rbind.regs or {}
|
||
local n_fields = #fields
|
||
local last_load_pc = atom.addr + (n_fields - 1) * MIPS_BYTES_PER_WORD
|
||
local transition_pc = last_load_pc + MIPS_LOAD_DELAY_BYTES
|
||
local tape_pieces = {}
|
||
for _, f in ipairs(fields) do
|
||
local offset = f.offset or 0
|
||
local offset_sleb = elf_dwarf.sleb128(offset)
|
||
table.insert(tape_pieces, string.char(DW_OP_breg0 + tape_reg) .. offset_sleb .. string.char(DW_OP_piece) .. uleb128(4))
|
||
end
|
||
local tape_expr = table.concat(tape_pieces)
|
||
local gpr_pieces = {}
|
||
for _, pair in ipairs(regs) do
|
||
table.insert(gpr_pieces, string.char(DW_OP_reg0 + pair.reg) .. string.char(DW_OP_piece) .. uleb128(4))
|
||
end
|
||
local gpr_expr = table.concat(gpr_pieces)
|
||
parts[#parts + 1] = string.char(DW_LLE_start_length)
|
||
.. elf_dwarf.write_u32_le(atom.addr)
|
||
.. uleb128(#tape_expr)
|
||
.. tape_expr
|
||
parts[#parts + 1] = string.char(DW_LLE_start_length)
|
||
.. elf_dwarf.write_u32_le(transition_pc)
|
||
.. uleb128(#gpr_expr)
|
||
.. gpr_expr
|
||
parts[#parts + 1] = string.char(DW_LLE_end_of_list)
|
||
end
|
||
end
|
||
local body = table.concat(parts)
|
||
local unit_length = 2 + 1 + 1 + 4 + #body
|
||
local header = elf_dwarf.write_u32_le(unit_length)
|
||
.. elf_dwarf.write_u16_le(5)
|
||
.. string.char(4)
|
||
.. string.char(0)
|
||
.. elf_dwarf.write_u32_le(0)
|
||
return header .. body
|
||
end
|
||
|
||
-- Compute the per-atom loclist offset within a .debug_loclists section.
|
||
-- @param atom_table table[] -- list of atoms with .rbind set
|
||
-- @return table -- {[atom_name] = offset_in_section}
|
||
local function compute_loclists_offsets(atom_table)
|
||
local offsets = {}
|
||
local cursor = 4 + 2 + 1 + 1 + 4
|
||
for _, atom in ipairs(atom_table) do
|
||
if atom.rbind then
|
||
offsets[atom.name] = cursor
|
||
local n_fields = #atom.rbind.fields
|
||
local tape_expr_len = n_fields * 3
|
||
local gpr_expr_len = n_fields * 3
|
||
local tape_entry = 1 + 4 + 1 + tape_expr_len
|
||
local gpr_entry = 1 + 4 + 1 + gpr_expr_len
|
||
local body_len = tape_entry + gpr_entry + 1
|
||
cursor = cursor + body_len
|
||
end
|
||
end
|
||
return offsets
|
||
end
|
||
|
||
-- 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 sources table[] -- source files with scan.skip_over associations
|
||
--- @field out_root string -- output root (e.g. "build/gen")
|
||
--- @field basename string -- input ELF basename (default "hello_gte")
|
||
|
||
--- Normalize a source path for GDB linespecs and component-association keys.
|
||
--- GDB accepts forward slashes on Windows; absolute paths avoid cwd-dependent sidecars and match provenance contract.
|
||
--- @param path string
|
||
--- @return string
|
||
local function normalize_debug_path(path)
|
||
return duffle.to_absolute_path(path):gsub("\\", "/")
|
||
end
|
||
|
||
--- Build a case-insensitive Windows path + exact component-name key.
|
||
--- @param path string
|
||
--- @param component_name string
|
||
--- @return string
|
||
local function component_skip_key(path, component_name)
|
||
return normalize_debug_path(path):lower() .. "\0" .. component_name
|
||
end
|
||
|
||
--- Consume the per-source scanner associations without naming any atom or component in production.
|
||
--- Whole atoms remain symbol-keyed; components are file-qualified internally so a source marker associates
|
||
--- with its exact component definition even though GDB 12 requires function-only skip entries for the resulting synthetic inline frame.
|
||
--- @param ctx DwarfInjectionCtx
|
||
--- @return table -- {atoms = {[symbol] = association}, components = {[file|name] = association}}
|
||
local function collect_skip_over(ctx)
|
||
local skip_over = { atoms = {}, components = {} }
|
||
for _, src in ipairs(ctx.sources or {}) do
|
||
local scan_skip = src.scan and src.scan.skip_over
|
||
if scan_skip then
|
||
for atom_name, association in pairs(scan_skip.atoms or {}) do
|
||
skip_over.atoms[atom_name] = {
|
||
name = atom_name,
|
||
source_path = normalize_debug_path(src.path),
|
||
association = association,
|
||
}
|
||
end
|
||
for component_name, association in pairs(scan_skip.components or {}) do
|
||
local source_path = normalize_debug_path(src.path)
|
||
skip_over.components[component_skip_key(source_path, component_name)] = {
|
||
name = component_name,
|
||
source_path = source_path,
|
||
association = association,
|
||
}
|
||
end
|
||
end
|
||
end
|
||
return skip_over
|
||
end
|
||
|
||
--- Merge the per-source scanner registries (register_alias_registry, type_name_registry, atom_views)
|
||
--- into a single set of tables that downstream consumers can read from without re-iterating ctx.sources.
|
||
---
|
||
--- Every `R_*` lookup and per-atom type override resolution in this file goes through this merged table.
|
||
--- Aliases without `atom_reg` adjacent are absent; the absence is treated as "not debug-visible" (see build_inserted_children for the precedence chain).
|
||
---
|
||
--- When two sources register the same key, the last-writer wins (later sources override earlier).
|
||
--- Today only one source declares wave-context enums, so collisions are absent;
|
||
--- future multi-source declarations would need a name-resolution rule (left as a follow-up).
|
||
--- @param ctx DwarfInjectionCtx
|
||
--- @return table -- {
|
||
--- register_alias_registry = {[R_Name] = AliasEntry},
|
||
--- type_name_registry = {[T] = TypeEntry},
|
||
--- atom_views = {[atom_name] = AtomViewEntry},
|
||
--- }
|
||
local function collect_per_source_registries(ctx)
|
||
local merged = {
|
||
register_alias_registry = {},
|
||
type_name_registry = {},
|
||
atom_views = {},
|
||
-- Per-atom atom_ctx declarations: atom_name -> {rbind_atom, ...}
|
||
-- (populated by scan_source from `atom_ctx(<atom_name>)` sub-calls inside `atom_info`)
|
||
atom_ctxs = {},
|
||
-- Per-phase atom groups: phase_label -> {atoms = {atom_name1, ...}}
|
||
-- (populated by scan_source from `atom_phase(<label>)` sub-calls inside `atom_info`; cross-source merged)
|
||
atom_phases = {},
|
||
-- Per-source already-resolved atom_infos (used by the precedence chain's ctx/phase steps)
|
||
atom_infos = {},
|
||
}
|
||
for _, src in ipairs(ctx.sources or {}) do
|
||
local scan = src.scan
|
||
if scan then
|
||
for k, v in pairs(scan.register_alias_registry or {}) do merged.register_alias_registry[k] = v end
|
||
for k, v in pairs(scan.type_name_registry or {}) do merged.type_name_registry[k] = v end
|
||
for k, v in pairs(scan.atom_views or {}) do merged.atom_views[k] = v end
|
||
for k, v in pairs(scan.atom_ctxs or {}) do merged.atom_ctxs[k] = v end
|
||
for k, v in pairs(scan.atom_phases or {}) do merged.atom_phases[k] = v end
|
||
for _, ai in ipairs(scan.atom_infos or {}) do merged.atom_infos[#merged.atom_infos + 1] = ai end
|
||
end
|
||
end
|
||
return merged
|
||
end
|
||
|
||
--- Render deterministic debugger skip commands. Ordering is stable by category:
|
||
--- exact atom symbols first (lexicographic), then exact component function names (lexicographic full command).
|
||
--- Atom commands come from the matched nm/source-map table so the emitted name is the actual ELF symbol.
|
||
--- The scanner tables and command set both deduplicate repeated source observations.
|
||
--- @param skip_over table
|
||
--- @param atom_table table[] -- nm/source-map cross-reference; names are actual ELF symbols
|
||
--- @return string
|
||
local function build_gdbinit(skip_over, atom_table)
|
||
local commands = {}
|
||
local atom_names = {}
|
||
for _, atom in ipairs(atom_table) do
|
||
if atom.skip_over then atom_names[#atom_names + 1] = atom.name end
|
||
end
|
||
table.sort(atom_names)
|
||
for _, atom_name in ipairs(atom_names) do
|
||
commands[#commands + 1] = "skip function " .. atom_name
|
||
end
|
||
|
||
local component_commands = {}
|
||
for _, component in pairs(skip_over.components) do
|
||
component_commands[#component_commands + 1] = "skip function mac_" .. component.name
|
||
end
|
||
table.sort(component_commands)
|
||
local prior = nil
|
||
for _, command in ipairs(component_commands) do
|
||
if command ~= prior then commands[#commands + 1] = command end
|
||
prior = command
|
||
end
|
||
|
||
return table.concat(commands, "\n") .. "\n"
|
||
end
|
||
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- 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).
|
||
-- File-scope `local uleb128` + `local sleb128` aliases live near the module top so they're resolvable by every function below.
|
||
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- DWARF line-program encoder
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
|
||
--- Build the byte sequence for ONE atom's line program:
|
||
--- DW_LNE_set_address(addr)
|
||
--- [entry 1 emission]
|
||
--- for each subsequent entry (idx 2..N):
|
||
--- DW_LNS_advance_pc(1 .word = 4 bytes)
|
||
--- [entry emission at new PC]
|
||
--- DW_LNE_end_sequence
|
||
---
|
||
--- Each "entry emission" emits one or more rows at the same PC, tracking the source state {file_idx, line}.
|
||
--- State transitions emit DW_LNS_set_file + DW_LNS_advance_line as needed;
|
||
--- same-state transitions emit only the row (copy).
|
||
---
|
||
--- Entry rules:
|
||
--- * RAW entry (no invocation): emit (call_file, entry.line) at this PC.
|
||
--- * Invocation entry, NOT first word: emit (comp_file, comp_line).
|
||
--- * Invocation entry, IS first word: emit TWO rows at the same PC in order: (call_file, inv.call_line), then (comp_file, inv.comp_line).
|
||
---
|
||
--- atom-entry rules:
|
||
--- * Atom entry 1 also gets a duplicate copy_op for the GDB 12 zero-instruction-prologue marker (no advance_pc — same PC).
|
||
---
|
||
--- 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.
|
||
---
|
||
--- Statement-state rules:
|
||
--- * A marked whole atom emits one opaque is_stmt=false range row and no nested component rows; its subprogram symbol/range remains available.
|
||
--- * A marked component keeps its first-word call-site row true, toggles only the definition row/range false, and restores before the next unmarked row.
|
||
--- * Whole-atom suppression wins over component markers; no nested inversion.
|
||
--- @param atom table -- {name, addr, size_bytes, words, entries, invocations?, skip_over?}
|
||
--- @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 negate_stmt() return string.char(DW_LNS_negate_stmt) 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
|
||
|
||
-- A jump-threaded atom is reached by `jr`, not a C call.
|
||
-- GDB therefore cannot apply the parent `skip function` entry before descending into a visible child inline frame.
|
||
-- Make an explicitly marked atom DWARF-opaque:
|
||
-- one non-statement row covers its complete address range, with no per-word or component source transitions.
|
||
-- The atom's subprogram DIE remains, so symbolic lookup, explicit address breakpoints, and stepi are unaffected.
|
||
if atom.skip_over then
|
||
return table.concat({
|
||
set_address(atom.addr),
|
||
set_file(ATOM_SOURCE_FILE_INDEX),
|
||
advance_line(atom.entries[1].line - 1),
|
||
negate_stmt(),
|
||
copy_op(),
|
||
advance_pc(atom.size_bytes),
|
||
negate_stmt(),
|
||
end_sequence(),
|
||
})
|
||
end
|
||
|
||
-- Find which invocation each entry belongs to (if any).
|
||
-- Returns the invocation record or nil. Pre-computed once so we don't rescan per emitted row.
|
||
local function inv_for_idx(idx)
|
||
if not atom.invocations then return nil end
|
||
for _, inv in ipairs(atom.invocations) do
|
||
-- inv.start_pos / inv.end_pos are 0-based .word positions; entries are 1-based.
|
||
if idx >= inv.start_pos + 1 and idx <= inv.end_pos + 1 then
|
||
return inv
|
||
end
|
||
end
|
||
return nil
|
||
end
|
||
|
||
local parts = {
|
||
set_address(atom.addr), -- 7 bytes: marker + size + sub + addr; PC := atom.addr
|
||
}
|
||
|
||
-- Source state tracker: emits set_file + advance_line only on transitions, keeps bytes minimal.
|
||
-- Both fields stay in sync with what we emit so we never duplicate a set_file or skip a state-change emit.
|
||
local cur_file = nil -- line-state.file_idx (nil = uninitialized)
|
||
local cur_line = 1 -- line-state.line starts at 1 (per DWARF spec)
|
||
local is_stmt = true -- main line unit default_is_stmt; every sequence ends restored
|
||
|
||
local function set_statement_state(want_stmt)
|
||
if is_stmt ~= want_stmt then
|
||
parts[#parts + 1] = negate_stmt()
|
||
is_stmt = want_stmt
|
||
end
|
||
end
|
||
|
||
-- Emit a state transition (set_file + advance_line + is_stmt as needed) before a row, then the row itself.
|
||
-- Used for the row(s) at one entry PC.
|
||
local function emit_row(file_idx, line, want_stmt)
|
||
if cur_file ~= file_idx then
|
||
parts[#parts + 1] = set_file(file_idx)
|
||
cur_file = file_idx
|
||
end
|
||
if cur_line ~= line then
|
||
parts[#parts + 1] = advance_line(line - cur_line)
|
||
cur_line = line
|
||
end
|
||
set_statement_state(want_stmt)
|
||
parts[#parts + 1] = copy_op()
|
||
end
|
||
|
||
-- Whole-atom suppression wraps the independent sequence itself.
|
||
-- File/line state setup follows while is_stmt is already false; the matching restore is emitted after the final row below.
|
||
if atom.skip_over then set_statement_state(false) end
|
||
|
||
-- --- Atom entry (idx 1) -------------------------------------------------
|
||
-- Determine entry-1's primary row (always the call-site file/line;
|
||
-- we may emit a second row immediately after if entry 1 is also a component invocation's first word).
|
||
local inv1 = inv_for_idx(1)
|
||
local entry_1 = atom.entries[1]
|
||
-- The atom body's own source file (the call site for any mac_X(...) inside it).
|
||
local call_file_idx = ATOM_SOURCE_FILE_INDEX
|
||
|
||
-- Primary row: call site at atom start.
|
||
-- Capture its (file, line) so we can re-emit it as the GDB 12 zero-instruction-prologue marker below.
|
||
local entry_1_call_file = call_file_idx
|
||
local entry_1_call_line = (inv1 and inv1.call_line) or entry_1.line
|
||
local atom_is_stmt = not atom.skip_over
|
||
|
||
-- Primary row: call site at atom start.
|
||
emit_row(entry_1_call_file, entry_1_call_line, atom_is_stmt)
|
||
|
||
-- If entry 1 is the first word of a component invocation, emit the component-definition row at the same PC immediately after.
|
||
-- The def line is always a statement target unless the inv is explicitly skip-over (atom_dbg_skip_over);
|
||
-- the whole-atom skip path takes the early-return at line ~497 above so atom_is_stmt is irrelevant here.
|
||
-- For the body row, prefer `body_lines[1]` (the actual source line of the first body word in the macro's expansion)
|
||
-- over `comp_line` (the macro signature line).
|
||
-- When `body_lines` is absent (the component was not indexed by the scan, e.g. an external macro),
|
||
-- fall back to `comp_line` so the line program remains valid.
|
||
if inv1 and 1 == inv1.start_pos + 1 then
|
||
local comp_file_idx_1 = resolve_provenance_file_index(inv1.comp_file)
|
||
local body_line_1 = (inv1.body_lines and inv1.body_lines[1]) or inv1.comp_line
|
||
emit_row(comp_file_idx_1, body_line_1, not inv1.skip_over)
|
||
end
|
||
|
||
-- GDB 12 zero-instruction-prologue marker: duplicate of the PRIMARY entry-1 emission (the call-site row).
|
||
-- We must explicitly restore the source state to (call_file, call_line) before emitting the duplicate,
|
||
-- otherwise the duplicate lands at whatever state we ended entry-1 in (the component row, if entry 1 is an invocation start),
|
||
---which would attach the marker to the wrong source statement.
|
||
---
|
||
-- Only emitted at atom entry, NOT at every component-invocation start.
|
||
emit_row(entry_1_call_file, entry_1_call_line, atom_is_stmt)
|
||
|
||
-- --- Subsequent entries (idx 2..N) --------------------------------------
|
||
for idx = 2, #atom.entries do
|
||
local entry = atom.entries[idx]
|
||
local inv = inv_for_idx(idx)
|
||
|
||
-- Advance PC by 1 .word (4 bytes on MIPS).
|
||
parts[#parts + 1] = advance_pc(MIPS_BYTES_PER_WORD)
|
||
|
||
if inv and idx == inv.start_pos + 1 then
|
||
-- First word of a component invocation: emit TWO rows at this PC.
|
||
-- 1) call-site row remains a statement target unless the whole atom is marked.
|
||
emit_row(call_file_idx, inv.call_line, atom_is_stmt)
|
||
-- 2) component body row at this PC.
|
||
-- Prefer `body_lines[1]` over `comp_line` so gdb's `step` lands on the macro's actual body line (not the signature line above `{ ... }`).
|
||
-- Fallback to `comp_line` when the component isn't indexed.
|
||
local body_file_idx = resolve_provenance_file_index(inv.comp_file)
|
||
local body_line_1 = (inv.body_lines and inv.body_lines[1]) or inv.comp_line
|
||
emit_row(body_file_idx, body_line_1, not inv.skip_over)
|
||
elseif inv then
|
||
-- Subsequent word of an invocation: 1-based offset into body_lines:
|
||
-- word 1 of the invocation corresponds to body_lines[1], word 2 -> body_lines[2], etc.
|
||
-- 1-based offset = idx - inv.start_pos (since idx = inv.start_pos + i for the i-th body word).
|
||
-- When `body_lines` is missing (older emitters / external macros), fall back to comp_line.
|
||
local body_file_idx = resolve_provenance_file_index(inv.comp_file)
|
||
local words_into = idx - inv.start_pos -- 1-based word position in invocation
|
||
local body_line_i = (inv.body_lines and inv.body_lines[words_into]) or inv.comp_line
|
||
emit_row(body_file_idx, body_line_i, not inv.skip_over)
|
||
else
|
||
-- RAW word: single call-site row. emit_row restores is_stmt after a selected component range before exposing this adjacent row.
|
||
emit_row(call_file_idx, entry.line, atom_is_stmt)
|
||
end
|
||
end
|
||
|
||
-- Every sequence starts from default_is_stmt=true.
|
||
-- Restore that state before DW_LNE_end_sequence so a marked whole atom has explicit bounded toggles
|
||
-- and no state can leak to a following independent atom sequence.
|
||
set_statement_state(true)
|
||
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.
|
||
--- Also consumes the provenance file to record per-component invocations.
|
||
--- Each atom gains an `invocations` field with one entry per `mac_X(...)` call site:
|
||
--- `{comp_name, call_file, call_line, comp_file, comp_line, start_pos, end_pos, body_lines}`.
|
||
---
|
||
--- `body_lines` is the per-word source line within the macro body
|
||
--- (lottes_tape.h:N where N is the actual line of this `.word` in the macro expansion).
|
||
--- Without this field, the line program emits `comp_line` for EVERY body word,
|
||
--- so gdb's `step` from a `mac_X(...)` call lands on the macro signature line and immediately
|
||
--- returns without traversing the body (since no PC reports a different line).
|
||
---
|
||
--- The data is computed by walking the component's pre-tokenized body in `ctx.sources[i].scan.atoms[j]`
|
||
--- (the MipsAtomComp_/MipsAtomComp_Proc_ declaration).
|
||
--- Atom labels (`atom_label(...)`) emit 0 `.word`s and are ignored (matching `passes/atoms_source_map.lua :: is_marker_token` + `count_marker_rest`).
|
||
--- @param ctx DwarfInjectionCtx
|
||
--- @param skip_over table -- collect_skip_over(ctx) result
|
||
--- @return table[] -- list of {name, addr, size_bytes, words, entries, invocations, skip_over?}
|
||
local function build_atom_table(ctx, skip_over)
|
||
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
|
||
|
||
-- Also read *.atoms.provenance.txt to extract per-component invocations.
|
||
-- Files are merged by atom name; entries carry the original {pos, call_file, call_line, comp_name, comp_file, comp_line} shape.
|
||
local prov_files = duffle.list_dir(ctx.out_root, "%.atoms.provenance%.txt$")
|
||
local prov_merged = {}
|
||
for _, prov_path in ipairs(prov_files) do
|
||
local prov = elf_dwarf.parse_provenance_file(prov_path, 1)
|
||
for name, prov_data in pairs(prov) do
|
||
prov_merged[name] = prov_data
|
||
end
|
||
end
|
||
|
||
-- Build a per-source component index keyed by the bare component name (e.g. `gte_load_tri_verts`, NOT `ac_gte_load_tri_verts`).
|
||
-- The bare name matches the provenance row's `comp_name` field (which is `strip_mac_prefix_from_token(tok)` — strips `mac_`, leaves the rest).
|
||
-- Each entry holds the data we need to walk the component body's tokens in lockstep with their word counts:
|
||
-- body_off -- byte offset of the `{` (start of body) in the component's source file.
|
||
-- body_tokens -- list of {tok, rel} pairs; `rel` is the byte offset within the body.
|
||
-- line_of -- closure resolving byte offsets in the component's source file to lines.
|
||
-- The data is consumed by `compute_invocation_body_lines` per invocation.
|
||
local component_index = {}
|
||
for _, src in ipairs(ctx.sources or {}) do
|
||
if src.scan and src.scan.atoms then
|
||
local line_of = src.scan.line_of
|
||
for _, atom in ipairs(src.scan.atoms) do
|
||
if atom.kind == "comp_bare" or atom.kind == "comp_proc" then
|
||
-- Prefer `atom.name` (stripped of `ac_` prefix); fall back to `raw_name`
|
||
-- only if `name` is missing (defensive; scan-source always sets both).
|
||
local name = atom.name or atom.raw_name
|
||
if name and not component_index[name] then
|
||
component_index[name] = {
|
||
body_off = atom.body_off,
|
||
body_tokens = atom.body_tokens,
|
||
line_of = line_of,
|
||
source_path = src.path,
|
||
}
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Per-word line lookup for a component body: walk body_tokens, count each token's emitted .words via count_token_words, attribute that count the same source line.
|
||
-- Atom labels (atom_label/atom_offset) emit 0 .words; their lines are skipped to stay aligned with `passes/atoms_source_map.lua :: count_marker_rest`.
|
||
-- @param comp_name string -- the bare name (e.g. `gte_load_tri_verts`)
|
||
-- @return table -- list of source lines, 1-based by word position; empty if no data
|
||
local wc = (ctx.shared and ctx.shared.word_counts) or {}
|
||
local function compute_invocation_body_lines(comp_name)
|
||
local comp_idx = component_index[comp_name]
|
||
if not (comp_idx and comp_idx.body_tokens and comp_idx.line_of) then return {} end
|
||
local lines = {}
|
||
for _, bt in ipairs(comp_idx.body_tokens) do
|
||
local tok = duffle.trim(bt.tok or "")
|
||
if tok ~= "" then
|
||
-- Match atoms_source_map.lua's marker check (no public export; duplicated for independence).
|
||
local leading = duffle.read_ident(tok, 1)
|
||
local words
|
||
if leading == "atom_label" or leading == "atom_offset" then
|
||
words = 0 -- markers emit 0 .words; do not advance the body line counter.
|
||
else
|
||
words = count_token_words(tok, wc)
|
||
end
|
||
if words > 0 then
|
||
local body_line = comp_idx.line_of(comp_idx.body_off + bt.rel)
|
||
for _ = 1, words do lines[#lines + 1] = body_line end
|
||
end
|
||
end
|
||
end
|
||
return lines
|
||
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
|
||
local atom = {
|
||
name = name,
|
||
addr = info[1],
|
||
size_bytes = info[2],
|
||
words = sm.total,
|
||
entries = sm.words,
|
||
skip_over = skip_over.atoms[name] ~= nil,
|
||
}
|
||
-- Group consecutive MACRO rows in this atom's provenance into invocations.
|
||
-- An invocation = one `mac_X(...)` call site spanning N consecutive .word rows.
|
||
-- Two consecutive rows with the same (comp_name, call_file, call_line, comp_file, comp_line) are part of the same invocation.
|
||
local prov_data = prov_merged[name]
|
||
if prov_data and prov_data.words then
|
||
local invocations = {}
|
||
local cur_inv = nil
|
||
for _, w in ipairs(prov_data.words) do
|
||
if w.comp_name then
|
||
local inv_key = w.comp_name .. "|" .. w.call_file .. "|" .. w.call_line .. "|" .. w.comp_file .. "|" .. w.comp_line
|
||
if cur_inv and cur_inv.key == inv_key then
|
||
-- Same invocation as the previous word — extend its range.
|
||
cur_inv.end_pos = w.pos
|
||
else
|
||
-- New invocation: flush the previous one and start fresh.
|
||
if cur_inv then invocations[#invocations + 1] = cur_inv end
|
||
cur_inv = {
|
||
key = inv_key,
|
||
comp_name = w.comp_name,
|
||
call_file = w.call_file,
|
||
call_line = w.call_line,
|
||
comp_file = w.comp_file,
|
||
comp_line = w.comp_line,
|
||
start_pos = w.pos,
|
||
end_pos = w.pos,
|
||
skip_over = skip_over.components[component_skip_key(w.comp_file, w.comp_name)] ~= nil,
|
||
}
|
||
-- Capture the per-word body lines for THIS invocation, indexed by 1-based word position within the invocation.
|
||
-- body_lines[1] is the line of the first body word (= the line of the first macro-body token, NOT the comp def line).
|
||
-- Downstream consumers (the line program emitter) fall back to comp_line when this is empty.
|
||
cur_inv.body_lines = compute_invocation_body_lines(w.comp_name)
|
||
end
|
||
else
|
||
-- RAW row: flush the current invocation.
|
||
if cur_inv then invocations[#invocations + 1] = cur_inv; cur_inv = nil end
|
||
end
|
||
end
|
||
if cur_inv then invocations[#invocations + 1] = cur_inv end
|
||
atom.invocations = invocations
|
||
end
|
||
out[#out + 1] = atom
|
||
end
|
||
end
|
||
table.sort(out, function(a, b) return a.addr < b.addr end)
|
||
return out
|
||
end
|
||
|
||
--- Compute the set of distinct components invoked across all atoms.
|
||
--- Returns `{name -> {kind, def_file, def_line}}` keyed by component name (e.g. `yield`, `gte_load_tri_verts`).
|
||
--- @param atom_table table[]
|
||
--- @return table<string, table>
|
||
local function collect_component_defs(atom_table)
|
||
local out = {}
|
||
for _, atom in ipairs(atom_table) do
|
||
for _, inv in ipairs(atom.invocations or {}) do
|
||
if not out[inv.comp_name] then
|
||
out[inv.comp_name] = {
|
||
name = inv.comp_name,
|
||
def_file = inv.comp_file,
|
||
def_line = inv.comp_line,
|
||
}
|
||
end
|
||
end
|
||
end
|
||
return out
|
||
end
|
||
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- 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).
|
||
--
|
||
-- The prior local `reparse_binds_body` fallback (and the 2nd source walk inside `parse_rbind_atoms`) is REMOVED.
|
||
-- parse_rbind_atoms consumes `scan.binds[i].fields` directly, which is populated by the scan-source pass with the typed-field record ({name, type_name, pointer_depth, offset, byte_size}).
|
||
|
||
--- 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)),
|
||
--- ...
|
||
---
|
||
--- The GPR for each `R_<reg>` is looked up in the merged register_alias_registry; aliases absent from the registry
|
||
--- (no `atom_reg` opt-in) are silently skipped — the resulting rbind record will be incomplete and the atom will fail to bind a usable piece chain.
|
||
--- This is intentional: silently falling back to a hardcoded GPR would mask the missing opt-in.
|
||
--- @param body string -- the atom body text (between { ... })
|
||
--- @param binds_name string -- expected Binds_X name (skip pairs with mismatching binds)
|
||
--- @param registries table -- merged registries from collect_per_source_registries
|
||
--- @return table[] -- list of {reg = <MIPS index>, field = <field name>}
|
||
local function parse_body_load_pairs(body, binds_name, registries)
|
||
local pairs = {}
|
||
local pos = 1
|
||
local body_len = #body
|
||
local reg_index_by_name = (registries and registries.register_alias_registry) or {}
|
||
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*%)$")
|
||
local alias_entry = reg_index_by_name[reg_name]
|
||
if b and b == binds_name and alias_entry and alias_entry.code then
|
||
pairs[#pairs + 1] = {
|
||
reg = alias_entry.code,
|
||
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)).
|
||
---
|
||
--- The 2nd source walk (the body-text `text:find("typedef Struct_(...)")` re-walk) is removed;
|
||
--- per-source `scan.binds[i].fields` already carries the typed-field record after the scan-source generalization.
|
||
--- @param ctx DwarfInjectionCtx
|
||
--- @param atom_table table[] -- the cross-ref'd atom table from build_atom_table
|
||
--- @param registries table -- merged registries from collect_per_source_registries
|
||
--- @return table, table -- (rbind_atoms, rbind_structs)
|
||
local function parse_rbind_atoms(ctx, atom_table, registries)
|
||
registries = registries or {}
|
||
local rbind_atoms = {}
|
||
local rbind_structs = {}
|
||
|
||
-- Index binds by struct name; consume `scan.binds[i].fields` directly (no body-text re-walk).
|
||
-- The scan-source pass emits each Binds_X's fields as {[type_name, pointer_depth, offset, byte_size, ...]}
|
||
-- so this pass can build the rbind_structs entry without re-parsing.
|
||
local binds_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_by_name[b.name] = b
|
||
end
|
||
end
|
||
end
|
||
|
||
for binds_name, b in pairs(binds_by_name) do
|
||
if b.fields and b.bytes then
|
||
rbind_structs[binds_name] = {
|
||
bytes = b.bytes,
|
||
fields = b.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, registries)
|
||
if #pairs > 0 then
|
||
rbind_atoms[atom_name] = {
|
||
binds = ai.binds,
|
||
fields = struct.fields, -- {name, offset} from scan.binds
|
||
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:
|
||
--- 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):
|
||
--- 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):
|
||
--- 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):
|
||
--- 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):
|
||
--- 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
|
||
--- Abbrev 107 (DW_TAG_subprogram, NO children) — abstract origin.
|
||
--- DW_AT_name (DW_FORM_string) -- "mac_yield" etc. (component ident)
|
||
--- DW_AT_inline (DW_FORM_data1) -- DW_INL_declared_inlined (= 3)
|
||
--- DW_AT_external (DW_FORM_data1) -- 1 (visible across the CU)
|
||
--- Abbrev 108 (DW_TAG_inlined_subroutine, with children):
|
||
--- DW_AT_abstract_origin (DW_FORM_ref4) -- → ABBREV_ABSTRACT_SUBPROGRAM
|
||
--- DW_AT_low_pc (DW_FORM_addr) -- invocation start PC
|
||
--- DW_AT_high_pc (DW_FORM_addr) -- invocation end PC
|
||
--- DW_AT_call_file (DW_FORM_udata) -- file index of the call site
|
||
--- DW_AT_call_line (DW_FORM_udata) -- line of the call site
|
||
---
|
||
--- Returns the 9 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)
|
||
|
||
-- 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))
|
||
|
||
-- Component step-into abstract + inline DIE abbreviations.
|
||
local DW_INL_declared_inlined = 0x03 -- DWARF5 §3.33.3: "this subroutine was declared inline"
|
||
-- Abstract subprograms now carry DW_AT_decl_file + DW_AT_decl_line so consumers can resolve the abstract origin back to its definition site
|
||
-- even when no inlined_subroutine instance currently maps to it.
|
||
-- DW_FORM_udata is consistent with the call_file/call_line forms on abbrev 108.
|
||
local abbrev_abstract_subprogram = abbrev(ABBREV_ABSTRACT_SUBPROGRAM, DW_TAG_subprogram, false, -- DW_CHILDREN_no
|
||
attr( DW_AT_name, DW_FORM_string)
|
||
.. attr(DW_AT_inline, DW_FORM_data1)
|
||
.. attr(DW_AT_external, DW_FORM_data1)
|
||
.. attr(DW_AT_decl_file, DW_FORM_udata)
|
||
.. attr(DW_AT_decl_line, DW_FORM_udata))
|
||
|
||
local abbrev_inlined_subroutine = abbrev(ABBREV_INLINED_SUBROUTINE, DW_TAG_inlined_subroutine, false, -- DW_CHILDREN_no (emits no per-inlined-instance children; the PC range IS the inlining scope)
|
||
attr( DW_AT_abstract_origin, DW_FORM_ref4)
|
||
.. attr(DW_AT_low_pc, DW_FORM_addr)
|
||
.. attr(DW_AT_high_pc, DW_FORM_addr)
|
||
.. attr(DW_AT_call_file, DW_FORM_udata)
|
||
.. attr(DW_AT_call_line, DW_FORM_udata))
|
||
|
||
-- bind_args with DW_FORM_sec_offset → .debug_loclists.
|
||
-- The .debug_loclists section holds a sequence of DW_LLE entries;
|
||
-- the first matching entry for a PC describes each field's value (tape memory DW_OP_breg24 or register DW_OP_regN).
|
||
local abbrev_bind_var_loclists = abbrev(ABBREV_BIND_VAR_LOCLIST, DW_TAG_variable, false, -- DW_CHILDREN_no
|
||
attr( DW_AT_name, DW_FORM_string)
|
||
.. attr(DW_AT_location, DW_FORM_sec_offset)
|
||
.. attr(DW_AT_type, DW_FORM_ref4))
|
||
|
||
return abbrev_cu .. abbrev_subprogram .. abbrev_variable
|
||
.. abbrev_struct_type .. abbrev_member .. abbrev_bind_var .. abbrev_base_type
|
||
.. abbrev_abstract_subprogram .. abbrev_inlined_subroutine
|
||
.. abbrev_bind_var_loclists
|
||
.. string.char(0x00)
|
||
end
|
||
|
||
--- Build the new strings to append to .debug_str. Each string is null-terminated.
|
||
-- The CU name + comp_dir + one entry per RR_<name> debug-visible alias all go into one new blob.
|
||
-- Register names are sourced from the merged registry filtered to MIPS GPR 0..31
|
||
-- (the same filter that build_inserted_children applies for the RR_<name> locals, so .debug_str entries stay in sync with .debug_info).
|
||
-- @param atom_table table[] -- list of {name, addr, size_bytes, ...}
|
||
-- @param registries table -- merged registries from collect_per_source_registries
|
||
-- @return string, table -- (new_strings_blob, map_of_name_to_offset_in_new_blob)
|
||
|
||
--- Strip a leading "R_" prefix from an enum alias name.
|
||
--- The .debug_str/.debug_info consumers display the local without the C-enum prefix (RR_PrimCursor, not RR_R_PrimCursor)
|
||
--- because the C-level identifier `R_PrimCursor` would collide with the enum constant value 15 when the user runs `print R_PrimCursor` in gdb.
|
||
--- @param r_name string -- e.g. "R_PrimCursor"
|
||
--- @return string -- "PrimCursor" (or the input unchanged if it does not start with "R_")
|
||
local function strip_r_prefix(r_name)
|
||
if #r_name >= 2 and r_name:byte(1) == 0x52 and r_name:byte(2) == 0x5F then -- "R_"
|
||
return r_name:sub(3)
|
||
end
|
||
return r_name
|
||
end
|
||
|
||
local function build_new_strings(atom_table, registries)
|
||
registries = registries or {}
|
||
-- 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 debug-visible R_Name alias from the merged registry,
|
||
-- filtered to MIPS GPR 0..31 — the same filter that build_inserted_children applies
|
||
-- for the RR_<name> locals, so .debug_str entries stay in sync with .debug_info).
|
||
for r_name, alias in pairs(registries.register_alias_registry or {}) do
|
||
if alias.code and alias.code >= 0 and alias.code <= 31 then
|
||
local rr_name = "RR_" .. strip_r_prefix(r_name)
|
||
if not map[rr_name] then
|
||
map[rr_name] = #table.concat(strings)
|
||
strings[#strings + 1] = rr_name .. "\0"
|
||
end
|
||
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; atom.invocations set if mac_X(...) calls)
|
||
--- @param rbind_structs table -- {[binds_name] = {bytes, fields, atom_names}}
|
||
--- @param loclists_offsets table -- {[atom_name] = section-relative offset into the new .debug_loclists}
|
||
--- @param registries table -- merged registries from collect_per_source_registries
|
||
--- @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, loclists_offsets, registries)
|
||
rbind_structs = rbind_structs or {}
|
||
loclists_offsets = loclists_offsets or {}
|
||
registries = registries or {}
|
||
|
||
-- by_alias: the merged register_alias_registry filtered to aliases whose `code` is a valid MIPS GPR 0..31.
|
||
-- Aliases absent from the merged registry are not debug-visible and are skipped entirely (no fallback GPR).
|
||
local by_alias = {}
|
||
for r_name, alias in pairs(registries.register_alias_registry or {}) do
|
||
if alias.code and alias.code >= 0 and alias.code <= 31 then
|
||
by_alias[r_name] = alias
|
||
end
|
||
end
|
||
|
||
-- Build a name->atom lookup for fast rbind_atom resolution during the per-atom phase/ctx propagation.
|
||
-- Cheap (O(atom_table)) and built once.
|
||
local function build_atom_name_index(atoms)
|
||
local m = {}
|
||
for _, a in ipairs(atoms or {}) do
|
||
if a and a.name then m[a.name] = a end
|
||
end
|
||
return m
|
||
end
|
||
local atom_by_name_global = build_atom_name_index(atom_table)
|
||
|
||
-- 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)
|
||
|
||
-- Typed local views.
|
||
-- For each unique (type_name, pointer_depth) pair across all rbind atom fields, emit a synthetic type chain.
|
||
-- The chain is: ... → pointer_type → typedef → base_type.
|
||
-- The outermost pointer_type (depth = N) is the variable's DW_AT_type.
|
||
-- For a field declared "V4_S2*" (depth=1), the chain is:
|
||
-- V4_S2 (typedef, references base_type 4-byte unsigned) + ptr_to_V4_S2_1 (pointer_type, byte_size 4, refs the typedef)
|
||
-- gdb walks: variable type = ptr_to_V4_S2_1 → V4_S2 → base_type, and displays "V4_S2 *" (the typedef's name + the pointer depth).
|
||
local type_offsets = {} -- {[type_name] = section_offset for the typedef DIE}
|
||
-- Always include the base_type "unsigned int" as the U4 target.
|
||
type_offsets["U4"] = base_type_section_offset
|
||
-- Collect every unique (type_name, max_pointer_depth) used by any rbind field.
|
||
local used_typed_views = {} -- { [type_name] = max_depth }
|
||
for _, atom in ipairs(atom_table) do
|
||
if atom.rbind and atom.rbind.fields then
|
||
for _, f in ipairs(atom.rbind.fields) do
|
||
if f.type_name and f.pointer_depth and f.pointer_depth > 0 then
|
||
local depth = used_typed_views[f.type_name] or 0
|
||
if f.pointer_depth > depth then
|
||
used_typed_views[f.type_name] = f.pointer_depth
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
-- Sort for deterministic emission.
|
||
local sorted_typed_types = {}
|
||
for tn in pairs(used_typed_views) do sorted_typed_types[#sorted_typed_types + 1] = tn end
|
||
table.sort(sorted_typed_types)
|
||
-- For each non-U4 type, emit a typedef (DW_TAG_typedef) named after the type and referencing the base_type "unsigned int" (4 bytes).
|
||
-- The typedef gives gdb a named anchor; the pointer_type chain wraps it.
|
||
-- We use a fresh abbrev for the typedef + the pointer_type.
|
||
-- To stay within the existing abbrev budget, we reuse the duplicated main table's abbrev 4 (DW_TAG_typedef) and abbrev 9 (DW_TAG_pointer_type)
|
||
-- via a synthetic-emit pattern: emit the abbrev code (a ULEB) + attribute bytes using DW_FORM values that match those abbrevs.
|
||
-- The cleanest path is to define new abbrevs in build_new_abbrev().
|
||
-- We emit fresh DW_TAG_base_type DIEs (the simplest correct shape: byte_size 4, encoding unsigned)
|
||
-- and the variable's DW_AT_type references the pointer chain's outermost base_type.
|
||
-- The displayed type name is the type_name (e.g. "V4_S2") because we use DW_FORM_string on the field type.
|
||
-- gdb walks the chain and displays the name.
|
||
--
|
||
-- For each (type_name, depth), emit a real structure_type with proper member layout,
|
||
-- then a single DW_TAG_pointer_type pointing at the structure_type. gdb's
|
||
-- `print *<var>` then expands the struct and lists its members (x, y, z, w for V4_S2; etc.).
|
||
--
|
||
-- Hardcoded member-info table for the 6 typed views the prototype uses (V2_S2 / V3_S2 / V4_S2 / V2_S4 / V3_S4 / V4_S4).
|
||
-- Each row is {byte_size, members}, where each member is {name, offset, byte_size, type_name}.
|
||
-- `type_name` is the base type name from `type_name_registry`; "S2" / "S4" need a signed base_type emit too.
|
||
--
|
||
-- The table is small + explicit (no introspection of the struct types from production source)
|
||
-- because the prototype principle treats the typed-view struct layout as data, not derived state.
|
||
local STRUCT_MEMBER_TABLE = {
|
||
-- 2-element signed short vector (rare; placeholder for future use).
|
||
V2_S2 = { byte_size = 4, members = {
|
||
{ name = "x", offset = 0, byte_size = 2 },
|
||
{ name = "y", offset = 2, byte_size = 2 },
|
||
}},
|
||
-- 4-element signed short vector (the dominant face-cursor type).
|
||
V4_S2 = { byte_size = 8, members = {
|
||
{ name = "x", offset = 0, byte_size = 2 },
|
||
{ name = "y", offset = 2, byte_size = 2 },
|
||
{ name = "z", offset = 4, byte_size = 2 },
|
||
{ name = "w", offset = 6, byte_size = 2 },
|
||
}},
|
||
-- 3-element signed short vector (the floor face-cursor type; includes explicit pad byte to match math.h's S2 pad member).
|
||
V3_S2 = { byte_size = 8, members = {
|
||
{ name = "x", offset = 0, byte_size = 2 },
|
||
{ name = "y", offset = 2, byte_size = 2 },
|
||
{ name = "z", offset = 4, byte_size = 2 },
|
||
{ name = "pad", offset = 6, byte_size = 2 },
|
||
}},
|
||
-- 2-element signed int vector.
|
||
V2_S4 = { byte_size = 8, members = {
|
||
{ name = "x", offset = 0, byte_size = 4 },
|
||
{ name = "y", offset = 4, byte_size = 4 },
|
||
}},
|
||
-- 3-element signed int vector.
|
||
V3_S4 = { byte_size = 16, members = {
|
||
{ name = "x", offset = 0, byte_size = 4 },
|
||
{ name = "y", offset = 4, byte_size = 4 },
|
||
{ name = "z", offset = 8, byte_size = 4 },
|
||
{ name = "pad", offset = 12, byte_size = 4 },
|
||
}},
|
||
-- 4-element signed int vector.
|
||
V4_S4 = { byte_size = 16, members = {
|
||
{ name = "x", offset = 0, byte_size = 4 },
|
||
{ name = "y", offset = 4, byte_size = 4 },
|
||
{ name = "z", offset = 8, byte_size = 4 },
|
||
{ name = "w", offset = 12, byte_size = 4 },
|
||
}},
|
||
}
|
||
local S2_TYPE_BYTE_SIZE = 2 -- S2 = signed 16-bit (see dsl.h)
|
||
local S4_TYPE_BYTE_SIZE = 4 -- S4 = signed 32-bit (see dsl.h)
|
||
|
||
-- Pre-emit the signed base types (S2, S4) once if any typed view's member needs them.
|
||
-- We emit per-typed-view lazily below; build a member-base-type offset cache (idempotent).
|
||
local member_base_type_offsets = {}
|
||
local function ensure_member_base_type(tn, byte_size, encoding)
|
||
local key = tn .. "|" .. byte_size .. "|" .. encoding
|
||
if member_base_type_offsets[key] then return member_base_type_offsets[key] end
|
||
local off = next_offset
|
||
emit(uleb128(ABBREV_BASE_TYPE))
|
||
emit(tn .. "\0")
|
||
emit(string.char(byte_size))
|
||
emit(string.char(encoding))
|
||
member_base_type_offsets[key] = off
|
||
return off
|
||
end
|
||
-- Always emit S2 + S4 (the v*_S2 / v*_S4 family base types) once before any typed-view,
|
||
-- even if no atom currently declares a v*_S2 field, so future atoms pick them up without rewiring.
|
||
ensure_member_base_type("S2", S2_TYPE_BYTE_SIZE, 5) -- DW_ATE_signed = 5
|
||
ensure_member_base_type("S4", S4_TYPE_BYTE_SIZE, 5)
|
||
|
||
local type_chain_offsets = {}
|
||
for _, tn in ipairs(sorted_typed_types) do
|
||
if tn ~= "U4" then
|
||
local depth = used_typed_views[tn]
|
||
local type_info = STRUCT_MEMBER_TABLE[tn]
|
||
if not type_info then
|
||
-- Unknown typed view — fall back to a generic base_type to keep the wire valid
|
||
-- (gdb will render as the typename but `print *ptr` will only see the first 4 bytes).
|
||
-- This is the pre-Track-A behavior; preserved here for forward compatibility with future typed views not in our table.
|
||
local innermost_offset = next_offset
|
||
emit(uleb128(ABBREV_BASE_TYPE))
|
||
emit(tn .. "\0")
|
||
emit(string.char(4)) -- byte_size = 4 (legacy stand-in)
|
||
emit(string.char(DW_ATE_unsigned)) -- encoding = unsigned
|
||
local outermost_offset = next_offset
|
||
emit(uleb128(9)) -- DW_TAG_pointer_type
|
||
emit(elf_dwarf.write_u32_le(ref4_of(innermost_offset)))
|
||
type_chain_offsets[tn .. "|" .. depth] = outermost_offset
|
||
else
|
||
-- Emit a proper structure_type DIE for this typed view.
|
||
local struct_offset = next_offset
|
||
emit(uleb128(ABBREV_STRUCT_TYPE))
|
||
emit(tn .. "\0") -- DW_AT_name (struct_type has children, no name in abbrev 103 [DW_FORM_string only])
|
||
emit(uleb128(type_info.byte_size)) -- DW_AT_byte_size (DW_FORM_udata)
|
||
-- For each member, emit ABBREV_MEMBER (name + data_member_location + type ref4).
|
||
for _, m in ipairs(type_info.members) do
|
||
emit(uleb128(ABBREV_MEMBER))
|
||
emit(m.name .. "\0")
|
||
emit(uleb128(m.offset))
|
||
-- The member's type is S2 (for v*_S2 family) or S4 (for v*_S4 family), based on the member's byte_size.
|
||
local member_type_name, member_type_encoding
|
||
if m.byte_size == 2 then
|
||
member_type_name = "S2"
|
||
member_type_encoding = 5 -- DW_ATE_signed = 5
|
||
elseif m.byte_size == 4 then
|
||
member_type_name = "S4"
|
||
member_type_encoding = 5 -- DW_ATE_signed = 5
|
||
else
|
||
-- Unexpected byte_size; fall back to U4 (unsigned int) since U4 base type is already emitted
|
||
member_type_name = "U4"
|
||
member_type_encoding = 7 -- DW_ATE_unsigned = 7
|
||
end
|
||
local member_base_off = ensure_member_base_type(member_type_name, m.byte_size, member_type_encoding)
|
||
emit(elf_dwarf.write_u32_le(ref4_of(member_base_off))) -- DW_AT_type ref4 → base_type
|
||
end
|
||
emit(string.char(0x00)) -- end of structure_type's children
|
||
-- Emit a single DW_TAG_pointer_type (abbrev 9) pointing at the structure_type.
|
||
-- For depth > 1, we'd chain pointer_type → pointer_type → ... → structure_type; not exercised.
|
||
if depth == 1 then
|
||
local outermost_offset = next_offset
|
||
emit(uleb128(9)) -- DW_TAG_pointer_type
|
||
emit(elf_dwarf.write_u32_le(ref4_of(struct_offset))) -- DW_AT_type → structure_type
|
||
type_chain_offsets[tn .. "|" .. depth] = outermost_offset
|
||
else
|
||
error("typed-view: pointer_depth > 1 is not yet supported in this emission path")
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
-- 1b) Emit the void* fallback chain (used by step (f) of the per-RR_<R_Name> precedence chain).
|
||
-- One DW_TAG_base_type DIE named "void" + one DW_TAG_pointer_type pointing at it.
|
||
-- Unannotated RR_<R_X> locals reference the outermost pointer_type; gdb displays the value as `(void *) 0x...`
|
||
-- (hex) per the prototype principle rather than `unsigned int`.
|
||
-- The void base_type is emitted BEFORE any other typed chain so its ref4 pointer remains stable.
|
||
-- Follow the SAME pattern as the typed-views chain above: capture the offset BEFORE the uleb tag
|
||
-- (this is the ref4 target), emit the DIE bytes, then emit the pointer_type pointing at the offset.
|
||
local void_chain_offset = next_offset
|
||
emit(uleb128(ABBREV_BASE_TYPE))
|
||
emit("void\0") -- DW_FORM_string (DW_AT_name)
|
||
emit(string.char(1)) -- DW_FORM_data1 (DW_AT_byte_size = 1; DWARF's "void" base_type)
|
||
emit(string.char(DW_ATE_unsigned)) -- DW_FORM_data1 (DW_AT_encoding = unsigned; DWARF doesn't define a void encoding but gdb reads the name "void" off the DIE and renders it correctly as `(void *)` when wrapped in a pointer_type)
|
||
emit(uleb128(9)) -- DW_TAG_pointer_type (abbrev 9; DW_AT_type = ref4 to its target)
|
||
emit(elf_dwarf.write_u32_le(ref4_of(void_chain_offset))) -- 4-byte ref4: points at the void base_type's tag byte
|
||
-- type_chain_offsets["void|1"] is what step (f) of the per-RR_<R_Name> chain looks up.
|
||
type_chain_offsets["void|1"] = void_chain_offset -- both the base_type offset and the pointer_type are emitted consecutively; the OUTERMOST is the pointer_type. The variable's DW_AT_type must reference the pointer_type, not the base_type. Patch below.
|
||
-- Capture the pointer_type's offset (the last-thing-emitted DIE start) and overwrite the lookup.
|
||
-- The pointer_type was emitted as: uleb(9) (1 byte) + 4-byte ref4 = 5 bytes. Its tag byte is at void_chain_offset + 8 (the base_type's 8 bytes: 1 tag + 5 name + 1 byte_size + 1 encoding).
|
||
local ptr_void_offset = void_chain_offset + 8
|
||
type_chain_offsets["void|1"] = ptr_void_offset
|
||
|
||
-- 1c) Emit the U4 * pointer chain (step (e) fallback for enum-site `atom_type(U4 *)`).
|
||
-- The chain is just one DW_TAG_pointer_type pointing at the pre-emitted "unsigned int" base_type at `base_type_section_offset`.
|
||
-- This is intentionally separate from the per-type typed-views chain (which emits V4_S2* / V3_S2* etc.)
|
||
-- because U4 is the C-side typedef alias for `unsigned int`, not a fresh struct;
|
||
-- reusing the pre-emitted base type keeps the wire consistent.
|
||
-- Once this chain is registered as `type_chain_offsets["U4|1"]`, step (e) of the per-RR_<R_Name> precedence chain will resolve `atom_type(U4 *)`
|
||
-- declarations on aliases like `R_PrimCursor` and `R_OtBase` to `U4 *` (gdb renders as `(unsigned int *)` with the value displayed in hex).
|
||
emit(uleb128(9)) -- DW_TAG_pointer_type (abbrev 9; DW_AT_type = ref4 to its target)
|
||
emit(elf_dwarf.write_u32_le(ref4_of(base_type_section_offset))) -- 4-byte ref4 → "unsigned int" base_type
|
||
local u4_chain_offset = next_offset - 5 -- 1 (uleb tag) + 4 (ref4) = 5 bytes; capture the pointer_type's start offset
|
||
type_chain_offsets["U4|1"] = u4_chain_offset
|
||
|
||
-- 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)
|
||
-- typed field.
|
||
-- For a pointer-typed field, the member's DW_AT_type points at the deepest pointer_type in its chain.
|
||
-- For U4 (no pointer), it points at the base_type.
|
||
local field_type_offset
|
||
if field.pointer_depth and field.pointer_depth > 0 then
|
||
field_type_offset = type_chain_offsets[field.type_name .. "|" .. field.pointer_depth]
|
||
end
|
||
if not field_type_offset then
|
||
field_type_offset = base_type_section_offset
|
||
end
|
||
emit(elf_dwarf.write_u32_le(ref4_of(field_type_offset))) -- DW_FORM_ref4 → type
|
||
end
|
||
|
||
emit(string.char(0x00)) -- end of structure_type's children
|
||
end
|
||
|
||
-- 3) Emit one abstract DW_TAG_subprogram per unique mac_X component.
|
||
-- Each abstract DIE is a CU-level child (sibling of the per-atom subprograms below).
|
||
-- The abstract DIE's section offset is later used by inlined_subroutine DIEs (which embed `DW_AT_abstract_origin = ref4 → abstract DIE`).
|
||
-- Each abstract DIE also carries DW_AT_decl_file + DW_AT_decl_line pointing at the component's definition site (file path + body line).
|
||
local component_defs = collect_component_defs(atom_table)
|
||
local abstract_offsets = {} -- name -> section offset
|
||
local sorted_comp_names = {}
|
||
for name in pairs(component_defs) do sorted_comp_names[#sorted_comp_names + 1] = name end
|
||
table.sort(sorted_comp_names)
|
||
-- DW_INL_inlined (1) = "this subroutine was inlined" — accurate for the mac_* components.
|
||
local DW_INL_inlined = 0x01
|
||
for _, comp_name in ipairs(sorted_comp_names) do
|
||
local def = component_defs[comp_name]
|
||
abstract_offsets[comp_name] = next_offset
|
||
emit(uleb128(ABBREV_ABSTRACT_SUBPROGRAM))
|
||
emit("mac_" .. comp_name .. "\0") -- DW_FORM_string (DW_AT_name)
|
||
emit(string.char(DW_INL_inlined)) -- DW_FORM_data1 (DW_AT_inline)
|
||
emit(string.char(0x01)) -- DW_FORM_data1 (DW_AT_external=1)
|
||
-- decl_file + decl_line resolve the abstract origin back to its definition site even when no inlined_subroutine instance maps to it.
|
||
emit(uleb128(resolve_provenance_file_index(def.def_file))) -- DW_FORM_udata (DW_AT_decl_file)
|
||
emit(uleb128(def.def_line)) -- DW_FORM_udata (DW_AT_decl_line)
|
||
end
|
||
|
||
-- 4) 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 debug-visible R_ alias (filtered to GPR 0..31 in `by_alias`): DW_TAG_variable.
|
||
-- Precedence chain (per atom, per RR_<R_Name>):
|
||
-- (a) per-atom callsite atom_type(R_X, <T>): atom_views reg_type_overrides[atom_name][R_Name] — most specific; user explicit override for THIS atom only
|
||
-- (b) atom_ctx(<rbind_atom>): propagate THIS atom's declared rbind atom's Binds_* field types
|
||
-- (c) self-binding: atom.rbind.fields / regs (only when this atom IS the rbind atom for its Binds_*)
|
||
-- (d) atom_phase(<label>)-grouped: first atom in source-order that shares this label AND has its own atom.rbind provides the Binds_* field types
|
||
-- (e) enum-site atom_type(<T>) default: register_alias_registry[R_Name].default_type (per-alias fallback declared in lottes_tape.h)
|
||
-- (f) void* fallback: the void_chain_offset built in section 1b; gdb renders `(void *) 0x...` (hex)
|
||
-- If (a)..(e) miss AND no alias registry entry exists for this R_Name, the emission is skipped for that alias entirely.
|
||
local atom_view_ctx_fields = nil -- populated by step (b); map field_name -> field entry
|
||
local reg_to_field_ctx = nil -- populated by step (b); map GPR index -> field name
|
||
local atom_view_phase_fields = nil -- populated by step (d); map field_name -> field entry
|
||
local reg_to_field_phase = nil -- populated by step (d); map GPR index -> field name
|
||
-- atom-name -> atom lookup is precomputed once as atom_by_name_global.
|
||
local field_type_by_name = {}
|
||
if atom.rbind and atom.rbind.fields then
|
||
for _, f in ipairs(atom.rbind.fields) do
|
||
if f.type_name then
|
||
field_type_by_name[f.name] = f
|
||
end
|
||
end
|
||
end
|
||
local reg_to_field = {}
|
||
if atom.rbind and atom.rbind.regs then
|
||
for _, pair in ipairs(atom.rbind.regs) do
|
||
reg_to_field[pair.reg] = pair.field
|
||
end
|
||
end
|
||
local atom_view = (registries.atom_views or {})[atom.name]
|
||
-- Build the atom-name lookup table once (cheap; O(atom_table)) so step (b) and step (d) can resolve rbind_atom names.
|
||
local atom_by_name = atom_by_name or (function() local m = {}; for _, a in ipairs(atom_table) do if a.name then m[a.name] = a end end; return m end)()
|
||
-- step (b) inputs: this atom's `atom_ctx(<rbind_atom>)` (resolved from the registries' atom_ctxs)
|
||
local this_ctx = registries.atom_ctxs and registries.atom_ctxs[atom.name]
|
||
if this_ctx and this_ctx.rbind_atom then
|
||
local rbind = atom_by_name_global[this_ctx.rbind_atom]
|
||
if rbind and rbind.rbind and rbind.rbind.fields then
|
||
atom_view_ctx_fields = {}
|
||
for _, f in ipairs(rbind.rbind.fields) do atom_view_ctx_fields[f.name] = f end
|
||
if rbind.rbind.regs then
|
||
reg_to_field_ctx = {}
|
||
for _, pair in ipairs(rbind.rbind.regs) do reg_to_field_ctx[pair.reg] = pair.field end
|
||
end
|
||
end
|
||
end
|
||
-- step (d) inputs: this atom's `atom_phase(<label>)`;
|
||
-- find the FIRST atom in the same phase group that has its own rbind and is in source-order (i.e., declared before this atom in any source file).
|
||
-- Find the per-atom_info entry that has phase == this atom's name.
|
||
local my_phase_label = nil
|
||
for _, ai in ipairs(registries.atom_infos or {}) do
|
||
if ai.atom_name == atom.name and ai.phase then
|
||
my_phase_label = ai.phase
|
||
break
|
||
end
|
||
end
|
||
if my_phase_label then
|
||
local group = (registries.atom_phases or {})[my_phase_label]
|
||
if group and group.atoms then
|
||
for _, group_atom_name in ipairs(group.atoms) do
|
||
if group_atom_name ~= atom.name then
|
||
local cand = atom_by_name_global[group_atom_name]
|
||
if cand and cand.rbind and cand.rbind.fields then
|
||
atom_view_phase_fields = {}
|
||
for _, f in ipairs(cand.rbind.fields) do atom_view_phase_fields[f.name] = f end
|
||
if cand.rbind.regs then
|
||
reg_to_field_phase = {}
|
||
for _, pair in ipairs(cand.rbind.regs) do reg_to_field_phase[pair.reg] = pair.field end
|
||
end
|
||
break
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
for r_name, alias in pairs(by_alias) do
|
||
local rr_name = "RR_" .. strip_r_prefix(r_name)
|
||
local alias_code = alias.code
|
||
emit(uleb128(ABBREV_VARIABLE))
|
||
emit(rr_name .. "\0") -- DW_FORM_string (DW_AT_name)
|
||
emit(reg_exprloc(alias_code)) -- DW_FORM_exprloc (DW_OP_regN from registry code)
|
||
-- Resolve the type for this register via the 6-step precedence chain.
|
||
local type_offset = type_chain_offsets["void|1"] -- step (f) fallback: void* (renders as hex)
|
||
|
||
-- step (a): per-atom callsite atom_type(R_X, <T>)
|
||
local override = atom_view and atom_view.reg_type_overrides and atom_view.reg_type_overrides[r_name]
|
||
if override and override.pointer_depth and override.pointer_depth > 0 then
|
||
local chain_offset = type_chain_offsets[override.type_name .. "|" .. override.pointer_depth]
|
||
if chain_offset then type_offset = chain_offset end
|
||
else
|
||
-- step (b): atom_ctx(<rbind_atom>) propagation
|
||
local ctx_field_name = reg_to_field_ctx and reg_to_field_ctx[alias_code]
|
||
local ctx_f = ctx_field_name and atom_view_ctx_fields and atom_view_ctx_fields[ctx_field_name]
|
||
if ctx_f and ctx_f.pointer_depth and ctx_f.pointer_depth > 0 then
|
||
local chain_offset = type_chain_offsets[ctx_f.type_name .. "|" .. ctx_f.pointer_depth]
|
||
if chain_offset then type_offset = chain_offset end
|
||
else
|
||
-- step (c): self-bind (this atom is the rbind atom)
|
||
local field_name = reg_to_field[alias_code]
|
||
local f = field_name and field_type_by_name[field_name]
|
||
if f and f.pointer_depth and f.pointer_depth > 0 then
|
||
local chain_offset = type_chain_offsets[f.type_name .. "|" .. f.pointer_depth]
|
||
if chain_offset then type_offset = chain_offset end
|
||
else
|
||
-- step (d): atom_phase(<label>)-grouped propagation
|
||
local phase_field_name = reg_to_field_phase and reg_to_field_phase[alias_code]
|
||
local phase_f = phase_field_name and atom_view_phase_fields and atom_view_phase_fields[phase_field_name]
|
||
if phase_f and phase_f.pointer_depth and phase_f.pointer_depth > 0 then
|
||
local chain_offset = type_chain_offsets[phase_f.type_name .. "|" .. phase_f.pointer_depth]
|
||
if chain_offset then type_offset = chain_offset end
|
||
else
|
||
-- step (e): enum-site atom_type(<T>) default on the registry entry
|
||
if alias.default_type and alias.default_depth and alias.default_depth > 0 then
|
||
local chain_offset = type_chain_offsets[alias.default_type .. "|" .. alias.default_depth]
|
||
if chain_offset then type_offset = chain_offset end
|
||
end
|
||
-- step (f): void* fallback (set as type_offset initial value above).
|
||
end
|
||
end
|
||
end
|
||
end
|
||
emit(elf_dwarf.write_u32_le(ref4_of(type_offset))) -- DW_FORM_ref4 → type
|
||
emit(string.char(0x01)) -- DW_AT_external=1 (visible at CU scope)
|
||
end
|
||
|
||
-- If rbind, emit bind_args variable with PC-ranged location list.
|
||
-- The loclist is in .debug_loclists, indexed by `DW_FORM_sec_offset` (4-byte section-relative offset).
|
||
-- The piece chain is replaced by two PC ranges: [atom.addr, last_load+8) where every field is described as a tape-memory
|
||
-- (DW_OP_bregN + offset) piece, and [last_load+8, atom.end) where every field is described as a GPR (DW_OP_regN) piece.
|
||
if atom.rbind then
|
||
local binds_name = atom.rbind.binds
|
||
local loclists_offset = loclists_offsets[atom.name] or 0
|
||
emit(uleb128(ABBREV_BIND_VAR_LOCLIST))
|
||
emit("bind_args\0") -- DW_FORM_string (DW_AT_name)
|
||
emit(elf_dwarf.write_u32_le(loclists_offset)) -- DW_FORM_sec_offset → .debug_loclists
|
||
emit(elf_dwarf.write_u32_le(ref4_of(struct_section_offsets[binds_name]))) -- DW_FORM_ref4 → struct_type
|
||
end
|
||
|
||
-- Per-component invocation inlined_subroutine instances.
|
||
-- Each invocation covers a contiguous .word range [start_pos, end_pos] within the atom.
|
||
-- We compute the corresponding PC range from the atom's start + .word offsets × MIPS_BYTES_PER_WORD.
|
||
-- call_file now resolves inv.call_file to the line-unit file index (previously hardcoded to ATOM_SOURCE_FILE_INDEX;
|
||
-- that lost the call-site attribution for any invocation whose call site was NOT the atom's source file).
|
||
if atom.invocations and not atom.skip_over then
|
||
for _, inv in ipairs(atom.invocations) do
|
||
local inv_low = atom.addr + inv.start_pos * MIPS_BYTES_PER_WORD
|
||
local inv_high = atom.addr + (inv.end_pos + 1) * MIPS_BYTES_PER_WORD
|
||
emit(uleb128(ABBREV_INLINED_SUBROUTINE))
|
||
emit(elf_dwarf.write_u32_le(ref4_of(abstract_offsets[inv.comp_name]))) -- DW_FORM_ref4 → abstract_origin
|
||
emit(elf_dwarf.write_u32_le(inv_low)) -- DW_FORM_addr (DW_AT_low_pc)
|
||
emit(elf_dwarf.write_u32_le(inv_high)) -- DW_FORM_addr (DW_AT_high_pc)
|
||
emit(uleb128(resolve_provenance_file_index(inv.call_file))) -- DW_FORM_udata (DW_AT_call_file)
|
||
emit(uleb128(inv.call_line)) -- DW_FORM_udata (DW_AT_call_line)
|
||
end
|
||
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[]
|
||
--- @param registries table -- merged registries from collect_per_source_registries
|
||
--- @return string, integer, table -- (new_str_bytes, new_strings_offset, string_map)
|
||
local function build_debug_str_section(existing, atom_table, registries)
|
||
local new_strings, string_map = build_new_strings(atom_table, registries)
|
||
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.
|
||
--- 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.
|
||
--- **No detached synthetic CU is appended.**
|
||
--- @param existing string -- existing .debug_info section bytes
|
||
--- @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}}
|
||
--- @param loclists_offsets table -- {[atom_name] = section-relative offset}
|
||
--- @param registries table -- merged registries from collect_per_source_registries
|
||
--- @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, loclists_offsets, registries)
|
||
-- 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, loclists_offsets, registries)
|
||
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,
|
||
-- debug_loclists is fed directly in M.run (it needs the merged registries for the R_TapePtr GPR lookup;
|
||
-- the SECTION_BUILDERS table cannot carry that context, so the dispatch is inlined in the per-run writers loop).
|
||
}
|
||
|
||
-- 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,
|
||
debug_loclists = function(out_root, basename) return out_root .. "\\" .. basename .. ".dwarf_loclists.bin" end,
|
||
}
|
||
|
||
local function write_gdbinit(out_root, basename, skip_over, atom_table)
|
||
local path = out_root .. "\\" .. basename .. ".gdbinit"
|
||
duffle.write_file_lf(path, build_gdbinit(skip_over, atom_table))
|
||
return { gdbinit = path }
|
||
end
|
||
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
-- Pass entry
|
||
-- ════════════════════════════════════════════════════════════════════════════
|
||
|
||
local M = {}
|
||
|
||
--- M.run — orchestrator entry.
|
||
--- @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 8 sections: .debug_line / .debug_aranges / .debug_rnglists get extended
|
||
-- (additional rows appended to the existing unit), and .debug_info / .debug_abbrev / .debug_str / .debug_loc / .debug_loclists
|
||
-- get spliced (the main CU's unit_length is patched;
|
||
-- no new compile unit is appended; debug_loc/debug_loclists may not exist in the source ELF so we add-section them on splice).
|
||
-- 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 and .debug_loclists don't exist in the source ELF (we add-section them on splice);
|
||
-- reading them just returns "" which is the "missing" case the builder handles.
|
||
".debug_loc", ".debug_loclists",
|
||
})
|
||
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 .debug_loclists=%d\n",
|
||
8,
|
||
#(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 ""),
|
||
#(existing_sections[".debug_loclists"] or "")))
|
||
|
||
-- Consume source-as-written skip associations from every scanned source,
|
||
-- then build the atom/provenance table with those generic selections.
|
||
local skip_over = collect_skip_over(ctx)
|
||
local registries = collect_per_source_registries(ctx)
|
||
local atom_table = build_atom_table(ctx, skip_over)
|
||
io.stderr:write(string.format("[dwarf_injection] matched %d atoms between nm + source-map\n", #atom_table))
|
||
|
||
-- Detect rbind atoms + index Binds_* struct fields (from ctx.sources[i].scan, populated by scan-source pass).
|
||
-- The merged registries are threaded through so parse_body_load_pairs resolves R_<reg> via register_alias_registry.
|
||
local _rbind_atoms, rbind_structs = parse_rbind_atoms(ctx, atom_table, registries)
|
||
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:
|
||
-- 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.
|
||
-- 4. Build .debug_loc (just a terminator).
|
||
--
|
||
-- .debug_str now also receives the new RR_<R_Name> entries
|
||
-- (the prior "left untouched in this slice" claim is obsolete; the merged registry drives the strings table to keep .debug_str and .debug_info in sync).
|
||
local basename = ctx.basename or duffle.basename_no_ext(elf_path) or DEFAULT_BASENAME
|
||
if ctx.out_root and ctx.out_root ~= "" then
|
||
duffle.ensure_dir(ctx.out_root)
|
||
local gdbinit_output = write_gdbinit(ctx.out_root, basename, skip_over, atom_table)
|
||
|
||
-- 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 existing_str = existing_sections[".debug_str"] or ""
|
||
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_str, was = #existing_str },
|
||
{ 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 = { gdbinit_output }
|
||
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..109.
|
||
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 = { gdbinit_output }, errors = {}, warnings = {"main abbrev-table validation failed"} }
|
||
end
|
||
|
||
-- Step 1b: compute per-atom loclist offsets BEFORE building .debug_info
|
||
-- (the bind_args variable references the loclist via DW_FORM_sec_offset).
|
||
local loclists_offsets = compute_loclists_offsets(atom_table)
|
||
|
||
-- 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, loclists_offsets, registries)
|
||
|
||
-- Step 2b: rebuild .debug_str now that we know which RR_<R_Name> entries
|
||
-- get emitted (this aligns with build_debug_info_section's by_alias loop).
|
||
local new_str, new_str_offset, string_map = build_debug_str_section(existing_sections[".debug_str"] or "", atom_table, registries)
|
||
|
||
-- Step 3-5: independent sections.
|
||
local results = {
|
||
{ name = "debug_abbrev", data = new_abbrev, was = #existing_abbrev },
|
||
{ name = "debug_info", data = new_info, was = #existing_info },
|
||
{ name = "debug_str", data = new_str, 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 "") },
|
||
{ name = "debug_loclists", data = build_debug_loclists_section(atom_table, registries), was = 0 },
|
||
}
|
||
|
||
local outputs = { gdbinit_output }
|
||
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
|