diff options
| author | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-09-23 13:48:00 -0400 |
|---|---|---|
| committer | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-09-23 13:48:00 -0400 |
| commit | c587eac377c1c5b720142edadebaace5cb1b17c0 (patch) | |
| tree | c381be95dd02151022f58c3e84c6eeb0dfe91b25 /userspace/cuda/tools/subsystem_digest.py | |
| parent | bd6c3ee69d7157e2dabf9350be52367c6a6e3c42 (diff) | |
| download | open-iov-c587eac377c1c5b720142edadebaace5cb1b17c0.tar.gz open-iov-c587eac377c1c5b720142edadebaace5cb1b17c0.zip | |
userspace/cuda: complete the analysis pass
* Name recovery: name_candidates.py proposes the hand-written API-layer
implementation for each exported wrapper by grouping version variants and
intersecting their callees; 180 high-confidence proposals recorded in
annotations/name-candidates.tsv, 29 of them body-verified and promoted to
names.tsv with evidence in subsystems.tsv.
* Engine records: identified the 0x14-byte per-engine descriptor, mapped its
fields (kind, engine type, instance, class id, CE caps) and the existence
callback, and documented how the missing async copy engine (COPY14) leaves
the secondary record zeroed and blocks context creation (docs/05 section
3.7, public-safe).
* Targeted type recovery: AnalyzeFunctions.java runs the parameter-ID
analyzer over a curated list of 80 functions; 16 improved materially and
the typed decompilations are kept under the version run tree.
* Cross-version: cudatool.py fdematch measures how names transfer between
patch versions (570.144 -> 570.211.01: exports 100%, verified internals
~70%, generated wrappers must be re-resolved by relocation, never by
address arithmetic); documented in the upgrade playbook.
* Corpus tooling: diff_corpus.py (version-to-version corpus diff) and
subsystem_digest.py (per-subsystem call trees).
* Deprecate the old per-version helpers whose address handling and analysis
timeout produced wrong results.
Generated corpora and typed outputs remain untracked under the version run
tree.
Diffstat (limited to 'userspace/cuda/tools/subsystem_digest.py')
| -rwxr-xr-x | userspace/cuda/tools/subsystem_digest.py | 190 |
1 files changed, 190 insertions, 0 deletions
diff --git a/userspace/cuda/tools/subsystem_digest.py b/userspace/cuda/tools/subsystem_digest.py new file mode 100755 index 0000000..68f0d09 --- /dev/null +++ b/userspace/cuda/tools/subsystem_digest.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# +# subsystem_digest.py - per-subsystem call-tree digest from the tracked +# subsystem annotations plus the callgraph index. +# +# For every subsystem in subsystems.tsv it prints: +# * members (address, name, role) - the annotated functions; +# * entry functions - members that are not called by another member of the +# same subsystem (the roots of the subsystem's call tree); +# * a depth-limited call tree from each entry, with the direct callees of a +# node grouped by their subsystem; +# * a cross-subsystem edge summary (how many calls each subsystem makes into +# every other one) - the fastest way to see the layering. +# +# The digest is a navigation aid: it shows where to start reading the +# decompiled corpus for a subsystem and which other subsystems it touches, +# without opening Ghidra. Addresses are FILE virtual addresses (libcuda+0x...). +# +# Usage: +# subsystem_digest.py <subsystems.tsv> <callgraph.tsv> [--out FILE] +# [--depth N] [--max-per-node N] [--subsystem NAME] +# +# <subsystems.tsv> format (comments allowed): +# file_vaddr<TAB>name<TAB>subsystem<TAB>role<TAB>evidence +# <callgraph.tsv> format (header): +# caller<TAB>callee<TAB>caller_file<TAB>callee_file +# +# SPDX-License-Identifier: BSD-3-Clause + +import argparse +import sys +from collections import defaultdict + + +def load_subsystems(path): + members = {} + with open(path) as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + p = line.split("\t") + if len(p) < 4: + continue + try: + addr = int(p[0], 16) + except ValueError: + continue + members[addr] = dict(name=p[1], subsystem=p[2], + role=p[3], + evidence=p[4] if len(p) > 4 else "") + return members + + +def load_callgraph(path): + callees = defaultdict(set) + callers = defaultdict(set) + with open(path) as f: + f.readline() + for line in f: + p = line.rstrip("\n").split("\t") + if len(p) < 4: + continue + try: + cf, kf = int(p[2], 16), int(p[3], 16) + except ValueError: + continue + callees[cf].add(kf) + callers[kf].add(cf) + return callees, callers + + +def main(): + ap = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("subsystems") + ap.add_argument("callgraph") + ap.add_argument("--out", default=None, + help="write the digest here (default: stdout)") + ap.add_argument("--depth", type=int, default=2, + help="call-tree depth below each entry (default 2)") + ap.add_argument("--max-per-node", type=int, default=8, + help="max callees shown per node (default 8)") + ap.add_argument("--subsystem", default=None, + help="only digest this subsystem") + args = ap.parse_args() + + members = load_subsystems(args.subsystems) + callees, callers = load_callgraph(args.callgraph) + + by_sub = defaultdict(dict) + for addr, m in members.items(): + by_sub[m["subsystem"]][addr] = m + + def name_of(a): + m = members.get(a) + if m: + return f"{m['name']} (libcuda+0x{a:x})" + return f"libcuda+0x{a:x}" + + def sub_of(a): + m = members.get(a) + return m["subsystem"] if m else "unknown" + + out = [] + out.append("# Subsystem call-tree digest (generated by tools/subsystem_digest.py)") + out.append(f"# subsystems.tsv: {args.subsystems}") + out.append(f"# callgraph.tsv : {args.callgraph}") + out.append("# Addresses are FILE virtual addresses (libcuda+0x...).") + out.append("# entry = annotated member with no caller inside its own subsystem.") + out.append("") + + # cross-subsystem edge counts + cross = defaultdict(int) + for addr, m in members.items(): + for c in callees.get(addr, ()): + cross[(m["subsystem"], sub_of(c))] += 1 + + selected = sorted(by_sub) + if args.subsystem: + selected = [s for s in selected if s == args.subsystem] + if not selected: + print(f"error: subsystem {args.subsystem!r} not in {args.subsystems}", + file=sys.stderr) + return 1 + + for sub in selected: + ms = by_sub[sub] + out.append("=" * 72) + out.append(f"## {sub} ({len(ms)} annotated members)") + out.append("=" * 72) + out.append("members:") + for a in sorted(ms): + m = ms[a] + out.append(f" libcuda+0x{a:x}\t{m['name']}\t{m['role']}") + entries = sorted(a for a in ms + if not (callers.get(a, set()) & set(ms))) + if not entries: + entries = sorted(ms) + out.append("entries: (no entry: every member is called from inside " + "the subsystem; showing all)") + else: + out.append("entries:") + for a in entries: + out.append(f" {name_of(a)}: {ms[a]['role']}") + out.append("call tree:") + + def walk(a, depth, indent, seen): + kids = sorted(callees.get(a, ())) + if not kids or depth <= 0: + return + groups = defaultdict(list) + for k in kids: + groups[sub_of(k)].append(k) + for g in sorted(groups): + shown = groups[g][:args.max_per_node] + more = f" (+{len(groups[g]) - args.max_per_node} more)" \ + if len(groups[g]) > args.max_per_node else "" + out.append(f"{indent}-> {g}: " + + "; ".join(name_of(k) for k in shown) + more) + for k in kids: + if k in seen: + continue + seen.add(k) + walk(k, depth - 1, indent + " ", seen) + + for a in entries: + out.append(f" {name_of(a)}") + walk(a, args.depth, " ", {a}) + out.append("") + + out.append("=" * 72) + out.append("## cross-subsystem calls (edges from -> to)") + out.append("=" * 72) + for (src, dst) in sorted(cross): + out.append(f" {src:<16} -> {dst:<16} {cross[(src, dst)]}") + + text = "\n".join(out) + if args.out: + with open(args.out, "w") as f: + f.write(text + "\n") + else: + print(text) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) |
