diff --git a/scripts/elf_dwarf.lua b/scripts/elf_dwarf.lua index 5f86829..aaab7ed 100644 --- a/scripts/elf_dwarf.lua +++ b/scripts/elf_dwarf.lua @@ -389,12 +389,99 @@ local function read_form_value(buf, str_buf, pos, form) -- The constant is declared in the abbrev; no value bytes in the DIE. return nil, pos elseif form == M.DW_FORM.ref_sig8 then - return M.read_u32_le(buf, pos), pos + 8 + -- DW_FORM_ref_sig8 (DWARF5 §7.4.2): an 8-byte value identifying a type + -- by signature. The low 4 bytes (LE) are the type signature (content hash); + -- the high 4 bytes (LE) are a CU-relative offset into the matching type unit. + -- Consumers use the low 4 to look up the type unit (see M.find_type_unit_by_signature) + -- then the high 4 to resolve the specific type within it. + -- Return the low 4 as the primary value to preserve the (value, next_pos) shape; + -- the high 4 is exposed via M.read_ref_sig8 (which returns both halves). + local _, _, next_pos = M.read_ref_sig8(buf, pos) + return M.read_u32_le(buf, pos), next_pos else return nil, pos end end +--- Read a `DW_FORM_ref_sig8` value at 0-based offset `pos` from `buf`. +--- Returns the low 4 bytes (LE) as `low`, the high 4 bytes (LE) as `high`, and +--- the cursor position after the 8-byte value as `next_pos`. +--- Callers that need the full type-unit + type-offset pair +--- (e.g. to resolve a type identifier embedded as a signature) +--- should use this directly rather than going through `read_form_value`, +--- which only exposes the low 4 bytes to preserve its existing (value, next_pos) return shape. +--- @param buf string +--- @param pos integer -- zero-based wire offset +--- @return integer -- low 4 bytes (LE), the type signature +--- @return integer -- high 4 bytes (LE), the offset within the matching type unit +--- @return integer -- cursor after the 8-byte value +function M.read_ref_sig8(buf, pos) + return M.read_u32_le(buf, pos), M.read_u32_le(buf, pos + 4), pos + 8 +end + +-- DWARF5 §7.5.6 (Type Entries). +-- Walk all units in `info` and return the 0-based offset of the first unit +-- whose `DW_AT_type_signature` (8-byte value at the end of the unit header) equals `target_sig`. +-- The signature is interpreted as two 32-bit halves (low/high) per the read_ref_sig8 contract; +-- we match both halves (i.e. the 8-byte value as a whole). Returns nil if no matching unit exists. +-- +-- Unit header layout (from pos 0): +-- unit_length(4) + version(2) + unit_type(1) + address_size(1) + debug_abbrev_offset(4) +-- -- followed by type_unit_specific fields: +-- type_signature(8) + type_offset(4) +-- The type_signature is at byte offset 8 of the body (right after debug_abbrev_offset). +-- @param info string -- the .debug_info section bytes +-- @param target_sig_lo integer -- low 4 bytes (LE) of the desired signature +-- @param target_sig_hi integer -- high 4 bytes (LE) of the desired signature +-- @return integer|nil, integer|nil -- unit offset, type_offset within the unit +function M.find_type_unit_by_signature(info, target_sig_lo, target_sig_hi) + local pos = 0 + local section_len = #info + while pos + 4 < section_len do + local unit_length = M.read_u32_le(info, pos) + if unit_length == 0xFFFFFFFF then + return nil, nil -- DWARF64 not supported + end + -- unit_length is the body size, NOT including the 4-byte unit_length field itself. + local body_start = pos + 4 + local body_end = body_start + unit_length + if body_end > section_len then + return nil, nil -- malformed + end + -- Per DWARF5 §7.5.6, the type_unit (DW_UT_type = 0x02) body layout is: + -- 0: version (2) + -- 2: unit_type (1) -- DW_UT_type = 0x02 + -- 3: address_size (1) + -- 4: debug_abbrev_offset (4) + -- 8: type_signature (8) + -- 16: type_offset (4) + -- 20: + if body_end - body_start >= 20 then + -- read_ref_sig8 / write_u32_le / etc. are 1-indexed (string:byte); + -- pos / body_start / body_end are 0-based wire offsets, so the + -- 1-indexed byte at 0-based wire offset X is string:byte(X + 1). + -- Per DWARF5 §7.5.6, the type_unit body is laid out as: + -- byte 0-1: version (2) + -- byte 2: unit_type (1) -- DW_UT_type = 0x02 + -- byte 3: address_size (1) + -- byte 4-7: debug_abbrev_offset (4) + -- byte 8-15: type_signature (8) + -- byte 16-19: type_offset (4) + local unit_type = info:byte(body_start + 2 + 1) -- 0-based +2 = unit_type in 1-indexed + if unit_type == 0x02 then -- DW_UT_type + local sig_lo, sig_hi, _ = M.read_ref_sig8(info, body_start + 8) -- 0-based +8 = type_signature in 1-indexed + if sig_lo == target_sig_lo and sig_hi == target_sig_hi then + local type_offset = M.read_u32_le(info, body_start + 16) -- 0-based +16 = type_offset in 1-indexed + return pos, type_offset + end + end + end + -- Advance to the next unit (the 4-byte unit_length + the body). + pos = body_end + end + return nil, nil +end + --- Return a 4-byte little-endian byte string for `value`. --- Caller concatenates with `..` if composing multi-word blobs. --- **Byte weights** written as `0x100` etc. (see `M.read_u32_le` for rationale). @@ -665,6 +752,44 @@ function M.sleb128(n) return table.concat(bytes) end +--- ULEB128 byte-length: number of bytes the encoder M.uleb128 would produce for `n`. +--- Used by callers that need to size a buffer before encoding (e.g. compute_loclists_offsets +--- needs the encoded length of an `uleb128(4)` for a `DW_OP_piece + uleb128(U4_BYTE_SIZE)` tail). +--- @param n integer -- non-negative +--- @return integer -- 1..5 for n in [0, 2^32) +function M.uleb128_size(n) + assert(n >= 0, "uleb128_size requires non-negative input") + if n == 0 then return 1 end + local bytes = 1 + while n >= 0x80 do + n = (n - (n % (LEB_DATA_MASK + 1))) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7 + bytes = bytes + 1 + end + return bytes +end + +--- SLEB128 byte-length: number of bytes the encoder M.sleb128 would produce for `n`. +--- Used by callers that need to size a buffer before encoding. +--- (e.g. compute_loclists_offsets needs the encoded length of an `sleb128(field.offset)` in a tape piece). +--- Handles the signed DWARF5 termination: positive terminator if (n == 0) and bit 6 of last byte is unset; +--- negative terminator if (n == -1) and bit 6 of last byte is set. +--- @param n integer -- any integer (negative allowed) +--- @return integer +function M.sleb128_size(n) + local more = true + local bytes = 0 + local v = n + while more do + local b = v % (LEB_DATA_MASK + 1) -- extract low 7 bits + v = (v - b) / (LEB_DATA_MASK + 1) -- arithmetic shift right by 7 + if v == 0 and b < SLEB_SIGN_BIT then more = false end -- positive terminator + if v == -1 and b >= SLEB_SIGN_BIT then more = false end -- negative terminator + if more then b = b + LEB_CONT_BIT end + bytes = bytes + 1 + end + return bytes +end + -- ════════════════════════════════════════════════════════════════════════════ -- I/O helpers: atoms source-map + native directory glob -- ════════════════════════════════════════════════════════════════════════════ diff --git a/scripts/passes/dwarf_injection.lua b/scripts/passes/dwarf_injection.lua index ca7dbff..865d87c 100644 --- a/scripts/passes/dwarf_injection.lua +++ b/scripts/passes/dwarf_injection.lua @@ -295,21 +295,42 @@ local function build_debug_loclists_section(atom_table, registries) return header .. body end +-- Compute the size of one tape piece for an atom's struct field offset. +-- The piece is: DW_OP_bregN(1) + sleb128(offset)(1..5) + DW_OP_piece(1) + uleb128(4)(1). +-- Sleb128(0) is 1 byte; sleb128(63) is 1 byte; sleb128(64) is 2 bytes; ...; sleb128(2^28) is 5 bytes. +-- 4 = U4_BYTE_SIZE: each field is sizeof(uint32_t) on MIPS32. +-- @param offset integer -- the field's offset within the struct (0..2^32-1) +-- @return integer -- encoded size in bytes (4 for offsets < 64, 5 for offsets 64..8191, ..., 8 for > 2^28) +local function tape_piece_size(offset) + return 1 + elf_dwarf.sleb128_size(offset) + 1 + elf_dwarf.uleb128_size(U4_BYTE_SIZE) +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 LOCLIST_ENTRY_HEADER_SIZE = 1 + 4 + 1 -- DW_LLE_start_length(1) + addr(4) + uleb_length(1) local offsets = {} - local cursor = 4 + 2 + 1 + 1 + 4 + -- Loclist unit header (DWARF5 §7.7.2): unit_length(4) + version(2) + address_size(1) + segment_size(1) + offset_entry_count(4) = 12 bytes. + -- The unit_length itself is not counted in the unit_length value, so the body starts at byte 12. + local cursor = 4 + 2 + 1 + 1 + 4 -- = 12 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 + -- Sum the actual size of each tape piece based on the field's offset, not an assumed constant. + -- The expression below mirrors what build_debug_loclists_section produces: + -- 1 (DW_LLE_start_length) + 4 (PC) + 1 (uleb length prefix) + sum(tape_piece_size(field.offset)) + -- + 1 (DW_LLE_start_length) + 4 (transition_pc) + 1 (uleb length prefix) + n_fields * 3 (gpr pieces) + -- + 1 (DW_LLE_end_of_list) + local tape_pieces_size = 0 + for _, f in ipairs(atom.rbind.fields or {}) do + tape_pieces_size = tape_pieces_size + tape_piece_size(f.offset or 0) + end + local gpr_pieces_size = n_fields * 3 -- each gpr piece: DW_OP_regN(1) + DW_OP_piece(1) + uleb(4)(1) = 3 bytes + local tape_entry = LOCLIST_ENTRY_HEADER_SIZE + tape_pieces_size + local gpr_entry = LOCLIST_ENTRY_HEADER_SIZE + gpr_pieces_size + local body_len = tape_entry + gpr_entry + 1 -- +1 for DW_LLE_end_of_list cursor = cursor + body_len end end @@ -1078,7 +1099,10 @@ end --- @param atom_table table --- @return string local function build_dwarf_aranges_section(existing, atom_table) - if #existing < 12 then return existing end -- header sanity + if #existing < 12 then + io.stderr:write("[dwarf_injection] WARN: .debug_aranges section too short (" .. #existing .. " bytes) to contain a unit header; passing through unchanged\n") + return existing + end -- header sanity -- .debug_aranges can contain multiple compilation units (CUs). -- gcc-mips-elf emits one CU per .text section TU. @@ -1105,6 +1129,7 @@ local function build_dwarf_aranges_section(existing, atom_table) local ul = elf_dwarf.read_u32_le(existing, i) if ul == elf_dwarf.ELF32.dw_dwarf32_terminator then -- DWARF64 marker - not supported. + io.stderr:write("[dwarf_injection] WARN: .debug_aranges contains a DWARF64 marker (0xFFFFFFFF); the 64-bit extension is not supported by this metaprogram; passing through unchanged\n") return existing end @@ -1137,8 +1162,10 @@ local function build_dwarf_aranges_section(existing, atom_table) 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. + -- Malformed: walked past the end of the section without finding a unit whose end aligns with #existing. + -- This means a unit's length field overruns the section, or there is no terminator on the last unit. + -- For now, return existing unchanged to avoid making it worse; warn so the user can investigate. + io.stderr:write("[dwarf_injection] WARN: .debug_aranges layout is malformed (no unit's end aligned with section end at " .. #existing .. " bytes); passing through unchanged\n") return existing end @@ -2420,4 +2447,10 @@ function M.run(ctx) return { outputs = {}, errors = {}, warnings = {} } end +-- Test-only re-exports: keep the module's main M table lean while letting scratch +-- tests drive the real emission and offset computation paths. +M.compute_loclists_offsets_for_test = compute_loclists_offsets +M.build_debug_loclists_section_for_test = build_debug_loclists_section +M.tape_piece_size_for_test = tape_piece_size + return M