aboutsummaryrefslogtreecommitdiffstats
path: root/server/patch_server.py
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 /server/patch_server.py
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 'server/patch_server.py')
-rw-r--r--server/patch_server.py57
1 files changed, 57 insertions, 0 deletions
diff --git a/server/patch_server.py b/server/patch_server.py
index 6601a0f..e40310a 100644
--- a/server/patch_server.py
+++ b/server/patch_server.py
@@ -967,6 +967,63 @@ def define_code_run(
return {"start": hex(start), "end": hex(ea), "count": count,
"stopped": stopped}
+
+@tool
+@idasync
+def set_thumb(
+ addr: Annotated[str, "Address to change the ARM decoding mode at"],
+ mode: Annotated[str, "'toggle', 'on' (Thumb) or 'off' (ARM)"] = "toggle",
+ end: Annotated[str, "Optional exclusive end address (default: this item)"] = "",
+) -> dict:
+ """Switch ARM/Thumb decoding at ``addr`` (IDA's T segment register).
+
+ Thumb is not a property of the bytes, it's a mode the CPU is in, so a raw
+ image gives IDA no way to know: at a Thumb entry point it decodes 16-bit
+ instructions as 32-bit ARM and produces confident nonsense
+ (``push {r3,lr}`` reads as ``SVCLT 0xBF00``).
+
+ Also forces the segment to 32-bit when turning Thumb ON. Thumb does not
+ exist in AArch64, and a headerless blob loaded with -parm defaults to
+ 64-bit — so setting T alone changes nothing and looks broken. Asking for
+ Thumb IS asking for ARM32."""
+ import ida_bytes
+ import ida_idp
+ import ida_segment
+ import ida_segregs
+
+ try:
+ ea = parse_address(addr)
+ except Exception as e:
+ return {"addr": str(addr), "error": str(e)}
+ treg = ida_idp.str2reg("T")
+ if treg is None or treg < 0:
+ return {"addr": hex(ea), "error": "no T register (not an ARM database)"}
+ seg = ida_segment.getseg(ea)
+ if not seg:
+ return {"addr": hex(ea), "error": "no segment"}
+
+ cur = ida_segregs.get_sreg(ea, treg)
+ cur = 0 if cur in (None, 0xFFFFFFFF, -1) else int(cur)
+ want = {"on": 1, "off": 0}.get(str(mode).lower(), 0 if cur else 1)
+
+ changed_bits = False
+ if want and seg.bitness != 1:
+ ida_segment.set_segm_addressing(seg, 1)
+ changed_bits = True
+
+ try:
+ stop = parse_address(end) if end else 0
+ except Exception:
+ stop = 0
+ size = max(int(stop) - ea, 0) or max(ida_bytes.get_item_size(ea), 2)
+ # The bytes are currently decoded in the OLD mode; leaving that item defined
+ # pins the wrong instruction length and the new mode has nothing to apply to.
+ ida_bytes.del_items(ea, 0, size)
+ ok = bool(ida_segregs.split_sreg_range(ea, treg, want, ida_segregs.SR_user))
+ now = ida_segregs.get_sreg(ea, treg)
+ return {"addr": hex(ea), "thumb": bool(now), "was": bool(cur), "ok": ok,
+ "bitness": ida_segment.getseg(ea).bitness,
+ "forced_32bit": changed_bits}
'''
SNIPPET = f"{BEGIN}\n{BODY.strip()}\n{END}\n"