diff options
| author | blasty <blasty@local> | 2026-07-26 15:04:51 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-26 15:04:51 +0200 |
| commit | 2c2124aff2f4ed6df23512603b6e1ecdcd4b699b (patch) | |
| tree | 2b2c2afe69e7f33e87ccb58e3bc466a94a4e689e /tests | |
| parent | arm: switch ARM/Thumb decoding with `t` (diff) | |
| download | ida-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')
| -rw-r--r-- | tests/test_formats.py | 18 | ||||
| -rw-r--r-- | tests/test_thumb_ui.py | 63 |
2 files changed, 79 insertions, 2 deletions
diff --git a/tests/test_formats.py b/tests/test_formats.py index ab72e8d..1045703 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -77,9 +77,22 @@ def main() -> int: load_args("arm", 0, "-T binary") == "-parm -T binary") check("the processor list leads with the common targets", - [n for n, _ in PROCESSORS[:3]] == ["arm", "armb", "metapc"], + [n for n, _ in PROCESSORS[:2]] == ["arm", "arm:ARMv7-A"], f"{[n for n, _ in PROCESSORS[:3]]}") + # 32-bit ARM has to be offered SEPARATELY from bare 'arm', which gives a + # 64-bit database. That isn't cosmetic: Hex-Rays refuses a 32-bit function + # in a 64-bit database, and Thumb doesn't exist in AArch64 at all, so a + # firmware image loaded as plain 'arm' can never be decompiled — and the + # database's bitness cannot be corrected after load. + check("a 32-bit ARM variant is offered", + any(n.startswith("arm:ARMv") for n, _ in PROCESSORS), + f"{[n for n, _ in PROCESSORS if n.startswith('arm')]}") + check("and the labels say which is 32- vs 64-bit", + all(("32-bit" in d or "64-bit" in d) + for n, d in PROCESSORS if n == "arm" or n.startswith("arm:")), + f"{[(n, d) for n, d in PROCESSORS if n.startswith('arm')]}") + # Every offered name must have been checked against a real IDA, because a # wrong one is REJECTED (rc=4) with nothing useful said — handing the user a # dead end from inside the dialog meant to rescue them. This list is the @@ -89,6 +102,9 @@ def main() -> int: "arm", "armb", "metapc", "mipsl", "mipsb", "ppc", "ppcl", "sh4", "68k", "riscv", "tricore", "xtensa", "avr", "z80", "tms320c6", "m32r", "arc", "h8300", "sparcb", "sparcl", "s390", + # variants: tools/verify_procs.py checks these report the base module + # AND change the database bitness, which is the reason they exist + "arm:ARMv7-A", "arm:ARMv7-M", "arm:ARMv6-M", "arm:ARMv5TE", } offered = {n for n, _ in PROCESSORS} check("every offered processor name is IDA-verified", 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 |
