summaryrefslogtreecommitdiffstats
path: root/userspace/cuda/tools/name_candidates.py
diff options
context:
space:
mode:
Diffstat (limited to 'userspace/cuda/tools/name_candidates.py')
-rwxr-xr-xuserspace/cuda/tools/name_candidates.py224
1 files changed, 224 insertions, 0 deletions
diff --git a/userspace/cuda/tools/name_candidates.py b/userspace/cuda/tools/name_candidates.py
new file mode 100755
index 0000000..7a36a7c
--- /dev/null
+++ b/userspace/cuda/tools/name_candidates.py
@@ -0,0 +1,224 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: BSD-3-Clause
+#
+# name_candidates.py - propose API-implementation names for the hand-written
+# functions called by the generated CUDA API wrappers.
+#
+# Background: every exported cu* entry is a generated stub that jumps through
+# an interposition slot to a generated wrapper (tracing/tool dispatch); the
+# wrapper then calls the hand-written implementation of that API. Export
+# families have several variants of one API (cuMemAlloc_v2, cuMemAlloc_ptsz,
+# cuMemAlloc_ptds, ...), all of which must reach the same implementation.
+#
+# Method (robust, structure-only):
+# 1. Group exported wrappers by BASE NAME: strip the variant suffixes
+# `_v<N>`, `_ptsz`, `_ptds` (in any order/combination) from the export
+# name; all exports in a group are variants of one API.
+# 2. The implementation of a base name is a DIRECT CALLEE shared by ALL of
+# its variants' wrappers, i.e. the intersection of the wrappers' direct
+# callee sets.
+# 3. Filter out global helpers: a candidate is dropped when it is called by
+# >= --max-outside wrappers OUTSIDE its group (trace/init/lock helpers
+# are called by dozens to hundreds of unrelated wrappers). Wrappers are
+# never candidates for themselves.
+# 4. Confidence:
+# high - exactly one candidate survives, and either the group has
+# >= 2 variants (the survivor is shared by all of them) or the
+# survivor is a unique callee (called by exactly one wrapper
+# in the whole library);
+# medium - exactly one survivor but it is also called by another
+# wrapper (possible shared implementation), or several
+# survivors remain (the best-ranked one is proposed).
+# Ranking among several survivors: fewest outside wrappers first, then
+# largest direct-callee count (implementations do work; helpers do not),
+# then lowest address, for a deterministic result.
+#
+# Inputs:
+# <exports.tsv> elf-index exports table: addr file_addr size name
+# <callgraph.tsv> elf-index callgraph: caller callee caller_file callee_file
+# <names.tsv> name map: file_vaddr<TAB>name (wrapper_<export> entries
+# provide the wrapper address of each export)
+# <out.tsv> output (comments + one row per proposal)
+#
+# Output rows (TSV):
+# file_vaddr proposed_name confidence evidence status
+# `proposed_name` is `impl_<base>`; `evidence` carries the wrapper group,
+# candidate callees before/after filtering and the outside-wrapper count;
+# `status` is always `unverified` (the tool proposes, it does not prove).
+# All addresses are FILE virtual addresses (libcuda+0x...).
+#
+# Usage:
+# name_candidates.py <exports.tsv> <callgraph.tsv> <names.tsv> <out.tsv>
+# [--max-outside N] [--quiet]
+
+import argparse
+import re
+import sys
+from collections import defaultdict
+
+
+SUFFIX_RE = re.compile(r"(_ptsz|_ptds)$|_v\d+$")
+
+
+def base_name(name):
+ """Strip CUDA variant suffixes (_v<N>, _ptsz, _ptds) from an export name."""
+ prev = None
+ while prev != name:
+ prev = name
+ name = SUFFIX_RE.sub("", name)
+ return name
+
+
+def load_exports(path):
+ rows = []
+ with open(path) as f:
+ f.readline()
+ for line in f:
+ p = line.rstrip("\n").split("\t")
+ if len(p) < 4:
+ continue
+ try:
+ rows.append(dict(addr=int(p[0], 16), file=int(p[1], 16),
+ size=int(p[2]), name=p[3]))
+ except ValueError:
+ continue
+ return rows
+
+
+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 load_callgraph(path):
+ callees = defaultdict(set)
+ callers = defaultdict(set)
+ with open(path) as f:
+ f.readline()
+ for line in f:
+ p = line.rstrip("\n").split("\t")
+ if len(p) < 4:
+ continue
+ try:
+ cf, kf = int(p[2], 16), int(p[3], 16)
+ except ValueError:
+ continue
+ callees[cf].add(kf)
+ callers[kf].add(cf)
+ return callees, callers
+
+
+def hx(a):
+ return f"0x{a:x}"
+
+
+def main():
+ ap = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+ ap.add_argument("exports")
+ ap.add_argument("callgraph")
+ ap.add_argument("names")
+ ap.add_argument("out")
+ ap.add_argument("--max-outside", type=int, default=2,
+ help="drop a candidate called by >= N wrappers outside "
+ "its base-name group (default 2)")
+ ap.add_argument("--quiet", action="store_true")
+ args = ap.parse_args()
+
+ exports = load_exports(args.exports)
+ names = load_names(args.names)
+ callees, callers = load_callgraph(args.callgraph)
+
+ wrapper_by_export = {}
+ for a, n in names.items():
+ if n.startswith("wrapper_"):
+ wrapper_by_export[n[len("wrapper_"):]] = a
+ wrapper_set = set(wrapper_by_export.values())
+
+ groups = defaultdict(list)
+ for e in exports:
+ w = wrapper_by_export.get(e["name"])
+ if w is not None:
+ groups[base_name(e["name"])].append((e["name"], w))
+
+ rows = []
+ n_high = n_med = 0
+ for group in sorted(groups):
+ variants = groups[group]
+ if not variants:
+ continue
+ variant_wrappers = {w for _, w in variants}
+ inter = None
+ for _, w in variants:
+ cs = set(callees.get(w, ()))
+ inter = cs if inter is None else (inter & cs)
+ if not inter:
+ continue
+ before = sorted(inter)
+ survivors = []
+ for c in before:
+ if c in wrapper_set:
+ continue
+ outside = [w for w in callers.get(c, ())
+ if w in wrapper_set and w not in variant_wrappers]
+ if len(outside) >= args.max_outside:
+ continue
+ survivors.append((len(outside), c))
+ if not survivors:
+ continue
+ # rank: fewest outside wrappers, then most direct callees, then address
+ survivors.sort(key=lambda t: (t[0], -len(callees.get(t[1], ())), t[1]))
+ best_outside, best = survivors[0]
+ after = [c for _, c in survivors]
+ if len(survivors) == 1 and (len(variants) >= 2 or
+ len(callers.get(best, ()) & wrapper_set) == 1):
+ confidence = "high"
+ n_high += 1
+ else:
+ confidence = "medium"
+ n_med += 1
+ evidence = (
+ f"group={group} variants=" + ",".join(n for n, _ in variants) +
+ "; before=" + ",".join(hx(c) for c in before) +
+ "; after=" + ",".join(hx(c) for c in after) +
+ f"; outside_wrappers={best_outside}")
+ rows.append((best, f"impl_{group}", confidence, evidence))
+
+ rows.sort()
+ with open(args.out, "w") as f:
+ f.write("# Open-IOV CUDA API-implementation candidates (our own work product; tracked).\n")
+ f.write("# Generated by tools/name_candidates.py from the elf-index exports,\n")
+ f.write("# callgraph and the version's names.tsv; see that script for the method.\n")
+ f.write("#\n")
+ f.write("# file_vaddr<TAB>proposed_name<TAB>confidence<TAB>evidence<TAB>status\n")
+ f.write("# confidence: high = exactly one filtered candidate, shared by >=2 variants\n")
+ f.write("# or a unique callee; medium = one survivor also called by another\n")
+ f.write("# wrapper, or several survivors (best-ranked proposed).\n")
+ f.write("# status: unverified (tool output). Verification = read the body in the\n")
+ f.write("# decompiled corpus and mark verified/corrected here; only verified\n")
+ f.write("# names may be copied into names.tsv.\n")
+ f.write("#\n")
+ for a, name, conf, ev in rows:
+ f.write(f"0x{a:08x}\t{name}\t{conf}\t{ev}\tunverified\n")
+
+ if not args.quiet:
+ print(f"[name_candidates] groups={len(groups)} proposals={len(rows)} "
+ f"(high={n_high} medium={n_med}) -> {args.out}",
+ file=sys.stderr)
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())