aboutsummaryrefslogtreecommitdiffstats
path: root/idatui
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-26 14:51:07 +0200
committerblasty <blasty@local>2026-07-26 14:51:07 +0200
commit7bdf3741c91f76bfff3f3e054a5cf41b7f476d0b (patch)
tree26404f9b55551980505dbc94f9757f7ee7c187e7 /idatui
parentTODO: DisasmView removed (diff)
downloadida-tui-7bdf3741c91f76bfff3f3e054a5cf41b7f476d0b.tar.gz
ida-tui-7bdf3741c91f76bfff3f3e054a5cf41b7f476d0b.tar.xz
ida-tui-7bdf3741c91f76bfff3f3e054a5cf41b7f476d0b.zip
arm: switch ARM/Thumb decoding with `t`
`c` could not carve Thumb code. Thumb isn't a property of the bytes — it's a mode the CPU is in — so a raw image gives IDA nothing to detect: at a Thumb entry point it decodes 16-bit instructions as 32-bit ARM and produces confident nonsense. experiments/fibonacci.bin starts with `08 b5` = push {r3,lr}, which IDA reads as SVCLT 0xBF00. `t` on the listing switches the mode at the cursor and disassembles in it: Thumb @ 0x0 (segment set to 32-bit; Thumb needs ARM32) — 10 instructions 0x0 PUSH {R3,LR} 0x2 MOVS R1, #0 0x4 MOV R4, R0 0x6 BL unk_E3C Setting the T segment register is only half of it. Thumb does not exist in AArch64, and a headerless blob loaded with -parm comes up 64-bit, so T alone changes nothing and looks broken — I watched exactly that happen while probing the API. Asking for Thumb IS asking for ARM32, so set_thumb forces the segment to 32-bit and says so rather than doing it silently. It also has to del_items over the range first: the bytes are currently decoded in the old mode, and leaving that item defined pins the wrong instruction length so the new mode has nothing to apply to. Implemented as a `thumb` kind in the existing edit-item flow, so it inherits the shared reload — same cache bump, same ViewAnchor restore, same status flash. It switches AND disassembles, because flipping T and leaving the bytes undefined shows you nothing and reading the code was the point. tests: new tests/test_thumb_ui.py (8) driving the real Thumb binary — `c` alone does NOT produce the prologue, `t` does, the instructions are 16-bit wide (in ARM mode those three rows would be one 4-byte instruction), the run continues, the status explains the 32-bit forcing, and `t` toggles back. Deletes the .i64 first, because T and the segment's addressing mode are saved in it and a stale database would answer the question for us. 209/0 scenarios, 26/0 blob, 8/0 thumb.
Diffstat (limited to 'idatui')
-rw-r--r--idatui/app.py23
-rw-r--r--idatui/domain.py8
2 files changed, 30 insertions, 1 deletions
diff --git a/idatui/app.py b/idatui/app.py
index 2190643..ff67eef 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -738,6 +738,7 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru
Binding("a", "make_string", "Str", show=False),
Binding("p", "define_func", "Func", show=False),
Binding("u", "undefine", "Undef", show=False),
+ Binding("t", "toggle_thumb", "ARM/Thumb", show=False),
*SearchMixin.SEARCH_BINDINGS,
*NavMixin.NAV_BINDINGS,
*ColumnCursor.COL_BINDINGS,
@@ -1126,6 +1127,9 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru
def action_undefine(self) -> None:
self.post_message(EditItemRequested(self, "undef"))
+ def action_toggle_thumb(self) -> None:
+ self.post_message(EditItemRequested(self, "thumb"))
+
def action_make_data(self) -> None:
self.post_message(MakeDataRequested(self))
@@ -5150,7 +5154,8 @@ class IdaTui(App):
anchor: ViewAnchor | None = None) -> None: # worker context
assert self.program is not None
verb = {"code": "defined code", "func": "created function",
- "undef": "undefined", "string": "made string"}[kind]
+ "undef": "undefined", "string": "made string",
+ "thumb": "switched decoding"}[kind]
try:
if kind == "code":
# Keep going until something stops it: one instruction is rarely
@@ -5175,6 +5180,22 @@ class IdaTui(App):
"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 == "thumb":
+ # Switch the mode, then disassemble in it: flipping T and
+ # leaving the bytes undefined shows nothing, and the reason you
+ # flipped it was to read the code.
+ r = self.program.set_thumb(ea)
+ run = self.program.define_code_run(ea)
+ n = int(run.get("count", 0))
+ mode = "Thumb" if r.get("thumb") else "ARM"
+ verb = f"{mode} @ {ea:#x}"
+ if r.get("forced_32bit"):
+ verb += " (segment set to 32-bit; Thumb needs ARM32)"
+ verb += (f" \u2014 {n} instruction{'s' if n != 1 else ''}"
+ if n else " \u2014 still doesn't decode")
+ # falls through to the shared reload: same cache bump, same
+ # anchor restore, same flash. That is the whole point of having
+ # one path.
elif kind == "func":
self.program.define_func(ea)
elif kind == "string":
diff --git a/idatui/domain.py b/idatui/domain.py
index 606cf42..8787431 100644
--- a/idatui/domain.py
+++ b/idatui/domain.py
@@ -1373,6 +1373,14 @@ class Program:
if res.get("error"):
raise IDAToolError("define_code", f"@ {ea:#x}: {res['error']}")
+ def set_thumb(self, ea: int, mode: str = "toggle") -> dict:
+ """Switch ARM/Thumb decoding at ``ea``. Returns the resulting state."""
+ r = self.client.call("set_thumb", addr=hex(ea), mode=mode)
+ if not isinstance(r, dict) or r.get("error"):
+ raise IDAToolError("set_thumb",
+ f"@ {ea:#x}: {(r or {}).get('error', 'failed')}")
+ return r
+
def define_code_run(self, ea: int, limit: int = 20000) -> dict:
"""Disassemble consecutively from ``ea`` until something stops it.