diff options
| -rw-r--r-- | TODO | 44 | ||||
| -rw-r--r-- | idatui/app.py | 26 | ||||
| -rw-r--r-- | tests/test_trace_ui.py | 21 |
3 files changed, 63 insertions, 28 deletions
@@ -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 |
