aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/codemode_client.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-08-07 14:08:12 +0200
committerblasty <blasty@local>2026-08-07 14:08:12 +0200
commitecc58d7725db6d4929ae3e299dea4f0202a81946 (patch)
tree896884021fe91ccff747af7aec52d210130a06f5 /idatui/codemode_client.py
parentcodemode: rename takes a LIST of edits per category, not just one (diff)
downloadida-tui-ecc58d7725db6d4929ae3e299dea4f0202a81946.tar.gz
ida-tui-ecc58d7725db6d4929ae3e299dea4f0202a81946.tar.xz
ida-tui-ecc58d7725db6d4929ae3e299dea4f0202a81946.zip
codemode: three defects the A/B benchmark found in the decompiler path
Benchmarking the port against master op-by-op (rather than only asking whether tests pass) turned up three real bugs, all in the most user-visible path: opening pseudocode. 1. decompile was doing decomp_map's job. It called the full per-column line map purely to fill in each line's /*0xEA*/ anchor. The tool ida-tui was written against takes ONE get_line_item at column 0 per line; the port took one per COLUMN, i.e. thousands of get_line_item+dstr() calls per function instead of one per line. Every pseudocode open cost the same as opening the split view. Carried the real implementation over: 1888ms -> 53ms. 2. decomp_map used the pre-optimisation line map. Ours memoises obj_id -> ea for the whole function (commit 853d90c: dstr() was 79% of the tool, and consecutive columns report the same ctree item), the port's did not. 1925ms -> 287ms. 3. _idatui_compact imported ida_pro_mcp on every call. Under Code Mode that package is not installed in the database process, so the import failed every time -- and a FAILED import is never cached, so each one re-searched the whole of sys.path: 422 failed imports per pc_nums call, which was most of its runtime. 1428ms -> 257ms. The same bug was a correctness bug hiding behind the perf bug: the fallback path collapsed whitespace INSIDE string literals, where the real function preserves it. Pseudocode columns are served in those coordinates, so on any line containing a string with two spaces, every literal's mark and every reformat would have been placed on the wrong column. It never fired on master because ida_pro_mcp is installed there. Now calls the byte-identical module-level shim directly, with the deviation from the extracted original documented in place. Narrow verification: decomp/split_view/opfmt/follow/comment/structs scenarios, 72 passed, 0 failed. Full gate running separately.
Diffstat (limited to 'idatui/codemode_client.py')
-rw-r--r--idatui/codemode_client.py65
1 files changed, 8 insertions, 57 deletions
diff --git a/idatui/codemode_client.py b/idatui/codemode_client.py
index 5e8089b..3bf01cc 100644
--- a/idatui/codemode_client.py
+++ b/idatui/codemode_client.py
@@ -171,24 +171,6 @@ def _script(args: dict[str, Any], body: str) -> str:
return f"import json\na = json.loads({encoded!r})\n{dedent(body).strip()}\n"
-_DECOMP_MAP_HELPER = r'''
-def line_map(cfunc):
- import ida_hexrays
- answer = []
- for sl in cfunc.get_pseudocode():
- tagged, eas, seen = sl.line, [], set()
- for x in range(len(tagged) + 1):
- head = ida_hexrays.ctree_item_t(); item = ida_hexrays.ctree_item_t(); tail = ida_hexrays.ctree_item_t()
- if not cfunc.get_line_item(tagged, x, False, head, item, tail): continue
- text = item.dstr() or ""
- try: ea = int(text.split(": ", 1)[0], 16)
- except (ValueError, IndexError): continue
- if ea not in seen: seen.add(ea); eas.append(ea)
- answer.append(eas)
- return answer
-'''
-
-
_OPERATIONS: dict[str, str] = {
"list_funcs": r'''
import fnmatch
@@ -812,45 +794,6 @@ result
}
-_OPERATIONS["decompile"] = _DECOMP_MAP_HELPER + r'''
-ea = int(str(a["addr"]), 16)
-fn = db.functions.get_at(ea)
-if fn is None:
- result = {"error": f"no function at {ea:#x}"}
-else:
- pseudo = db.pseudocode.decompile(fn)
- mapping = line_map(pseudo.raw_cfunc)
- plain = pseudo.to_text()
- marked = [line + (f" /*0x{eas[0]:X}*/" if eas else "")
- for line, eas in zip(plain, mapping)]
- import ida_name
- refs, seen = [], set()
- for expr in pseudo.find_objects():
- target = int(expr.obj_ea)
- if target in seen or not (db.is_valid_ea(target) or db.is_private_ea(target)): continue
- seen.add(target)
- name = expr.obj_name or ida_name.get_name(target) or ""
- try: string = db.bytes.get_string_at(target) if db.is_valid_ea(target) else None
- except Exception: string = None
- refs.append({"addr": hex(target), "name": name, "string": string})
- result = {"addr": hex(int(fn.start_ea)), "code": "\n".join(marked), "refs": refs}
-result
-'''
-
-_OPERATIONS["decomp_map"] = _DECOMP_MAP_HELPER + r'''
-ea = int(str(a["addr"]), 16)
-fn = db.functions.get_at(ea)
-if fn is None:
- result = {"error": f"no function at {ea:#x}"}
-else:
- pseudo = db.pseudocode.decompile(fn)
- mapping = line_map(pseudo.raw_cfunc)
- result = {"addr": hex(int(fn.start_ea)),
- "lines": [{"ea": hex(eas[0]) if eas else None,
- "eas": [hex(item) for item in eas]} for eas in mapping]}
-result
-'''
-
_OPERATIONS["define_code_run"] = r'''
import ida_bytes, ida_idp, ida_segment, ida_ua, idaapi
ea, limit = int(str(a["addr"]), 16), max(1, min(int(a.get("limit", 20000)), 200000))
@@ -877,6 +820,7 @@ else:
result
'''
+
_OPERATIONS["define_func_run"] = r'''
import ida_bytes, ida_funcs, ida_segment
ea = int(str(a["addr"]), 16)
@@ -901,6 +845,7 @@ else:
result
'''
+
_OPERATIONS["set_thumb"] = r'''
import ida_bytes, ida_ida, ida_idp, ida_segment, ida_segregs
ea = int(str(a["addr"]), 16); treg = ida_idp.str2reg("T")
@@ -926,6 +871,7 @@ else:
result
'''
+
_OPERATIONS["thumb_scan"] = r'''
import ida_bytes, ida_funcs, ida_idp, ida_segment, ida_segregs, ida_ua
lo, hi = int(str(a["start"]), 16), int(str(a["end"]), 16)
@@ -950,6 +896,7 @@ result = {"start": hex(lo), "end": hex(hi), "found": found, "applied": applied,
result
'''
+
_OPERATIONS["decomp_error"] = r'''
import ida_hexrays, ida_ida
ea = int(str(a["addr"]), 16); fn = db.functions.get_at(ea)
@@ -1016,6 +963,10 @@ _OPERATIONS["op_format"] = _remote_op(
'op_format(addr=a["addr"], mode=a.get("mode", "cycle"),'
' col=int(a.get("col", -1)), n=int(a.get("n", -1)))')
_OPERATIONS["pc_nums"] = _remote_op('pc_nums(addr=a["addr"])')
+_OPERATIONS["decompile"] = _remote_op(
+ 'decompile(addr=a["addr"],'
+ ' include_addresses=bool(a.get("include_addresses", True)))')
+_OPERATIONS["decomp_map"] = _remote_op('decomp_map(addr=a["addr"])')
_OPERATIONS["pc_num_format"] = _remote_op(
'pc_num_format(addr=a["addr"], mode=a.get("mode", "cycle"),'
' line=int(a.get("line", -1)), col=int(a.get("col", -1)),'