summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/greeneye/symbols.py
diff options
context:
space:
mode:
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/greeneye/symbols.py')
-rw-r--r--debug/risc-v-gsp-rm-vm/greeneye/symbols.py313
1 files changed, 313 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/greeneye/symbols.py b/debug/risc-v-gsp-rm-vm/greeneye/symbols.py
new file mode 100644
index 0000000..7b2e9dd
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/greeneye/symbols.py
@@ -0,0 +1,313 @@
+"""Symbol resolution and address translation for green-eye.
+
+Inputs supported (all optional, merged into one database):
+
+* ELF ``.symtab`` symbols (demo firmware);
+* the project's Ghidra exports:
+ - ``names.tsv`` ``<file_vaddr>\t<name>[\t<source>]`` (comments with #),
+ - ``decompiled_index.tsv`` / bucket TSVs
+ ``<program_addr>\t<file_addr>\t<size>\t<name>\t<bucket>\t<line>\t<status>``,
+ - ``exports.tsv`` ``addr\tfile_addr\tsize\tname``,
+* green-eye native JSON (``greeneye-symbols.json``).
+
+Address conventions (matching the project docs):
+ * Ghidra program address = file vaddr + image base (``0x100000`` for libcuda);
+ * runtime address = file vaddr + runtime load bias;
+ * for firmware images linked at their load address the bias is 0.
+
+``SymbolDB`` stores runtime addresses by default; each loader takes a ``bias``
+so a relocated firmware or a different image base can be normalised.
+"""
+from __future__ import annotations
+
+import bisect
+import json
+import os
+from dataclasses import dataclass, field as _field
+
+from .errors import SymbolNotFound
+
+
+@dataclass
+class Symbol:
+ addr: int
+ name: str
+ size: int = 0
+ source: str = ""
+ subsystem: str | None = None
+ extras: dict = _field(default_factory=dict)
+
+ @property
+ def end(self) -> int:
+ return self.addr + self.size if self.size else self.addr
+
+ def contains(self, addr: int) -> bool:
+ if self.size:
+ return self.addr <= addr < self.addr + self.size
+ return addr == self.addr
+
+ def label(self, addr: int | None = None) -> str:
+ if addr is None or addr == self.addr or not self.size:
+ return self.name
+ return f"{self.name}+{addr - self.addr:#x}"
+
+ def to_json(self) -> dict:
+ out = {"addr": self.addr, "name": self.name}
+ if self.size:
+ out["size"] = self.size
+ if self.source:
+ out["source"] = self.source
+ if self.subsystem:
+ out["subsystem"] = self.subsystem
+ if self.extras:
+ out.update(self.extras)
+ return out
+
+
+class SymbolDB:
+ def __init__(self) -> None:
+ self._symbols: list[Symbol] = []
+ self._sorted = False
+ self._by_name: dict[str, Symbol] = {}
+ self._addr_index: list[int] = []
+
+ # -- loading -----------------------------------------------------------
+
+ def add(self, symbol: Symbol) -> None:
+ self._symbols.append(symbol)
+ self._sorted = False
+ existing = self._by_name.get(symbol.name)
+ if existing is None or (symbol.source and not existing.source):
+ self._by_name[symbol.name] = symbol
+
+ def load_elf(self, image, bias: int = 0, source: str = "elf", only_code: bool = True) -> int:
+ """Load ``.symtab`` symbols from a FirmwareImage/ELF path."""
+ from .elf import ElfSymbol, load_elf
+
+ if isinstance(image, str):
+ image = load_elf(image)
+ count = 0
+ for sym in image.symbols: # type: ignore[union-attr]
+ if only_code and not (sym.is_function or sym.is_object):
+ continue
+ self.add(
+ Symbol(
+ addr=sym.value + bias,
+ name=sym.name,
+ size=sym.size,
+ source=source,
+ extras={"type": "func" if sym.is_function else "object"},
+ )
+ )
+ count += 1
+ return count
+
+ def load_names_tsv(self, path: str, bias: int = 0) -> int:
+ """Load the project's Ghidra ``names.tsv`` (file vaddrs)."""
+ count = 0
+ with open(path, "r", encoding="utf-8", errors="replace") as fh:
+ for line in fh:
+ line = line.rstrip("\n")
+ if not line or line.startswith("#"):
+ continue
+ cols = line.split("\t")
+ if len(cols) < 2:
+ continue
+ try:
+ addr = int(cols[0], 16)
+ except ValueError:
+ continue
+ self.add(
+ Symbol(addr=addr + bias, name=cols[1], source=cols[2] if len(cols) > 2 else "ghidra-names")
+ )
+ count += 1
+ return count
+
+ def load_index_tsv(self, path: str, bias: int = 0) -> int:
+ """Load ``decompiled_index.tsv``/bucket TSVs/exports.tsv heuristically."""
+ count = 0
+ with open(path, "r", encoding="utf-8", errors="replace") as fh:
+ for line in fh:
+ line = line.rstrip("\n")
+ if not line or line.startswith("#"):
+ continue
+ cols = line.split("\t")
+ if cols[0] in ("addr", "caller"):
+ continue
+ if len(cols) >= 7:
+ program, file_addr, size, name, bucket, lineno, status = cols[:7]
+ addr = _parse_hex(file_addr)
+ extras = {"program_addr": _parse_hex(program), "bucket": bucket, "line": lineno, "status": status}
+ elif len(cols) >= 4:
+ program, file_addr, size, name = cols[:4]
+ addr = _parse_hex(file_addr)
+ extras = {"program_addr": _parse_hex(program)}
+ elif len(cols) >= 2:
+ addr = _parse_hex(cols[0])
+ name = cols[1]
+ size = "0"
+ extras = {}
+ else:
+ continue
+ if addr is None or not name:
+ continue
+ try:
+ size_val = int(size, 0)
+ except ValueError:
+ size_val = 0
+ self.add(
+ Symbol(addr=addr + bias, name=name, size=size_val, source="ghidra-index", extras=extras)
+ )
+ count += 1
+ return count
+
+ def load_callgraph(self, path: str) -> int:
+ """Attach caller/callee lists from ``callgraph.tsv`` (program addrs)."""
+ by_program: dict[int, Symbol] = {}
+ for sym in self._symbols:
+ prog = sym.extras.get("program_addr")
+ if prog is not None:
+ by_program[int(prog)] = sym
+ if not by_program:
+ return 0
+ for sym in self._symbols:
+ sym.extras.setdefault("callers", [])
+ sym.extras.setdefault("callees", [])
+ count = 0
+ with open(path, "r", encoding="utf-8", errors="replace") as fh:
+ for line in fh:
+ line = line.rstrip("\n")
+ if not line or line.startswith("#") or line.startswith("caller"):
+ continue
+ cols = line.split("\t")
+ if len(cols) < 2:
+ continue
+ caller, callee = _parse_hex(cols[0]), _parse_hex(cols[1])
+ if caller is None or callee is None:
+ continue
+ if caller in by_program:
+ by_program[caller].extras["callees"].append(callee)
+ count += 1
+ if callee in by_program:
+ by_program[callee].extras["callers"].append(caller)
+ return count
+
+ def load_json(self, path: str) -> int:
+ with open(path, "r", encoding="utf-8") as fh:
+ payload = json.load(fh)
+ symbols = payload.get("symbols", payload if isinstance(payload, list) else [])
+ count = 0
+ for item in symbols:
+ try:
+ addr = int(item["addr"], 0) if isinstance(item["addr"], str) else int(item["addr"])
+ except (KeyError, TypeError, ValueError):
+ continue
+ extras = {k: v for k, v in item.items() if k not in ("addr", "name", "size", "source", "subsystem")}
+ self.add(
+ Symbol(
+ addr=addr,
+ name=item.get("name", f"sub_{addr:x}"),
+ size=int(item.get("size", 0)),
+ source=item.get("source", "json"),
+ subsystem=item.get("subsystem"),
+ extras=extras,
+ )
+ )
+ count += 1
+ return count
+
+ def save_json(self, path: str, meta: dict | None = None) -> None:
+ self._ensure_sorted()
+ payload = {
+ "format": "greeneye-symbols/1",
+ "count": len(self._symbols),
+ "meta": meta or {},
+ "symbols": [s.to_json() for s in self._symbols],
+ }
+ with open(path, "w", encoding="utf-8") as fh:
+ json.dump(payload, fh, indent=1, sort_keys=True)
+
+ # -- queries -----------------------------------------------------------
+
+ def _ensure_sorted(self) -> None:
+ if self._sorted:
+ return
+ self._symbols.sort(key=lambda s: (s.addr, -s.size))
+ self._addr_index = [s.addr for s in self._symbols]
+ self._sorted = True
+
+ def __len__(self) -> int:
+ return len(self._symbols)
+
+ def __iter__(self):
+ return iter(self._symbols)
+
+ def get(self, name: str) -> Symbol | None:
+ return self._by_name.get(name)
+
+ def lookup(self, name: str) -> int:
+ sym = self._by_name.get(name)
+ if sym is None:
+ raise SymbolNotFound(f"symbol {name!r} not found")
+ return sym.addr
+
+ def resolve(self, addr: int, max_delta: int = 0x10000) -> Symbol | None:
+ """Resolve an address to a containing or nearest preceding symbol."""
+ self._ensure_sorted()
+ if not self._symbols:
+ return None
+ pos = bisect.bisect_right(self._addr_index, addr) - 1
+ if pos < 0:
+ return None
+ candidate = self._symbols[pos]
+ if candidate.contains(addr):
+ return candidate
+ if candidate.size and addr < candidate.addr + candidate.size:
+ return candidate
+ # nearest-within-delta fallback for zero-size symbols (Ghidra names)
+ if not candidate.size and 0 <= addr - candidate.addr <= max_delta:
+ return candidate
+ return None
+
+ def annotate(self, addr: int, max_delta: int = 0x10000) -> str:
+ sym = self.resolve(addr, max_delta=max_delta)
+ if sym is None:
+ return f"{addr:#x} <unknown>"
+ return f"{addr:#x} {sym.label(addr)}"
+
+ def nearest(self, addr: int, max_delta: int = 0x10000) -> Symbol | None:
+ return self.resolve(addr, max_delta=max_delta)
+
+ def names(self) -> list[str]:
+ return sorted(self._by_name)
+
+ def by_subsystem(self, subsystem: str) -> list[Symbol]:
+ return [s for s in self._symbols if s.subsystem == subsystem]
+
+
+def _parse_hex(value: str) -> int | None:
+ value = value.strip()
+ if not value:
+ return None
+ try:
+ return int(value, 16)
+ except ValueError:
+ try:
+ return int(value, 0)
+ except ValueError:
+ return None
+
+
+def translation_for(
+ *,
+ file_vaddr: int,
+ program_addr: int | None = None,
+ runtime_addr: int | None = None,
+) -> dict:
+ """Explain the three address spaces for a debug session (docs aid)."""
+ out = {"file_vaddr": file_vaddr}
+ if program_addr is not None:
+ out["image_base"] = program_addr - file_vaddr
+ if runtime_addr is not None:
+ out["load_bias"] = runtime_addr - file_vaddr
+ return out