diff options
| author | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-09-23 12:11:04 -0400 |
|---|---|---|
| committer | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-09-23 12:11:04 -0400 |
| commit | 21b8ebc9db1d0ae649d6406087ea22b54e3b5775 (patch) | |
| tree | 192aa5d3c76e0cf3a785936d9902c6848609d37e /userspace/cuda/tools/annotate_corpus.py | |
| parent | 631bd20c63a4058775a4fa549b9473915c918020 (diff) | |
| download | open-iov-21b8ebc9db1d0ae649d6406087ea22b54e3b5775.tar.gz open-iov-21b8ebc9db1d0ae649d6406087ea22b54e3b5775.zip | |
userspace/cuda: add CUDA userspace driver analysis, tooling and name maps
Add the analysis of the NVIDIA CUDA userspace driver used by Open-IOV VMs:
* docs/01-08: architecture (layers, object model, threads, sync, memory,
channels, launch), build and execution model, the driver/kernel/GPU
interface contract, virtualization approaches, the 570.144 decompilation
map with the corrected libcuda crash path and the current engine-topology
blocker, tooling, and a version-upgrade playbook.
* tools/: version-agnostic toolchain - stdlib ELF/interface toolkit
(exports, unwind-table function map, wrapper resolution, version delta,
crash address decode), callgraph walker, corpus annotator, and Ghidra
analysis/decompilation/name-map scripts with resumable parallel drivers.
* 570.144/annotations/: our name map (exports + resolved wrappers + verified
internals) and subsystem index, tracked so naming work accumulates.
* READ-ME-FIRST.md: reading guide with the fast lookup recipes.
The full decompiled corpus and annotated corpus stay untracked (vendor-
derived, gitignored under the version run/ tree).
Diffstat (limited to 'userspace/cuda/tools/annotate_corpus.py')
| -rwxr-xr-x | userspace/cuda/tools/annotate_corpus.py | 155 |
1 files changed, 155 insertions, 0 deletions
diff --git a/userspace/cuda/tools/annotate_corpus.py b/userspace/cuda/tools/annotate_corpus.py new file mode 100755 index 0000000..972f923 --- /dev/null +++ b/userspace/cuda/tools/annotate_corpus.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# +# annotate_corpus.py - turn the raw decompiled corpus into an annotated, +# human-navigable corpus. +# +# For every function in the decompiled buckets it rewrites the header comment +# with: +# * the recovered name (from a names.tsv map) or the Ghidra FUN_ name; +# * file virtual address and size; +# * direct callers and callees (from callgraph.tsv), labelled with names; +# * a "seen in" marker for functions referenced by the docs (optional). +# It also substitutes recovered names for FUN_<addr> references inside the +# function bodies, so a renamed function is readable everywhere. +# +# Inputs (produced by the toolchain): +# <outdir>/buckets/bucket_*.c raw decompiled C +# <outdir>/decompiled_index.tsv addr file_addr size name file line status +# <indexdir>/callgraph.tsv caller callee caller_file callee_file +# <names.tsv> file_vaddr<TAB>name +# +# Usage: +# annotate_corpus.py <outdir> <indexdir> <names.tsv> <annotated_outdir> +# +# The output directory is gitignored like the raw corpus; this tool is tracked +# and reusable. Addresses are file virtual addresses in all inputs; the corpus +# uses Ghidra program addresses, so the tool translates via the index's +# addr/file_addr columns (no hard-coded image base). + +import os +import re +import sys +from collections import defaultdict + + +def load_index(path): + rows = {} + with open(path) as f: + header = f.readline() + for line in f: + p = line.rstrip("\n").split("\t") + if len(p) < 6: + continue + prog = int(p[0], 16) + rows[prog] = dict(file=int(p[1], 16), size=int(p[2]), + name=p[3], file_name=p[4], line=int(p[5])) + return rows + + +def load_callgraph(path): + callees = defaultdict(list) + callers = defaultdict(list) + with open(path) as f: + f.readline() + for line in f: + p = line.rstrip("\n").split("\t") + if len(p) < 4: + continue + cf, kf = int(p[2], 16), int(p[3], 16) + callees[cf].append(kf) + callers[kf].append(cf) + return callees, callers + + +def load_names(path): + names = {} + 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) >= 2: + try: + names[int(p[0], 16)] = p[1] + except ValueError: + pass + return names + + +def label(file_addr, names, index): + name = names.get(file_addr) + if name: + return f"{name} (libcuda+0x{file_addr:x})" + return f"libcuda+0x{file_addr:x}" + + +def main(): + if len(sys.argv) < 5: + print(__doc__) + sys.exit(2) + outdir, indexdir, namemap, out = sys.argv[1:5] + index = load_index(os.path.join(outdir, "decompiled_index.tsv")) + callees, callers = load_callgraph(os.path.join(indexdir, "callgraph.tsv")) + names = load_names(namemap) + # program address -> recovered name, for body substitution + prog_names = {p: names[r["file"]] for p, r in index.items() if r["file"] in names} + + os.makedirs(os.path.join(out, "buckets"), exist_ok=True) + n = 0 + for bucket in sorted(os.listdir(os.path.join(outdir, "buckets"))): + if not bucket.endswith(".c"): + continue + src = os.path.join(outdir, "buckets", bucket) + dst = os.path.join(out, "buckets", bucket) + with open(src) as f: + text = f.read() + + def annotate(m): + nonlocal n + prog = int(m.group("addr"), 16) + file_addr = int(m.group("file"), 16) + size = int(m.group("size")) + rec = index.get(prog, {}) + name = names.get(file_addr, rec.get("name", f"FUN_{prog:08x}")) + cl = sorted(set(callees.get(file_addr, []))) + cr = sorted(set(callers.get(file_addr, []))) + lines = [ + "/* ------------------------------------------------------------------", + f" * {name}", + f" * file_addr=libcuda+0x{file_addr:x} size={size}", + ] + if cr: + shown = [label(a, names, index) for a in cr[:8]] + more = f" (+{len(cr)-8} more)" if len(cr) > 8 else "" + lines.append(" * called by: " + "; ".join(shown) + more) + if cl: + shown = [label(a, names, index) for a in cl[:8]] + more = f" (+{len(cl)-8} more)" if len(cl) > 8 else "" + lines.append(" * calls: " + "; ".join(shown) + more) + lines.append(" *----------------------------------------------------------------- */") + n += 1 + return "\n" + "\n".join(lines) + "\n" + + text = re.sub( + r"\n/\* -+\n \* (?P<name>[^\n]*)\n \* addr=(?P<addr>0x[0-9a-f]+) " + r"file_addr=libcuda\+(?P<file>0x[0-9a-f]+) size=(?P<size>\d+)\n" + r" \*-+ \*/", + annotate, text) + + if prog_names: + def sub(m): + a = int(m.group(1), 16) + return prog_names.get(a, m.group(0)) + text = re.sub(r"\bFUN_([0-9a-f]{8})\b", sub, text) + + with open(dst, "w") as f: + f.write(text) + + print(f"[annotate_corpus] annotated {n} functions -> {out}/buckets") + print(f"[annotate_corpus] names applied: {len(names)}") + + +if __name__ == "__main__": + main() |
