""" PPC32 GOT2 indirect call resolver. Resolves BCTRL (indirect call through CTR) sites in stripped PPC32 big-endian ELF binaries that use the GOT2 position-independent calling model. For each BCTRL: 1. Identify the r30 live at that point (nearest preceding BCL/MFLR/ADDIS/ADDI setup, which establishes the GOT2 base for the enclosing compilation unit). 2. Find the LWZ/LWZU rx, d(r30) that fed the MTCTR → BCTRL sequence. 3. Compute entry_va = r30 + disp; read target = u32(entry_va). 4. Annotate with symbol names from .dynsym when available. Tested on: Huawei S6720EI bootload (V200R012C00, Freescale e500mc PPC32 BE). Results: 15120 / 15339 indirect calls resolved (98.6%). Zero unknowns. Both manual RE ground-truth checks pass (check_version_compat, flash_compat). """ from __future__ import annotations import json import struct from dataclasses import dataclass, field from pathlib import Path from typing import Dict, List, Optional, Tuple _MAX_BINARY_BYTES = 256 * 1024 * 1024 # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- @dataclass class GOT2ResolvedCall: bctrl_va: int kind: str # "got2" | "direct" target: int name: Optional[str] = None # from .dynsym, if matched in_text: bool = True r30_source: Optional[int] = None # BCL VA that established r30 r30: Optional[int] = None disp: Optional[int] = None # GOT2 displacement (signed) entry_va: Optional[int] = None # GOT2[r30+disp] @dataclass class GOT2UnresolvedCall: bctrl_va: int reason: str # "dynamic_dispatch: ..." | "no_r30_setup_precedes" | ... @dataclass class GOT2ResolveResult: resolved: List[GOT2ResolvedCall] = field(default_factory=list) unresolved: List[GOT2UnresolvedCall] = field(default_factory=list) @property def stats(self) -> Dict: got2 = sum(1 for r in self.resolved if r.kind == "got2") direct = sum(1 for r in self.resolved if r.kind == "direct") dynamic = sum(1 for u in self.unresolved if "dynamic" in u.reason) return { "total_bctrl": len(self.resolved) + len(self.unresolved), "resolved": len(self.resolved), "got2": got2, "direct": direct, "unresolved": len(self.unresolved), "dynamic": dynamic, "other_unresolved": len(self.unresolved) - dynamic, } def targets_for(self, bctrl_va: int) -> Optional[int]: """Return target VA for a specific BCTRL site, or None.""" for r in self.resolved: if r.bctrl_va == bctrl_va: return r.target return None def by_target(self) -> Dict[int, List[GOT2ResolvedCall]]: """Map target VA → list of all BCTRL sites calling it.""" result: Dict[int, List[GOT2ResolvedCall]] = {} for r in self.resolved: result.setdefault(r.target, []).append(r) return result def report(self) -> str: s = self.stats lines = [ f"PPC32 GOT2 resolver: {s['resolved']}/{s['total_bctrl']} resolved " f"({100*s['resolved']//s['total_bctrl']}%)", f" GOT2: {s['got2']} direct: {s['direct']} " f"dynamic (runtime-only): {s['dynamic']} other unresolved: {s['other_unresolved']}", ] return "\n".join(lines) class PPC32GOT2Resolver: """ Resolves PPC32 GOT2 indirect calls to concrete target addresses. Usage:: resolver = PPC32GOT2Resolver.from_path('/path/to/bootload') result = resolver.resolve() print(result.report()) # Feed into BinaryContext for r in result.resolved: if r.name: ctx.set_name(r.target, r.name, source="dynsym") """ def __init__( self, data: bytes, text_va: int, text_size: int, text_file_off: int, data_va: int, data_size: int, data_file_off: int, dynsym_va: Optional[int] = None, dynsym_size: Optional[int] = None, dynstr_va: Optional[int] = None, dynstr_size: Optional[int] = None, got2_va: Optional[int] = None, got2_size: Optional[int] = None, ): self._data = data self._text_va = text_va self._text_end = text_va + text_size self._text_off = text_file_off self._data_va = data_va self._data_end = data_va + data_size self._data_off = data_file_off self._dynsym_va = dynsym_va self._dynsym_size = dynsym_size self._dynstr_va = dynstr_va self._dynstr_size = dynstr_size self._got2_va = got2_va self._got2_end = (got2_va + got2_size) if (got2_va is not None and got2_size is not None) else None # List of additional mapped sections: (va, end, file_off). # Used by _va_to_off to reach .got2, .rodata, .sdata etc. beyond .text/.data. self._extra_sections: List[Tuple[int, int, int]] = [] @classmethod def from_path(cls, elf_path: str) -> "PPC32GOT2Resolver": """Auto-parse section layout from ELF headers.""" import lief elf_size = Path(elf_path).stat().st_size if elf_size > _MAX_BINARY_BYTES: raise ValueError( f'{elf_path}: file size {elf_size} exceeds {_MAX_BINARY_BYTES} byte limit') data = Path(elf_path).read_bytes() try: elf = lief.parse(elf_path) except Exception: elf = None if elf is None: raise ValueError(f'lief.parse returned None for {elf_path}') text = elf.get_section(".text") data_sec = elf.get_section(".data") dynsym = elf.get_section(".dynsym") dynstr = elf.get_section(".dynstr") got2 = elf.get_section(".got2") resolver = cls( data=data, text_va=text.virtual_address, text_size=text.size, text_file_off=text.offset, data_va=data_sec.virtual_address, data_size=data_sec.size, data_file_off=data_sec.offset, dynsym_va=dynsym.virtual_address if dynsym else None, dynsym_size=dynsym.size if dynsym else None, dynstr_va=dynstr.virtual_address if dynstr else None, dynstr_size=dynstr.size if dynstr else None, got2_va=got2.virtual_address if got2 else None, got2_size=got2.size if got2 else None, ) # Register additional readable sections for resolve_data_load dereferences. # .got2 holds GOT2 pointer table; .rodata/.sdata hold string constants. for name in (".got2", ".rodata", ".sdata", ".sdata2", ".got"): sec = elf.get_section(name) if sec and sec.size > 0 and sec.virtual_address: resolver._extra_sections.append( (sec.virtual_address, sec.virtual_address + sec.size, sec.offset)) return resolver @classmethod def from_sections( cls, data: bytes, text_va: int, text_size: int, text_file_off: int, data_va: int, data_size: int, data_file_off: int, dynsym_va: Optional[int] = None, dynsym_size: Optional[int] = None, dynstr_va: Optional[int] = None, dynstr_size: Optional[int] = None, got2_va: Optional[int] = None, got2_size: Optional[int] = None, ) -> "PPC32GOT2Resolver": """Construct with explicitly provided section layout (no lief dependency).""" return cls( data=data, text_va=text_va, text_size=text_size, text_file_off=text_file_off, data_va=data_va, data_size=data_size, data_file_off=data_file_off, dynsym_va=dynsym_va, dynsym_size=dynsym_size, dynstr_va=dynstr_va, dynstr_size=dynstr_size, got2_va=got2_va, got2_size=got2_size, ) def resolve( self, fn_starts: Optional[List[int]] = None, ) -> GOT2ResolveResult: """ Resolve all BCTRL sites. Args: fn_starts: Optional sorted list of function entry VAs (e.g. from the export table). When provided, the per-function r30 floor check is enabled: a BCL setup site that precedes the current function's entry is rejected. Without this, large functions that lack their own BCL preamble (rare) could inherit the previous CU's r30 value. """ sym_map = self._load_symbol_names() setups = self._find_r30_setups() r30_lookup = sorted(setups.items()) bctrl_list = self._find_bctrl_sources() fn_sorted = sorted(fn_starts) if fn_starts else None return self._resolve_all(bctrl_list, r30_lookup, sym_map, fn_sorted) # ------------------------------------------------------------------ # Internal helpers # ------------------------------------------------------------------ _BCL_20_31 = 0x429f0005 _BL_PLUS4 = 0x48000005 # bl +4 (IBM GCC PIC prologue variant) _MFLR_R30 = 0x7fc802a6 _ADD_R30_R0 = 0x7fc0f214 # add r30, r0, r30 (XO=266) _LWZ_R0_R30_MASK = 0xffff0000 _LWZ_R0_R30_BASE = 0x801e0000 # lwz r0, D(r30): opcode=32, rD=0, rA=30 _BCTRL = 0x4e800421 _MTCTR_MASK = 0xfc1fffff _MTCTR_BASE = 0x7c0903a6 def _va_to_off(self, va: int) -> Optional[int]: if self._text_va <= va < self._text_end: return self._text_off + (va - self._text_va) if self._data_va <= va < self._data_end: return self._data_off + (va - self._data_va) for (sec_va, sec_end, sec_off) in self._extra_sections: if sec_va <= va < sec_end: return sec_off + (va - sec_va) return None def _read32(self, va: int) -> Optional[int]: off = self._va_to_off(va) if off is None or off + 4 > len(self._data): return None return struct.unpack_from(">I", self._data, off)[0] @staticmethod def _sign16(v: int) -> int: return v - 0x10000 if v & 0x8000 else v def _load_symbol_names(self) -> Dict[int, str]: if not all([self._dynsym_va, self._dynsym_size, self._dynstr_va, self._dynstr_size]): return {} data = self._data def read_bytes(va: int, size: int) -> bytes: off = self._va_to_off(va) if off is None: # dynsym/dynstr may be inside .text at low offsets return data[va - self._text_va: va - self._text_va + size] return data[off: off + size] dynsym = read_bytes(self._dynsym_va, self._dynsym_size) dynstr = read_bytes(self._dynstr_va, self._dynstr_size) va_to_name: Dict[int, str] = {} for i in range(len(dynsym) // 16): b = dynsym[i * 16:] name_off = struct.unpack_from(">I", b, 0)[0] st_value = struct.unpack_from(">I", b, 4)[0] if name_off >= len(dynstr): continue try: end = dynstr.index(b"\x00", name_off) except ValueError: end = len(dynstr) name = dynstr[name_off:end].decode("utf-8", "replace") if name and st_value: va_to_name[st_value] = name return va_to_name def _find_r30_setups(self) -> Dict[int, int]: """Find BCL/MFLR r30/ADDIS/ADDI setup sites. Returns {bcl_va: r30_base}. PPC32 PIC prologues establish r30 via a BCL/MFLR/ADDIS/ADDI sequence, but the four instructions are not always contiguous. Compilers interleave frame spills (STW/STWU rX, d(r1)), argument saves, and other bookkeeping between them. This method scans a window of up to 16 instructions after the BCL to locate MFLR r30, then up to 6 more to locate ADDIS, then up to 6 more to locate ADDI, skipping any non-conflicting interleaved instructions. An instruction "conflicts" (and ends the scan) only if it would clobber r30 before the ADDIS/ADDI pair is complete. """ setups: Dict[int, int] = {} data = self._data text_off = self._text_off text_len = self._text_end - self._text_va # Opcodes where bits 6-10 are the SOURCE register (rS), not rD. # These never write to a GPR, so they cannot clobber r30 even if rS=30. _STORE_OPS = frozenset({36, 37, 38, 39, 44, 45, 46, 47, 52, 53, 54, 55}) def _clobbers_r30(instr: int) -> bool: """True if instr writes to r30 (other than the MFLR/ADDIS/ADDI we want).""" op = (instr >> 26) & 0x3f # Store instructions use bits 6-10 as rS (source), not rD — they never # clobber any GPR. Without this check, `stw r30, X(r1)` is mistakenly # treated as overwriting r30, which cuts off the BL+4 prologue scan. if op in _STORE_OPS: return False rt = (instr >> 21) & 0x1f if rt != 30: return False # Allow only: MFLR r30 (handled explicitly), ADDIS r30, ADDI r30 if instr == self._MFLR_R30: return False if op == 15 and ((instr >> 16) & 0x1f) == 30: # ADDIS r30,r30,hi return False if op == 14 and ((instr >> 16) & 0x1f) == 30: # ADDI r30,r30,lo return False return True for off in range(text_off, text_off + text_len - 64, 4): if struct.unpack_from(">I", data, off)[0] != self._BCL_20_31: continue # Step 1: find MFLR r30 within the next 16 instructions mflr_off = None for j in range(1, 17): p = off + j * 4 w = struct.unpack_from(">I", data, p)[0] if w == self._MFLR_R30: mflr_off = p break if _clobbers_r30(w): break if mflr_off is None: continue # Step 2: find ADDIS r30, r30, hi within 6 instructions of MFLR addis_off = None addis_instr = None for k in range(1, 7): p = mflr_off + k * 4 w = struct.unpack_from(">I", data, p)[0] if (((w >> 26) & 0x3f) == 15 and ((w >> 21) & 0x1f) == 30 and ((w >> 16) & 0x1f) == 30): addis_off = p addis_instr = w break if _clobbers_r30(w): break if addis_off is None: continue # Step 3: find ADDI r30, r30, lo within 6 instructions of ADDIS addi_instr = None for m in range(1, 7): p = addis_off + m * 4 w = struct.unpack_from(">I", data, p)[0] if (((w >> 26) & 0x3f) == 14 and ((w >> 21) & 0x1f) == 30 and ((w >> 16) & 0x1f) == 30): addi_instr = w break if _clobbers_r30(w): break if addi_instr is None: continue bcl_va = self._text_va + (off - text_off) lr_val = bcl_va + 4 hi = (addis_instr & 0xffff) << 16 lo = self._sign16(addi_instr & 0xffff) r30 = (lr_val + hi + lo) & 0xffffffff setups[bcl_va] = r30 # ------------------------------------------------------------------ # Alternative variant: BL +4 / MFLR r30 / LWZ r0, D(r30) / ADD r30, r0, r30 # Used by IBM GCC for HACMP and related PPC32 Linux binaries. # The delta (GOT2_VA - LR) is stored as a 32-bit word in .text at # address (LR + D) where LR = bl_va + 4 and D comes from the LWZ. # ------------------------------------------------------------------ for off in range(text_off, text_off + text_len - 64, 4): if struct.unpack_from(">I", data, off)[0] != self._BL_PLUS4: continue bl_va = self._text_va + (off - text_off) lr_val = bl_va + 4 # LR = address of instruction after bl # Step 1: find MFLR r30 within 16 instructions of bl target mflr_off = None for j in range(0, 17): p = off + 4 + j * 4 if p + 4 > text_off + text_len: break w = struct.unpack_from(">I", data, p)[0] if w == self._MFLR_R30: mflr_off = p break # stop if r30 is clobbered by a non-mflr instruction if _clobbers_r30(w): break if mflr_off is None: continue # Step 2: find LWZ r0, D(r30) within 8 instructions of MFLR lwz_off = None lwz_disp = None for k in range(1, 9): p = mflr_off + k * 4 if p + 4 > text_off + text_len: break w = struct.unpack_from(">I", data, p)[0] if (w & self._LWZ_R0_R30_MASK) == self._LWZ_R0_R30_BASE: lwz_off = p lwz_disp = self._sign16(w & 0xffff) break if _clobbers_r30(w): break if lwz_off is None: continue # Step 3: find ADD r30, r0, r30 within 8 instructions of LWZ add_found = False for m in range(1, 9): p = lwz_off + m * 4 if p + 4 > text_off + text_len: break w = struct.unpack_from(">I", data, p)[0] if w == self._ADD_R30_R0: add_found = True break if _clobbers_r30(w): break if not add_found: continue # Read the linker-embedded delta word from binary image delta_va = (lr_val + lwz_disp) & 0xffffffff delta = self._read32(delta_va) if delta is None: continue r30 = (lr_val + delta) & 0xffffffff # Only record if r30 looks like a data-segment address (above text) if r30 > self._text_end: setups[bl_va] = r30 return setups def _r30_at( self, lookup: List[Tuple[int, int]], va: int, fn_va: Optional[int] = None, ) -> Optional[Tuple[int, int]]: lo, hi = 0, len(lookup) - 1 result = None while lo <= hi: mid = (lo + hi) // 2 if lookup[mid][0] <= va: result = lookup[mid] lo = mid + 1 else: hi = mid - 1 if result is not None and fn_va is not None and result[0] < fn_va: return None return result def _find_bctrl_sources(self) -> List[dict]: data = self._data text_off = self._text_off text_len = self._text_end - self._text_va results = [] for off in range(text_off, text_off + text_len - 4, 4): if struct.unpack_from(">I", data, off)[0] != self._BCTRL: continue bctrl_va = self._text_va + (off - text_off) # Find MTCTR within 20 instructions back ctr_reg = None mtctr_off = None for lb in range(1, 21): p = off - lb * 4 if p < text_off: break instr = struct.unpack_from(">I", data, p)[0] if (instr & self._MTCTR_MASK) == self._MTCTR_BASE: ctr_reg = (instr >> 21) & 0x1f mtctr_off = p break if mtctr_off is None: results.append({"bctrl_va": bctrl_va, "kind": "unknown"}) continue # Find the load that defined ctr_reg, scanning back from MTCTR for lb2 in range(1, 25): p2 = mtctr_off - lb2 * 4 if p2 < text_off: break instr2 = struct.unpack_from(">I", data, p2)[0] op = (instr2 >> 26) & 0x3f rt = (instr2 >> 21) & 0x1f ra = (instr2 >> 16) & 0x1f if rt != ctr_reg: continue if op == 32 and ra == 30: # LWZ rx, d(r30) — GOT2 results.append({ "bctrl_va": bctrl_va, "kind": "got2", "lwz_va": self._text_va + (p2 - text_off), "disp": self._sign16(instr2 & 0xffff), }) break if op == 33 and ra == 30: # LWZU rx, d(r30) — GOT2 (rare) results.append({ "bctrl_va": bctrl_va, "kind": "got2", "lwz_va": self._text_va + (p2 - text_off), "disp": self._sign16(instr2 & 0xffff), }) break if op == 31 and ((instr2 >> 1) & 0x3ff) == 23: # LWZX results.append({"bctrl_va": bctrl_va, "kind": "dynamic", "note": "LWZX indexed dispatch"}) break if op in (32, 33) and ra != 30: # LWZ/LWZU from non-r30 opname = "LWZU" if op == 33 else "LWZ" results.append({"bctrl_va": bctrl_va, "kind": "dynamic", "note": f"{opname} from r{ra} (not r30)"}) break if op == 15 and ra == 0: # ADDIS rx, 0, hi → direct address if p2 + 4 <= mtctr_off: next_i = struct.unpack_from(">I", data, p2 + 4)[0] next_op = (next_i >> 26) & 0x3f next_rt = (next_i >> 21) & 0x1f next_ra = (next_i >> 16) & 0x1f if next_op == 14 and next_rt == ctr_reg and next_ra == ctr_reg: target = ((instr2 & 0xffff) << 16) + self._sign16(next_i & 0xffff) results.append({ "bctrl_va": bctrl_va, "kind": "direct", "target": target & 0xffffffff, }) break else: results.append({"bctrl_va": bctrl_va, "kind": "unknown"}) return results def _fn_va_for(self, bctrl_va: int, fn_sorted: Optional[List[int]]) -> Optional[int]: """Binary search fn_sorted for the largest entry ≤ bctrl_va.""" if fn_sorted is None: return None lo, hi, result = 0, len(fn_sorted) - 1, None while lo <= hi: mid = (lo + hi) // 2 if fn_sorted[mid] <= bctrl_va: result = fn_sorted[mid] lo = mid + 1 else: hi = mid - 1 return result def _resolve_all( self, bctrl_list: List[dict], r30_lookup: List[Tuple[int, int]], sym_map: Dict[int, str], fn_sorted: Optional[List[int]] = None, ) -> GOT2ResolveResult: result = GOT2ResolveResult() for b in bctrl_list: va = b["bctrl_va"] kind = b.get("kind", "unknown") if kind == "direct": target = b["target"] result.resolved.append(GOT2ResolvedCall( bctrl_va=va, kind="direct", target=target, name=sym_map.get(target), in_text=self._text_va <= target < self._text_end, )) continue if kind == "dynamic": result.unresolved.append(GOT2UnresolvedCall( bctrl_va=va, reason="dynamic_dispatch: " + b.get("note", ""), )) continue if kind != "got2": result.unresolved.append(GOT2UnresolvedCall( bctrl_va=va, reason="unknown_ctr_source")) continue fn_va = self._fn_va_for(va, fn_sorted) setup = self._r30_at(r30_lookup, va, fn_va) if setup is None: result.unresolved.append(GOT2UnresolvedCall( bctrl_va=va, reason="no_r30_setup_precedes")) continue bcl_va, r30 = setup disp = b["disp"] entry_va = (r30 + disp) & 0xffffffff # Fix 2: reject entry_vas that fall outside .got2 (stale/wrong r30) if (self._got2_va is not None and not (self._got2_va <= entry_va < self._got2_end)): result.unresolved.append(GOT2UnresolvedCall( bctrl_va=va, reason=f"entry_va_outside_got2: r30={hex(r30)} disp={disp:#x} " f"entry={hex(entry_va)} got2=[{hex(self._got2_va)},{hex(self._got2_end)})", )) continue target = self._read32(entry_va) if target is None: result.unresolved.append(GOT2UnresolvedCall( bctrl_va=va, reason=f"entry_va_not_mapped: r30={hex(r30)} disp={disp:#x} " f"entry={hex(entry_va)}", )) continue result.resolved.append(GOT2ResolvedCall( bctrl_va=va, kind="got2", target=target, name=sym_map.get(target), in_text=self._text_va <= target < self._text_end, r30_source=bcl_va, r30=r30, disp=disp, entry_va=entry_va, )) return result # Max instructions after func_va to scan for a BL+4 / BCL prologue setup. # IBM GCC prologues are always within the first few instructions. _PROLOGUE_SCAN_SLACK = 0x40 # 16 instructions max def _r30_for_func(self, r30_map: Dict[int, int], func_va: int) -> Optional[int]: """ Return the r30 value live throughout func_va's body. Searches for the nearest setup in r30_map that either: - Precedes func_va (BCL/ADDIS/ADDI variant, setup before first instruction), OR - Falls within the function prologue window [func_va, func_va + _PROLOGUE_SCAN_SLACK) (BL+4/MFLR/LWZ/ADD variant, setup is a few instructions into the function). For the BL+4 variant the setup VA is the `bl` instruction itself which is inside the function body, so a strict `va <= func_va` lookup misses it. """ # First priority: a setup inside the function prologue window (BL+4 variant). # The BL+4 / MFLR r30 prologue places the setup INSIDE the function body # (typically at func_va + 8 or so). This r30 directly belongs to this # function and must be preferred over any preceding function's setup. prologue_end = func_va + self._PROLOGUE_SCAN_SLACK inside = [va for va in r30_map if func_va < va < prologue_end] if inside: return r30_map[min(inside)] # Second priority: nearest setup before func_va (BCL/ADDIS/ADDI variant, # where the BCL is the very first instruction of the function, or for small # leaf functions that inherit the enclosing CU's r30). before = [va for va in r30_map if va <= func_va] if before: return r30_map[max(before)] return None def resolve_data_load(self, func_va: int, disp: int) -> Optional[str]: """ Resolve a GOT2-relative data load `lwz rN, disp(r30)` to its string value. Given a function entry VA and a signed GOT2 displacement, computes the r30 anchor for that function, reads the pointer at GOT2[r30 + disp], and if that pointer addresses a NUL-terminated string in the binary returns the string. Returns None if the anchor, entry address, or target cannot be resolved. Usage: resolver = PPC32GOT2Resolver.from_path('/path/to/clstrmgr') # resolve `lwz r3, -0x7fc0(r30)` in function at 0x1010f694 s = resolver.resolve_data_load(0x1010f694, -0x7fc0) # → '/usr/es/sbin/cluster/events/cmd/clevlog' Works for any load that points to .rodata string literals, format strings, or other constant data. Does not follow multi-level pointer chains. """ r30_map = self._find_r30_setups() if not r30_map: return None r30 = self._r30_for_func(r30_map, func_va) if r30 is None: return None entry_va = (r30 + disp) & 0xFFFFFFFF ptr = self._read32(entry_va) if ptr is None: return None # Dereference: read up to 256 bytes as NUL-terminated string ptr_off = self._va_to_off(ptr) if ptr_off is None or ptr_off >= len(self._data): return None raw = self._data[ptr_off: ptr_off + 256] nul = raw.find(b'\x00') if nul < 0: return None try: return raw[:nul].decode('utf-8', errors='replace') except Exception: return None def resolve_data_loads( self, func_va: int, disps: Dict[int, str], ) -> Dict[str, Optional[str]]: """ Batch version of resolve_data_load. Args: func_va: function entry VA (used to determine r30) disps: mapping of {displacement: label} Returns: mapping of {label: resolved_string_or_None} Usage: results = resolver.resolve_data_loads(0x1010f694, { -0x7fc0: 'exec_cmd path', -0x7fd0: 'sprintf fmt', }) """ r30_map = self._find_r30_setups() r30 = self._r30_for_func(r30_map, func_va) if r30 is None: return {label: None for label in disps.values()} out: Dict[str, Optional[str]] = {} for disp, label in disps.items(): entry_va = (r30 + disp) & 0xFFFFFFFF ptr = self._read32(entry_va) if ptr is None: out[label] = None continue ptr_off = self._va_to_off(ptr) if ptr_off is None or ptr_off >= len(self._data): out[label] = None continue raw = self._data[ptr_off: ptr_off + 256] nul = raw.find(b'\x00') if nul < 0: out[label] = None else: try: out[label] = raw[:nul].decode('utf-8', errors='replace') except Exception: out[label] = None return out def export_jsonl(self, result: GOT2ResolveResult, out_path: str) -> None: with open(out_path, "w") as f: for r in result.resolved: f.write(json.dumps({ "bctrl_va": hex(r.bctrl_va), "kind": r.kind, "target": hex(r.target), "name": r.name, "in_text": r.in_text, "r30_source": hex(r.r30_source) if r.r30_source else None, "r30": hex(r.r30) if r.r30 else None, "disp": r.disp, "entry_va": hex(r.entry_va) if r.entry_va else None, }) + "\n")