diff options
| author | blasty <blasty@local> | 2026-07-09 21:05:07 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-09 21:05:07 +0200 |
| commit | 143ec78053313c95770b02f385ec0c3ee5cf48ac (patch) | |
| tree | 3b0afb5127b27b89db226152f0a84cb3888c08a8 | |
| parent | comments: ';' sets a comment on the current line (disasm + pseudocode) (diff) | |
| download | ida-tui-143ec78053313c95770b02f385ec0c3ee5cf48ac.tar.gz ida-tui-143ec78053313c95770b02f385ec0c3ee5cf48ac.tar.xz ida-tui-143ec78053313c95770b02f385ec0c3ee5cf48ac.zip | |
decompiler: hide the per-line /*0xEA*/ address markers (clutter)
Hex-Rays appends a /*0xEA*/ VMA marker to every pseudocode line (we fetch with
include_addresses). It's the only per-line address anchor the app has, so we
can't turn it off server-side without breaking follow/comments/xref-jumps.
Instead, strip it at display time: DecompView.show pulls each line's marker into
a parallel _line_eas list and highlights the cleaned text. _line_ea now reads
that list; _follow_decomp takes the line ea explicitly (was re-parsing the
marker). Stripping only edits within lines, so indices still align with the
domain's raw code (used by _decomp_line_for). Var-location comments (// [rsp+..])
are Hex-Rays output, not VMAs -> kept.
full suite 63/63.
| -rw-r--r-- | idatui/app.py | 45 | ||||
| -rw-r--r-- | tests/test_tui.py | 11 |
2 files changed, 37 insertions, 19 deletions
diff --git a/idatui/app.py b/idatui/app.py index 5e4061c..0e68f47 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -55,6 +55,12 @@ _S_CELL = Style(reverse=True) # the block cursor cell _S_LINENO = Style(color="grey37") # pseudocode line-number gutter _S_LINENO_CUR = Style(color="grey66", bold=True) # gutter on the cursor line +# Hex-Rays appends a `/*0xEA*/` address marker to each pseudocode line (we fetch +# with include_addresses so we have a per-line anchor). Matched here to extract +# the address and strip the marker from the display. +_ADDR_MARK_RE = re.compile(r"/\*\s*0x([0-9A-Fa-f]+)\s*\*/") +_ADDR_MARK_STRIP_RE = re.compile(r"\s*/\*\s*0x[0-9A-Fa-f]+\s*\*/") + @dataclass class NavEntry: @@ -818,6 +824,7 @@ class DecompView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=True self._strips: list[Strip] = [] self._texts: list[str] = [] self._gutter = 0 # line-number gutter width (cells) + self._line_eas: list[int | None] = [] # per-line address (marker stripped) self._term = "" self._matches: list[int] = [] self._ranges: dict[int, list[tuple[int, int]]] = {} @@ -838,11 +845,22 @@ class DecompView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=True def show(self, ea: int, text: str, cursor: int = 0, cursor_x: int = 0, scroll_y: int = -1, scroll_x: int = 0) -> None: - seglists = highlight_c(text) + # Pull each line's `/*0xEA*/` marker into _line_eas, then strip it from + # the displayed text (clutter) before highlighting. Stripping only edits + # within lines, so line indices still align with the domain's raw code. + line_eas: list[int | None] = [] + clean: list[str] = [] + for ln in text.split("\n"): + eas = _ADDR_MARK_RE.findall(ln) + line_eas.append(int(eas[-1], 16) if eas else None) + clean.append(_ADDR_MARK_STRIP_RE.sub("", ln).rstrip()) + seglists = highlight_c("\n".join(clean)) self._strips = [Strip(segs) for segs in seglists] self._texts = ["".join(seg.text for seg in segs) for segs in seglists] - self.loaded_ea = ea total = len(self._strips) + # highlight_c may drop a lone trailing blank line; keep _line_eas aligned. + self._line_eas = (line_eas[:total] + [None] * total)[:total] + self.loaded_ea = ea self.cursor = max(0, min(max(total - 1, 0), cursor)) self.cursor_x = cursor_x self._matches = [] @@ -865,10 +883,7 @@ class DecompView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=True return Static("― decompiling… ―", classes="decomp-loading") def _line_ea(self, idx: int) -> int | None: - if not (0 <= idx < len(self._texts)): - return None - ms = re.findall(r"/\*\s*0x([0-9A-Fa-f]+)\s*\*/", self._texts[idx]) - return int(ms[-1], 16) if ms else None + return self._line_eas[idx] if 0 <= idx < len(self._line_eas) else None def _after_cursor_move(self) -> None: self.post_message(DecompView.CursorMoved(self.cursor, self._line_ea(self.cursor))) @@ -1316,7 +1331,8 @@ class IdaTui(App): if ea is not None: self._follow_disasm(ea, word, nxt.ea if nxt else None) elif isinstance(view, DecompView) and view._texts: - self._follow_decomp(view._texts[view.cursor], word) + self._follow_decomp(view._texts[view.cursor], word, + view._line_ea(view.cursor)) def on_xrefs_requested(self, msg: XrefsRequested) -> None: view = msg.view @@ -1360,7 +1376,8 @@ class IdaTui(App): self._do_navigate(tgt.to, push=True) @work(thread=True, group="nav") - def _follow_decomp(self, line: str, word: str | None) -> None: + def _follow_decomp(self, line: str, word: str | None, + line_ea: int | None = None) -> None: if self._cur is None: return dec = self.program.decompile(self._cur.ea) @@ -1376,14 +1393,12 @@ class IdaTui(App): addr = None if addr is None: addr = self._ref_on_line(line) - if addr is None: - # Address-based fallback: use the line's /*0xEA*/ marker and follow a - # code xref from that statement. Immune to a stale name (e.g. right + if addr is None and line_ea is not None: + # Address-based fallback: follow a code xref from this statement's ea + # (the stripped /*0xEA*/ anchor). Immune to a stale name (e.g. right # after a rename, before the pseudocode text catches up). - m = re.search(r"/\*\s*0x([0-9A-Fa-f]+)\s*\*/", line or "") - if m: - xr = self.program.xrefs_from(int(m.group(1), 16)) - addr = next((x.to for x in xr if x.type == "code" and x.to), None) + xr = self.program.xrefs_from(line_ea) + addr = next((x.to for x in xr if x.type == "code" and x.to), None) if addr is None: self.app.call_from_thread(self._status, "nothing to follow on this line") return diff --git a/tests/test_tui.py b/tests/test_tui.py index b8eba79..2f80270 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -517,13 +517,15 @@ async def run(db): # fallback follows by address regardless of the (old) token. dstale = app.program.resolve(dsym) old_line = dec._texts[drow] if drow < len(dec._texts) else "" - if "/*0x" in old_line and dsym in old_line: + old_ea = dec._line_ea(drow) + if old_ea is not None and dsym in old_line: tmpname = f"stale_{os.getpid()}" app.program.client.call( "rename", batch={"func": {"addr": hex(dstale), "name": tmpname}}) app.program.bump_names() d2 = len(app._nav) - app._follow_decomp(old_line, dsym) # dsym is now the OLD name + # dsym is now the OLD name; follow must fall back to the line's ea + app._follow_decomp(old_line, dsym, old_ea) await wait_until(pilot, lambda: len(app._nav) > d2, timeout=25) check("decomp follow works with a stale name (ea-marker fallback)", app._cur.ea == dstale, f"cur={app._cur.ea:#x} want={dstale:#x}") @@ -631,8 +633,9 @@ async def run(db): # Add a comment on the current pseudocode line via ';' and confirm it # renders after the decompiler refreshes. - cline = next((i for i, t in enumerate(dec._texts) - if i > 5 and "/*0x" in t and t.strip() and "//" not in t), None) + cline = next((i for i in range(len(dec._texts)) + if i > 5 and dec._line_ea(i) is not None + and dec._texts[i].strip() and "//" not in dec._texts[i]), None) if cline is not None: cea = dec._line_ea(cline) dec.focus() |
