From 1e84c0e79ed76b19923caab250189c17464a5287 Mon Sep 17 00:00:00 2001 From: blasty Date: Mon, 27 Jul 2026 21:50:07 +0200 Subject: trace: paint the execution trail — including on the decompiler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both code views now show where you came from and where you're going: the instruction you're on ('now'), the ~96 steps behind it ('past', warm) and the ~96 ahead ('future', cool). A trail, not all of history. Painting every address the trace ever touched says almost nothing on a loop-heavy program; the last and next few dozen steps say how you GOT here. Where an address appears on both sides — a loop body, which is most of them — the nearer side wins, because that's the one explaining the step you just took or are about to. **The pseudocode is painted too**, which is the reason to build this here rather than use Tenet. A trace records instructions, so that's what Tenet paints. We already have decomp_map from the split-view work, saying which instructions each C line covers, so the same trail lands on the decompilation: line 46 now | v3 = getenv("POSIXLY_CORRECT"); line 47 future | v4 = (__int64)*a2; line 49 future | if ( v3 ) A C line covers many instructions, so it takes the strongest kind present: now beats past beats future — if the instruction you're standing on belongs to this line, this line is where you are. Two things kept cheap: the trail is recomputed per SEEK rather than per repaint (~200 lookups, and repaints vastly outnumber steps), and decomp_map is cached per function because it's an RPC and stepping is interactive. The colours sit deliberately under the code palette — the trail says "you came through here", the text still has to read as code. tests: +8 UI (21) — the listing carries now/past/future and it reaches the screen; pseudocode is painted; exactly ONE C line is 'now' and it is the line covering the current instruction (not merely some executed line, which is the mistake this check exists to catch). 209/0 scenarios, 27/0 model, 10/0 diff. --- tests/test_trace_ui.py | 56 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/test_trace_ui.py b/tests/test_trace_ui.py index a30362f..a85c089 100644 --- a/tests/test_trace_ui.py +++ b/tests/test_trace_ui.py @@ -16,7 +16,8 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from textual.widgets import Static # noqa: E402 -from idatui.app import IdaTui, ListingView, TraceDock # noqa: E402 +from idatui.app import (DecompView, IdaTui, ListingView, # noqa: E402 + TraceDock) REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) TRACER = os.path.expanduser( @@ -140,6 +141,59 @@ async def run() -> int: app._t == ret, f"{i} -> {app._t}, expected {ret}") check("which is further than a plain step", app._t > i + 1) + # -- trails ------------------------------------------------------ # + # Not "every address the trace ever touched": on a loop-heavy + # program that's almost everything and says nothing. The last/next + # few dozen steps say how you got here and where you're going. + app._seek(min(40, t.length - 1)) + await pilot.pause(0.6) + trail = lst.trail + kinds = {k for k in trail.values()} + check("the listing is painted with an execution trail", + {"now", "past", "future"} <= kinds, f"{sorted(kinds)}") + check("'now' is the instruction we're standing on", + trail.get(t.ip(app._t)) == "now", f"{trail.get(t.ip(app._t))}") + check("the step behind is past, the step ahead is future", + trail.get(t.ip(app._t - 1)) == "past" + and trail.get(t.ip(app._t + 1)) == "future", + f"{trail.get(t.ip(app._t - 1))}, {trail.get(t.ip(app._t + 1))}") + painted = [y for y in range(min(lst.size.height, 30)) + if any(seg.style and seg.style.bgcolor + for seg in lst.render_line(y))] + check("and it actually reaches the screen", painted, "no tinted rows") + + # -- the same trail on PSEUDOCODE -------------------------------- # + # The point of doing this in our app rather than using Tenet: a + # trace's addresses are instructions, but decomp_map (built for the + # split view) says which instructions each pseudocode line covers, + # so the trail lands on C. + main_ea = app.program.resolve("main") + first = t.first_execution(main_ea) + if first is None: + check("main was executed in the trace", False) + else: + app._seek(first + 12) + await wait(lambda: app._t == first + 12, pilot, 20) + lst.focus() + await pilot.press("tab") + got = await wait(lambda: app.query_one(DecompView).display + and app.query_one(DecompView)._texts, pilot, 120) + dec = app.query_one(DecompView) + check("pseudocode is available for the traced function", got) + app._seek(first + 12) + await pilot.pause(1.0) + check("pseudocode lines are painted with the trail", + len(dec.trail) > 2, f"{len(dec.trail)} lines") + now = [i for i, k in dec.trail.items() if k == "now"] + check("exactly one pseudocode line is 'now'", + len(now) == 1, f"{now}") + # The 'now' line must be the one covering the current + # instruction, not merely some executed line. + covered = app._trail_map[now[0]] if now and app._trail_map else [] + check("and it's the line covering the current instruction", + t.ip(app._t) in covered, + f"pc={t.ip(app._t):#x} line covers {[hex(a) for a in covered][:4]}") + print(f"\n{PASS} passed, {FAIL} failed") return 1 if FAIL else 0 -- cgit v1.3.1-sl0p