mirror of
https://github.com/Ed94/pikuma_ps1.git
synced 2026-07-24 00:17:51 +00:00
curation: first pass
This commit is contained in:
+244
-288
@@ -1,12 +1,12 @@
|
||||
--- passes/dwarf_injection.lua — Per-atom DWARF injection for tape-atom step-debug (F' + G').
|
||||
--- passes/dwarf_injection.lua — Per-atom DWARF injection for tape-atom step-debug.
|
||||
---
|
||||
--- 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.
|
||||
--- 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` 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>
|
||||
@@ -20,11 +20,11 @@
|
||||
--- 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').
|
||||
--- Result: VSCode's source gutter follows per-stepi inside atom bodies, AND the Variables pane shows the wave-context regs as atom-scoped locals.
|
||||
--- 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.
|
||||
--- **Conventions:** tabs (1/level), EmmyLua annotations, Lua 5.3 compatible.
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- Bootstrap
|
||||
@@ -43,12 +43,17 @@ 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 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.
|
||||
-- File-scope aliases to elf_dwarf helpers; the canonical implementations live in scripts/elf_dwarf.lua.
|
||||
-- (2-caller lift: these were duplicated file-locals; the canonical is in elf_dwarf.lua, used by parse_abbrev_table + read_form_value.)
|
||||
local read_uleb128_at = elf_dwarf.read_uleb128_at
|
||||
local read_sleb128_at = elf_dwarf.read_sleb128_at
|
||||
local find_abbrev_table_end = elf_dwarf.find_abbrev_table_end
|
||||
-- Note: uleb128 + sleb128 are encoders; read_uleb128_at + read_sleb128_at are decoders. Different functions.
|
||||
local uleb128 = elf_dwarf.uleb128
|
||||
local sleb128 = elf_dwarf.sleb128
|
||||
|
||||
@@ -58,7 +63,7 @@ local sleb128 = elf_dwarf.sleb128
|
||||
|
||||
-- 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
|
||||
-- Local aliases preserve the 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
|
||||
@@ -109,8 +114,17 @@ 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 DW_OP_piece = 0x93
|
||||
local MIPS_LOAD_DELAY_BYTES = 0x08 -- 1 load word + 1 BD-slot word
|
||||
|
||||
-- DIE children-list terminator: each DIE that has children ends with a single 0 byte (DWARF5 §7.5.3).
|
||||
-- This is NOT the same as DW_LLE_end_of_list despite sharing the value 0x00 — different spec sections.
|
||||
local DIE_CHILDREN_TERMINATOR = 0x00
|
||||
|
||||
-- Field/piece byte sizes for the typed-view piece chains.
|
||||
-- All Binds_* struct fields are U4 (sizeof(uint32_t) on MIPS32 = 4 bytes).
|
||||
local U4_BYTE_SIZE = 4
|
||||
|
||||
-- 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.
|
||||
@@ -171,7 +185,7 @@ local function resolve_provenance_file_index(path)
|
||||
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]
|
||||
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'). "
|
||||
@@ -198,7 +212,7 @@ local DW_OP_piece = 0x93 -- followed by ULEB128 byte count (per DWA
|
||||
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.)
|
||||
-- (DW_LANG_Mips_Assembler = 0x8001 was used in the, 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).
|
||||
@@ -211,7 +225,6 @@ local DW_LANG_C99 = 0x0C -- = 12 (C99); use as a safe "C-like" plac
|
||||
--
|
||||
-- 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
|
||||
@@ -239,12 +252,16 @@ local function build_debug_loclists_section(atom_table, registries)
|
||||
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))
|
||||
-- (DW_OP_bregN, SLEB128(offset), DW_OP_piece, ULEB128(U4_BYTE_SIZE))
|
||||
-- 4 = U4_BYTE_SIZE: each piece is sizeof(uint32_t) on MIPS32.
|
||||
table.insert(tape_pieces, string.char(DW_OP_breg0 + tape_reg) .. offset_sleb .. string.char(DW_OP_piece) .. uleb128(U4_BYTE_SIZE))
|
||||
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))
|
||||
-- (DW_OP_regN, DW_OP_piece, ULEB128(4)) — one piece per GPR-resident field.
|
||||
-- The 4 = U4_BYTE_SIZE: each piece is sizeof(uint32_t) on MIPS32.
|
||||
table.insert(gpr_pieces, string.char(DW_OP_reg0 + pair.reg) .. string.char(DW_OP_piece) .. uleb128(U4_BYTE_SIZE))
|
||||
end
|
||||
local gpr_expr = table.concat(gpr_pieces)
|
||||
parts[#parts + 1] = string.char(DW_LLE_start_length)
|
||||
@@ -258,13 +275,17 @@ local function build_debug_loclists_section(atom_table, registries)
|
||||
parts[#parts + 1] = string.char(DW_LLE_end_of_list)
|
||||
end
|
||||
end
|
||||
-- Loclist unit header (DWARF5 §7.7.2):
|
||||
-- unit_length(4) + version(2) + address_size(1) + segment_size(1) + offset_entry_count(4) = 12 bytes header.
|
||||
-- version = 5 (DWARF5); address_size = 4 (MIPS32); segment_size = 0; offset_entry_count = 0 (we use DW_LLE_start_length, not offsets).
|
||||
local LOCLIST_HEADER_SIZE = 12
|
||||
local body = table.concat(parts)
|
||||
local unit_length = 2 + 1 + 1 + 4 + #body
|
||||
local unit_length = LOCLIST_HEADER_SIZE - 4 + #body -- -4 because unit_length excludes itself
|
||||
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)
|
||||
.. elf_dwarf.write_u16_le(5) -- DWARF5
|
||||
.. string.char(U4_BYTE_SIZE) -- address_size
|
||||
.. string.char(0) -- segment_size
|
||||
.. elf_dwarf.write_u32_le(0) -- offset_entry_count
|
||||
return header .. body
|
||||
end
|
||||
|
||||
@@ -273,7 +294,7 @@ end
|
||||
-- @return table -- {[atom_name] = offset_in_section}
|
||||
local function compute_loclists_offsets(atom_table)
|
||||
local offsets = {}
|
||||
local cursor = 4 + 2 + 1 + 1 + 4
|
||||
local cursor = 4 + 2 + 1 + 1 + 4
|
||||
for _, atom in ipairs(atom_table) do
|
||||
if atom.rbind then
|
||||
offsets[atom.name] = cursor
|
||||
@@ -290,7 +311,7 @@ local function compute_loclists_offsets(atom_table)
|
||||
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_NAME = "tape_atom_locals"
|
||||
local DEFAULT_CU_COMP_DIR = "."
|
||||
|
||||
-- Path templates for the .bin outputs are now in SECTION_WRITERS (see below).
|
||||
@@ -316,16 +337,8 @@ 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
|
||||
--- 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}}
|
||||
@@ -343,7 +356,9 @@ local function collect_skip_over(ctx)
|
||||
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)] = {
|
||||
-- component_skip_key (the lower() .. "\0" .. component_name part) inlined at this single call site
|
||||
-- (no other callers remain after pass 17).
|
||||
skip_over.components[source_path:lower() .. "\0" .. component_name] = {
|
||||
name = component_name,
|
||||
source_path = source_path,
|
||||
association = association,
|
||||
@@ -361,8 +376,7 @@ end
|
||||
--- 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).
|
||||
--- Today only one source declares wave-context enums, so collisions are absent.
|
||||
--- @param ctx DwarfInjectionCtx
|
||||
--- @return table -- {
|
||||
--- register_alias_registry = {[R_Name] = AliasEntry},
|
||||
@@ -660,7 +674,7 @@ end
|
||||
--- (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
|
||||
--- @param skip_over table -- {atoms = {[symbol] = association}, components = {[file|name] = association}}
|
||||
--- @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
|
||||
@@ -688,7 +702,7 @@ local function build_atom_table(ctx, skip_over)
|
||||
|
||||
-- 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_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)
|
||||
@@ -793,7 +807,9 @@ local function build_atom_table(ctx, skip_over)
|
||||
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,
|
||||
-- component_skip_key: case-insensitive Windows path + "\0" separator + exact component-name.
|
||||
-- (Inlined from `component_skip_key`; the lookup is in skip_over.components keyed by the result.)
|
||||
skip_over = skip_over.components[normalize_debug_path(w.comp_file):lower() .. "\0" .. 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).
|
||||
@@ -850,7 +866,6 @@ end
|
||||
-- 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)),
|
||||
@@ -859,49 +874,36 @@ end
|
||||
--- 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
|
||||
---
|
||||
--- Pre-tokenized: `body_tokens` is the scan-source pass's pre-split list of top-level
|
||||
--- statements (each entry is a single `load_word(...)` call or other statement).
|
||||
--- Plex: prepass move — use the pre-computed data instead of re-walking the body text.
|
||||
--- @param body_tokens table[] -- the atom's pre-tokenized body statements (from atom.body_tokens)
|
||||
--- @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_tokens, binds_name, registries)
|
||||
local pairs = {}
|
||||
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
|
||||
for _, t in ipairs(body_tokens or {}) do
|
||||
local tok = duffle.trim(t.tok or "")
|
||||
-- Match "load_word(...)" — the entire call is one body_tokens entry.
|
||||
local inner = tok:match("^load_word%s*%((.*)%)$")
|
||||
if inner then
|
||||
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
|
||||
pos = p_after
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -955,13 +957,13 @@ local function parse_rbind_atoms(ctx, atom_table, registries)
|
||||
end
|
||||
end
|
||||
|
||||
-- Walk every atom_info; if `binds` is set, find the atom body + parse load_word pairs.
|
||||
local body_by_atom = {}
|
||||
-- Walk every atom_info; if `binds` is set, find the atom body_tokens + parse load_word pairs.
|
||||
local body_tokens_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
|
||||
body_tokens_by_atom[atom.name] = atom.body_tokens
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -978,10 +980,10 @@ local function parse_rbind_atoms(ctx, atom_table, registries)
|
||||
|
||||
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)
|
||||
local struct = rbind_structs[ai.binds]
|
||||
local body_toks = body_tokens_by_atom[atom_name]
|
||||
if struct and body_toks then
|
||||
local pairs = parse_body_load_pairs(body_toks, ai.binds, registries)
|
||||
if #pairs > 0 then
|
||||
rbind_atoms[atom_name] = {
|
||||
binds = ai.binds,
|
||||
@@ -1182,19 +1184,12 @@ local function build_dwarf_rnglists_section(existing, atom_table)
|
||||
end
|
||||
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- G' helpers: per-atom .debug_info synthesis (DW_TAG_subprogram + DW_TAG_variable)
|
||||
-- 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.
|
||||
@@ -1262,101 +1257,6 @@ end
|
||||
-- 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
|
||||
@@ -1549,16 +1449,9 @@ local function build_new_abbrev()
|
||||
.. abbrev_struct_type .. abbrev_member .. abbrev_bind_var .. abbrev_base_type
|
||||
.. abbrev_abstract_subprogram .. abbrev_inlined_subroutine
|
||||
.. abbrev_bind_var_loclists
|
||||
.. string.char(0x00)
|
||||
.. string.char(DW_LLE_end_of_list) -- section terminator (the loclist-unit end marker per DWARF5 §7.7.3)
|
||||
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)
|
||||
@@ -1572,6 +1465,13 @@ local function strip_r_prefix(r_name)
|
||||
return r_name
|
||||
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)
|
||||
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).
|
||||
@@ -1601,13 +1501,20 @@ local function build_new_strings(atom_table, registries)
|
||||
-- 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).
|
||||
-- Plex: Lua's pairs() is non-deterministic; sort the alias names first so the emitted
|
||||
-- .debug_str bytes are byte-identical across runs.
|
||||
local sorted_alias_names = {}
|
||||
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
|
||||
sorted_alias_names[#sorted_alias_names + 1] = r_name
|
||||
end
|
||||
end
|
||||
table.sort(sorted_alias_names)
|
||||
for _, r_name in ipairs(sorted_alias_names) do
|
||||
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
|
||||
|
||||
@@ -1672,12 +1579,19 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
|
||||
-- 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).
|
||||
-- by_alias_order: sorted list of by_alias keys, for deterministic iteration order.
|
||||
-- (Lua's pairs() order is implementation-defined and may vary between runs;
|
||||
-- without sorting, the per-atom variable emission order would be non-deterministic
|
||||
-- and the .debug_info bytes would differ across builds.)
|
||||
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
|
||||
local by_alias_order = {}
|
||||
for r_name in pairs(by_alias) do by_alias_order[#by_alias_order + 1] = r_name end
|
||||
table.sort(by_alias_order)
|
||||
|
||||
-- Build a name->atom lookup for fast rbind_atom resolution during the per-atom phase/ctx propagation.
|
||||
-- Cheap (O(atom_table)) and built once.
|
||||
@@ -1694,18 +1608,31 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
-- 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
|
||||
|
||||
-- Closure state: bytes + next_offset + the typed-view/structure/abstract offset caches.
|
||||
-- All step-emitters mutate this state in place. Plex: Fleury expose structure over interface.
|
||||
local S = {
|
||||
bytes = {},
|
||||
next_offset = insertion_start, -- 0-based section offset of the NEXT byte to emit
|
||||
type_chain_offsets = {}, -- {[type_name.."|"..depth] = section_offset for the outermost pointer_type}
|
||||
struct_section_offsets = {}, -- {[binds_name] = section_offset for the structure_type}
|
||||
abstract_offsets = {}, -- {[comp_name] = section_offset for the abstract subprogram}
|
||||
member_base_type_offsets = {}, -- {[tn.."|"..byte_size.."|"..encoding] = section_offset}
|
||||
base_type_section_offset = nil, -- set by emit_unsigned_int_base_type
|
||||
}
|
||||
-- Plex helpers.
|
||||
local function emit(s)
|
||||
bytes[#bytes + 1] = s
|
||||
next_offset = next_offset + #s
|
||||
S.bytes[#S.bytes + 1] = s
|
||||
S.next_offset = S.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).
|
||||
-- (Use a local alias for S.next_offset; the rest of the function body still
|
||||
-- uses the bare name `next_offset` from the previous unrefactored version).
|
||||
local next_offset = S.next_offset
|
||||
local base_type_section_offset = next_offset
|
||||
emit(uleb128(ABBREV_BASE_TYPE))
|
||||
emit("unsigned int\0") -- DW_FORM_string (DW_AT_name)
|
||||
@@ -1829,13 +1756,12 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
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
|
||||
-- Unknown typed view — fall back to a generic 4-byte unsigned 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(U4_BYTE_SIZE)) -- byte_size = 4 (fallback for unknown types)
|
||||
emit(string.char(DW_ATE_unsigned)) -- encoding = unsigned
|
||||
local outermost_offset = next_offset
|
||||
emit(uleb128(9)) -- DW_TAG_pointer_type
|
||||
@@ -1845,8 +1771,8 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
-- 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)
|
||||
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))
|
||||
@@ -1868,7 +1794,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
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(string.char(DIE_CHILDREN_TERMINATOR)) -- end of structure_type's children (DWARF5 §7.5.3)
|
||||
-- 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
|
||||
@@ -1947,7 +1873,7 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
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
|
||||
emit(string.char(DIE_CHILDREN_TERMINATOR)) -- end of structure_type's children (DWARF5 §7.5.3)
|
||||
end
|
||||
|
||||
-- 3) Emit one abstract DW_TAG_subprogram per unique mac_X component.
|
||||
@@ -2058,51 +1984,72 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
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
|
||||
-- 5-step precedence chain. Plex: data table; the dispatch loop runs the first step that yields a non-nil offset.
|
||||
-- Each step returns the type's section offset or nil if it missed.
|
||||
-- Adding a step = 1 row in the table + 1 function. The 5-level nested if/else is gone.
|
||||
-- Per-atom precomputed state is captured in upvalues: atom_view, reg_to_field_ctx, atom_view_ctx_fields,
|
||||
-- reg_to_field_phase, atom_view_phase_fields, field_type_by_name, reg_to_field, alias, type_chain_offsets.
|
||||
local PRECEDENCE_STEPS = {
|
||||
-- (a) per-atom callsite atom_type(R_X, <T>): most specific; user explicit override for THIS atom only.
|
||||
function(r_name, alias_code)
|
||||
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
|
||||
return type_chain_offsets[override.type_name .. "|" .. override.pointer_depth]
|
||||
end
|
||||
end,
|
||||
-- (b) atom_ctx(<rbind_atom>) propagation.
|
||||
function(r_name, alias_code)
|
||||
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
|
||||
return type_chain_offsets[ctx_f.type_name .. "|" .. ctx_f.pointer_depth]
|
||||
end
|
||||
end,
|
||||
-- (c) self-binding (this atom IS the rbind atom for its Binds_*).
|
||||
function(r_name, alias_code)
|
||||
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
|
||||
return type_chain_offsets[f.type_name .. "|" .. f.pointer_depth]
|
||||
end
|
||||
end,
|
||||
-- (d) atom_phase(<label>)-grouped propagation.
|
||||
function(r_name, alias_code)
|
||||
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
|
||||
return type_chain_offsets[phase_f.type_name .. "|" .. phase_f.pointer_depth]
|
||||
end
|
||||
end,
|
||||
-- (e) enum-site atom_type(<T>) default on the registry entry.
|
||||
function(r_name, alias_code)
|
||||
if alias and alias.default_type and alias.default_depth and alias.default_depth > 0 then
|
||||
return type_chain_offsets[alias.default_type .. "|" .. alias.default_depth]
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
-- Plex: iterate `by_alias` in sorted order (Lua's pairs() is non-deterministic;
|
||||
-- sorting ensures byte-identical DWARF output across builds).
|
||||
for _, r_name in ipairs(by_alias_order) do
|
||||
local alias = by_alias[r_name]
|
||||
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)
|
||||
-- DW_FORM_exprloc: ULEB byte count + DW_OP_regN byte.
|
||||
-- DW_OP_reg0..reg31 occupy opcodes 0x50..0x6f; DW_OP_reg15 is 0x5f.
|
||||
-- Inlined from `reg_exprloc` (single caller; the function was 3 LOC).
|
||||
-- DW_FORM_exprloc: ULEB byte count + DW_OP_regN byte.
|
||||
-- Inlined from `reg_exprloc` (was at lines 1185-1188, dwarf_injection.lua; the function was 3 LOC and 1 caller).
|
||||
-- The `1` is the length prefix — DW_OP_regN occupies exactly 1 byte (the base opcode is 0x50; regN = 0x50 + N).
|
||||
-- `alias_code` is the MIPS GPR index (0..31) from the merged register_alias_registry.
|
||||
emit(uleb128(1) .. string.char(DW_OP_reg0 + alias_code)) -- DW_FORM_exprloc (DW_OP_regN from registry code)
|
||||
-- Precedence chain (a..e); step (f) is the void* fallback (initial value).
|
||||
local type_offset = type_chain_offsets["void|1"]
|
||||
for _, step in ipairs(PRECEDENCE_STEPS) do
|
||||
local candidate = step(r_name, alias_code)
|
||||
if candidate then type_offset = candidate; break 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)
|
||||
@@ -2139,12 +2086,11 @@ local function build_inserted_children(main_cu_offset, main_cu_end_excl, atom_ta
|
||||
end
|
||||
end
|
||||
|
||||
emit(string.char(0x00)) -- end of subprogram's children
|
||||
emit(string.char(DIE_CHILDREN_TERMINATOR)) -- end of subprogram's children (DWARF5 §7.5.3)
|
||||
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)
|
||||
-- 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(S.bytes)
|
||||
end
|
||||
|
||||
--- Build the new .debug_abbrev: existing + duplicate of the main CU's table + the new declarations 100..106 (with their terminating 0).
|
||||
@@ -2252,10 +2198,8 @@ end
|
||||
--- 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
|
||||
--- Single-caller (M.run's SECTION_BUILDERS table below); replaced by a literal at the call site.
|
||||
-- local function build_debug_loc_section() return string.char(0x00) end
|
||||
|
||||
local SECTION_BUILDERS = {
|
||||
debug_line = build_dwarf_line_section,
|
||||
@@ -2264,7 +2208,7 @@ local SECTION_BUILDERS = {
|
||||
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_loc = function() return string.char(DW_LLE_end_of_list) 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).
|
||||
}
|
||||
@@ -2282,6 +2226,34 @@ local SECTION_WRITERS = {
|
||||
debug_loclists = function(out_root, basename) return out_root .. "\\" .. basename .. ".dwarf_loclists.bin" end,
|
||||
}
|
||||
|
||||
-- Write a list of {name, data, was} section records to disk via SECTION_WRITERS.
|
||||
-- Used twice in M.run (safe-path + production-path).
|
||||
-- The only difference between the two call sites is the `is_production` flag, which gates the per-section progress line.
|
||||
-- @param results table[] -- list of {name=, data=, was=} records to write
|
||||
-- @param ctx PassCtx
|
||||
-- @param basename string -- output file basename (e.g. "hello_gte")
|
||||
-- @param is_production bool -- true → emit progress line; false → silent
|
||||
-- @param atom_table table -- used by the progress line (only when is_production)
|
||||
-- @return table -- list of {name_bin = path} entries to append to M.run's outputs
|
||||
local function write_sections(results, ctx, basename, is_production, atom_table)
|
||||
local outputs = {}
|
||||
for _, r in ipairs(results) do
|
||||
local path = SECTION_WRITERS[r.name](ctx.out_root, basename)
|
||||
local f = io.open(path, "wb")
|
||||
if not f then
|
||||
io.stderr:write(string.format("[dwarf_injection] failed to open %s for write\n", path))
|
||||
else
|
||||
f:write(r.data); f:close()
|
||||
if is_production then
|
||||
io.stderr:write(string.format("[dwarf_injection] new .%s = %d bytes (was %d, +%d atoms)\n"
|
||||
, r.name, #r.data, r.was, #atom_table))
|
||||
end
|
||||
outputs[#outputs + 1] = { [r.name .. "_bin"] = path }
|
||||
end
|
||||
end
|
||||
return outputs
|
||||
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))
|
||||
@@ -2340,6 +2312,8 @@ function M.run(ctx)
|
||||
|
||||
-- Consume source-as-written skip associations from every scanned source,
|
||||
-- then build the atom/provenance table with those generic selections.
|
||||
-- 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.
|
||||
local skip_over = collect_skip_over(ctx)
|
||||
local registries = collect_per_source_registries(ctx)
|
||||
local atom_table = build_atom_table(ctx, skip_over)
|
||||
@@ -2364,7 +2338,7 @@ function M.run(ctx)
|
||||
-- 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).
|
||||
-- (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)
|
||||
@@ -2384,19 +2358,11 @@ function M.run(ctx)
|
||||
{ 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_loc", data = string.char(DW_LLE_end_of_list), was = #(existing_sections[".debug_loc"] or "") },
|
||||
}
|
||||
local section_outputs = write_sections(results_safe, ctx, basename, false, atom_table)
|
||||
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
|
||||
for _, o in ipairs(section_outputs) do outputs_safe[#outputs_safe + 1] = o end
|
||||
return { outputs = outputs_safe, errors = {}, warnings = {} }
|
||||
end
|
||||
|
||||
@@ -2426,23 +2392,13 @@ function M.run(ctx)
|
||||
{ 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_loc", data = string.char(DW_LLE_end_of_list), was = #(existing_sections[".debug_loc"] or "") },
|
||||
{ name = "debug_loclists", data = build_debug_loclists_section(atom_table, registries), was = 0 },
|
||||
}
|
||||
|
||||
local section_outputs = write_sections(results, ctx, basename, true, atom_table)
|
||||
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
|
||||
for _, o in ipairs(section_outputs) do outputs[#outputs + 1] = o end
|
||||
return {
|
||||
outputs = outputs,
|
||||
errors = {},
|
||||
|
||||
Reference in New Issue
Block a user