diff options
Diffstat (limited to 'idatui/remote_tools.py')
| -rw-r--r-- | idatui/remote_tools.py | 622 |
1 files changed, 435 insertions, 187 deletions
diff --git a/idatui/remote_tools.py b/idatui/remote_tools.py index 0e26965..6681379 100644 --- a/idatui/remote_tools.py +++ b/idatui/remote_tools.py @@ -1,8 +1,8 @@ -"""The IDAPython ida-tui runs inside the Code Mode sandbox. +"""The IDAPython ida-tui runs inside the IDA Nexus sandbox. Two features have no ida-domain surface at all and are carried over VERBATIM from the tools ida-tui was developed against (`server/patch_server.py`'s -injected BODY, which the Code Mode port deletes): +injected BODY, which the IDA Nexus port deletes): * `heads` -- the continuous listing. ida-domain enumerates defined heads and renders plain disassembly; the listing also needs coalesced undefined runs, @@ -20,15 +20,16 @@ what you see). A re-implementation drifts from it silently. This file is SOURCE SHIPPED AS TEXT to the database process; it is never imported here, because the ida_* modules do not exist in the TUI's interpreter. -`codemode_client` reads it and prepends it to the relevant snippets. Keep it -self-contained: no relative imports, nothing beyond what Code Mode provides. +`nexus_client` reads it and prepends it to the relevant snippets. Keep it +self-contained: no relative imports, nothing beyond what IDA Nexus provides. """ + # ruff: noqa import re as _re # IDAPython, imported ONCE at module scope. # -# This file is never imported by the client -- codemode_client reads it as +# This file is never imported by the client -- nexus_client reads it as # TEXT and installs it as a module inside the database process -- so the # no-IDA house rule that keeps idatui importable without IDA does not apply # here, and these need not be function-local. @@ -134,7 +135,7 @@ def _idatui_head_row(ea, flags=None, text=True): """ f = ida_bytes.get_flags(ea) if flags is None else flags - cls = f & _MS_CLS # == is_code(f) / is_data(f), without the calls + cls = f & _MS_CLS # == is_code(f) / is_data(f), without the calls if cls == _FF_CODE: kind = "code" elif cls == _FF_DATA: @@ -211,10 +212,20 @@ _IDATUI_SPAN_KINDS = { # NB the real constant names: DATNAME/CODNAME, not "DNAME". Guessing here # fails silently — an unmapped tag renders as plain body text, so symbols # just quietly aren't blue and nothing tells you why. - "name": ("SCOLOR_DATNAME", "SCOLOR_CODNAME", "SCOLOR_LOCNAME", - "SCOLOR_IMPNAME", "SCOLOR_DEMNAME", "SCOLOR_LIBNAME", - "SCOLOR_CNAME", "SCOLOR_DNAME", - "SCOLOR_CREF", "SCOLOR_DREF", "SCOLOR_CREFTAIL", "SCOLOR_DREFTAIL"), + "name": ( + "SCOLOR_DATNAME", + "SCOLOR_CODNAME", + "SCOLOR_LOCNAME", + "SCOLOR_IMPNAME", + "SCOLOR_DEMNAME", + "SCOLOR_LIBNAME", + "SCOLOR_CNAME", + "SCOLOR_DNAME", + "SCOLOR_CREF", + "SCOLOR_DREF", + "SCOLOR_CREFTAIL", + "SCOLOR_DREFTAIL", + ), "seg": ("SCOLOR_SEGNAME",), "cmt": ("SCOLOR_AUTOCMT", "SCOLOR_REGCMT", "SCOLOR_RPTCMT", "SCOLOR_VOIDOP"), "punct": ("SCOLOR_SYMBOL", "SCOLOR_ALTOP", "SCOLOR_HIDNAME"), @@ -241,7 +252,7 @@ _IDATUI_TAGS = None _IDATUI_OPND_TAGS = None -_IDATUI_CTL = None # re: a tag = one of three control chars plus its argument +_IDATUI_CTL = None # re: a tag = one of three control chars plus its argument _IDATUI_TAGINFO = None @@ -280,6 +291,7 @@ def _idatui_spans(line): _IDATUI_OPND_TAGS = _idatui_opnd_tag_map() if _IDATUI_CTL is None: import re as _re + # One capturing split gives [text, tag, text, tag, ..., text] in a # single C pass. A per-character python loop over the line used to be # the most expensive thing the `heads` tool did, and a line is ~54 @@ -289,17 +301,18 @@ def _idatui_spans(line): if _IDATUI_TAGINFO is None: _IDATUI_TAGINFO = { tag: (_IDATUI_TAGS.get(tag, "text"), _IDATUI_OPND_TAGS.get(tag)) - for tag in set(_IDATUI_TAGS) | set(_IDATUI_OPND_TAGS)} + for tag in set(_IDATUI_TAGS) | set(_IDATUI_OPND_TAGS) + } taginfo = _IDATUI_TAGINFO plain_tag = ("text", None) on, off, esc = "\x01", "\x02", "\x03" addr_tag = chr(getattr(ida_lines, "COLOR_ADDR", 0x28)) addr_len = int(getattr(ida_lines, "COLOR_ADDR_SIZE", 16)) parts = _IDATUI_CTL.split(line) - spans, stack = [], [] # stack entries: (kind, operand index|None) - kind, opnd = "text", None # state the current run of text belongs to + spans, stack = [], [] # stack entries: (kind, operand index|None) + kind, opnd = "text", None # state the current run of text belongs to pend = "" - skip = 0 # characters of an address payload still due + skip = 0 # characters of an address payload still due i, n = 0, len(parts) while i < n: txt = parts[i] @@ -317,11 +330,11 @@ def _idatui_spans(line): break pair = parts[i] i += 1 - if skip: # a tag INSIDE an address payload: 2 chars + if skip: # a tag INSIDE an address payload: 2 chars skip = skip - 2 if skip > 2 else 0 continue ch = pair[0] - if ch == esc: # escaped literal: keep the char it guards + if ch == esc: # escaped literal: keep the char it guards pend += pair[1] continue tag = pair[1] @@ -337,7 +350,7 @@ def _idatui_spans(line): stack.append((kind, opnd)) kind, o = taginfo.get(tag, plain_tag) if o is not None: - opnd = o # operands nest: an inner colour keeps the operand + opnd = o # operands nest: an inner colour keeps the operand elif stack: kind, opnd = stack.pop() else: @@ -359,7 +372,7 @@ def _idatui_spans(line): prev_space = False out.append([kind, txt, opnd]) continue - if not core: # the span is nothing but padding + if not core: # the span is nothing but padding if not prev_space: prev_space = True out.append([kind, " ", opnd]) @@ -396,7 +409,7 @@ def _idatui_spans(line): ops.append([start, pos, cur]) text = "".join(t for _k, t, _o in out) trimmed = [] - for lo, hi, k in ops: # don't let a range own trailing space + for lo, hi, k in ops: # don't let a range own trailing space while hi > lo and text[hi - 1].isspace(): hi -= 1 while lo < hi and text[lo].isspace(): @@ -435,8 +448,17 @@ def _idatui_rows_digest(rows): sh = seen.get(key) if sh is None: sh = seen[key] = hash(tuple(map(tuple, sp))) - acc = hash((acc, r.get("ea"), r.get("kind"), r.get("size"), - r.get("text"), r.get("name"), sh)) + acc = hash( + ( + acc, + r.get("ea"), + r.get("kind"), + r.get("size"), + r.get("text"), + r.get("name"), + sh, + ) + ) return acc @@ -448,8 +470,12 @@ def _idatui_unknown_row(ea, size): if size <= 1: return _idatui_head_row(ea) - row = {"ea": hex(ea), "kind": "unknown", "size": int(size), - "text": f"db {size} dup(?)"} + row = { + "ea": hex(ea), + "kind": "unknown", + "size": int(size), + "text": f"db {size} dup(?)", + } nm = ida_name.get_ea_name(ea) if nm: row["name"] = nm @@ -481,8 +507,7 @@ def _idatui_struct_member_rows(ea): sz = 0 name = m.name or "" text = f"+{off:X} {name}" + (f" {mtype}" if mtype else "") - rows.append({"ea": hex(ea + off), "kind": "member", "size": sz, - "text": text}) + rows.append({"ea": hex(ea + off), "kind": "member", "size": sz, "text": text}) return rows @@ -494,8 +519,13 @@ def _idatui_func_header_rows(ea): return [ {"ea": hex(ea), "kind": "sep", "size": 0, "text": ""}, {"ea": hex(ea), "kind": "sep", "size": 0, "text": "; " + bar}, - {"ea": hex(ea), "kind": "funchdr", "size": 0, - "text": name + " proc", "name": name}, + { + "ea": hex(ea), + "kind": "funchdr", + "size": 0, + "text": name + " proc", + "name": name, + }, ] @@ -504,8 +534,13 @@ def _idatui_func_footer_rows(ea, func): name = ida_funcs.get_func_name(func.start_ea) or "sub_%X" % func.start_ea return [ - {"ea": hex(ea), "kind": "funchdr", "size": 0, - "text": name + " endp", "name": name}, + { + "ea": hex(ea), + "kind": "funchdr", + "size": 0, + "text": name + " endp", + "name": name, + }, {"ea": hex(ea), "kind": "sep", "size": 0, "text": "; " + "-" * 60}, ] @@ -543,9 +578,12 @@ def _idatui_segment_detail(addr, end, page_rows): except Exception: pass - K_CODE = _IDATUI_KIND_ID["code"]; K_DATA = _IDATUI_KIND_ID["data"] - K_UNK = _IDATUI_KIND_ID["unknown"]; K_SEP = _IDATUI_KIND_ID["sep"] - K_FUNC = _IDATUI_KIND_ID["funchdr"]; K_LABEL = _IDATUI_KIND_ID["label"] + K_CODE = _IDATUI_KIND_ID["code"] + K_DATA = _IDATUI_KIND_ID["data"] + K_UNK = _IDATUI_KIND_ID["unknown"] + K_SEP = _IDATUI_KIND_ID["sep"] + K_FUNC = _IDATUI_KIND_ID["funchdr"] + K_LABEL = _IDATUI_KIND_ID["label"] K_MEMBER = _IDATUI_KIND_ID["member"] eas = array.array("Q") @@ -569,8 +607,8 @@ def _idatui_segment_detail(addr, end, page_rows): # are the same number until a segment contains an undefined run) and then # silently yields pages that do not line up with a refetch. anchors = [] - page_phys = 0 # physical rows emitted into the page being filled - rows = 0 # logical rows so far (what the scrollbar counts) + page_phys = 0 # physical rows emitted into the page being filled + rows = 0 # logical rows so far (what the scrollbar counts) fn = None ea = ida_bytes.get_item_head(lo) while ea != BAD and ea < hi: @@ -584,7 +622,9 @@ def _idatui_segment_detail(addr, end, page_rows): nh = next_head(ea, hi) stop = nh if (nh != BAD and ea < nh <= hi) else hi run = stop - ea - ea_ap(ea); kind_ap(K_UNK); size_ap(run) + ea_ap(ea) + kind_ap(K_UNK) + size_ap(run) rows += run if run > 1 else 1 page_phys += len(eas) - before ea = stop @@ -593,23 +633,32 @@ def _idatui_segment_detail(addr, end, page_rows): fn = get_func(ea) at_start = fn is not None and fn.start_ea == ea if at_start: - for k in (K_SEP, K_SEP, K_FUNC): # blank, banner, `name proc` - ea_ap(ea); kind_ap(k); size_ap(0) + for k in (K_SEP, K_SEP, K_FUNC): # blank, banner, `name proc` + ea_ap(ea) + kind_ap(k) + size_ap(0) rows += 3 elif cls == _FF_CODE and get_ea_name(ea): - ea_ap(ea); kind_ap(K_LABEL); size_ap(0) + ea_ap(ea) + kind_ap(K_LABEL) + size_ap(0) rows += 1 - ea_ap(ea); kind_ap(K_CODE if cls == _FF_CODE else K_DATA) - size_ap(int(get_item_size(ea))); rows += 1 + ea_ap(ea) + kind_ap(K_CODE if cls == _FF_CODE else K_DATA) + size_ap(int(get_item_size(ea))) + rows += 1 if cls == _FF_DATA: for m in _idatui_struct_member_rows(ea): ea_ap(int(m["ea"], 16) if isinstance(m["ea"], str) else m["ea"]) kind_ap(_IDATUI_KIND_ID.get(m.get("kind", "member"), K_MEMBER)) - size_ap(int(m.get("size", 0) or 0)); rows += 1 + size_ap(int(m.get("size", 0) or 0)) + rows += 1 item_end = get_item_end(ea) if fn is not None and item_end >= fn.end_ea: - for k in (K_FUNC, K_SEP): # `name endp`, separator - ea_ap(ea); kind_ap(k); size_ap(0) + for k in (K_FUNC, K_SEP): # `name endp`, separator + ea_ap(ea) + kind_ap(k) + size_ap(0) rows += 2 page_phys += len(eas) - before ea = item_end if item_end > ea else ea + 1 @@ -618,19 +667,28 @@ def _idatui_segment_detail(addr, end, page_rows): # which turns a bytes object into its repr -- 4 characters per byte and # unparseable at the other end. Learned by watching 2.97MB arrive as 11.26MB. import base64 + b64 = base64.b64encode - return {"addr": hex(lo), "end": hex(hi), "rows": rows, "heads": len(eas), - "anchors": anchors, "kind_names": list(_IDATUI_KINDS), - "eas": b64(eas.tobytes()).decode(), - "kinds": b64(kinds.tobytes()).decode(), - "sizes": b64(sizes.tobytes()).decode()} + return { + "addr": hex(lo), + "end": hex(hi), + "rows": rows, + "heads": len(eas), + "anchors": anchors, + "kind_names": list(_IDATUI_KINDS), + "eas": b64(eas.tobytes()).decode(), + "kinds": b64(kinds.tobytes()).decode(), + "sizes": b64(sizes.tobytes()).decode(), + } def segment_index( addr: Annotated[str, "Any address in the segment to index"], end: Annotated[str, "Optional exclusive end address; default = segment end"] = "", page_rows: Annotated[int, "Rows between anchors (default 500)"] = 500, - detail: Annotated[bool, "Also return every row's ea/kind/size as packed arrays"] = False, + detail: Annotated[ + bool, "Also return every row's ea/kind/size as packed arrays" + ] = False, ) -> dict: """How many listing rows a segment has, and where to seek into it. @@ -662,6 +720,7 @@ def segment_index( if detail: return _idatui_segment_detail(addr, end, count) import ida_segment + seg = ida_segment.getseg(start) if not seg: return {"addr": str(addr), "error": "no segment", "rows": 0, "anchors": []} @@ -703,19 +762,24 @@ def segment_index( fn = get_func(ea) n = 1 if fn is not None and fn.start_ea == ea: - n += 3 # blank, banner, `proc` + n += 3 # blank, banner, `proc` elif cls == _FF_CODE and get_ea_name(ea): - n += 1 # loc_XXX label on its own row + n += 1 # loc_XXX label on its own row if cls == _FF_DATA: n += len(_idatui_struct_member_rows(ea)) item_end = get_item_end(ea) if fn is not None and item_end >= fn.end_ea: - n += 2 # `endp` + separator + n += 2 # `endp` + separator rows += n n_heads += 1 ea = item_end if item_end > ea else ea + 1 - return {"addr": hex(lo), "end": hex(hi), "rows": rows, - "heads": n_heads, "anchors": anchors} + return { + "addr": hex(lo), + "end": hex(hi), + "rows": rows, + "heads": n_heads, + "anchors": anchors, + } def heads( @@ -723,10 +787,21 @@ def heads( count: Annotated[int, "Max heads to return (default 200, max 2000)"] = 200, offset: Annotated[int, "Skip first N heads from addr (default 0)"] = 0, end: Annotated[str, "Optional exclusive end address; default = segment end"] = "", - back: Annotated[bool, "Walk backwards: return the count heads ENDING just before addr, in forward order"] = False, - annotate: Annotated[bool, "Emit IDA-style function boundary banner rows (kind sep/funchdr)"] = False, - expect: Annotated[str, "Digest a caller already holds: the rows are omitted when they still hash to it"] = "", - text: Annotated[bool, "Render each row's disassembly text (default true). False = a skeleton page: same rows, same addresses, no text"] = True, + back: Annotated[ + bool, + "Walk backwards: return the count heads ENDING just before addr, in forward order", + ] = False, + annotate: Annotated[ + bool, "Emit IDA-style function boundary banner rows (kind sep/funchdr)" + ] = False, + expect: Annotated[ + str, + "Digest a caller already holds: the rows are omitted when they still hash to it", + ] = "", + text: Annotated[ + bool, + "Render each row's disassembly text (default true). False = a skeleton page: same rows, same addresses, no text", + ] = True, ) -> dict: """Walk item heads from ``addr`` as a flat listing: every head is rendered (code OR data OR undefined) via generate_disasm_line and stepped with @@ -740,10 +815,20 @@ def heads( try: start = parse_address(addr) except Exception as e: - return {"addr": str(addr), "error": str(e), "heads": [], "cursor": {"done": True}} + return { + "addr": str(addr), + "error": str(e), + "heads": [], + "cursor": {"done": True}, + } seg = ida_segment.getseg(start) if not seg: - return {"addr": str(addr), "error": "no segment", "heads": [], "cursor": {"done": True}} + return { + "addr": str(addr), + "error": "no segment", + "heads": [], + "cursor": {"done": True}, + } lo, hi = seg.start_ea, seg.end_ea if end: try: @@ -766,7 +851,9 @@ def heads( rows = [_idatui_head_row(e) for e in chosen] first = chosen[0] if chosen else start pea = ida_bytes.prev_head(first, lo) - cursor = {"done": True} if pea == idaapi.BADADDR or pea < lo else {"prev": hex(pea)} + cursor = ( + {"done": True} if pea == idaapi.BADADDR or pea < lo else {"prev": hex(pea)} + ) return {"addr": str(addr), "heads": rows, "cursor": cursor} # Walk by item END (not next_head): next_head SKIPS undefined bytes, but a @@ -823,8 +910,9 @@ def heads( # A code label (loc_XXX/jump target) gets its OWN line at depth 0, # like IDA; strip it from the instruction row below. nm = row["name"] - out.append({"ea": hex(e), "kind": "label", "size": 0, - "text": nm + ":", "name": nm}) + out.append( + {"ea": hex(e), "kind": "label", "size": 0, "text": nm + ":", "name": nm} + ) row = dict(row) row["name"] = None out.append(row) @@ -845,7 +933,7 @@ def heads( if len(rows) >= count: more = True break - f = get_flags(ea) # once per head, not once per consumer + f = get_flags(ea) # once per head, not once per consumer rows.extend(_rows_for(ea, f)) # a struct head expands into member rows ea = _advance(ea, f) cursor = {"next": hex(ea)} if more else {"done": True} @@ -871,8 +959,18 @@ def heads( _IDATUI_FMT_CYCLE = ("hex", "dec", "bin", "char", "offset", "default") -_IDATUI_FMT_SETTABLE = ("hex", "dec", "oct", "bin", "char", "offset", "seg", - "float", "stack", "default") +_IDATUI_FMT_SETTABLE = ( + "hex", + "dec", + "oct", + "bin", + "char", + "offset", + "seg", + "float", + "stack", + "default", +) def _idatui_fmt_nibbles(): @@ -880,13 +978,20 @@ def _idatui_fmt_nibbles(): this module is injected into a file that is imported before a database is open.""" return { - "default": ida_bytes.FF_N_VOID, "hex": ida_bytes.FF_N_NUMH, - "dec": ida_bytes.FF_N_NUMD, "char": ida_bytes.FF_N_CHAR, - "seg": ida_bytes.FF_N_SEG, "offset": ida_bytes.FF_N_OFF, - "bin": ida_bytes.FF_N_NUMB, "oct": ida_bytes.FF_N_NUMO, - "enum": ida_bytes.FF_N_ENUM, "forced": ida_bytes.FF_N_FOP, - "stroff": ida_bytes.FF_N_STRO, "stack": ida_bytes.FF_N_STK, - "float": ida_bytes.FF_N_FLT, "custom": ida_bytes.FF_N_CUST, + "default": ida_bytes.FF_N_VOID, + "hex": ida_bytes.FF_N_NUMH, + "dec": ida_bytes.FF_N_NUMD, + "char": ida_bytes.FF_N_CHAR, + "seg": ida_bytes.FF_N_SEG, + "offset": ida_bytes.FF_N_OFF, + "bin": ida_bytes.FF_N_NUMB, + "oct": ida_bytes.FF_N_NUMO, + "enum": ida_bytes.FF_N_ENUM, + "forced": ida_bytes.FF_N_FOP, + "stroff": ida_bytes.FF_N_STRO, + "stack": ida_bytes.FF_N_STK, + "float": ida_bytes.FF_N_FLT, + "custom": ida_bytes.FF_N_CUST, } @@ -932,8 +1037,12 @@ def _idatui_op_value(ea, n): size = 0 return int(v), size size = int(ida_bytes.get_item_size(ea)) - read = {1: ida_bytes.get_byte, 2: ida_bytes.get_word, - 4: ida_bytes.get_dword, 8: ida_bytes.get_qword}.get(size) + read = { + 1: ida_bytes.get_byte, + 2: ida_bytes.get_word, + 4: ida_bytes.get_dword, + 8: ida_bytes.get_qword, + }.get(size) if read is None: return None, size try: @@ -991,9 +1100,9 @@ def _idatui_op_candidates(ea): F = ida_bytes.get_flags(ea) if ida_bytes.is_data(F): - return [0] # a data item's value is operand 0 + return [0] # a data item's value is operand 0 if not ida_bytes.is_code(F): - return [] # undefined bytes: IDA refuses a format outright + return [] # undefined bytes: IDA refuses a format outright insn = ida_ua.insn_t() if ida_ua.decode_insn(insn, ea) <= 0: return [] @@ -1037,7 +1146,7 @@ def _idatui_op_spans(ea, text): if not op: continue i = text.find(op, pos) - if i < 0: # duplicated operand text (mov eax, eax) + if i < 0: # duplicated operand text (mov eax, eax) i = text.find(op) if i < 0: continue @@ -1070,21 +1179,34 @@ def _idatui_apply_fmt(ea, n, fmt): if base in (idaapi.BADADDR, None) or base < 0: base = 0 return bool(ida_offset.op_plain_offset(ea, n, base)), "" - fn = {"hex": ida_bytes.op_hex, "dec": ida_bytes.op_dec, - "oct": ida_bytes.op_oct, "bin": ida_bytes.op_bin, - "char": ida_bytes.op_chr, "seg": ida_bytes.op_seg, - "float": ida_bytes.op_flt, "stack": ida_bytes.op_stkvar}.get(fmt) + fn = { + "hex": ida_bytes.op_hex, + "dec": ida_bytes.op_dec, + "oct": ida_bytes.op_oct, + "bin": ida_bytes.op_bin, + "char": ida_bytes.op_chr, + "seg": ida_bytes.op_seg, + "float": ida_bytes.op_flt, + "stack": ida_bytes.op_stkvar, + }.get(fmt) if fn is None: - return False, (f"can't set {fmt!r} from a name alone" - if fmt in _idatui_fmt_nibbles() else - f"unknown format {fmt!r}") + return False, ( + f"can't set {fmt!r} from a name alone" + if fmt in _idatui_fmt_nibbles() + else f"unknown format {fmt!r}" + ) return bool(fn(ea, n)), "" def op_format( addr: Annotated[str, "Address of the instruction or data item"], - mode: Annotated[str, "cycle | back | show | hex | dec | oct | bin | char | offset | stack | default"] = "cycle", - col: Annotated[int, "Cursor column inside the rendered line (-1: first literal)"] = -1, + mode: Annotated[ + str, + "cycle | back | show | hex | dec | oct | bin | char | offset | stack | default", + ] = "cycle", + col: Annotated[ + int, "Cursor column inside the rendered line (-1: first literal)" + ] = -1, n: Annotated[int, "Operand index; -1 derives it from ``col``"] = -1, ) -> dict: """Change how a literal is DISPLAYED (IDA's 'o' family): hex, decimal, @@ -1126,19 +1248,27 @@ def op_format( # a different operand would make that highlight a lie -- say # which one can be changed instead. where = before[lo:hi].strip() - alt = (f"; the literal on this line is operand {cands[0]} " - f"({_idatui_op_text(ea, before, cands[0])})" - if cands else "") - return {"addr": hex(ea), "n": i, "text": before, - "error": f"operand {i} ({where}) has no format to " - f"change{alt}"} + alt = ( + f"; the literal on this line is operand {cands[0]} " + f"({_idatui_op_text(ea, before, cands[0])})" + if cands + else "" + ) + return { + "addr": hex(ea), + "n": i, + "text": before, + "error": f"operand {i} ({where}) has no format to change{alt}", + } if n < 0: if not cands: F = ida_bytes.get_flags(ea) - why = ("no literal on this line to reformat" - if ida_bytes.is_code(F) or ida_bytes.is_data(F) else - "undefined bytes have no format to change -- define " - "them first ('d' makes data, 'c' makes code)") + why = ( + "no literal on this line to reformat" + if ida_bytes.is_code(F) or ida_bytes.is_data(F) + else "undefined bytes have no format to change -- define " + "them first ('d' makes data, 'c' makes code)" + ) return {"addr": hex(ea), "text": before, "error": why} n = cands[0] @@ -1148,9 +1278,12 @@ def op_format( # The ring is a property of the OPERAND, not of what you last pressed: every # stop is one that changes what you see for this value, and it is the same # ring at every step, so a lap always comes home. - choices = [f for f in _IDATUI_FMT_CYCLE - if (f != "char" or _idatui_printable(value)) - and (f != "offset" or _idatui_offset_worth(value))] + choices = [ + f + for f in _IDATUI_FMT_CYCLE + if (f != "char" or _idatui_printable(value)) + and (f != "offset" or _idatui_offset_worth(value)) + ] # A stack variable is deliberately NOT a stop: ``[rbp+var_40]`` is a frame # member, not a way of writing a number, and IDA's own "is this a stack # variable" test isn't exposed to Python here (calc_stkvar_struc_offset @@ -1160,10 +1293,18 @@ def op_format( mode = str(mode or "cycle").lower() if mode == "show": - return {"addr": hex(ea), "n": n, "format": cur, "prev": cur, - "choices": choices, "text": before, "before": before, - "value": None if value is None else hex(value), - "width": width, "applied": False} + return { + "addr": hex(ea), + "n": n, + "format": cur, + "prev": cur, + "choices": choices, + "text": before, + "before": before, + "value": None if value is None else hex(value), + "width": width, + "applied": False, + } if mode in ("cycle", "back"): step = 1 if mode == "cycle" else -1 if cur in choices: @@ -1176,31 +1317,51 @@ def op_format( else: want = mode if want not in _idatui_fmt_nibbles(): - return {"addr": hex(ea), "n": n, "text": before, - "error": f"unknown format {mode!r}; one of " - + ", ".join(_IDATUI_FMT_SETTABLE)} + return { + "addr": hex(ea), + "n": n, + "text": before, + "error": f"unknown format {mode!r}; one of " + + ", ".join(_IDATUI_FMT_SETTABLE), + } if want == "offset" and not mapped: - return {"addr": hex(ea), "n": n, "text": before, "format": cur, - "error": (f"{'0x%x' % value if value is not None else 'this operand'}" - " isn't a mapped address -- an offset to it would" - " invent a name for nothing")} + return { + "addr": hex(ea), + "n": n, + "text": before, + "format": cur, + "error": ( + f"{'0x%x' % value if value is not None else 'this operand'}" + " isn't a mapped address -- an offset to it would" + " invent a name for nothing" + ), + } ok, err = _idatui_apply_fmt(ea, n, want) if err: - return {"addr": hex(ea), "n": n, "text": before, "format": cur, - "error": err} + return {"addr": hex(ea), "n": n, "text": before, "format": cur, "error": err} got = _idatui_op_fmt(ea, n) - out = {"addr": hex(ea), "n": n, "prev": cur, "format": got, - "requested": want, "applied": bool(ok), "choices": choices, - "before": before, "text": _idatui_line_text(ea), - "value": None if value is None else hex(value), "width": width} + out = { + "addr": hex(ea), + "n": n, + "prev": cur, + "format": got, + "requested": want, + "applied": bool(ok), + "choices": choices, + "before": before, + "text": _idatui_line_text(ea), + "value": None if value is None else hex(value), + "width": width, + } if not ok: out["error"] = f"IDA refused {want} on operand {n}" elif lossy: - out["warn"] = ( - f"operand {n} was {cur} and the ring has no stop there -- " - + (f"'{cur}' sets it again" if cur in _IDATUI_FMT_SETTABLE else - f"{cur} names a type this can't put back, reassign it by hand")) + out["warn"] = f"operand {n} was {cur} and the ring has no stop there -- " + ( + f"'{cur}' sets it again" + if cur in _IDATUI_FMT_SETTABLE + else f"{cur} names a type this can't put back, reassign it by hand" + ) return out @@ -1267,13 +1428,18 @@ def _idatui_lit_extent(plain, x): around the column, which cannot reach a ``)`` or a space.""" if x >= len(plain): return None - if plain[x] == "'": # a character constant: '-' + if plain[x] == "'": # a character constant: '-' end = plain.find("'", x + 1) return (x, end + 1) if end > x else None lo = plain.rfind("'", 0, x) - if lo >= 0 and plain.find("'", x) > x and "'" in plain[lo:x] and \ - plain[lo:x].count("'") == 1 and " " not in plain[lo:x]: - return (lo, plain.find("'", x) + 1) # inside 'c' + if ( + lo >= 0 + and plain.find("'", x) > x + and "'" in plain[lo:x] + and plain[lo:x].count("'") == 1 + and " " not in plain[lo:x] + ): + return (lo, plain.find("'", x) + 1) # inside 'c' if plain[x] not in _IDATUI_LIT_CHARS: return None lo = x @@ -1282,7 +1448,7 @@ def _idatui_lit_extent(plain, x): hi = x while hi < len(plain) and plain[hi] in _IDATUI_LIT_CHARS: hi += 1 - if lo > 0 and plain[lo - 1] == "-": # a unary minus is part of it + if lo > 0 and plain[lo - 1] == "-": # a unary minus is part of it lo -= 1 return (lo, hi) @@ -1321,26 +1487,36 @@ def _idatui_pc_nums(cf, sl): continue nf = e.n.nf opnum = ord(nf.opnum) if isinstance(nf.opnum, str) else int(nf.opnum) - nbytes = (ord(nf.org_nbytes) if isinstance(nf.org_nbytes, str) - else int(nf.org_nbytes)) + nbytes = ( + ord(nf.org_nbytes) if isinstance(nf.org_nbytes, str) else int(nf.org_nbytes) + ) ea = int(e.ea) if ea == idaapi.BADADDR: x = extent[1] - continue # synthesised: nothing to key on + continue # synthesised: nothing to key on nib = (nf.flags >> ida_bytes.get_operand_type_shift(opnum)) & 0xF # Whether this format is the USER's or Hex-Rays' own guess. The nibble # can't say: an untouched number reads back as whatever it happens to # be printed as, and cycling from there would skip that stop forever # (default already looks like it) and never come back to it. loc = ida_hexrays.operand_locator_t(ea, opnum) - user = (ida_hexrays.user_numforms_find(cf.numforms, loc) - != ida_hexrays.user_numforms_end(cf.numforms)) - out.append({"x0": extent[0], "x1": extent[1], "ea": ea, - "opnum": opnum, "value": int(e.n._value), - "nbytes": nbytes, "user": user, - "fmt": _idatui_fmt_name(nib) if user else "default", - "shown": _idatui_fmt_name(nib)}) - x = extent[1] # past this literal, not into it + user = ida_hexrays.user_numforms_find( + cf.numforms, loc + ) != ida_hexrays.user_numforms_end(cf.numforms) + out.append( + { + "x0": extent[0], + "x1": extent[1], + "ea": ea, + "opnum": opnum, + "value": int(e.n._value), + "nbytes": nbytes, + "user": user, + "fmt": _idatui_fmt_name(nib) if user else "default", + "shown": _idatui_fmt_name(nib), + } + ) + x = extent[1] # past this literal, not into it return out @@ -1367,31 +1543,36 @@ def pc_nums( try: cf = ida_hexrays.decompile(f.start_ea) except Exception as e: - return {"addr": hex(f.start_ea), "error": f"decompile failed: {e}", - "nums": []} + return {"addr": hex(f.start_ea), "error": f"decompile failed: {e}", "nums": []} if cf is None: - return {"addr": hex(f.start_ea), "error": "decompilation failed", - "nums": []} + return {"addr": hex(f.start_ea), "error": "decompilation failed", "nums": []} sv = cf.get_pseudocode() out = [] for i in range(len(sv)): plain = ida_lines.tag_remove(sv[i].line) compact = _idatui_compact(plain) for rec in _idatui_pc_nums(cf, sv[i]): - out.append({ - "line": i, - "x0": _idatui_compact_col(plain, compact, rec["x0"]), - "x1": _idatui_compact_col(plain, compact, rec["x1"]), - "ea": hex(rec["ea"]), "opnum": rec["opnum"], - "value": hex(rec["value"]), "fmt": rec["fmt"], - "shown": rec["shown"], "user": bool(rec["user"]), - }) + out.append( + { + "line": i, + "x0": _idatui_compact_col(plain, compact, rec["x0"]), + "x1": _idatui_compact_col(plain, compact, rec["x1"]), + "ea": hex(rec["ea"]), + "opnum": rec["opnum"], + "value": hex(rec["value"]), + "fmt": rec["fmt"], + "shown": rec["shown"], + "user": bool(rec["user"]), + } + ) return {"addr": hex(f.start_ea), "nums": out, "lines": len(sv)} def pc_num_format( addr: Annotated[str, "Function address (or any address inside it)"], - mode: Annotated[str, "cycle | back | show | hex | dec | oct | char | default"] = "cycle", + mode: Annotated[ + str, "cycle | back | show | hex | dec | oct | char | default" + ] = "cycle", line: Annotated[int, "0-based pseudocode line index"] = -1, col: Annotated[int, "Cursor column in the DISPLAYED line (-1: first literal)"] = -1, ea: Annotated[str, "Address of the number instead of line/col"] = "", @@ -1431,8 +1612,9 @@ def pc_num_format( return {"addr": hex(f.start_ea), "error": str(e)} for i in range(len(sv)): for rec in _idatui_pc_nums(cf, sv[i]): - if rec["ea"] == want_ea and (int(opnum) < 0 - or rec["opnum"] == int(opnum)): + if rec["ea"] == want_ea and ( + int(opnum) < 0 or rec["opnum"] == int(opnum) + ): target, line = rec, i break if target: @@ -1446,26 +1628,42 @@ def pc_num_format( target = next((r for r in nums if r["x0"] <= x < r["x1"]), None) target = target or nums[0] else: - return {"addr": hex(f.start_ea), - "error": f"line {line} is outside the {len(sv)}-line decompilation"} + return { + "addr": hex(f.start_ea), + "error": f"line {line} is outside the {len(sv)}-line decompilation", + } if target is None: - return {"addr": hex(f.start_ea), "line": line, - "text": (ida_lines.tag_remove(sv[line].line).strip() - if 0 <= line < len(sv) else ""), - "error": "no number literal on this line"} + return { + "addr": hex(f.start_ea), + "line": line, + "text": ( + ida_lines.tag_remove(sv[line].line).strip() + if 0 <= line < len(sv) + else "" + ), + "error": "no number literal on this line", + } cur, value = target["fmt"], target["value"] - choices = [c for c in _IDATUI_PC_FMT_CYCLE - if c != "char" or _idatui_printable(value)] + choices = [ + c for c in _IDATUI_PC_FMT_CYCLE if c != "char" or _idatui_printable(value) + ] # Same rule as the listing: one ring per literal, every step. A format the # ring can't hold (an enum set in the GUI) is reported on the way out # instead of being kept for one lap and then lost. lossy = cur not in choices and cur != "default" - out = {"addr": hex(f.start_ea), "ea": hex(target["ea"]), - "opnum": target["opnum"], "line": line, "prev": cur, - "format": cur, "shown": target["shown"], "choices": choices, - "value": hex(value), - "before": ida_lines.tag_remove(sv[line].line).strip()} + out = { + "addr": hex(f.start_ea), + "ea": hex(target["ea"]), + "opnum": target["opnum"], + "line": line, + "prev": cur, + "format": cur, + "shown": target["shown"], + "choices": choices, + "value": hex(value), + "before": ida_lines.tag_remove(sv[line].line).strip(), + } mode = str(mode or "cycle").lower() if mode == "show": @@ -1481,13 +1679,16 @@ def pc_num_format( else: want = mode if want in ("bin", "offset", "stack", "seg", "float"): - out["error"] = (f"Hex-Rays has no {want} format for a number " - f"-- set it on the listing instead") + out["error"] = ( + f"Hex-Rays has no {want} format for a number " + f"-- set it on the listing instead" + ) out["text"] = out["before"] return out if want not in ("hex", "dec", "oct", "char", "default"): - out["error"] = (f"unknown format {mode!r}; one of hex, dec, oct, " - f"char, default") + out["error"] = ( + f"unknown format {mode!r}; one of hex, dec, oct, char, default" + ) out["text"] = out["before"] return out @@ -1499,8 +1700,9 @@ def pc_num_format( ida_hexrays.user_numforms_erase(cf.numforms, it) if want != "default": nf = ida_hexrays.number_format_t(target["opnum"]) - nf.flags = ida_bytes.get_operand_flag(_idatui_fmt_nibbles()[want], - target["opnum"]) + nf.flags = ida_bytes.get_operand_flag( + _idatui_fmt_nibbles()[want], target["opnum"] + ) try: nf.org_nbytes = target["nbytes"] except Exception: @@ -1515,14 +1717,18 @@ def pc_num_format( out["format"] = want out["applied"] = True if lossy: - out["warn"] = (f"this number was {cur}, which names a type a radix " - f"can't put back -- reassign it in IDA") + out["warn"] = ( + f"this number was {cur}, which names a type a radix " + f"can't put back -- reassign it in IDA" + ) try: - cf2 = ida_hexrays.decompile(f.start_ea, - flags=ida_hexrays.DECOMP_NO_CACHE) + cf2 = ida_hexrays.decompile(f.start_ea, flags=ida_hexrays.DECOMP_NO_CACHE) sv2 = cf2.get_pseudocode() if cf2 is not None else None - out["text"] = (ida_lines.tag_remove(sv2[line].line).strip() - if sv2 is not None and line < len(sv2) else out["before"]) + out["text"] = ( + ida_lines.tag_remove(sv2[line].line).strip() + if sv2 is not None and line < len(sv2) + else out["before"] + ) except Exception as e: out["text"] = out["before"] out["warn"] = f"re-render failed: {e}" @@ -1534,7 +1740,7 @@ def decompile(addr, include_addresses=True): Faithful to the tool ida-tui was written against, and in particular to its COST: the per-line address anchor comes from ONE ``get_line_item`` at column - 0 per line. The Code Mode port asked for the full per-column line map (what + 0 per line. The IDA Nexus port asked for the full per-column line map (what ``decomp_map`` is for) purely to fill in that anchor, which is thousands of ``get_line_item``+``dstr()`` calls per function instead of one per line, and made every pseudocode open cost the same as opening the split view. @@ -1557,11 +1763,17 @@ def decompile(addr, include_addresses=True): try: cfunc = ida_hexrays.decompile_func(fn, failure) except Exception as e: - return {"addr": hex(int(fn.start_ea)), "code": None, - "error": f"Decompilation failed at {ea:#x}: {e}"} + return { + "addr": hex(int(fn.start_ea)), + "code": None, + "error": f"Decompilation failed at {ea:#x}: {e}", + } if cfunc is None: - return {"addr": hex(int(fn.start_ea)), "code": None, - "error": failure.desc() or f"Decompilation failed at {ea:#x}"} + return { + "addr": hex(int(fn.start_ea)), + "code": None, + "error": failure.desc() or f"Decompilation failed at {ea:#x}", + } lines = [] for sl in cfunc.get_pseudocode(): @@ -1569,7 +1781,9 @@ def decompile(addr, include_addresses=True): item = ida_hexrays.ctree_item_t() tail = ida_hexrays.ctree_item_t() line_ea = None - if include_addresses and cfunc.get_line_item(sl.line, 0, False, head, item, tail): + if include_addresses and cfunc.get_line_item( + sl.line, 0, False, head, item, tail + ): parts = (item.dstr() or "").split(": ") if len(parts) == 2: try: @@ -1595,9 +1809,13 @@ def decompile(addr, include_addresses=True): text = raw.decode("utf-8", "replace") if raw else None except Exception: text = None - refs.append({"addr": hex(target), - "name": ida_name.get_name(target) or "", - "string": text}) + refs.append( + { + "addr": hex(target), + "name": ida_name.get_name(target) or "", + "string": text, + } + ) return 0 try: @@ -1615,6 +1833,7 @@ def decomp_map( swept across the line's columns via get_line_item. Shape: {addr, lines:[{ea: primary|None, eas:[hex,...]}, ...]}.""" import ida_hexrays + try: ea = int(str(addr), 16) except ValueError: @@ -1691,3 +1910,32 @@ def decomp_map( eas.append(hex(e)) lines.append({"ea": eas[0] if eas else None, "eas": eas}) return {"addr": hex(func.start_ea), "lines": lines} + + +def profile_remote(operation, args, reps=5): + """Profile one persistent remote tool entirely inside IDA.""" + import cProfile + import io + import pstats + + functions = { + "heads": heads, + "segment_index": segment_index, + "op_format": op_format, + "pc_nums": pc_nums, + "decompile": decompile, + "decomp_map": decomp_map, + "pc_num_format": pc_num_format, + } + function = functions.get(str(operation)) + if function is None: + raise ValueError(f"unknown profile operation: {operation!r}") + profiler = cProfile.Profile() + for _ in range(max(1, int(reps))): + profiler.runcall(function, **dict(args)) + stats = pstats.Stats(profiler) + output = io.StringIO() + stats.stream = output + stats.sort_stats("tottime") + stats.print_stats(80) + return {"stats": output.getvalue(), "total": stats.total_tt, "reps": reps} |
