summaryrefslogtreecommitdiffstats
path: root/userspace/cuda/tools
diff options
context:
space:
mode:
Diffstat (limited to 'userspace/cuda/tools')
-rwxr-xr-xuserspace/cuda/tools/annotate_corpus.py155
-rwxr-xr-xuserspace/cuda/tools/callwalk.py133
-rwxr-xr-xuserspace/cuda/tools/cudatool.py515
-rw-r--r--userspace/cuda/tools/ghidra/ApplyNameMap.java86
-rw-r--r--userspace/cuda/tools/ghidra/BatchDecompile.java141
-rw-r--r--userspace/cuda/tools/ghidra/ConfigureAnalysis.java84
-rw-r--r--userspace/cuda/tools/ghidra/DecompileAt.java86
-rw-r--r--userspace/cuda/tools/ghidra/ExportFunctionIndex.java192
-rw-r--r--userspace/cuda/tools/ghidra/PrintProgramInfo.java38
-rwxr-xr-xuserspace/cuda/tools/ghidra_batch_decompile.sh83
-rwxr-xr-xuserspace/cuda/tools/ghidra_decompile_at.sh29
-rwxr-xr-xuserspace/cuda/tools/ghidra_export_index.sh27
-rwxr-xr-xuserspace/cuda/tools/ghidra_full_analysis.sh49
-rwxr-xr-xuserspace/cuda/tools/ghidra_index_merge.sh18
14 files changed, 1636 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()
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 <callgraph.tsv> callees <hex_file_vaddr> [depth] [max_per_node]
+# callwalk.py <callgraph.tsv> callers <hex_file_vaddr> [depth] [max_per_node]
+# callwalk.py <callgraph.tsv> chain <hex_from> <hex_to> # 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_vaddr<TAB>name) 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 <lib> ELF/version summary
+# exports <lib> [--json] exported symbols (file vaddrs)
+# functions <lib> [--json] function ranges from .eh_frame_hdr
+# nearest <lib> <hex_addr> nearest preceding function/export for a vaddr
+# crash <lib> <map_base> <ip> dmesg mapping base + ip -> file vaddr + symbol
+# strings <lib> [--grep RE] defined strings with file offsets
+# delta <libA> <libB> interface-level diff between two versions
+# wrappers <lib> export stub -> generated wrapper target
+# note <lib> 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(
+ "<HHIQQQIHHHHHH", self.data, 16)
+
+ self.program_headers = []
+ for i in range(self.e_phnum):
+ off = self.e_phoff + i * self.e_phentsize
+ (p_type, p_flags, p_offset, p_vaddr, p_paddr, p_filesz, p_memsz,
+ p_align) = struct.unpack_from("<IIQQQQQQ", self.data, off)
+ self.program_headers.append(dict(
+ type=p_type, flags=p_flags, offset=p_offset, vaddr=p_vaddr,
+ paddr=p_paddr, filesz=p_filesz, memsz=p_memsz, align=p_align,
+ index=i))
+
+ self.sections = []
+ self.section_by_name = {}
+ for i in range(self.e_shnum):
+ off = self.e_shoff + i * self.e_shentsize
+ (sh_name, sh_type, sh_flags, sh_addr, sh_offset, sh_size,
+ sh_link, sh_info, sh_addralign, sh_entsize) = struct.unpack_from(
+ "<IIQQQQIIQQ", self.data, off)
+ self.sections.append(dict(
+ index=i, name_off=sh_name, type=sh_type, flags=sh_flags,
+ addr=sh_addr, offset=sh_offset, size=sh_size, link=sh_link,
+ info=sh_info, addralign=sh_addralign, entsize=sh_entsize))
+ if self.e_shstrndx:
+ shstr = self.sections[self.e_shstrndx]
+ base = shstr["offset"]
+ for s in self.sections:
+ end = self.data.find(b"\0", base + s["name_off"])
+ s["name"] = self.data[base + s["name_off"]:end].decode(
+ "utf-8", "replace")
+ self.section_by_name[s["name"]] = s
+
+ # ---- helpers ---------------------------------------------------------
+
+ def section(self, name):
+ return self.section_by_name.get(name)
+
+ def exec_segment_base(self):
+ """Base address reported by dmesg/libcuda mappings: first executable
+ PT_LOAD vaddr aligned as Linux maps it (page-aligned start)."""
+ for ph in self.program_headers:
+ if ph["type"] == 1 and (ph["flags"] & 1): # PT_LOAD | PF_X
+ return ph["vaddr"] & ~0xFFF
+ raise ELFError("no executable PT_LOAD segment")
+
+ def read_string(self, offset):
+ end = self.data.find(b"\0", offset)
+ return self.data[offset:end].decode("utf-8", "replace")
+
+ def build_id(self):
+ for s in self.sections:
+ if s["name"] == ".note.gnu.build-id":
+ off = s["offset"]
+ namesz, descsz, ntype = struct.unpack_from("<III", self.data, off)
+ name = self.data[off + 12:off + 12 + namesz].rstrip(b"\0")
+ if name == b"GNU" and ntype == 3:
+ desc = self.data[off + 12 + ((namesz + 3) & ~3):
+ off + 12 + ((namesz + 3) & ~3) + descsz]
+ return desc.hex()
+ return None
+
+ # ---- symbols ---------------------------------------------------------
+
+ def dynamic_symbols(self):
+ symtab = self.section(".dynsym")
+ if not symtab:
+ return []
+ strtab = self.sections[symtab["link"]]
+ out = []
+ n = symtab["size"] // 24
+ for i in range(n):
+ off = symtab["offset"] + i * 24
+ st_name, st_info, st_other, st_shndx, st_value, st_size = \
+ struct.unpack_from("<IBBHQQ", self.data, off)
+ name = self.read_string(strtab["offset"] + st_name)
+ out.append(dict(
+ name=name,
+ value=st_value,
+ size=st_size,
+ type=st_info & 0xF,
+ bind=st_info >> 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("<H", self.data, ptr)[0]
+ if fmt == 0x3:
+ return struct.unpack_from("<I", self.data, ptr)[0]
+ if fmt == 0x4:
+ return struct.unpack_from("<Q", self.data, ptr)[0]
+ if fmt == 0x9:
+ val, shift = 0, 0
+ while True:
+ b = self.data[ptr]
+ ptr += 1
+ val |= (b & 0x7F) << shift
+ shift += 7
+ if not (b & 0x80):
+ if b & 0x40:
+ val -= 1 << shift
+ break
+ return val
+ if fmt == 0xA:
+ return struct.unpack_from("<h", self.data, ptr)[0]
+ if fmt == 0xB:
+ return struct.unpack_from("<i", self.data, ptr)[0]
+ if fmt == 0xC:
+ return struct.unpack_from("<q", self.data, ptr)[0]
+ raise ELFError(f"unsupported eh encoding 0x{enc:02x}")
+
+ def absolute(enc, ptr):
+ val = decode(enc, ptr, anchor)
+ kind = enc & 0x70
+ if kind == 0x10: # DW_EH_PE_pcrel: relative to field address
+ val += anchor + (ptr - base)
+ elif kind == 0x30: # DW_EH_PE_datarel
+ val += anchor
+ return val
+
+ p = base + 4
+ p += enc_size(eh_ptr_enc) # skip eh_frame_ptr
+ count = decode(fde_count_enc, p, anchor)
+ p += enc_size(fde_count_enc)
+ starts = []
+ for _ in range(count):
+ pc = absolute(table_enc, p)
+ p += enc_size(table_enc)
+ p += enc_size(table_enc) # skip fde address
+ starts.append(pc)
+ starts.sort()
+ out = []
+ for i, pc in enumerate(starts):
+ end = starts[i + 1] if i + 1 < len(starts) else None
+ out.append((pc, end))
+ return out
+
+ # ---- CUDA forward compatibility note ---------------------------------
+
+ def cuda_note(self):
+ s = self.section(".note.cuda.fwd_compatibility")
+ if not s:
+ return None
+ off = s["offset"]
+ namesz, descsz, ntype = struct.unpack_from("<III", self.data, off)
+ name_off = off + 12
+ name = self.data[name_off:name_off + namesz].rstrip(b"\0").decode()
+ desc_off = name_off + ((namesz + 3) & ~3)
+ desc = self.data[desc_off:desc_off + descsz].rstrip(b"\0").decode()
+ try:
+ parsed = json.loads(desc)
+ except json.JSONDecodeError:
+ parsed = None
+ return dict(owner=name, raw=desc, parsed=parsed)
+
+ def debuglink(self):
+ s = self.section(".gnu_debuglink")
+ if not s:
+ return None
+ name = self.read_string(s["offset"])
+ crc = struct.unpack_from("<I", self.data, s["offset"] + (len(name) + 4) // 4 * 4)[0]
+ return dict(name=name, crc=crc)
+
+ # ---- export stubs -> 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("<QQq", self.data, off)
+ if (r_info & 0xFFFFFFFF) == 8: # R_X86_64_RELATIVE
+ out[r_offset] = r_addend
+ return out
+
+ def wrappers(self):
+ """Resolve each exported cu* stub to its initial wrapper target.
+
+ The generated stubs are:
+ cmp dword ptr [rip+d], 0x321cba00 (7 bytes)
+ je +0xc (2 bytes)
+ jmp qword ptr [rip+d] (6 bytes, disp at stub+14)
+ so the interposition slot is stub + 18 + disp32. Its initial value is
+ set by a R_X86_64_RELATIVE relocation and points at the generated
+ wrapper (tracing/tool dispatch), whose first callee is the hand-written
+ implementation.
+ """
+ relocs = self.relative_relocs()
+ out = []
+ for s in self.exports():
+ if not s["name"].startswith("cu"):
+ continue
+ a = s["value"]
+ # sanity-check the cmp/jmp pattern before trusting the arithmetic
+ if self.data[a] != 0x81 or self.data[a + 10] != 0x74 or \
+ self.data[a + 12] != 0xFF or self.data[a + 13] != 0x25:
+ continue
+ disp = struct.unpack_from("<i", self.data, a + 14)[0]
+ slot = a + 18 + disp
+ target = relocs.get(slot)
+ out.append(dict(name=s["name"], stub=a, slot=slot, wrapper=target))
+ return out
+
+ def text_bounds(self):
+ t = self.section(".text")
+ if not t:
+ return None
+ return (t["addr"], t["addr"] + t["size"])
+
+
+# ---- commands ------------------------------------------------------------
+
+
+def cmd_info(args):
+ elf = ELF(args.lib)
+ tb = elf.text_bounds()
+ note = elf.cuda_note()
+ print(f"file : {args.lib}")
+ print(f"build id : {elf.build_id()}")
+ print(f"e_type : {elf.e_type} (3=DYN shared object)")
+ print(f"exec segment base: 0x{elf.exec_segment_base():x}")
+ print(f".text : 0x{tb[0]:x} .. 0x{tb[1]:x} ({tb[1]-tb[0]} bytes)")
+ print(f"exports : {len(elf.exports())}")
+ print(f"imports : {len(elf.imports())}")
+ if note and note["parsed"]:
+ p = note["parsed"]
+ print(f"cuda note : CUDA {p.get('CUDA version')}, "
+ f"driver ABI {p.get('driver')}, device {p.get('device')}")
+ dl = elf.debuglink()
+ if dl:
+ print(f"debuglink : {dl['name']} (crc 0x{dl['crc']:08x})")
+ for s in elf.sections:
+ print(f" section {s['name']:<28} addr=0x{s['addr']:08x} "
+ f"off=0x{s['offset']:08x} size={s['size']}")
+
+
+def cmd_exports(args):
+ elf = ELF(args.lib)
+ ex = sorted(elf.exports(), key=lambda s: s["value"])
+ if args.json:
+ print(json.dumps([{k: v for k, v in s.items()} for s in ex], indent=1))
+ return
+ for s in ex:
+ print(f"0x{s['value']:08x} {s['size']:>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 : <not covered by .eh_frame_hdr>")
+ if prev:
+ print(f"nearest export : {prev['name']} + 0x{target - prev['value']:x} "
+ f"(export at 0x{prev['value']:x})")
+ else:
+ print("nearest export : <none>")
+
+
+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):
+// <hex-addr> <name> [<optional namespace>]
+// Lines starting with '#' and empty lines are ignored. Addresses are file
+// virtual addresses ("libcuda+0x..."), not Ghidra image-base offsets.
+//
+// Usage (analyzeHeadless):
+// -process <program> -noanalysis \
+// -postScript ApplyNameMap.java <namemap.tsv> [--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 <namemap.tsv> [--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 <program> -readOnly -noanalysis \
+// -postScript BatchDecompile.java <startAddrHex> <endAddrHex> <outdir> [timeoutSecs]
+//
+// Outputs:
+// <outdir>/buckets/bucket_<start>_<end>.c decompiled C, functions in order
+// <outdir>/index/bucket_<start>_<end>.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 <startAddrHex> <endAddrHex> <outdir> [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 <proj> <name> -process <program> -readOnly -noanalysis \
+// -postScript DecompileAt.java <hex-vaddr> [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 <hex-vaddr> [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 <stdint.h>\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 <program> -noanalysis \
+// -postScript ExportFunctionIndex.java <outdir>
+//
+// Outputs (all TSV, one row per item, with a header row):
+// <outdir>/functions.tsv addr size name namespace is_exported
+// is_external is_thunk has_body entry_bytes
+// <outdir>/exports.tsv addr size name
+// <outdir>/imports.tsv addr name library
+// <outdir>/blocks.tsv name start end size r/w/x
+// <outdir>/callgraph.tsv caller_addr callee_addr call_sites
+// <outdir>/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 <outdir>");
+ 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<Long, String> 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 <proj> <name> -process <program> -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 <outdir>/wk/ and
+# round-robins the chunks across the copies. Copies are kept (they make
+# resuming cheap) and can be removed with: rm -rf <outdir>/wk
+#
+# Usage:
+# ghidra_batch_decompile.sh <project_dir> <project_name> <program> <outdir> \
+# <start_hex> <end_hex> [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 <outdir>
+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 <project_dir> <project_name> <program> <outdir> <start_hex> <end_hex> [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<END_D; a+=CHUNK_D)); do
+ b=$((a+CHUNK_D)); [ "$b" -gt "$END_D" ] && b=$END_D
+ ranges+=$(printf "0x%x 0x%x %d\n" "$a" "$b" "$n")$'\n'
+ n=$((n+1))
+done
+
+# Create project copies for the chunks that still need work.
+while read -r _a _b w; do
+ [ -z "$w" ] && continue
+ out="$OUTDIR/buckets/bucket_${_a#0x}_${_b#0x}.c"
+ idx="$OUTDIR/index/bucket_${_a#0x}_${_b#0x}.tsv"
+ if [ -f "$out" ] && [ -f "$idx" ]; then continue; fi
+ wdir="$OUTDIR/wk"
+ if [ ! -d "$wdir/$PROJECT_NAME.w$w.rep" ]; then
+ echo "[*] creating project copy $w"
+ cp -a "$PROJECT_DIR/$PROJECT_NAME.gpr" "$wdir/$PROJECT_NAME.w$w.gpr"
+ cp -a "$PROJECT_DIR/$PROJECT_NAME.rep" "$wdir/$PROJECT_NAME.w$w.rep"
+ fi
+done <<< "$ranges"
+
+echo "[*] decompiling $PROGRAM in $n chunks across $JOBS workers"
+echo "$ranges" | xargs -P "$JOBS" -n 3 bash -c '
+ set -e
+ a="$1"; b="$2"; w="$3"
+ tag="${a#0x}_${b#0x}"
+ log="'$OUTDIR'/logs/decompile_${tag}.log"
+ GHIDRA_HEADLESS_MAXMEM="'$GHIDRA_HEADLESS_MAXMEM'" "'$ANALYZE'" "'$OUTDIR'/wk" "'$PROJECT_NAME'.w$w" \
+ -process "'$PROGRAM'" -readOnly -noanalysis \
+ -scriptPath "'$HERE'/ghidra" \
+ -postScript BatchDecompile.java "$a" "$b" "'$OUTDIR'" "'$TIMEOUT'" \
+ > "$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 <project_dir> <project_name> <program> <hex_vaddr> [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 <project_dir> <project_name> <program> <hex_vaddr> [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 <project_dir> <project_name> <program> <outdir>
+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_dir> <project_name> <program> <outdir>}"
+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 <binary> [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 <binary> [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 <outdir>
+set -euo pipefail
+OUTDIR="${1:?usage: $0 <outdir>}"
+
+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"