aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/app.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-28 15:16:34 +0200
committerblasty <blasty@local>2026-07-28 15:16:34 +0200
commitd1deca0826a9e04da195ba3eeb344cd18d38ea17 (patch)
tree7e2d6a8504b1b138081a99924d45b80e730007d8 /idatui/app.py
parenttrace: a step in split view moves the listing cursor to the pc (diff)
downloadida-tui-d1deca0826a9e04da195ba3eeb344cd18d38ea17.tar.gz
ida-tui-d1deca0826a9e04da195ba3eeb344cd18d38ea17.tar.xz
ida-tui-d1deca0826a9e04da195ba3eeb344cd18d38ea17.zip
trace: stop the decompiler thrashing during a step (and correct the record)
I blamed decomp_map in the last commit. It was innocent: called directly it returns 769 lines, 475 with addresses, for exactly the function I said it returned four for. The four-line map belonged to a PLT stub the decompiler had momentarily switched to, and I sampled mid-bounce. The actual fault: _seek_split decided "has execution left the decompiled function?" from _split_range, which is maintained by a guarded async path (_apply_split_map drops its result if _cur moved while in flight) and therefore lags during stepping. A stale range made every step look like a function change, so the decompiler bounced main -> stub -> main, each bounce paying a synchronous 769-line map fetch on the UI thread. Now the decision comes from the map the trail painting already holds, keyed to what the decompiler currently HAS loaded. The bouncing is gone — three map fetches across twelve steps instead of one per step — and the pseudocode cursor follows every instruction the decompiler attributes to a line, including across a call into another function. What it does NOT do: guess. Roughly half of a function's instructions have no line attributed, and the obvious fallback (nearest mapped address at or before the pc) is unsound — C lines are not monotonic in address, and it put an instruction early in main on line 708, "sub_2040();", near the end. The cursor waits instead; the trail still marks where you are. tests: +1 trace UI (26) — over ~28 steps, every instruction that IS mapped is followed by the pseudocode cursor. 212/0 scenarios. TODO corrected: the entry blaming decomp_map now says what actually happened, including that _split_ea2line/_split_range are still fed by the laggy path and remain a latent issue for the split view's own sync.
Diffstat (limited to 'idatui/app.py')
-rw-r--r--idatui/app.py26
1 files changed, 22 insertions, 4 deletions
diff --git a/idatui/app.py b/idatui/app.py
index 76d5441..d44cc20 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -3249,6 +3249,8 @@ class IdaTui(App):
self._trail_map_ea = None
self._pending_trace_line = None # step waiting on a re-decompile
self._trail_line_of: dict[int, int] = {} # ea -> pseudocode line
+ self._trail_span = None # ea span of that function
+ self._trail_eas: list[int] = [] # sorted keys of _trail_line_of
self._t = 0 # current timestamp in that trace
self._do_keepalive = keepalive
self._rpc_path = rpc_path
@@ -5529,10 +5531,16 @@ class IdaTui(App):
lst.cursor = row
lst._scroll_cursor_into_view()
- rng = self._split_range
- if rng is None or not (rng[0] <= pc <= rng[1]):
- # Execution left the decompiled function. Load the new one, and
- # remember where to land once its line map exists.
+ # Has execution actually left the decompiled function? Ask the map the
+ # trail painting keeps, which is keyed to what the decompiler currently
+ # HOLDS. _split_range comes from the guarded async path and lags, so a
+ # stale one made every step look like a function change: the decompiler
+ # bounced main -> PLT stub -> main, each bounce costing a synchronous
+ # 769-line map fetch on the UI thread.
+ span = self._trail_span
+ inside = (pc in self._trail_line_of
+ or (span is not None and span[0] <= pc <= span[1]))
+ if not inside:
self._pending_trace_line = pc
self._resync_decomp_async(pc)
return True
@@ -5552,6 +5560,13 @@ class IdaTui(App):
dec = self.query_one(DecompView)
line = None
if self._trail_map_ea == dec.loaded_ea and self._trail_line_of:
+ # EXACT match only. The decompiler doesn't attribute every
+ # instruction to a line (about half of main's aren't), and the
+ # tempting fallback — the nearest mapped instruction at or before
+ # the pc — is unsound: C lines are not monotonic in address, so
+ # 0x24a8 early in main resolved to line 708, "sub_2040();", near the
+ # end. A cursor that jumps to an unrelated statement is worse than
+ # one that waits; the trail still marks where we are.
line = self._trail_line_of.get(pc)
if line is None:
line = self._split_ea2line.get(pc)
@@ -5616,6 +5631,9 @@ class IdaTui(App):
for i, eas in enumerate(self._trail_map or []):
for a in eas:
self._trail_line_of.setdefault(a, i)
+ self._trail_eas = sorted(self._trail_line_of)
+ self._trail_span = ((self._trail_eas[0], self._trail_eas[-1])
+ if self._trail_eas else None)
rank = {"future": 0, "past": 1, "now": 2}
lines: dict[int, str] = {}
for i, eas in enumerate(self._trail_map or []):