From f874d709e5a45d63c8ee9ab6b0300195aa5d20a5 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Rasmusson Date: Wed, 23 Sep 2026 12:11:04 -0400 Subject: 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). --- userspace/cuda/tools/annotate_corpus.py | 155 +++++++ userspace/cuda/tools/callwalk.py | 133 ++++++ userspace/cuda/tools/cudatool.py | 515 +++++++++++++++++++++ userspace/cuda/tools/ghidra/ApplyNameMap.java | 86 ++++ userspace/cuda/tools/ghidra/BatchDecompile.java | 141 ++++++ userspace/cuda/tools/ghidra/ConfigureAnalysis.java | 84 ++++ userspace/cuda/tools/ghidra/DecompileAt.java | 86 ++++ .../cuda/tools/ghidra/ExportFunctionIndex.java | 192 ++++++++ userspace/cuda/tools/ghidra/PrintProgramInfo.java | 38 ++ userspace/cuda/tools/ghidra_batch_decompile.sh | 83 ++++ userspace/cuda/tools/ghidra_decompile_at.sh | 29 ++ userspace/cuda/tools/ghidra_export_index.sh | 27 ++ userspace/cuda/tools/ghidra_full_analysis.sh | 49 ++ userspace/cuda/tools/ghidra_index_merge.sh | 18 + 14 files changed, 1636 insertions(+) create mode 100755 userspace/cuda/tools/annotate_corpus.py create mode 100755 userspace/cuda/tools/callwalk.py create mode 100755 userspace/cuda/tools/cudatool.py create mode 100644 userspace/cuda/tools/ghidra/ApplyNameMap.java create mode 100644 userspace/cuda/tools/ghidra/BatchDecompile.java create mode 100644 userspace/cuda/tools/ghidra/ConfigureAnalysis.java create mode 100644 userspace/cuda/tools/ghidra/DecompileAt.java create mode 100644 userspace/cuda/tools/ghidra/ExportFunctionIndex.java create mode 100644 userspace/cuda/tools/ghidra/PrintProgramInfo.java create mode 100755 userspace/cuda/tools/ghidra_batch_decompile.sh create mode 100755 userspace/cuda/tools/ghidra_decompile_at.sh create mode 100755 userspace/cuda/tools/ghidra_export_index.sh create mode 100755 userspace/cuda/tools/ghidra_full_analysis.sh create mode 100755 userspace/cuda/tools/ghidra_index_merge.sh (limited to 'userspace/cuda/tools') 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_ references inside the +# function bodies, so a renamed function is readable everywhere. +# +# Inputs (produced by the toolchain): +# /buckets/bucket_*.c raw decompiled C +# /decompiled_index.tsv addr file_addr size name file line status +# /callgraph.tsv caller callee caller_file callee_file +# file_vaddrname +# +# Usage: +# annotate_corpus.py +# +# 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[^\n]*)\n \* addr=(?P0x[0-9a-f]+) " + r"file_addr=libcuda\+(?P0x[0-9a-f]+) size=(?P\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() diff --git a/userspace/cuda/tools/callwalk.py b/userspace/cuda/tools/callwalk.py new file mode 100755 index 0000000..15feb45 --- /dev/null +++ b/userspace/cuda/tools/callwalk.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# +# callwalk.py - walk the Open-IOV callgraph index and print callee trees / +# caller trees around a function. This is the quick way to go from an export +# wrapper to the internal implementation chain without opening Ghidra. +# +# Usage: +# callwalk.py callees [depth] [max_per_node] +# callwalk.py callers [depth] [max_per_node] +# callwalk.py chain # shortest path +# +# Addresses are FILE virtual addresses (libcuda+0x...); the callgraph columns +# `caller_file`/`callee_file` are used, so Ghidra image-base offsets do not +# matter. `names.tsv` (file_vaddrname) is picked up next to the callgraph +# when present, and used to label nodes. +# +# SPDX-License-Identifier: BSD-3-Clause + +import sys +from collections import defaultdict, deque + + +def load_graph(path): + callees = defaultdict(list) + callers = defaultdict(list) + with open(path) as f: + header = f.readline() + for line in f: + parts = line.rstrip("\n").split("\t") + if len(parts) < 4: + continue + cf, kf = int(parts[2], 16), int(parts[3], 16) + callees[cf].append(kf) + callers[kf].append(cf) + return callees, callers + + +def load_names(path): + names = {} + try: + with open(path) as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + parts = line.split("\t") + if len(parts) >= 2: + try: + names[int(parts[0], 16)] = parts[1] + except ValueError: + pass + except FileNotFoundError: + pass + return names + + +def label(a, names): + return f"0x{a:08x}({names[a]})" if a in names else f"0x{a:08x}" + + +def walk(adj, start, depth, limit): + seen = {start} + frontier = [start] + for d in range(depth): + nxt = [] + for node in frontier: + kids = sorted(set(adj.get(node, []))) + if not kids: + continue + print(" " * (d + 1) + " | ".join(label(k, NAMES) for k in kids[:limit]) + + (" ..." if len(kids) > limit else "")) + for k in kids: + if k not in seen: + seen.add(k) + nxt.append(k) + frontier = nxt + if not frontier: + break + + +def shortest_path(callees, start, goal): + if start == goal: + return [start] + prev = {start: None} + q = deque([start]) + while q: + node = q.popleft() + for k in callees.get(node, []): + if k in prev: + continue + prev[k] = node + if k == goal: + path = [goal] + while prev[path[-1]] is not None: + path.append(prev[path[-1]]) + return list(reversed(path)) + q.append(k) + return None + + +def main(): + if len(sys.argv) < 4: + print(__doc__) + sys.exit(2) + graph = sys.argv[1] + mode = sys.argv[2] + addr = int(sys.argv[3], 16) + depth = int(sys.argv[4]) if len(sys.argv) > 4 else 4 + limit = int(sys.argv[5]) if len(sys.argv) > 5 else 8 + + global NAMES + callees, callers = load_graph(graph) + NAMES = load_names(graph.rsplit("/", 1)[0] + "/names.tsv") + + if mode == "chain": + goal = int(sys.argv[4], 16) + path = shortest_path(callees, addr, goal) + if not path: + print("no direct path found") + else: + for a in path: + print(label(a, NAMES)) + return + + print(f"{mode} of {label(addr, NAMES)}:") + walk(callees if mode == "callees" else callers, addr, depth, limit) + + +NAMES = {} + +if __name__ == "__main__": + main() diff --git a/userspace/cuda/tools/cudatool.py b/userspace/cuda/tools/cudatool.py new file mode 100755 index 0000000..2e4c1c6 --- /dev/null +++ b/userspace/cuda/tools/cudatool.py @@ -0,0 +1,515 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# +# cudatool.py - Open-IOV CUDA userspace driver analysis toolkit. +# +# A dependency-free (stdlib-only) ELF toolkit for the NVIDIA CUDA userspace +# driver, usable on every driver version without a Ghidra project. It gives +# the fast facts that do not require decompilation: exports, function ranges +# derived from the exception-handling tables, the CUDA forward-compatibility +# note, and a dmesg/crash-address decoder. It is also the version-delta tool: +# `delta` compares two driver versions and reports exactly what changed at +# the interface level, which is what keeps the Open-IOV side anti-fragile +# across driver upgrades. +# +# Commands +# info ELF/version summary +# exports [--json] exported symbols (file vaddrs) +# functions [--json] function ranges from .eh_frame_hdr +# nearest nearest preceding function/export for a vaddr +# crash dmesg mapping base + ip -> file vaddr + symbol +# strings [--grep RE] defined strings with file offsets +# delta interface-level diff between two versions +# wrappers export stub -> generated wrapper target +# note CUDA forward-compatibility note (JSON) +# +# Address convention: every address printed by this tool is a FILE VIRTUAL +# ADDRESS, i.e. the `libcuda+0x...` numbers used in the project docs. Ghidra +# may load the same file at a non-zero image base (0x100000 for these builds); +# use tools/ghidra_decompile_at.sh which translates file vaddrs automatically. +# +# License note: this tool only *reads* the vendor binary for interoperability +# analysis; it contains no vendor code and is published under BSD-3-Clause. + +import argparse +import json +import re +import struct +import sys + +ELF_MAGIC = b"\x7fELF" + + +class ELFError(Exception): + pass + + +class ELF: + """Minimal 64-bit little-endian ELF reader.""" + + def __init__(self, path): + self.path = path + with open(path, "rb") as f: + self.data = f.read() + if self.data[:4] != ELF_MAGIC: + raise ELFError(f"{path}: not an ELF file") + if self.data[4] != 2: + raise ELFError(f"{path}: not ELF64") + if self.data[5] != 1: + raise ELFError(f"{path}: not little-endian") + (self.e_type, self.e_machine, _v, self.e_entry, self.e_phoff, self.e_shoff, + self.e_flags, self.e_ehsize, self.e_phentsize, self.e_phnum, + self.e_shentsize, self.e_shnum, self.e_shstrndx) = struct.unpack_from( + "> 4, + shndx=st_shndx, + defined=st_shndx != 0, + )) + return out + + def exports(self): + return [s for s in self.dynamic_symbols() + if s["defined"] and s["type"] == 2 and s["value"]] + + def imports(self): + return [s for s in self.dynamic_symbols() + if not s["defined"] and s["name"]] + + # ---- exception handling function table ------------------------------- + + def function_ranges(self): + """Return [(start, end)] from .eh_frame_hdr. + + .eh_frame_hdr layout (DWARF .eh_frame_hdr, version 1): + u8 version; u8 eh_frame_ptr_enc; u8 fde_count_enc; u8 table_enc; + encoded eh_frame_ptr; encoded fde_count; + table[count] { encoded pc_begin; encoded fde_addr; } + + GCC emits: eh_frame_ptr_enc = DW_EH_PE_pcrel|sdata4 (0x1b), + fde_count_enc = udata4 (0x03), table_enc = DW_EH_PE_datarel|sdata4 + (0x3b). The datarel anchor is the start of .eh_frame_hdr. The parser + below handles the encodings generally (pcrel/datarel + 1/2/4/8-byte + and ULEB/SLEB), so it also works for other toolchains. + """ + hdr = self.section(".eh_frame_hdr") + if not hdr: + return [] + base = hdr["offset"] + anchor = hdr["addr"] + version = self.data[base] + if version != 1: + raise ELFError(f"unsupported .eh_frame_hdr version {version}") + eh_ptr_enc, fde_count_enc, table_enc = self.data[base + 1:base + 4] + + def enc_size(enc): + return {0x0: 0, 0x1: 1, 0x2: 2, 0x3: 4, 0x4: 8, + 0x9: 1, 0xA: 2, 0xB: 4, 0xC: 8}.get(enc & 0x0F, 0) + + def decode(enc, ptr, field_addr): + fmt = enc & 0x0F + if fmt == 0x0: + return 0 + if fmt == 0x1: + val, shift = 0, 0 + while True: + b = self.data[ptr] + ptr += 1 + val |= (b & 0x7F) << shift + if not (b & 0x80): + break + shift += 7 + return val + if fmt == 0x2: + return struct.unpack_from(" generated wrapper --------------------------------- + + def relative_relocs(self): + """Map address -> R_X86_64_RELATIVE addend (the initial slot value).""" + out = {} + s = self.section(".rela.dyn") + if not s: + return out + n = s["size"] // 24 + for i in range(n): + off = s["offset"] + i * 24 + r_offset, r_info, r_addend = struct.unpack_from("8} {s['name']}") + + +def cmd_functions(args): + elf = ELF(args.lib) + fr = elf.function_ranges() + if args.json: + print(json.dumps([dict(start=hex(a), end=hex(b) if b else None) + for a, b in fr], indent=1)) + return + for a, b in fr: + print(f"0x{a:08x} 0x{b:08x}" if b else f"0x{a:08x} -") + + +def cmd_nearest(args): + elf = ELF(args.lib) + target = int(args.addr, 16) + ex = sorted(elf.exports(), key=lambda s: s["value"]) + prev = None + for s in ex: + if s["value"] <= target: + prev = s + else: + break + fr = elf.function_ranges() + fstart = None + for a, b in fr: + if a <= target and (b is None or target < b): + fstart = a + break + print(f"vaddr : 0x{target:x}") + print(f"function start : 0x{fstart:x}" if fstart is not None else + "function start : ") + if prev: + print(f"nearest export : {prev['name']} + 0x{target - prev['value']:x} " + f"(export at 0x{prev['value']:x})") + else: + print("nearest export : ") + + +def cmd_crash(args): + elf = ELF(args.lib) + base = int(args.map_base, 16) + ip = int(args.ip, 16) + vaddr = ip - base + elf.exec_segment_base() + print(f"file vaddr : 0x{vaddr:x}") + args.addr = hex(vaddr) + cmd_nearest(args) + + +def cmd_strings(args): + elf = ELF(args.lib) + pat = re.compile(args.grep) if args.grep else None + rodata = [s for s in elf.sections if s["type"] == 1 and not (s["flags"] & 4)] + seen = set() + for s in rodata: + blob = elf.data[s["offset"]:s["offset"] + s["size"]] + for m in re.finditer(rb"[\x20-\x7e]{5,}", blob): + text = m.group().decode() + if pat and not pat.search(text): + continue + key = (s["name"], m.start()) + if key in seen: + continue + seen.add(key) + print(f"0x{s['addr'] + m.start():08x} {text}") + + +def cmd_delta(args): + a, b = ELF(args.libA), ELF(args.libB) + ea = {s["name"]: s for s in a.exports()} + eb = {s["name"]: s for s in b.exports()} + added = sorted(set(eb) - set(ea)) + removed = sorted(set(ea) - set(eb)) + moved = [] + resized = [] + for n in sorted(set(ea) & set(eb)): + if ea[n]["value"] != eb[n]["value"]: + moved.append((n, ea[n]["value"], eb[n]["value"])) + if ea[n]["size"] != eb[n]["size"]: + resized.append((n, ea[n]["size"], eb[n]["size"])) + print(f"A: {args.libA}") + print(f" build-id {a.build_id()} exports {len(ea)}") + print(f"B: {args.libB}") + print(f" build-id {b.build_id()} exports {len(eb)}") + na, nb = a.cuda_note(), b.cuda_note() + print(f"note A: {na['raw'] if na else None}") + print(f"note B: {nb['raw'] if nb else None}") + tb_a, tb_b = a.text_bounds(), b.text_bounds() + print(f"text A: 0x{tb_a[0]:x}..0x{tb_a[1]:x} ({tb_a[1]-tb_a[0]})") + print(f"text B: 0x{tb_b[0]:x}..0x{tb_b[1]:x} ({tb_b[1]-tb_b[0]})") + print(f"exports added ({len(added)}): " + ", ".join(added[:40]) + + (" ..." if len(added) > 40 else "")) + print(f"exports removed ({len(removed)}): " + ", ".join(removed[:40]) + + (" ..." if len(removed) > 40 else "")) + print(f"exports moved ({len(moved)})" + + (": " + ", ".join(f"{n} 0x{x:x}->0x{y:x}" for n, x, y in moved[:10]) + if moved else "")) + print(f"exports resized ({len(resized)})" + + (": " + ", ".join(f"{n} {x}->{y}" for n, x, y in resized[:10]) + if resized else "")) + print(f"imports A={len(a.imports())} B={len(b.imports())}") + + +def cmd_wrappers(args): + elf = ELF(args.lib) + ws = elf.wrappers() + if args.json: + print(json.dumps(ws, indent=1)) + return + for w in ws: + if w["wrapper"] is None: + continue + print(f"0x{w['stub']:08x} -> 0x{w['wrapper']:08x} {w['name']}") + + +def cmd_note(args): + elf = ELF(args.lib) + print(json.dumps(elf.cuda_note(), indent=2)) + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + sub = ap.add_subparsers(dest="cmd", required=True) + + p = sub.add_parser("info"); p.add_argument("lib"); p.set_defaults(fn=cmd_info) + p = sub.add_parser("exports"); p.add_argument("lib") + p.add_argument("--json", action="store_true"); p.set_defaults(fn=cmd_exports) + p = sub.add_parser("functions"); p.add_argument("lib") + p.add_argument("--json", action="store_true"); p.set_defaults(fn=cmd_functions) + p = sub.add_parser("nearest"); p.add_argument("lib"); p.add_argument("addr") + p.set_defaults(fn=cmd_nearest) + p = sub.add_parser("crash"); p.add_argument("lib") + p.add_argument("map_base"); p.add_argument("ip"); p.set_defaults(fn=cmd_crash) + p = sub.add_parser("strings"); p.add_argument("lib") + p.add_argument("--grep"); p.set_defaults(fn=cmd_strings) + p = sub.add_parser("delta"); p.add_argument("libA"); p.add_argument("libB") + p.set_defaults(fn=cmd_delta) + p = sub.add_parser("note"); p.add_argument("lib"); p.set_defaults(fn=cmd_note) + p = sub.add_parser("wrappers"); p.add_argument("lib") + p.add_argument("--json", action="store_true"); p.set_defaults(fn=cmd_wrappers) + + args = ap.parse_args() + try: + args.fn(args) + except ELFError as e: + print(f"error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/userspace/cuda/tools/ghidra/ApplyNameMap.java b/userspace/cuda/tools/ghidra/ApplyNameMap.java new file mode 100644 index 0000000..b8be68f --- /dev/null +++ b/userspace/cuda/tools/ghidra/ApplyNameMap.java @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// ApplyNameMap.java - Ghidra post-script: apply a symbol name map to a +// program. This is the version-transfer hook of the Open-IOV CUDA tooling: +// once a name map exists for version N (derived from public documentation, +// decompiled behaviour or an earlier version), it can be applied to version +// N+1 in seconds, so analysis effort accumulates instead of restarting. +// +// Input format (TSV, no header required): +// [] +// Lines starting with '#' and empty lines are ignored. Addresses are file +// virtual addresses ("libcuda+0x..."), not Ghidra image-base offsets. +// +// Usage (analyzeHeadless): +// -process -noanalysis \ +// -postScript ApplyNameMap.java [--dry-run] +// +// The script prints how many names were applied, and leaves a +// "cuda-names-applied" user property on the program with the map path and +// count so a later session can see it happened. + +import ghidra.app.script.GhidraScript; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Listing; +import ghidra.program.model.symbol.SourceType; + +import java.io.BufferedReader; +import java.io.FileReader; + +public class ApplyNameMap extends GhidraScript { + + @Override + public void run() throws Exception { + String[] args = getScriptArgs(); + if (args.length < 1) { + println("usage: ApplyNameMap.java [--dry-run]"); + return; + } + boolean dryRun = args.length > 1 && args[1].equals("--dry-run"); + Listing listing = currentProgram.getListing(); + long imageBase = currentProgram.getImageBase().getOffset(); + int applied = 0, missing = 0, lines = 0; + + try (BufferedReader r = new BufferedReader(new FileReader(args[0]))) { + String line; + while ((line = r.readLine()) != null && !monitor.isCancelled()) { + line = line.trim(); + if (line.isEmpty() || line.startsWith("#")) continue; + lines++; + String[] parts = line.split("[ \t]+"); + if (parts.length < 2) continue; + long addr; + try { + addr = Long.parseUnsignedLong(parts[0].replaceFirst("^0x", ""), 16); + } catch (NumberFormatException e) { + continue; + } + String name = parts[1]; + // Map keys are file virtual addresses (libcuda+0x...). + Address a = toAddr(addr + imageBase); + Function f = listing.getFunctionAt(a); + if (f == null) { + missing++; + continue; + } + if (!dryRun) { + try { + f.setName(name, SourceType.USER_DEFINED); + } catch (Exception e) { + println(" cannot rename 0x" + Long.toHexString(addr) + + " -> " + name + ": " + e.getMessage()); + continue; + } + } + applied++; + } + } + if (!dryRun) { + currentProgram.getOptions("OpenIOV") + .setString("cuda-names-applied", args[0] + " (" + applied + " names)"); + } + println(String.format("[ApplyNameMap] lines=%d applied=%s missing=%d%s", + lines, String.valueOf(applied), missing, dryRun ? " (dry run)" : "")); + } +} diff --git a/userspace/cuda/tools/ghidra/BatchDecompile.java b/userspace/cuda/tools/ghidra/BatchDecompile.java new file mode 100644 index 0000000..0319681 --- /dev/null +++ b/userspace/cuda/tools/ghidra/BatchDecompile.java @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// BatchDecompile.java - Ghidra post-script: decompile every function whose +// entry address lies in [start, end) and write the C output plus a +// machine-readable index. +// +// Designed for a very large stripped vendor library (~100k functions, 70 MB). +// The caller splits the code address range into chunks and runs one +// analyzeHeadless process per chunk (all with -readOnly against the same +// analyzed project), which parallelizes and makes the run resumable: a chunk +// whose output already exists is skipped. +// +// Usage (analyzeHeadless, on an already analyzed program): +// -process -readOnly -noanalysis \ +// -postScript BatchDecompile.java [timeoutSecs] +// +// Outputs: +// /buckets/bucket__.c decompiled C, functions in order +// /index/bucket__.tsv addr size name file line status +// +// Decompiled output is vendor-derived analysis output: it stays in the +// version's gitignored run/ tree and is NEVER committed. + +import ghidra.app.script.GhidraScript; +import ghidra.app.decompiler.DecompInterface; +import ghidra.app.decompiler.DecompileResults; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.FunctionIterator; +import ghidra.program.model.listing.Listing; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.PrintWriter; + +public class BatchDecompile extends GhidraScript { + + private static int countLines(String s) { + int n = 0; + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == '\n') n++; + } + return n; + } + + @Override + public void run() throws Exception { + String[] args = getScriptArgs(); + if (args.length < 3) { + println("usage: BatchDecompile.java [timeoutSecs]"); + return; + } + long start = Long.parseUnsignedLong(args[0].replaceFirst("^0x", ""), 16); + long end = Long.parseUnsignedLong(args[1].replaceFirst("^0x", ""), 16); + String outdir = args[2]; + int timeout = args.length > 3 ? Integer.parseInt(args[3]) : 120; + + File buckets = new File(outdir, "buckets"); + File indexDir = new File(outdir, "index"); + buckets.mkdirs(); + indexDir.mkdirs(); + String tag = String.format("%08x_%08x", start, end); + File outFile = new File(buckets, "bucket_" + tag + ".c"); + File idxFile = new File(indexDir, "bucket_" + tag + ".tsv"); + + if (outFile.isFile() && idxFile.isFile()) { + println("[BatchDecompile] chunk " + tag + " already done, skipping"); + return; + } + + Listing listing = currentProgram.getListing(); + long imageBase = currentProgram.getImageBase().getOffset(); + DecompInterface dif = new DecompInterface(); + dif.toggleCCode(true); + dif.toggleSyntaxTree(false); + dif.openProgram(currentProgram); + + int n = 0, fail = 0, skip = 0, lines = 0; + + try (PrintWriter idx = new PrintWriter(new BufferedWriter(new FileWriter(idxFile))); + PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(outFile)))) { + + String banner = "/* CUDA userspace driver decompilation - Open-IOV project\n" + + " * program: " + currentProgram.getName() + "\n" + + " * range: [0x" + Long.toHexString(start) + ", 0x" + + Long.toHexString(end) + ")\n" + + " * Machine-generated analysis output; not part of the public tree.\n" + + " */\n"; + out.print(banner); + lines += countLines(banner); + idx.println("addr\tfile_addr\tsize\tname\tfile\tline\tstatus"); + + FunctionIterator fit = listing.getFunctions(true); + while (fit.hasNext() && !monitor.isCancelled()) { + Function f = fit.next(); + if (f.isExternal()) continue; + long addr = f.getEntryPoint().getOffset(); + if (addr < start || addr >= end) continue; + + long size = f.getBody().getNumAddresses(); + String header = String.format( + "\n/* ------------------------------------------------------------------" + + "----------\n" + + " * %s\n" + + " * addr=0x%08x file_addr=libcuda+0x%x size=%d\n" + + " *-------------------------------------------------------------------" + + "-------- */\n", + f.getName(), addr, addr - imageBase, size); + out.print(header); + lines += countLines(header); + + String status = "ok"; + String code; + if (size > 200000) { + status = "skipped-huge"; + skip++; + code = "/* skipped: body too large (" + size + " bytes) */\n"; + } else { + DecompileResults res = dif.decompileFunction(f, timeout, monitor); + if (res != null && res.decompileCompleted()) { + code = res.getDecompiledFunction().getC(); + } else { + status = "failed"; + fail++; + code = "/* decompilation failed */\n"; + } + } + out.print(code); + out.print("\n"); + lines += countLines(code) + 1; + idx.printf("0x%08x\t0x%08x\t%d\t%s\t%s\t%d\t%s%n", + addr, addr - imageBase, size, f.getName(), + outFile.getName(), lines - countLines(code), status); + n++; + } + } + + println(String.format("[BatchDecompile] %s: functions=%d failed=%d skipped=%d -> %s", + tag, n, fail, skip, outFile.getAbsolutePath())); + } +} diff --git a/userspace/cuda/tools/ghidra/ConfigureAnalysis.java b/userspace/cuda/tools/ghidra/ConfigureAnalysis.java new file mode 100644 index 0000000..71bb75e --- /dev/null +++ b/userspace/cuda/tools/ghidra/ConfigureAnalysis.java @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// ConfigureAnalysis.java - Ghidra pre-script used by the Open-IOV CUDA +// tooling to make headless auto-analysis of a large stripped vendor library +// tractable and reproducible. +// +// The CUDA userspace driver is a ~70 MB, fully stripped x86-64 shared object +// with roughly a hundred thousand functions. The default analyzer set spends +// the majority of its time in the two optional "identifier" analyzers, which +// are not needed to obtain readable decompilation: +// +// * Decompiler Parameter ID - decompiles every function to recover +// parameter types (very expensive, no value +// for a stripped binary where we have no +// type definitions for internal structs) +// * Function ID (FID) - matches function bodies against .fidb +// signature databases (liberal, and we have +// no curated database for this toolchain) +// +// Everything else (disassembly, function start search, aggressive instruction +// finder, exception handling, stack analysis, reference analyzers, ...) is +// kept, because function discovery quality on a stripped binary depends on +// them. +// +// Usage (analyzeHeadless): +// -preScript ConfigureAnalysis.java [verbose] +// +// Re-runnable: the same settings are applied on every analysis of a program. + +import ghidra.app.script.GhidraScript; +import ghidra.framework.options.Options; + +public class ConfigureAnalysis extends GhidraScript { + + private static final String[] DISABLE = { + "Decompiler Parameter ID", + "Function ID", + }; + + private static final String[] ENABLE = { + "Aggressive Instruction Finder", + "GCC Exception Handlers", + "Function Start Search", + "Shared Return Calls", + "Non-Returning Functions - Known", + "Non-Returning Functions - Discovered", + }; + + @Override + public void run() throws Exception { + boolean verbose = getScriptArgs().length > 0; + Options opts = currentProgram.getOptions("Analyzers"); + + if (verbose) { + println("[ConfigureAnalysis] analyzer options before:"); + for (String name : opts.getOptionNames()) { + println(" " + name + " = " + opts.getObject(name, null)); + } + } + + for (String name : DISABLE) { + if (opts.contains(name)) { + opts.setBoolean(name, false); + println("[ConfigureAnalysis] disabled: " + name); + } else { + println("[ConfigureAnalysis] WARN not found: " + name); + } + } + + for (String name : ENABLE) { + if (opts.contains(name)) { + opts.setBoolean(name, true); + println("[ConfigureAnalysis] enabled: " + name); + } + } + + if (verbose) { + println("[ConfigureAnalysis] analyzer options after:"); + for (String name : opts.getOptionNames()) { + println(" " + name + " = " + opts.getObject(name, null)); + } + } + } +} diff --git a/userspace/cuda/tools/ghidra/DecompileAt.java b/userspace/cuda/tools/ghidra/DecompileAt.java new file mode 100644 index 0000000..ed8cf5d --- /dev/null +++ b/userspace/cuda/tools/ghidra/DecompileAt.java @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// DecompileAt.java - Ghidra post-script: decompile the function containing a +// given file virtual address. Version-agnostic copy of the per-version +// helper used with ghidra_decompile_at.sh. +// +// Usage: +// analyzeHeadless -process -readOnly -noanalysis \ +// -postScript DecompileAt.java [outfile] +// +// If the address does not fall inside a known function, the script tries to +// disassemble at the address; if the target is outside any function body it +// prints a comment with the nearest preceding function instead of failing. + +import ghidra.app.script.GhidraScript; +import ghidra.app.decompiler.DecompInterface; +import ghidra.app.decompiler.DecompileResults; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.FunctionIterator; +import ghidra.program.model.listing.Listing; + +import java.io.FileWriter; + +public class DecompileAt extends GhidraScript { + @Override + public void run() throws Exception { + String[] args = getScriptArgs(); + if (args.length < 1) { + println("usage: DecompileAt.java [outfile]"); + return; + } + long fileAddr = Long.parseUnsignedLong(args[0].replaceFirst("^0x", ""), 16); + // Arguments are FILE virtual addresses (libcuda+0x...). Ghidra may have + // loaded the program at a non-zero image base (0x100000 in this build), + // so translate before resolving. If image base is 0 this is a no-op. + long imageBase = currentProgram.getImageBase().getOffset(); + Address addr = toAddr(fileAddr + imageBase); + long target = addr.getOffset(); + + Function f = getFunctionContaining(addr); + if (f == null) { + Listing listing = currentProgram.getListing(); + FunctionIterator it = listing.getFunctions(addr, false); + if (it.hasNext()) f = it.next(); + } + if (f == null) { + disassemble(addr); + f = createFunction(addr, null); + } + StringBuilder sb = new StringBuilder(); + if (f == null) { + sb.append("// no function at 0x").append(Long.toHexString(target)).append("\n"); + } else { + DecompInterface dif = new DecompInterface(); + dif.toggleCCode(true); + dif.toggleSyntaxTree(true); + dif.openProgram(currentProgram); + DecompileResults res = dif.decompileFunction(f, 300, monitor); + sb.append("// function ").append(f.getName()) + .append(" at libcuda+0x") + .append(Long.toHexString(f.getEntryPoint().getOffset() - imageBase)) + .append(" (program 0x").append(Long.toHexString(f.getEntryPoint().getOffset())) + .append(", query file 0x").append(Long.toHexString(fileAddr)).append(")\n"); + if (res != null && res.decompileCompleted()) { + sb.append("#include \n") + .append("typedef unsigned char undefined1;\n") + .append("typedef unsigned short undefined2;\n") + .append("typedef unsigned int undefined4;\n") + .append("typedef unsigned long long undefined8;\n") + .append("typedef long long longlong;\n") + .append("typedef unsigned long long ulonglong;\n\n") + .append(res.getDecompiledFunction().getC()); + } else { + sb.append("// decompilation failed\n"); + } + } + String text = sb.toString(); + if (args.length > 1) { + try (FileWriter w = new FileWriter(args[1])) { + w.write(text); + } + } + println(text); + } +} diff --git a/userspace/cuda/tools/ghidra/ExportFunctionIndex.java b/userspace/cuda/tools/ghidra/ExportFunctionIndex.java new file mode 100644 index 0000000..c36f98f --- /dev/null +++ b/userspace/cuda/tools/ghidra/ExportFunctionIndex.java @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// ExportFunctionIndex.java - Ghidra post-script: dump a complete inventory of +// the analyzed program: functions, symbols (exports/imports), memory blocks +// and per-function references to interesting callees, plus the address of +// every function that is reachable from the exported CUDA entry points. +// +// This is the primary "map" artifact for a CUDA userspace driver version: it +// lets later tooling and analysis answer "what is at libcuda+0xNNNNNN" and +// "which internal function implements exported cuFoo". +// +// Usage (analyzeHeadless, on an already analyzed program): +// -process -noanalysis \ +// -postScript ExportFunctionIndex.java +// +// Outputs (all TSV, one row per item, with a header row): +// /functions.tsv addr size name namespace is_exported +// is_external is_thunk has_body entry_bytes +// /exports.tsv addr size name +// /imports.tsv addr name library +// /blocks.tsv name start end size r/w/x +// /callgraph.tsv caller_addr callee_addr call_sites +// /strings.tsv addr length string (first 200 chars) +// +// The callgraph is produced from Ghidra's reference model for call +// instructions inside each function body. + +import ghidra.app.script.GhidraScript; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.listing.Data; +import ghidra.program.model.listing.DataIterator; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.FunctionIterator; +import ghidra.program.model.listing.Listing; +import ghidra.program.model.mem.MemoryBlock; +import ghidra.program.model.symbol.Reference; +import ghidra.program.model.symbol.ReferenceIterator; +import ghidra.program.model.symbol.Symbol; +import ghidra.program.model.symbol.SymbolIterator; +import ghidra.program.model.symbol.SymbolTable; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; + +public class ExportFunctionIndex extends GhidraScript { + + private PrintWriter open(String dir, String name) throws Exception { + return new PrintWriter(new BufferedWriter(new FileWriter(dir + "/" + name))); + } + + @Override + public void run() throws Exception { + String[] args = getScriptArgs(); + if (args.length < 1) { + println("usage: ExportFunctionIndex.java "); + return; + } + String outdir = args[0]; + new java.io.File(outdir).mkdirs(); + + Listing listing = currentProgram.getListing(); + SymbolTable symtab = currentProgram.getSymbolTable(); + // Ghidra may load a PIE/shared object at a non-zero image base (this + // build uses 0x100000). All *file virtual addresses* (what the public + // docs cite as libcuda+0x...) are program addresses minus this base. + long imageBase = currentProgram.getImageBase().getOffset(); + + // ---- functions ------------------------------------------------------- + int nfunc = 0, nexport = 0, nexternal = 0; + Map exported = new HashMap<>(); + try (PrintWriter w = open(outdir, "functions.tsv")) { + w.println("addr\tfile_addr\tsize\tname\tnamespace\texported\texternal\tthunk\tbody"); + FunctionIterator fit = listing.getFunctions(true); + while (fit.hasNext() && !monitor.isCancelled()) { + Function f = fit.next(); + Address ep = f.getEntryPoint(); + boolean isExt = f.isExternal(); + boolean isThunk = f.isThunk(); + String ns = f.getParentNamespace() != null + ? f.getParentNamespace().getName(true) : ""; + String name = f.getName(); + long size = f.getBody().getNumAddresses(); + w.printf("0x%08x\t0x%08x\t%d\t%s\t%s\t%s\t%s\t%s\t%s%n", + ep.getOffset(), ep.getOffset() - imageBase, size, name, ns, + isExt ? "0" : (symtab.isExternalEntryPoint(ep) ? "1" : "0"), + isExt ? "1" : "0", isThunk ? "1" : "0", + size > 0 ? "1" : "0"); + nfunc++; + if (!isExt) { + for (Symbol s : symtab.getSymbols(ep)) { + if (s.isExternalEntryPoint() || s.isGlobal()) { + exported.put(ep.getOffset(), s.getName(true)); + } + } + } + if (isExt) nexternal++; + } + } + + // ---- exports: symbols flagged external-entry / global in .text ------ + // The loader marks ELF .dynsym definitions; enumerating them via the + // symbol table is exact and independent of the function iterator. + try (PrintWriter w = open(outdir, "exports.tsv")) { + w.println("addr\tfile_addr\tsize\tname"); + SymbolIterator sit = symtab.getAllSymbols(true); + while (sit.hasNext() && !monitor.isCancelled()) { + Symbol s = sit.next(); + Address a = s.getAddress(); + if (a == null || !a.isMemoryAddress()) continue; + if (!symtab.isExternalEntryPoint(a)) continue; + Function f = listing.getFunctionAt(a); + long size = f != null ? f.getBody().getNumAddresses() : 0; + w.printf("0x%08x\t0x%08x\t%d\t%s%n", a.getOffset(), a.getOffset() - imageBase, size, s.getName(true)); + nexport++; + } + } + + // ---- imports: external functions and the library they come from ---- + try (PrintWriter w = open(outdir, "imports.tsv")) { + w.println("addr\tfile_addr\tname\tlibrary"); + FunctionIterator fit = listing.getFunctions(true); + while (fit.hasNext() && !monitor.isCancelled()) { + Function f = fit.next(); + if (!f.isExternal()) continue; + String lib = f.getParentNamespace() != null + ? f.getParentNamespace().getName(true) : ""; + w.printf("0x%08x\t0x%08x\t%s\t%s%n", + f.getEntryPoint().getOffset(), + f.getEntryPoint().getOffset() - imageBase, f.getName(), lib); + } + } + + // ---- memory blocks --------------------------------------------------- + try (PrintWriter w = open(outdir, "blocks.tsv")) { + w.println("name\tstart\tend\tsize\tr\tw\tx\tinitialized"); + for (MemoryBlock b : currentProgram.getMemory().getBlocks()) { + w.printf("%s\t0x%08x\t0x%08x\t%d\t%s\t%s\t%s\t%s%n", + b.getName(), b.getStart().getOffset(), b.getEnd().getOffset(), + b.getSize(), b.isRead() ? "1" : "0", b.isWrite() ? "1" : "0", + b.isExecute() ? "1" : "0", b.isInitialized() ? "1" : "0"); + } + } + + // ---- call graph ------------------------------------------------------ + // Function.getCalledFunctions() is the reliable API: it resolves both + // direct calls and call-through references recorded by the analyzers, + // independent of how the reference manager indexes them. + int edges = 0; + try (PrintWriter w = open(outdir, "callgraph.tsv")) { + w.println("caller\tcallee\tcaller_file\tcallee_file"); + FunctionIterator fit = listing.getFunctions(true); + while (fit.hasNext() && !monitor.isCancelled()) { + Function f = fit.next(); + if (f.isExternal()) continue; + for (Function callee : f.getCalledFunctions(monitor)) { + w.printf("0x%08x\t0x%08x\t0x%08x\t0x%08x%n", + f.getEntryPoint().getOffset(), + callee.getEntryPoint().getOffset(), + f.getEntryPoint().getOffset() - imageBase, + callee.getEntryPoint().getOffset() - imageBase); + edges++; + } + } + } + + // ---- defined strings ------------------------------------------------- + int nstr = 0; + try (PrintWriter w = open(outdir, "strings.tsv")) { + w.println("addr\tfile_addr\tlength\tstring"); + DataIterator dit = listing.getDefinedData(true); + while (dit.hasNext() && !monitor.isCancelled()) { + Data d = dit.next(); + if (!d.hasStringValue()) continue; + Object v = d.getValue(); + if (v == null) continue; + String s = v.toString().replace("\t", " ").replace("\n", "\\n"); + if (s.length() > 200) s = s.substring(0, 200); + w.printf("0x%08x\t0x%08x\t%d\t%s%n", d.getAddress().getOffset(), + d.getAddress().getOffset() - imageBase, d.getLength(), s); + nstr++; + } + } + + println(String.format( + "[ExportFunctionIndex] functions=%d exports=%d imports=%d callgraph_edges=%d strings=%d", + nfunc, nexport, nexternal, edges, nstr)); + } +} diff --git a/userspace/cuda/tools/ghidra/PrintProgramInfo.java b/userspace/cuda/tools/ghidra/PrintProgramInfo.java new file mode 100644 index 0000000..86a31b3 --- /dev/null +++ b/userspace/cuda/tools/ghidra/PrintProgramInfo.java @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// PrintProgramInfo.java - Ghidra post-script: print the program's image base, +// executable block ranges and function count. Used to establish the address +// convention of a project before running decompilation tools. +// +// Usage: +// analyzeHeadless -process -readOnly -noanalysis \ +// -postScript PrintProgramInfo.java +import ghidra.app.script.GhidraScript; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.FunctionIterator; +import ghidra.program.model.mem.MemoryBlock; + +public class PrintProgramInfo extends GhidraScript { + @Override + public void run() throws Exception { + println("program = " + currentProgram.getName()); + println("language = " + currentProgram.getLanguageID()); + println("image base = 0x" + Long.toHexString(currentProgram.getImageBase().getOffset())); + println("executable blocks:"); + for (MemoryBlock b : currentProgram.getMemory().getBlocks()) { + if (b.isExecute()) { + println(" 0x" + Long.toHexString(b.getStart().getOffset()) + + " - 0x" + Long.toHexString(b.getEnd().getOffset()) + + " " + b.getName()); + } + } + int n = 0; + FunctionIterator fit = currentProgram.getListing().getFunctions(true); + while (fit.hasNext() && !monitor.isCancelled()) { + Function f = fit.next(); + if (!f.isExternal()) n++; + } + println("functions = " + n); + } +} diff --git a/userspace/cuda/tools/ghidra_batch_decompile.sh b/userspace/cuda/tools/ghidra_batch_decompile.sh new file mode 100755 index 0000000..bce8cb9 --- /dev/null +++ b/userspace/cuda/tools/ghidra_batch_decompile.sh @@ -0,0 +1,83 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# +# ghidra_batch_decompile.sh - decompile the whole code range of an analyzed +# CUDA program in parallel. Chunks already present are skipped, so the command +# is safe to re-run and to interrupt. +# +# Ghidra takes an exclusive project lock even in -readOnly mode, so a single +# project cannot be opened by several processes. To parallelize, this script +# makes one copy of the analyzed project per worker under /wk/ and +# round-robins the chunks across the copies. Copies are kept (they make +# resuming cheap) and can be removed with: rm -rf /wk +# +# Usage: +# ghidra_batch_decompile.sh \ +# [chunk_size_hex] [jobs] [timeout_s] +# +# Defaults: chunk 0x100000, jobs 5, timeout 120. +# env: GHIDRA_HOME, GHIDRA_HEADLESS_MAXMEM (default 4G for batch jobs), MAX_CPU +# +# After it completes, merge per-chunk indexes with: +# ghidra_index_merge.sh +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +GHIDRA_HOME="${GHIDRA_HOME:-/snap/ghidra/current/ghidra}" +ANALYZE="$GHIDRA_HOME/support/analyzeHeadless" +export GHIDRA_HEADLESS_MAXMEM="${GHIDRA_HEADLESS_MAXMEM:-4G}" + +PROJECT_DIR="${1:?usage: $0 [chunk_hex] [jobs] [timeout_s]}" +PROJECT_NAME="${2:?}" +PROGRAM="${3:?}" +OUTDIR="${4:?}" +START="${5:?}" +END="${6:?}" +CHUNK="${7:-0x100000}" +JOBS="${8:-5}" +TIMEOUT="${9:-120}" + +mkdir -p "$OUTDIR/buckets" "$OUTDIR/index" "$OUTDIR/logs" "$OUTDIR/wk" + +START_D=$((START)); END_D=$((END)); CHUNK_D=$((CHUNK)) + +# Build the chunk list first: every chunk gets its OWN project copy, so two +# concurrent jobs can never race on the same Ghidra project lock. +ranges="" +n=0 +for ((a=START_D; a "$log" 2>&1 || { echo "[FAIL] $a-$b (see $log)"; exit 1; } + tail -1 "$log" +' _ + +echo "[ok] batch decompilation done -> $OUTDIR/buckets" +echo " merge indexes: $HERE/ghidra_index_merge.sh $OUTDIR" diff --git a/userspace/cuda/tools/ghidra_decompile_at.sh b/userspace/cuda/tools/ghidra_decompile_at.sh new file mode 100755 index 0000000..1fa9035 --- /dev/null +++ b/userspace/cuda/tools/ghidra_decompile_at.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# +# ghidra_decompile_at.sh - decompile the function containing a file virtual +# address from an already analyzed project. Version-agnostic replacement for +# the per-version helper (same idea, any project/binary). +# +# Usage: ghidra_decompile_at.sh [outfile] +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +GHIDRA_HOME="${GHIDRA_HOME:-/snap/ghidra/current/ghidra}" +ANALYZE="$GHIDRA_HOME/support/analyzeHeadless" +export GHIDRA_HEADLESS_MAXMEM="${GHIDRA_HEADLESS_MAXMEM:-12G}" + +PROJECT_DIR="${1:?usage: $0 [outfile]}" +PROJECT_NAME="${2:?}" +PROGRAM="${3:?}" +VADDR="${4:?}" +OUT="${5:-$PROJECT_DIR/fn_${VADDR}.c}" + +"$ANALYZE" "$PROJECT_DIR" "$PROJECT_NAME" \ + -process "$PROGRAM" -readOnly -noanalysis \ + -scriptPath "$HERE/ghidra" \ + -postScript DecompileAt.java "$VADDR" "$OUT" \ + 2>&1 | tail -3 + +[ -f "$OUT" ] || { echo "[FAIL] no output"; exit 1; } +echo "[ok] $OUT" diff --git a/userspace/cuda/tools/ghidra_export_index.sh b/userspace/cuda/tools/ghidra_export_index.sh new file mode 100755 index 0000000..3709aae --- /dev/null +++ b/userspace/cuda/tools/ghidra_export_index.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# +# ghidra_export_index.sh - export the complete function/symbol/callgraph/ +# string index of an analyzed CUDA program. +# +# Usage: ghidra_export_index.sh +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +GHIDRA_HOME="${GHIDRA_HOME:-/snap/ghidra/current/ghidra}" +ANALYZE="$GHIDRA_HOME/support/analyzeHeadless" +export GHIDRA_HEADLESS_MAXMEM="${GHIDRA_HEADLESS_MAXMEM:-12G}" + +PROJECT_DIR="${1:?usage: $0 }" +PROJECT_NAME="${2:?}" +PROGRAM="${3:?}" +OUTDIR="${4:?}" +mkdir -p "$OUTDIR" + +"$ANALYZE" "$PROJECT_DIR" "$PROJECT_NAME" \ + -process "$PROGRAM" \ + -readOnly -noanalysis \ + -scriptPath "$HERE/ghidra" \ + -postScript ExportFunctionIndex.java "$OUTDIR" + +echo "[ok] index written to $OUTDIR" diff --git a/userspace/cuda/tools/ghidra_full_analysis.sh b/userspace/cuda/tools/ghidra_full_analysis.sh new file mode 100755 index 0000000..a143a80 --- /dev/null +++ b/userspace/cuda/tools/ghidra_full_analysis.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# +# ghidra_full_analysis.sh - import a CUDA userspace library into a fresh +# Ghidra project and run a full auto-analysis tuned for a large stripped +# vendor binary. +# +# This is the fixed version of the original per-version helper: passing +# `-analysisTimeoutPerFile 0` made this Ghidra build abort analysis after +# 0 seconds (see run/ghidra-analyze.log: "Analysis timed out at 0 seconds"), +# so the old project contains no analysis at all. +# +# Usage: ghidra_full_analysis.sh [project_dir] [project_name] +# env: GHIDRA_HOME (default /snap/ghidra/current/ghidra) +# GHIDRA_HEADLESS_MAXMEM (default 12G) +# +# The project is kept so the expensive analysis runs exactly once; all later +# decompilation uses ghidra_batch_decompile.sh / ghidra_decompile_at.sh with +# -noanalysis against the same project. +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +GHIDRA_HOME="${GHIDRA_HOME:-/snap/ghidra/current/ghidra}" +ANALYZE="$GHIDRA_HOME/support/analyzeHeadless" +export GHIDRA_HEADLESS_MAXMEM="${GHIDRA_HEADLESS_MAXMEM:-12G}" + +BIN="${1:?usage: $0 [project_dir] [project_name]}" +PROJECT_DIR="${2:-$(dirname "$BIN")/ghidra-full}" +PROJECT_NAME="${3:-cuda-full}" + +[ -x "$ANALYZE" ] || { echo "analyzeHeadless not found at $ANALYZE"; exit 1; } +[ -f "$BIN" ] || { echo "no such binary: $BIN"; exit 1; } +mkdir -p "$PROJECT_DIR" + +echo "[*] importing + analyzing $BIN" +echo " project: $PROJECT_DIR/$PROJECT_NAME" +echo " heap: $GHIDRA_HEADLESS_MAXMEM" +echo " (Decompiler Parameter ID and Function ID are disabled by" +echo " ConfigureAnalysis.java; everything else stays on.)" + +"$ANALYZE" "$PROJECT_DIR" "$PROJECT_NAME" \ + -import "$BIN" \ + -overwrite \ + -scriptPath "$HERE/ghidra" \ + -preScript ConfigureAnalysis.java \ + -analysisTimeoutPerFile 14400 \ + -max-cpu "${MAX_CPU:-8}" + +echo "[ok] analysis complete: $PROJECT_DIR/$PROJECT_NAME" diff --git a/userspace/cuda/tools/ghidra_index_merge.sh b/userspace/cuda/tools/ghidra_index_merge.sh new file mode 100755 index 0000000..44479e9 --- /dev/null +++ b/userspace/cuda/tools/ghidra_index_merge.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# +# ghidra_index_merge.sh - merge the per-chunk decompiled_index.tsv files from +# ghidra_batch_decompile.sh into one sorted index and print coverage stats. +# +# Usage: ghidra_index_merge.sh +set -euo pipefail +OUTDIR="${1:?usage: $0 }" + +head -1 "$OUTDIR"/index/bucket_*.tsv | head -1 > "$OUTDIR/decompiled_index.tsv" +cat "$OUTDIR"/index/bucket_*.tsv | grep -v '^addr' | sort -t$'\t' -k1,1 >> "$OUTDIR/decompiled_index.tsv" + +total=$(grep -c . "$OUTDIR/decompiled_index.tsv" || true) +failed=$(grep -c $'\tfailed$' "$OUTDIR/decompiled_index.tsv" || true) +skipped=$(grep -c 'skipped-huge' "$OUTDIR/decompiled_index.tsv" || true) +echo "[index] rows=$((total-1)) failed=$failed skipped-huge=$skipped" +echo "[index] $OUTDIR/decompiled_index.tsv" -- cgit v1.2.3