diff options
| author | blasty <blasty@local> | 2026-07-09 22:28:53 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-09 22:28:53 +0200 |
| commit | 6a761c222a6f08cb2fd825a15f3d3f44390a716d (patch) | |
| tree | 8a0a6c3add4eab61555783dd407a7c2aa83070ec | |
| parent | decompiler: xref jump lands the cursor on the reference token's column (diff) | |
| download | ida-tui-6a761c222a6f08cb2fd825a15f3d3f44390a716d.tar.gz ida-tui-6a761c222a6f08cb2fd825a15f3d3f44390a716d.tar.xz ida-tui-6a761c222a6f08cb2fd825a15f3d3f44390a716d.zip | |
xrefs: informative labels — function+offset, and section instead of '?'
The xref list showed just the containing function name, so multiple sites in the
same function were indistinguishable ('sub_2300' repeated), and references not in
any function collapsed to a bare '?'.
- In-function sites now show fn_name+offset (e.g. main+0xb08), so each site is
distinct and you can see where in the function it is.
- Non-function sites (GOT/reloc data slots, loose thunks) show the section they
live in (.got, .data.rel.ro, .text, LOAD, ...) instead of '?'. Add
Program.sections()/section_of() over survey_binary's segment map (fetched once,
cached lazily).
Pilot checks for both. full suite 67/67.
| -rw-r--r-- | idatui/app.py | 13 | ||||
| -rw-r--r-- | idatui/domain.py | 34 | ||||
| -rw-r--r-- | tests/test_tui.py | 43 |
3 files changed, 89 insertions, 1 deletions
diff --git a/idatui/app.py b/idatui/app.py index cc4cca7..c1b98a5 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -1501,7 +1501,18 @@ class IdaTui(App): focus = subj_name if self._looks_like_symbol(subj_name) else None if focus is None and fn is not None and fn.addr == subj: focus = fn.name - items = [(x.frm, f"{x.frm:08X} {x.fn_name or '?':<24} [{x.type}]") for x in xr] + items = [] + for x in xr: + if x.fn_name: + # location = function + offset, so multiple sites in the same + # function are distinguishable (sub_2300+0x1c, not just sub_2300). + off = (x.frm - x.fn_addr) if x.fn_addr is not None else 0 + loc = f"{x.fn_name}+{off:#x}" if off else x.fn_name + else: + # not inside a function: name the section it lives in (a GOT/reloc + # data slot or a loose thunk) instead of a bare '?'. + loc = self.program.section_of(x.frm) or "<no seg>" + items.append((x.frm, f"{x.frm:08X} {loc:<26} [{x.type}]")) self.app.call_from_thread(self._present_xrefs, label, items, focus) def _present_xrefs(self, label: str, items: list[tuple[int, str]], diff --git a/idatui/domain.py b/idatui/domain.py index 4902a64..cdbdd67 100644 --- a/idatui/domain.py +++ b/idatui/domain.py @@ -376,6 +376,7 @@ class Program: self._disasm: dict[int, DisasmModel] = {} self._decomp: dict[int, tuple[Decompilation, int]] = {} self._name_gen = 0 # bumped on rename; invalidates stale name caches + self._sections: list[tuple[int, int, str]] | None = None self._lock = threading.Lock() # -- prefetch plumbing ------------------------------------------------- # @@ -397,6 +398,39 @@ class Program: self._indices[filter] = idx return idx + # -- sections / segments ---------------------------------------------- # + def sections(self) -> list[tuple[int, int, str]]: + """Sorted, non-overlapping [(start, end, name)] segment map (cached). + Fetched once from ``survey_binary`` (~0.7s) on first use.""" + if self._sections is not None: + return self._sections + secs: list[tuple[int, int, str]] = [] + try: + sb = self.client.call("survey_binary") + for s in (sb.get("segments", []) if isinstance(sb, dict) else []): + try: + secs.append((_as_int(s["start"]), _as_int(s["end"]), + s.get("name", ""))) + except (KeyError, ValueError, TypeError): + continue + except Exception: # noqa: BLE001 -- best-effort; callers handle None + secs = [] + secs.sort() + with self._lock: + self._sections = secs + return secs + + def section_of(self, ea: int) -> str | None: + """Name of the segment/section containing ``ea`` (e.g. '.got', '.text', + '.data.rel.ro', 'LOAD'), or None if unmapped.""" + secs = self.sections() + if not secs: + return None + i = bisect.bisect_right([s[0] for s in secs], ea) - 1 + if 0 <= i < len(secs) and secs[i][0] <= ea < secs[i][1]: + return secs[i][2] + return None + # -- disassembly ------------------------------------------------------- # def disasm(self, ea: int, name: str | None = None) -> DisasmModel: with self._lock: diff --git a/tests/test_tui.py b/tests/test_tui.py index 60ba2c5..567c302 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -453,6 +453,49 @@ async def run(db): check("follows the symbol under the cursor", app._cur.ea == want, f"cur={app._cur.ea:#x} want={want:#x}") + # Xref labels: function + offset (so several sites in one function are + # distinguishable) and a section name instead of a bare '?' for a + # reference that isn't inside a function. + from collections import Counter, defaultdict + multi = None + for cand in app.program.functions().all_loaded()[:200]: + callers = Counter(x.fn_addr for x in app.program.xrefs_to(cand.addr) + if x.fn_name) + if any(n >= 2 for n in callers.values()): + multi = cand + break + if multi is not None: + table.focus() + await pilot.press("g") + await pilot.pause(0.1) + for ch in multi.name: + await pilot.press(ch) + await pilot.press("enter") + await wait_until(pilot, lambda: app._cur and app._cur.ea == multi.addr, 20) + if app._active != "disasm": + await pilot.press("tab") + dis.focus() + dis.cursor, dis.cursor_x = 0, 0 + dis.refresh() + await pilot.press("x") + await wait_until(pilot, lambda: isinstance(app.screen, XrefsScreen), 25) + labels = [t for _, t in app.screen._items] + # location column = the text between the leading addr and the [type] + locs = [l.split("[")[0].split(None, 1)[1].strip() for l in labels] + byfn = defaultdict(set) + for x in locs: + if "+0x" in x: + nm, off = x.split("+0x", 1) + byfn[nm].add(off) + check("xref labels distinguish multiple sites in a function by offset", + any(len(offs) >= 2 for offs in byfn.values()), f"locs={locs[:8]}") + check("no xref label is a bare '?'", + all(x != "?" for x in locs), f"locs={locs[:8]}") + await pilot.press("escape") + await wait_until(pilot, lambda: not isinstance(app.screen, XrefsScreen), 25) + else: + check("found a function with multiple same-caller xrefs", False) + # Mouse: single click places the cursor; double-click follows. table.focus() table.move_cursor(row=biggest_i) |
