aboutsummaryrefslogtreecommitdiffstats
path: root/experiments/inproc_spike.py
diff options
context:
space:
mode:
Diffstat (limited to 'experiments/inproc_spike.py')
-rw-r--r--experiments/inproc_spike.py112
1 files changed, 79 insertions, 33 deletions
diff --git a/experiments/inproc_spike.py b/experiments/inproc_spike.py
index 469aa21..1b12204 100644
--- a/experiments/inproc_spike.py
+++ b/experiments/inproc_spike.py
@@ -25,6 +25,7 @@ Key facts this spike encodes (all verified):
another process, so the TUI's event loop never blocks. In-process, a slow
call (decompile ~150ms, analysis seconds) blocks the UI for its duration.
"""
+
from __future__ import annotations
import argparse
@@ -60,13 +61,21 @@ class DirectBackend:
def __init__(self, path: str) -> None:
import idapro
+
idapro.enable_console_messages(False)
t = time.time()
rc = idapro.open_database(path, run_auto_analysis=True)
self.open_secs = time.time() - t
if rc:
raise RuntimeError(f"open_database({path!r}) failed rc={rc}")
- import ida_bytes, ida_funcs, ida_hexrays, ida_name, idaapi, idautils, idc
+ import ida_bytes
+ import ida_funcs
+ import ida_hexrays
+ import ida_name
+ import idaapi
+ import idautils
+ import idc
+
self._idapro = idapro
self.idaapi, self.idautils, self.idc = idaapi, idautils, idc
self.ida_bytes, self.ida_hexrays = ida_bytes, ida_hexrays
@@ -108,6 +117,7 @@ class McpBackend:
if REPO not in sys.path: # idalib's init can reset sys.path out from under us
sys.path.insert(0, REPO)
from idatui.client import IDAClient
+
self.IDAClient = IDAClient
self.c = IDAClient(url, db=db)
self.c.connect()
@@ -150,8 +160,12 @@ class McpBackend:
return r.get("code", "") if isinstance(r, dict) else str(r)
def xrefs_to(self, ea):
- r = self.c.call("xref_query", queries=[{"addr": hex(ea), "direction": "to",
- "include_fn": True, "count": 2000}])
+ r = self.c.call(
+ "xref_query",
+ queries=[
+ {"addr": hex(ea), "direction": "to", "include_fn": True, "count": 2000}
+ ],
+ )
res = r.get("result", []) if isinstance(r, dict) else []
refs = res[0].get("refs", []) if res and isinstance(res[0], dict) else []
return [int(x["frm"], 16) for x in refs if x.get("frm")]
@@ -200,14 +214,21 @@ def _recv(sock):
return _unpack(_recvn(sock, n))
-_WORKER_OPS = ("functions", "resolve", "read_bytes", "disasm_line",
- "decompile", "xrefs_to")
+_WORKER_OPS = (
+ "functions",
+ "resolve",
+ "read_bytes",
+ "disasm_line",
+ "decompile",
+ "xrefs_to",
+)
def _worker_main(sockpath: str, dbpath: str) -> None:
"""Runs in a child process. Opens idalib on ITS main thread (constraint
satisfied), then serves one client serially over a unix socket."""
import socket as sk
+
direct = DirectBackend(dbpath)
ops = {name: getattr(direct, name) for name in _WORKER_OPS}
try:
@@ -237,10 +258,17 @@ class UnixWorkerBackend:
def __init__(self, dbpath: str) -> None:
import socket as sk
+
self.sockpath = f"/tmp/inproc_spike_{os.getpid()}.sock"
self.proc = __import__("subprocess").Popen(
- [sys.executable, os.path.abspath(__file__),
- "--worker", self.sockpath, dbpath])
+ [
+ sys.executable,
+ os.path.abspath(__file__),
+ "--worker",
+ self.sockpath,
+ dbpath,
+ ]
+ )
deadline = time.time() + 120
self.sock = None
while time.time() < deadline:
@@ -263,12 +291,23 @@ class UnixWorkerBackend:
raise RuntimeError(val)
return val
- def functions(self): return self._call("functions")
- def resolve(self, name): return self._call("resolve", name)
- def read_bytes(self, ea, n): return self._call("read_bytes", ea, n)
- def disasm_line(self, ea): return self._call("disasm_line", ea)
- def decompile(self, ea): return self._call("decompile", ea)
- def xrefs_to(self, ea): return self._call("xrefs_to", ea)
+ def functions(self):
+ return self._call("functions")
+
+ def resolve(self, name):
+ return self._call("resolve", name)
+
+ def read_bytes(self, ea, n):
+ return self._call("read_bytes", ea, n)
+
+ def disasm_line(self, ea):
+ return self._call("disasm_line", ea)
+
+ def decompile(self, ea):
+ return self._call("decompile", ea)
+
+ def xrefs_to(self, ea):
+ return self._call("xrefs_to", ea)
def close(self):
try:
@@ -321,6 +360,7 @@ def bench(target: str, n: int) -> None:
try:
import socket
+
socket.create_connection(("127.0.0.1", 8745), 0.3).close()
backends["mcp"] = McpBackend()
print("benching the running mcp server on :8745 too", flush=True)
@@ -338,15 +378,15 @@ def bench(target: str, n: int) -> None:
return sample[_nx[0]]
ops = {
- "resolve(main)": (lambda b: b.resolve("main"), n),
- "read_bytes(16)": (lambda b: b.read_bytes(main, 16), n),
- "read_bytes(4096)": (lambda b: b.read_bytes(main, 4096), n),
- "disasm_line": (lambda b: b.disasm_line(main), n),
- "xrefs_to": (lambda b: b.xrefs_to(_next_sample()), min(n, 200)),
- "decompile(cached)": (lambda b: b.decompile(main), min(n, 40)),
+ "resolve(main)": (lambda b: b.resolve("main"), n),
+ "read_bytes(16)": (lambda b: b.read_bytes(main, 16), n),
+ "read_bytes(4096)": (lambda b: b.read_bytes(main, 4096), n),
+ "disasm_line": (lambda b: b.disasm_line(main), n),
+ "xrefs_to": (lambda b: b.xrefs_to(_next_sample()), min(n, 200)),
+ "decompile(cached)": (lambda b: b.decompile(main), min(n, 40)),
}
names = list(backends)
- hdr = f"{'op':20}" + "".join(f"{nm+' us':>14}" for nm in names)
+ hdr = f"{'op':20}" + "".join(f"{nm + ' us':>14}" for nm in names)
print(hdr)
print("-" * len(hdr))
results: dict[str, dict[str, float]] = {nm: {} for nm in names}
@@ -371,7 +411,7 @@ def bench(target: str, n: int) -> None:
if nm == "mcp":
continue
v = results[nm].get(label, float("nan"))
- parts.append(f"{nm} {m/v:.0f}x" if v == v and v else f"{nm} -")
+ parts.append(f"{nm} {m / v:.0f}x" if v == v and v else f"{nm} -")
print(f" {label:20} {' '.join(parts)}")
for b in backends.values():
@@ -408,10 +448,12 @@ def _build_spike_app(backend: "DirectBackend"):
for ea, nm in backend.functions():
ol.add_option(Option(f"{ea:08x} {nm}", id=str(ea)))
yield ol
- yield Static("select a function, press F5 to decompile inline",
- id="code")
- yield Static("in-process idalib — every call runs on the UI thread",
- id="status")
+ yield Static(
+ "select a function, press F5 to decompile inline", id="code"
+ )
+ yield Static(
+ "in-process idalib — every call runs on the UI thread", id="status"
+ )
yield Footer()
def on_mount(self):
@@ -428,13 +470,13 @@ def _build_spike_app(backend: "DirectBackend"):
if ea is None:
return
t = time.perf_counter()
- code = backend.decompile(ea) # <-- BLOCKS the event loop
+ code = backend.decompile(ea) # <-- BLOCKS the event loop
dt = (time.perf_counter() - t) * 1e3
- self.query_one("#code", Static).update(
- "\n".join(code.splitlines()[:40]))
+ self.query_one("#code", Static).update("\n".join(code.splitlines()[:40]))
self.query_one("#status", Static).update(
f"decompiled {backend.idc.get_func_name(ea)} in {dt:.0f} ms "
- f"(UI was frozen for those {dt:.0f} ms)")
+ f"(UI was frozen for those {dt:.0f} ms)"
+ )
def action_decompile_all(self):
funcs = backend.functions()
@@ -442,14 +484,15 @@ def _build_spike_app(backend: "DirectBackend"):
n = 0
for ea, _ in funcs:
try:
- backend.decompile(ea) # <-- long, uninterruptible freeze
+ backend.decompile(ea) # <-- long, uninterruptible freeze
n += 1
except Exception: # noqa: BLE001
pass
dt = (time.perf_counter() - t) * 1e3
self.query_one("#status", Static).update(
f"decompiled {n} funcs in {dt:.0f} ms — the whole UI was frozen "
- f"the entire time (no spinner, no input)")
+ f"the entire time (no spinner, no input)"
+ )
def action_bytes(self):
ea = self._sel_ea()
@@ -459,7 +502,8 @@ def _build_spike_app(backend: "DirectBackend"):
b = backend.read_bytes(ea, 64)
dt = (time.perf_counter() - t) * 1e6
self.query_one("#status", Static).update(
- f"read 64 bytes in {dt:.1f} us: {b[:16].hex()}…")
+ f"read 64 bytes in {dt:.1f} us: {b[:16].hex()}…"
+ )
return Spike()
@@ -472,7 +516,9 @@ def _open_copy(target: str) -> "DirectBackend":
os.remove(tmp + e)
except OSError:
pass
- print("opening in-process (blocks the terminal until analysis is done)…", flush=True)
+ print(
+ "opening in-process (blocks the terminal until analysis is done)…", flush=True
+ )
return DirectBackend(tmp) # MAIN THREAD open, before the event loop starts