aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_thumb_ui.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-26 15:04:51 +0200
committerblasty <blasty@local>2026-07-26 15:04:51 +0200
commit2c2124aff2f4ed6df23512603b6e1ecdcd4b699b (patch)
tree2b2c2afe69e7f33e87ccb58e3bc466a94a4e689e /tests/test_thumb_ui.py
parentarm: switch ARM/Thumb decoding with `t` (diff)
downloadida-tui-2c2124aff2f4ed6df23512603b6e1ecdcd4b699b.tar.gz
ida-tui-2c2124aff2f4ed6df23512603b6e1ecdcd4b699b.tar.xz
ida-tui-2c2124aff2f4ed6df23512603b6e1ecdcd4b699b.zip
arm: offer 32-bit ARM at load, and fix `p` on carved code
Reported as "after c a few times and p at the entry point, Tab just flashes and nothing decompiles". Three separate things, found by following it down: **1. `p` failed on hand-carved code.** ida_funcs.add_func(ea) asks IDA to find the function's end and on carved code it often can't — a run ending in a tail call, or whose last instruction isn't recognised as a return, fails with no reason given. add_func(ea, end) with an explicit end succeeds. define_func_run tries IDA's way first, then falls back to the end of the contiguous instruction run, and says which it used. **2. The database was 64-bit, so Hex-Rays refused it regardless.** Bare `-parm` gives an AArch64 database. Ask Hex-Rays for the failure object rather than reading None as "dunno" and it says exactly what's wrong: "only 64-bit functions can be decompiled in the current database". So the disassembly looked right and F5 could never work. That is decided at LOAD and cannot be corrected — inf_set_app_bitness(32) afterwards makes the decompiler INTERR 50735. The fix is at the load dialog: arm:ARMv7-A (most firmware), arm:ARMv7-M / arm:ARMv6-M (Cortex-M, Thumb only) and arm:ARMv5TE now sit alongside 64-bit `arm`, labelled with their bitness. With arm:ARMv7-A, experiments/fibonacci.bin decompiles: void __fastcall __noreturn sub_0(int a1) { int v2; v2 = sub_E3C(a1, 0); ... } — and IDA's own auto-analysis finds 54 Thumb functions on load, versus none as plain `arm`. **3. `t` was silently building an undecompilable state.** It forced the SEGMENT to 32-bit in a 64-bit database, which produces correct-looking disassembly that F5 will never touch. It now says so and names the fix (Ctrl+L, arm:ARMv7-A) rather than leaving you to discover it. tools/verify_procs.py now reports each processor's resulting bitness, since that is the reason the variants exist — and it compares against the base module name, because a variant reports "ARM". tests: test_thumb_ui.py +5 (13 total) — a 64-bit database warns and names the fix, a 32-bit one finds functions by itself, Tab decompiles a Thumb function and the result reads like C. test_formats.py +2 (34) pinning that a 32-bit variant is offered and the ARM labels state their bitness. 209/0 scenarios, 26/0 blob, 30/0 project UI.
Diffstat (limited to 'tests/test_thumb_ui.py')
-rw-r--r--tests/test_thumb_ui.py63
1 files changed, 62 insertions, 1 deletions
diff --git a/tests/test_thumb_ui.py b/tests/test_thumb_ui.py
index dce1635..090dc2a 100644
--- a/tests/test_thumb_ui.py
+++ b/tests/test_thumb_ui.py
@@ -17,7 +17,7 @@ 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 # noqa: E402
+from idatui.app import DecompView, IdaTui, ListingView # noqa: E402
PASS = FAIL = 0
BIN = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
@@ -115,6 +115,67 @@ async def run() -> int:
status = str(app.query_one("#status", Static).render())
check("`t` toggles back to ARM", "ARM @" in status, status[:80])
+ # -- and the reason a carved function wouldn't decompile ---------------- #
+ # Bare 'arm' gives a 64-BIT database. Thumb doesn't exist there, and
+ # Hex-Rays refuses a 32-bit function outright ("only 64-bit functions can be
+ # decompiled in the current database") — so `t` produces correct-looking
+ # disassembly that F5 can never turn into pseudocode. The database's bitness
+ # is fixed at load and cannot be corrected afterwards, so the only honest
+ # thing is to say so.
+ for ext in (".i64", ".id0", ".id1", ".id2", ".nam", ".til"):
+ try:
+ os.remove(BIN + ext)
+ except OSError:
+ pass
+ app = IdaTui(open_path=BIN, keepalive=False, load_args="-parm") # 64-bit
+ async with app.run_test(size=(140, 44)) as pilot:
+ await wait(lambda: app._func_index is not None
+ and app._func_index.complete, pilot)
+ await wait(lambda: app._cur is not None, pilot, 60)
+ lst = app.query_one(ListingView)
+ lst.focus()
+ lst.cursor = lst.model.index_of_ea(0)
+ lst._scroll_cursor_into_view()
+ await pilot.pause(0.3)
+ m = lst.model
+ await pilot.press("t")
+ await wait(lambda: lst.model is not m and lst.model is not None, pilot, 60)
+ await pilot.pause(0.5)
+ status = str(app.query_one("#status", Static).render())
+ check("a 64-bit database warns that Hex-Rays won't decompile",
+ "64-bit" in status and "decompile" in status, status[:120])
+ check("and names the fix", "ARMv7-A" in status, status[:120])
+
+ # -- the whole point: a 32-bit database decompiles ---------------------- #
+ for ext in (".i64", ".id0", ".id1", ".id2", ".nam", ".til"):
+ try:
+ os.remove(BIN + ext)
+ except OSError:
+ pass
+ app = IdaTui(open_path=BIN, keepalive=False, load_args="-parm:ARMv7-A")
+ async with app.run_test(size=(140, 44)) as pilot:
+ await wait(lambda: app._func_index is not None
+ and app._func_index.complete, pilot)
+ # A 32-bit ARM database also lets auto-analysis do its job on Thumb code,
+ # which is why this one lands in the symbol picker rather than nowhere.
+ check("a 32-bit ARM database finds functions by itself",
+ len(app._func_index) > 5, f"n={len(app._func_index)}")
+ await pilot.press("escape")
+ await pilot.pause(0.5)
+ f = app._func_index.all_loaded()[0]
+ app._goto_ea(f.addr, push=True)
+ await wait(lambda: app._cur is not None
+ and app.query_one(ListingView).model is not None, pilot, 60)
+ app.query_one(ListingView).focus()
+ await pilot.press("tab")
+ dec = app.query_one(DecompView)
+ got = await wait(lambda: dec.display and dec._texts, pilot, 90)
+ check("Tab decompiles a Thumb function", got and len(dec._texts) > 3,
+ f"lines={len(dec._texts or [])}")
+ check("and it reads like C",
+ any("(" in t and ")" in t for t in (dec._texts or [])[:3]),
+ f"{(dec._texts or [])[:3]}")
+
print(f"\n{PASS} passed, {FAIL} failed")
return 1 if FAIL else 0