aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--TODO44
-rw-r--r--idatui/app.py26
-rw-r--r--tests/test_trace_ui.py21
3 files changed, 63 insertions, 28 deletions
diff --git a/TODO b/TODO
index 679318c..5d037a7 100644
--- a/TODO
+++ b/TODO
@@ -89,31 +89,27 @@ can't be trusted to accuse the code. tests/test_blob_ui.py builds a throwaway
binary; test_project_ui.py stages copies; test_thumb_ui.py deletes the .i64
before each phase because the T flag and segment bitness are SAVED in it.
-## decomp_map returns almost nothing for some functions (blocks split stepping)
+## decomp_map was NOT the problem (corrected)
-Found while making a trace step move BOTH cursors in split view. For cat's
-main() — 700+ pseudocode lines — decomp_map() came back with FOUR entries, so
-almost no instruction address maps to a C line and the pseudocode cursor can't
-follow the program counter. On echo's main the same call maps plenty (the trail
-painting test relies on it), so it is function- or timing-dependent, not simply
-broken.
+I recorded here that decomp_map returned four entries for cat's main and blamed
+the tool. It doesn't: called directly it returns 769 lines, 475 with addresses,
+for exactly that function. The four-line map belonged to a PLT stub the
+decompiler had momentarily switched to, sampled mid-bounce.
-Two things to separate when picking this up:
+The real fault was the resync decision in _seek_split using _split_range, which
+is maintained by a guarded async path and lags. A stale range made every step
+look like a function change, so the decompiler thrashed
+(main -> stub -> main), each bounce paying a synchronous 769-line map fetch on
+the UI thread. Fixed by deciding from the map the trail painting already holds,
+which is keyed to what the decompiler currently HAS loaded.
-* Is the map itself sparse (the tool's item.dstr() walk missing most items for
- this function), or is it being FETCHED at a moment when the decompiler has a
- different function loaded? _trail_map_ea and dec.loaded_ea both read 0x24a0
- when it happened, which argues for the former.
-* dec.goto(96) left dec.cursor at 0 in the same run. That may be a symptom of
- the same confusion (a stale/short _strips) or an independent bug. Check it on
- its own before assuming.
+Still true and worth knowing: the decompiler attributes only about half of a
+function's instructions to a line, so the pseudocode cursor moves on those and
+waits on the rest. The tempting fallback — nearest mapped address at or before
+the pc — is UNSOUND: C lines are not monotonic in address, and it resolved an
+instruction early in main to a line near the end of the function.
-Also noticed: _split_ea2line is refreshed by a guarded async path
-(_apply_split_map drops its result if _cur moved while in flight). A burst of
-trace steps moves _cur constantly, so during stepping it is frequently the map
-of the function you just left. _place_decomp_at now prefers the trail's map for
-that reason, but the split view's own sync still uses the laggy one.
-
-Consequence today: in split view a trace step moves the LISTING cursor onto the
-current instruction (works, tested) but the pseudocode cursor only follows when
-the map happens to cover that address.
+Left alone: _split_ea2line and _split_range are still fed by that guarded path,
+so the split view's own sync can still work from the map of the function you
+just left. It has not caused a visible problem outside stepping, but it is the
+same latent issue.
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 []):
diff --git a/tests/test_trace_ui.py b/tests/test_trace_ui.py
index 7ffe20f..5d620a9 100644
--- a/tests/test_trace_ui.py
+++ b/tests/test_trace_ui.py
@@ -231,6 +231,27 @@ async def run() -> int:
lst.trail.get(t.ip(app._t)) == "now",
f"{lst.trail.get(t.ip(app._t))}")
+ # The pseudocode cursor follows too — but only for instructions
+ # the decompiler actually attributes to a line. About half
+ # aren't, and the tempting fallback (nearest mapped address at
+ # or before the pc) is unsound because C lines are not monotonic
+ # in address: an early instruction resolved to a line near the
+ # END of the function. Better to wait than to jump somewhere
+ # unrelated.
+ dec = app.query_one(DecompView)
+ mapped = missed = 0
+ for k in range(2, 30):
+ app._seek(base + k)
+ await pilot.pause(0.3)
+ pc = t.ip(app._t)
+ if app._trail_map_ea == dec.loaded_ea and pc in app._trail_line_of:
+ mapped += 1
+ if dec.cursor != app._trail_line_of[pc]:
+ missed += 1
+ check("the pseudocode cursor follows every mapped instruction",
+ mapped > 3 and missed == 0,
+ f"{mapped} mapped, {missed} not followed")
+
print(f"\n{PASS} passed, {FAIL} failed")
return 1 if FAIL else 0