diff options
| author | blasty <blasty@local> | 2026-08-06 15:11:54 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-08-06 15:11:54 +0200 |
| commit | 4fbd6b61eaa3461db673b220e2376b3d1e922a9b (patch) | |
| tree | 6b8251b47b075c7c0f14569549c19abbf22656c7 /experiments/cfg_dump.py | |
| parent | tests: graph scenarios (diff) | |
| download | ida-tui-4fbd6b61eaa3461db673b220e2376b3d1e922a9b.tar.gz ida-tui-4fbd6b61eaa3461db673b220e2376b3d1e922a9b.tar.xz ida-tui-4fbd6b61eaa3461db673b220e2376b3d1e922a9b.zip | |
graph: docs, and the offline layout tools
cfg_dump freezes real CFGs to JSON; graph_spike renders one or --stats a
whole corpus through the shipping engine; graph_smoke is the end-to-end
tool->domain->layout check; graph_shot renders the real view headless at a
chosen size, which is the only sane way to judge it (a tiled pane is far
too narrow and the minimap sits on top of the graph).
Diffstat (limited to 'experiments/cfg_dump.py')
| -rw-r--r-- | experiments/cfg_dump.py | 119 |
1 files changed, 119 insertions, 0 deletions
diff --git a/experiments/cfg_dump.py b/experiments/cfg_dump.py new file mode 100644 index 0000000..30b6ba9 --- /dev/null +++ b/experiments/cfg_dump.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Dump real basic-block CFGs to JSON, so graph-layout work can be done offline. + +Run with a python that has ``idapro`` (i.e. the worker python, /usr/bin/python3): + + /usr/bin/python3 experiments/cfg_dump.py targets/echo -o /tmp/cfg-echo.json + +Copies the binary to a scratch dir first (never touches the tracked .i64), opens +it with idalib, and writes one record per function: + + {"name","ea","blocks":[{"id","start","end","lines":[str],"succs":[[id,kind]]}]} + +``kind`` is "fall" (falls through to the next address), "jump" (unconditional or +taken branch) or "switch" (n-way). That is exactly the input a graph view needs; +everything after this point (layering, ordering, routing, rendering) is pure +python and needs no IDA at all. +""" +from __future__ import annotations + +import argparse +import json +import os +import shutil +import sys +import tempfile + + +def dump(path: str, want: list[str], max_blocks: int) -> list[dict]: + import idapro + + scratch = tempfile.mkdtemp(prefix="cfgdump-") + local = os.path.join(scratch, os.path.basename(path)) + shutil.copy2(path, local) + if idapro.open_database(local, run_auto_analysis=True) != 0: + raise SystemExit(f"failed to open {local}") + + import ida_funcs + import ida_gdl + import ida_lines + import ida_bytes + import idautils + import idaapi + + out = [] + try: + for fea in idautils.Functions(): + fn = ida_funcs.get_func(fea) + if not fn: + continue + name = ida_funcs.get_func_name(fea) + if want and not any(w in name for w in want): + continue + fc = ida_gdl.FlowChart(fn, flags=ida_gdl.FC_PREDS) + blocks = [] + index = {} + for i, bb in enumerate(fc): + index[bb.start_ea] = i + for bb in fc: + lines = [] + ea = bb.start_ea + while ea < bb.end_ea and ea != idaapi.BADADDR: + txt = ida_lines.tag_remove( + ida_lines.generate_disasm_line(ea, 0) or "") + lines.append(txt.rstrip()) + nxt = ida_bytes.next_head(ea, bb.end_ea) + if nxt <= ea: + break + ea = nxt + succs = [] + sl = list(bb.succs()) + for s in sl: + if s.start_ea not in index: + continue + if len(sl) > 2: + kind = "switch" + elif s.start_ea == bb.end_ea: + kind = "fall" + else: + kind = "jump" + succs.append([index[s.start_ea], kind]) + blocks.append({ + "id": index[bb.start_ea], + "start": bb.start_ea, + "end": bb.end_ea, + "lines": lines, + "succs": succs, + }) + if max_blocks and len(blocks) > max_blocks: + continue + out.append({"name": name, "ea": fea, "blocks": blocks}) + finally: + idapro.close_database(save=False) + shutil.rmtree(scratch, ignore_errors=True) + return out + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("binary") + ap.add_argument("-o", "--out", default="/tmp/cfg.json") + ap.add_argument("-f", "--func", action="append", default=[], + help="only functions whose name contains this (repeatable)") + ap.add_argument("--max-blocks", type=int, default=0, + help="skip functions with more blocks than this") + args = ap.parse_args() + + recs = dump(os.path.abspath(args.binary), args.func, args.max_blocks) + recs.sort(key=lambda r: len(r["blocks"]), reverse=True) + with open(args.out, "w") as f: + json.dump(recs, f) + tot = sum(len(r["blocks"]) for r in recs) + print(f"{len(recs)} functions, {tot} blocks -> {args.out}") + for r in recs[:12]: + print(f" {len(r['blocks']):4d} blocks {r['name']}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) |
