aboutsummaryrefslogtreecommitdiffstats
path: root/idatui
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-26 09:30:12 +0200
committerblasty <blasty@local>2026-07-26 09:30:12 +0200
commit4e65c3ba16b13c3bb042830b020788a745cee9a9 (patch)
tree791b3a94aec4e57801377943088a1dc0870704b4 /idatui
parentlisting: make every undefined byte its own row, so you can carve anywhere (diff)
downloadida-tui-4e65c3ba16b13c3bb042830b020788a745cee9a9.tar.gz
ida-tui-4e65c3ba16b13c3bb042830b020788a745cee9a9.tar.xz
ida-tui-4e65c3ba16b13c3bb042830b020788a745cee9a9.zip
listing: `c` disassembles until something stops it
One instruction per keypress means pressing `c` once per opcode for the length of a routine, which on a raw image is the whole job. IDA's `c` runs; ours now does too. New define_code_run tool: create instructions consecutively and report why it stopped — 'undecodable' (bytes aren't an instruction), 'flow' (control flow ends here), 'defined' (ran into existing code/data), 'segment' or 'limit'. It loops inside the worker; from the client this would be one round trip per instruction, minutes on a real image. Stops AT a ret rather than past it: beyond the end of a routine the bytes are usually padding or data, and running on turns a clean carve into something you have to undo by hand. Stopping at already-defined items is the same principle — undefining someone's existing work to keep a speculative run going isn't a trade the user asked for. The ret test is ida_idp.is_ret_insn, NOT canonical features: on AArch64 insn.get_canon_feature() returns 0 for RET, so a CF_STOP check silently never fires and the run walks straight through the end of the function. Verified against a live IDA before relying on it. `c` on something already defined now says "already defined @ addr" instead of claiming the instruction failed to be created — count==0 from a run means two very different things. Also: the result message survives the reload. Defining an item rebuilds the view, and the reload's own cursor handler had the last word, so every edit reported itself as "ROM @ 0x4040 [listing]". A one-shot _flash is handed to whichever status write lands first after the edit. (Third time this clobber pattern has turned up: split view, the no-functions hint, now this.) Verified: nop/nop/nop/ret at 0x4040 -> "defined 4 instructions (0x4040–0x4050) — control flow ends here", with 0x4050 left as an undefined byte. Starting on existing code -> no-op. Random bytes -> stops at the first that won't decode. tests: +4 blob UI (runs to the end of flow, stops at the ret, doesn't touch the junk after it, and the status reports it). 22/0 blob, 202/0 scenarios, 30/0 project UI.
Diffstat (limited to 'idatui')
-rw-r--r--idatui/app.py42
-rw-r--r--idatui/domain.py16
2 files changed, 56 insertions, 2 deletions
diff --git a/idatui/app.py b/idatui/app.py
index 4cf0ee0..90678d3 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -3338,6 +3338,7 @@ class IdaTui(App):
self._hops: list[str] = [] # binaries a navigation crossed FROM
self._load_for_label = None # project binary the dialog is for
self._no_functions = False # analysis produced nothing at all
+ self._flash: str | None = None # message a pending reload must keep
self._pending_switch = None # switch waiting on that answer
self._nav_seq = 0 # bumped per navigation; drops stale ones
# None = teardown wasn't an explicit quit (crash/kill): save defensively.
@@ -5407,7 +5408,28 @@ class IdaTui(App):
"undef": "undefined", "string": "made string"}[kind]
try:
if kind == "code":
- self.program.define_code(ea)
+ # Keep going until something stops it: one instruction is rarely
+ # what you want, and on a raw image it means pressing `c` once
+ # per opcode for the length of a function.
+ r = self.program.define_code_run(ea)
+ n, why = int(r.get("count", 0)), r.get("stopped", "")
+ if n == 0 and why == "defined":
+ # Already code/data here — a no-op, not a failure. Saying
+ # "failed to create instruction" for it would be a lie.
+ self.app.call_from_thread(
+ self._status, f"already defined @ {ea:#x}")
+ return
+ if n == 0:
+ raise IDAToolError("define_code",
+ f"@ {ea:#x}: Failed to create instruction")
+ end = int(str(r.get("end", hex(ea))), 0)
+ reason = {"undecodable": "hit bytes that don't decode",
+ "flow": "control flow ends here",
+ "defined": "ran into existing code/data",
+ "segment": "end of segment",
+ "limit": "instruction limit"}.get(why, why)
+ verb = (f"defined {n} instruction{'s' if n != 1 else ''} "
+ f"({ea:#x}\u2013{end:#x}) \u2014 {reason}")
elif kind == "func":
self.program.define_func(ea)
elif kind == "string":
@@ -5438,7 +5460,12 @@ class IdaTui(App):
def _edit_item_done(self, verb: str, ea: int) -> None:
self._dirty = True
- self._status(f"{verb} @ {ea:#x} (Ctrl+S to save)")
+ # Defining an item reloads the view, and that reload writes its own
+ # status when it lands — after this one. Hand the message over as a
+ # flash so the result of the edit is what you actually read, instead of
+ # "ROM @ 0x4040 [listing]" every time.
+ self._flash = f"{verb} @ {ea:#x} (Ctrl+S to save)"
+ self._status(self._flash)
@work(thread=True, exclusive=True, group="save")
def _save(self) -> None:
@@ -6038,6 +6065,10 @@ class IdaTui(App):
self._goto_ea(msg.va, push=True)
def _status_for_cur(self, mode: str) -> None:
+ flash, self._flash = self._flash, None
+ if flash:
+ self._status(flash) # the edit that caused this reload wins
+ return
if self._cur is not None:
self._status(f"{self._cur.name} @ {self._cur.ea:#x} [{mode}]")
@@ -6274,6 +6305,13 @@ class IdaTui(App):
return
ea = msg.ea
if ea is not None:
+ # A pending flash (the result of an edit that caused this reload)
+ # outranks the idle hint: the cursor lands here as part of the
+ # reload, so this handler would otherwise always have the last word.
+ flash, self._flash = self._flash, None
+ if flash:
+ self._status(flash)
+ return
sec = self.program.section_of(ea) if self.program else None
self._status(f"{sec or '?'} @ {ea:#x} [listing] "
"(c code · p func · u undefine · Enter follow)")
diff --git a/idatui/domain.py b/idatui/domain.py
index 846af89..e4fbec9 100644
--- a/idatui/domain.py
+++ b/idatui/domain.py
@@ -1366,6 +1366,22 @@ class Program:
if res.get("error"):
raise IDAToolError("define_code", f"@ {ea:#x}: {res['error']}")
+ def define_code_run(self, ea: int, limit: int = 20000) -> dict:
+ """Disassemble consecutively from ``ea`` until something stops it.
+
+ Falls back to a single instruction when the worker predates the tool, so
+ an old worker degrades to the previous behaviour instead of failing.
+ """
+ try:
+ r = self.client.call("define_code_run", addr=hex(ea), limit=int(limit))
+ except IDAToolError:
+ self.define_code(ea)
+ return {"count": 1, "stopped": "single", "end": hex(ea)}
+ if not isinstance(r, dict) or r.get("error"):
+ raise IDAToolError("define_code_run",
+ f"@ {ea:#x}: {(r or {}).get('error', 'failed')}")
+ return r
+
def define_func(self, ea: int) -> None:
"""Create a function starting at ``ea`` (IDA's 'p')."""
res = self._first_result(