aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-09 22:28:53 +0200
committerblasty <blasty@local>2026-07-09 22:28:53 +0200
commit6a761c222a6f08cb2fd825a15f3d3f44390a716d (patch)
tree8a0a6c3add4eab61555783dd407a7c2aa83070ec
parentdecompiler: xref jump lands the cursor on the reference token's column (diff)
downloadida-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.py13
-rw-r--r--idatui/domain.py34
-rw-r--r--tests/test_tui.py43
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)