summaryrefslogtreecommitdiffstats
path: root/userspace/cuda/tools/ghidra
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-09-23 13:48:00 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-09-23 13:48:00 -0400
commitbfcb856e09207d8edb714373d775537165d06b05 (patch)
tree5404762f261ed0e20bee6a5f484865ae2e18d929 /userspace/cuda/tools/ghidra
parenta93455bc2e3a10fe070477cad5e098e40b4c069a (diff)
downloadopen-iov-bfcb856e09207d8edb714373d775537165d06b05.tar.gz
open-iov-bfcb856e09207d8edb714373d775537165d06b05.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/ghidra')
-rw-r--r--userspace/cuda/tools/ghidra/AnalyzeFunctions.java343
1 files changed, 343 insertions, 0 deletions
diff --git a/userspace/cuda/tools/ghidra/AnalyzeFunctions.java b/userspace/cuda/tools/ghidra/AnalyzeFunctions.java
new file mode 100644
index 0000000..e0ae6cb
--- /dev/null
+++ b/userspace/cuda/tools/ghidra/AnalyzeFunctions.java
@@ -0,0 +1,343 @@
+// SPDX-License-Identifier: BSD-3-Clause
+//
+// AnalyzeFunctions.java - Ghidra post-script: run the "Decompiler Parameter ID"
+// analyzer over a caller-supplied list of functions, using the analyzer's
+// AddressSetView entry point instead of a whole-program analysis pass.
+//
+// This is the targeted type-recovery pass for the CUDA userspace driver: the
+// full auto-analysis deliberately disables the parameter-ID analyzer because it
+// decompiles every function and is very expensive on a 29k-function stripped
+// binary. Re-running it over a curated address set (crash path, engine-record
+// path, handle pool, RM alloc, resolved wrapper implementations, the largest
+// .text functions) recovers parameter counts/types and local variables where
+// the decompiler can infer them, without touching the rest of the program.
+//
+// Usage:
+// analyzeHeadless <proj_dir> <proj_name> -process <program> -noanalysis \
+// -scriptPath tools/ghidra \
+// -postScript AnalyzeFunctions.java <addresses_file> \
+// [--timeout <secs>] [--out <tsv>] [--list] [--batch <n>]
+//
+// <addresses_file> one entry per line: "0x<file_vaddr> [# comment]".
+// Addresses are FILE virtual addresses (libcuda+0x...).
+// Blank lines and lines starting with '#' are ignored.
+// --timeout <secs> per-function decompiler timeout for the analyzer
+// (default 60, matching the analyzer's own default).
+// --out <tsv> write the before/after report to this file as well.
+// --list only resolve the addresses to functions and print them;
+// do not run the analyzer (dry run).
+// --batch <n> run the analyzer in batches of n functions (default 0 =
+// one batch). Useful to checkpoint very large sets.
+//
+// The script translates file vaddr -> program address via the program's image
+// base (0x100000 for this binary), exactly like DecompileAt.java. If an address
+// is not inside a known function it is disassembled and turned into one.
+//
+// The analyzer class was renamed between Ghidra versions
+// (DecompilerParameterIdAnalyzer -> DecompilerFunctionAnalyzer); the script
+// looks up both and invokes the AddressSetView-based entry point
+// added(AddressSetView, TaskMonitor) when present, falling back to
+// added(Program, AddressSetView, TaskMonitor, MessageLog).
+
+import ghidra.app.script.GhidraScript;
+import ghidra.app.services.Analyzer;
+import ghidra.app.util.importer.MessageLog;
+import ghidra.framework.options.Options;
+import ghidra.program.model.address.Address;
+import ghidra.program.model.address.AddressSet;
+import ghidra.program.model.address.AddressSetView;
+import ghidra.program.model.listing.Function;
+import ghidra.program.model.listing.FunctionIterator;
+import ghidra.program.model.listing.Listing;
+import ghidra.program.model.listing.Variable;
+import ghidra.util.classfinder.ClassSearcher;
+import ghidra.util.task.TaskMonitor;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.PrintWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+public class AnalyzeFunctions extends GhidraScript {
+
+ private static final String[] ANALYZER_CLASSES = {
+ "ghidra.app.plugin.core.analysis.DecompilerFunctionAnalyzer",
+ "ghidra.app.plugin.core.analysis.DecompilerParameterIdAnalyzer",
+ };
+
+ private static final String OPTION_TIMEOUT = "Analysis Decompiler Timeout (sec)";
+
+ private static class Target {
+ long fileAddr;
+ Address entry;
+ String name;
+ int paramsBefore;
+ int localsBefore;
+ int paramsAfter = -1;
+ int localsAfter = -1;
+ }
+
+ @Override
+ public void run() throws Exception {
+ String[] args = getScriptArgs();
+ if (args.length < 1) {
+ println("usage: AnalyzeFunctions.java <addresses_file> [--timeout secs] " +
+ "[--out tsv] [--list] [--batch n]");
+ return;
+ }
+
+ String addrFile = args[0];
+ int timeoutSecs = 60;
+ String outFile = null;
+ boolean listOnly = false;
+ int batchSize = 0;
+ for (int i = 1; i < args.length; i++) {
+ switch (args[i]) {
+ case "--timeout":
+ timeoutSecs = Integer.parseInt(args[++i]);
+ break;
+ case "--out":
+ outFile = args[++i];
+ break;
+ case "--list":
+ listOnly = true;
+ break;
+ case "--batch":
+ batchSize = Integer.parseInt(args[++i]);
+ break;
+ default:
+ println("unknown argument: " + args[i]);
+ return;
+ }
+ }
+
+ long imageBase = currentProgram.getImageBase().getOffset();
+ List<Long> fileAddrs = readAddresses(addrFile);
+ println("AnalyzeFunctions: " + fileAddrs.size() + " address(es), image base 0x"
+ + Long.toHexString(imageBase));
+
+ List<Target> targets = resolveTargets(fileAddrs, imageBase);
+ println("AnalyzeFunctions: resolved " + targets.size() + " function(s)");
+
+ if (listOnly) {
+ for (Target t : targets) {
+ println(String.format(" libcuda+0x%06x %s program=0x%x params=%d locals=%d",
+ t.fileAddr, t.name, t.entry.getOffset(), t.paramsBefore,
+ t.localsBefore));
+ }
+ return;
+ }
+
+ Analyzer analyzer = newAnalyzer();
+ if (analyzer == null) {
+ println("ERROR: no Decompiler Parameter ID analyzer class found");
+ return;
+ }
+ println("AnalyzeFunctions: analyzer = " + analyzer.getClass().getName()
+ + ", timeout = " + timeoutSecs + "s");
+
+ Options opts = currentProgram.getOptions("Analyzers");
+ analyzer.registerOptions(opts, currentProgram);
+ if (opts.contains(OPTION_TIMEOUT)) {
+ opts.setInt(OPTION_TIMEOUT, timeoutSecs);
+ }
+ analyzer.optionsChanged(opts, currentProgram);
+
+ int txId = currentProgram.startTransaction("AnalyzeFunctions parameter ID");
+ boolean commit = false;
+ try {
+ runAnalyzer(analyzer, targets, batchSize);
+ commit = true;
+ }
+ finally {
+ currentProgram.endTransaction(txId, commit);
+ }
+
+ snapshotAfter(targets, imageBase);
+ report(targets, outFile);
+ }
+
+ private List<Long> readAddresses(String path) throws Exception {
+ List<Long> out = new ArrayList<>();
+ try (BufferedReader r = new BufferedReader(new FileReader(path))) {
+ String line;
+ while ((line = r.readLine()) != null) {
+ line = line.trim();
+ if (line.isEmpty() || line.startsWith("#")) {
+ continue;
+ }
+ String tok = line.split("\\s+")[0];
+ if (tok.startsWith("0x") || tok.startsWith("0X")) {
+ tok = tok.substring(2);
+ }
+ try {
+ out.add(Long.parseUnsignedLong(tok, 16));
+ }
+ catch (NumberFormatException e) {
+ println("WARN: bad address line: " + line);
+ }
+ }
+ }
+ return out;
+ }
+
+ private List<Target> resolveTargets(List<Long> fileAddrs, long imageBase) {
+ List<Target> targets = new ArrayList<>();
+ Listing listing = currentProgram.getListing();
+ for (long fileAddr : fileAddrs) {
+ Address addr = toAddr(fileAddr + imageBase);
+ Function f = getFunctionContaining(addr);
+ if (f == null) {
+ FunctionIterator it = listing.getFunctions(addr, false);
+ if (it.hasNext()) {
+ f = it.next();
+ }
+ }
+ if (f == null) {
+ disassemble(addr);
+ f = createFunction(addr, null);
+ }
+ if (f == null) {
+ println("WARN: no function at libcuda+0x" + Long.toHexString(fileAddr));
+ continue;
+ }
+ Target t = new Target();
+ t.fileAddr = fileAddr;
+ t.entry = f.getEntryPoint();
+ t.name = f.getName();
+ t.paramsBefore = f.getParameterCount();
+ t.localsBefore = f.getLocalVariables().length;
+ targets.add(t);
+ }
+ return targets;
+ }
+
+ private Analyzer newAnalyzer() throws Exception {
+ // Preferred path: the plugin service discovers analyzers via ClassSearcher,
+ // which also works when the analyzer lives in a module that is not visible
+ // to the script's class loader (Class.forName then fails).
+ try {
+ ClassSearcher.search(monitor);
+ for (Class<? extends Analyzer> c : ClassSearcher.getClasses(Analyzer.class)) {
+ Object o = c.getDeclaredConstructor().newInstance();
+ Analyzer a = (Analyzer) o;
+ if ("Decompiler Parameter ID".equals(a.getName())) {
+ return a;
+ }
+ }
+ }
+ catch (Exception e) {
+ println("WARN: ClassSearcher lookup failed: " + e);
+ }
+ // Fallback: direct reflection over the known class names.
+ for (String cn : ANALYZER_CLASSES) {
+ try {
+ Class<?> c = Class.forName(cn);
+ Object o = c.getDeclaredConstructor().newInstance();
+ if (o instanceof Analyzer) {
+ return (Analyzer) o;
+ }
+ }
+ catch (ClassNotFoundException e) {
+ // try next candidate
+ }
+ }
+ return null;
+ }
+
+ private void runAnalyzer(Analyzer analyzer, List<Target> targets, int batchSize)
+ throws Exception {
+ Method addedSet = findMethod(analyzer.getClass(), "added",
+ AddressSetView.class, TaskMonitor.class);
+ Method addedProgram = findMethod(analyzer.getClass(), "added",
+ ghidra.program.model.listing.Program.class, AddressSetView.class,
+ TaskMonitor.class, MessageLog.class);
+ if (addedSet == null && addedProgram == null) {
+ throw new NoSuchMethodException("no supported added(...) entry point on "
+ + analyzer.getClass().getName());
+ }
+
+ int n = (batchSize > 0) ? batchSize : targets.size();
+ for (int start = 0; start < targets.size(); start += n) {
+ int end = Math.min(start + n, targets.size());
+ AddressSet set = new AddressSet();
+ for (int i = start; i < end; i++) {
+ set.addRange(targets.get(i).entry, targets.get(i).entry);
+ }
+ println(String.format("AnalyzeFunctions: batch %d..%d (%d functions)",
+ start, end - 1, end - start));
+ try {
+ if (addedSet != null) {
+ addedSet.invoke(analyzer, set, monitor);
+ }
+ else {
+ addedProgram.invoke(analyzer, currentProgram, set, monitor,
+ new MessageLog());
+ }
+ }
+ catch (InvocationTargetException e) {
+ Throwable cause = e.getCause();
+ println("WARN: analyzer threw for batch " + start + ".." + (end - 1)
+ + ": " + (cause == null ? e : cause));
+ }
+ }
+ }
+
+ private Method findMethod(Class<?> c, String name, Class<?>... params) {
+ for (Class<?> k = c; k != null; k = k.getSuperclass()) {
+ try {
+ Method m = k.getDeclaredMethod(name, params);
+ m.setAccessible(true);
+ return m;
+ }
+ catch (NoSuchMethodException e) {
+ // keep walking up the hierarchy
+ }
+ }
+ return null;
+ }
+
+ private void snapshotAfter(List<Target> targets, long imageBase) {
+ for (Target t : targets) {
+ Function f = getFunctionAt(t.entry);
+ if (f == null) {
+ continue;
+ }
+ t.paramsAfter = f.getParameterCount();
+ Variable[] locals = f.getLocalVariables();
+ t.localsAfter = locals.length;
+ if (t.name.startsWith("FUN_") && !f.getName().equals(t.name)) {
+ t.name = f.getName();
+ }
+ }
+ }
+
+ private void report(List<Target> targets, String outFile) throws Exception {
+ StringBuilder sb = new StringBuilder();
+ sb.append("# libcuda 570.144 targeted Decompiler Parameter ID pass\n");
+ sb.append("# file_vaddr\tname\tparams_before\tparams_after\tlocals_before\tlocals_after\n");
+ int improved = 0;
+ for (Target t : targets) {
+ boolean gain = t.paramsAfter > t.paramsBefore || t.localsAfter > t.localsBefore;
+ if (gain) {
+ improved++;
+ }
+ sb.append(String.format("0x%06x\t%s\t%d\t%d\t%d\t%d%s\n",
+ t.fileAddr, t.name, t.paramsBefore, t.paramsAfter,
+ t.localsBefore, t.localsAfter, gain ? "\tGAIN" : ""));
+ }
+ sb.append(String.format("# %d/%d functions gained parameters or locals\n",
+ improved, targets.size()));
+ String text = sb.toString();
+ println(text);
+ if (outFile != null) {
+ try (PrintWriter w = new PrintWriter(new FileWriter(outFile))) {
+ w.print(text);
+ }
+ }
+ }
+}