diff options
| author | blasty <blasty@local> | 2026-07-26 20:49:30 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-26 20:49:30 +0200 |
| commit | 781c5eff4218d7ffdc9905f652ca08ce10953e43 (patch) | |
| tree | ff240cd5d515ac9b64c3c0578d85307a4f293601 /idatui | |
| parent | decomp: say the FIX, not the diagnosis (diff) | |
| download | ida-tui-781c5eff4218d7ffdc9905f652ca08ce10953e43.tar.gz ida-tui-781c5eff4218d7ffdc9905f652ca08ce10953e43.tar.xz ida-tui-781c5eff4218d7ffdc9905f652ca08ce10953e43.zip | |
arm: find Thumb entry points from a vector table (Shift+T)
An ARM function pointer carries the mode in bit 0: odd means Thumb. A Cortex-M
vector table is therefore a list of Thumb entry points, and IDA won't follow them
on a headerless image because nothing tells it those words are pointers at all.
Shift+T scans forward from the cursor and marks them.
0 functions -> 3 Thumb entries found, 3 disassembled
A word only counts when it is odd, lands in a loaded segment, and its target is
executable and not already data. The even words in a vector table — the initial
stack pointer — fail the first test, which is the point: marking a data word as
code corrupts the listing, so a false positive costs more than a miss. The
fixture includes an even in-range word and an odd OUT-of-range word to keep that
honest.
A note on how this started: I recommended this feature, then probed
experiments/fibonacci.bin for the signal and found ZERO odd in-range pointers —
it's a flat code blob, not a firmware image. Rather than build a detector I
couldn't test, I wrote experiments/cortexm.bin: a real vector table pointing at
small self-contained Thumb handlers. The first version of that fixture aimed its
handlers into the middle of copied code, so two "entries" were really inside one
function — the tool was right and the fixture was wrong, which is worth stating
because I nearly filed it as a bug.
Function creation goes through one _idatui_add_func helper now, shared with
define_func_run: add_func(ea) alone fails on freshly-marked code (IDA can't find
the end), and the scan hit exactly the same wall `p` did.
Status precedence, fixed properly this time. An action's result kept being
overwritten by the reload it triggered — cursor moved, filter re-applied,
functions re-counted. I patched that at FIVE separate call sites before
admitting it's one problem. _status(text, priority=True) now marks a result: it
holds the bar for 8s or until the next keypress, and routine chatter can't
outrank it. The per-site special cases are gone.
tests: +4 thumb (20) — a bare vector table gives IDA nothing, scanning finds
exactly the three handlers, the non-pointer words are ignored, and the result
survives both the reload and the reindex. 209/0 scenarios, 30/0 blob, 30/0
project UI.
Diffstat (limited to 'idatui')
| -rw-r--r-- | idatui/app.py | 61 | ||||
| -rw-r--r-- | idatui/domain.py | 9 |
2 files changed, 48 insertions, 22 deletions
diff --git a/idatui/app.py b/idatui/app.py index 1ea630e..ea68620 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -741,6 +741,7 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru Binding("p", "define_func", "Func", show=False), Binding("u", "undefine", "Undef", show=False), Binding("t", "toggle_thumb", "ARM/Thumb", show=False), + Binding("T", "thumb_scan", "Scan vectors", show=False), *SearchMixin.SEARCH_BINDINGS, *NavMixin.NAV_BINDINGS, *ColumnCursor.COL_BINDINGS, @@ -1132,6 +1133,9 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru def action_toggle_thumb(self) -> None: self.post_message(EditItemRequested(self, "thumb")) + def action_thumb_scan(self) -> None: + self.post_message(EditItemRequested(self, "thumbscan")) + def action_make_data(self) -> None: self.post_message(MakeDataRequested(self)) @@ -3092,6 +3096,7 @@ class IdaTui(App): 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._flash_until = 0.0 # ...until this monotonic time 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. @@ -3416,7 +3421,23 @@ class IdaTui(App): await self._rpc.stop() # -- status helper ----------------------------------------------------- # - def _status(self, text: str) -> None: + def _status(self, text: str, priority: bool = False) -> None: + """Write the status bar. ``priority`` marks the RESULT of something the + user did. + + An action's result is written, and then the reload it triggered writes + its own idle status on top — cursor moved, filter re-applied, functions + re-counted. I patched that at five separate call sites before admitting + it's one problem: routine chatter must not outrank an answer. A priority + message holds the bar briefly and is cleared by the next keypress, i.e. + when the user has read it and moved on. + """ + import time as _time + if priority: + self._flash = text + self._flash_until = _time.monotonic() + 8.0 + elif self._flash and _time.monotonic() < self._flash_until: + text = self._flash if self._binary: # project mode: always say which binary you're in text = f"[{self._binary}] {text}" # An image with no functions at all is nearly always a blob described @@ -5163,7 +5184,7 @@ class IdaTui(App): assert self.program is not None verb = {"code": "defined code", "func": "created function", "undef": "undefined", "string": "made string", - "thumb": "switched decoding"}[kind] + "thumb": "switched decoding", "thumbscan": "scanned"}[kind] try: if kind == "code": # Keep going until something stops it: one instruction is rarely @@ -5188,6 +5209,19 @@ 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 == "thumbscan": + # A vector table is a list of Thumb entry points that IDA won't + # follow on a headerless image, because nothing tells it those + # words are pointers. Scan from the cursor. + anchor.refresh_functions = True + r = self.program.thumb_scan(ea, ea + 0x400) + n, applied = int(r.get("n", 0)), int(r.get("applied", 0)) + if not n: + verb = (f"no Thumb entry pointers in {ea:#x}\u2013{ea+0x400:#x}" + " (odd words pointing into the image)") + else: + verb = (f"{n} Thumb entr{'y' if n == 1 else 'ies'} found, " + f"{applied} disassembled") elif kind == "thumb": # Switch the mode, then disassemble in it: flipping T and # leaving the bytes undefined shows nothing, and the reason you @@ -5257,9 +5291,8 @@ class IdaTui(App): written and lost. """ self._dirty = True - self._flash = anchor.flash if anchor.flash: - self._status(anchor.flash) # and again from the reload, via _flash + self._status(anchor.flash, priority=True) if anchor.refresh_functions: # Creating (or destroying) a function changes the index that the # names pane, Ctrl+N and the "no functions" hint all read. Without @@ -5936,13 +5969,6 @@ class IdaTui(App): self._goto_ea(msg.va, push=True) def _status_for_cur(self, mode: str) -> None: - if self._flash: - # Shown, NOT consumed. A reload emits several of these (prime, then - # cursor), so consuming on the first one meant the second erased the - # message the user was meant to read. It clears on the next keypress - # instead — i.e. when they've moved on. - self._status(self._flash) - return if self._cur is not None: self._status(f"{self._cur.name} @ {self._cur.ea:#x} [{mode}]") @@ -5981,15 +6007,12 @@ class IdaTui(App): # status afterwards — which is precisely how "F5 does nothing" # looked like nothing at all. msg = f"{name}: cannot decompile{detail}" - self._flash = msg + self._status(msg, priority=True) self._open_entry(ret, push=False) - self._status(msg) return self._active = "disasm" - msg = f"{name}: cannot decompile{detail}" - self._flash = msg + self._status(f"{name}: cannot decompile{detail}", priority=True) self._show_active() - self._status(msg) return if self._active == "decomp": view.focus() # loading cover had blurred it; restore focus @@ -6196,12 +6219,6 @@ 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. - if self._flash: - self._status(self._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 084312a..97b042d 100644 --- a/idatui/domain.py +++ b/idatui/domain.py @@ -1393,6 +1393,15 @@ class Program: return "this database is 64-bit \u2014 Ctrl+L, pick arm:ARMv7-A" return reason + def thumb_scan(self, start: int, end: int, apply: bool = True) -> dict: + """Find Thumb entry points from odd pointers in ``[start, end)``.""" + r = self.client.call("thumb_scan", start=hex(start), end=hex(end), + apply=bool(apply)) + if not isinstance(r, dict) or r.get("error"): + raise IDAToolError("thumb_scan", + f"@ {start:#x}: {(r or {}).get('error', 'failed')}") + return r + 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) |
