summaryrefslogtreecommitdiffstats
path: root/userspace/cuda/tools/cudatool.py
diff options
context:
space:
mode:
Diffstat (limited to 'userspace/cuda/tools/cudatool.py')
-rwxr-xr-xuserspace/cuda/tools/cudatool.py154
1 files changed, 154 insertions, 0 deletions
diff --git a/userspace/cuda/tools/cudatool.py b/userspace/cuda/tools/cudatool.py
index 2e4c1c6..3417d2b 100755
--- a/userspace/cuda/tools/cudatool.py
+++ b/userspace/cuda/tools/cudatool.py
@@ -21,6 +21,7 @@
# 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
+# fdematch <libA> <libB> function-range match report across versions
# note <lib> CUDA forward-compatibility note (JSON)
#
# Address convention: every address printed by this tool is a FILE VIRTUAL
@@ -476,6 +477,151 @@ def cmd_wrappers(args):
print(f"0x{w['stub']:08x} -> 0x{w['wrapper']:08x} {w['name']}")
+def cmd_fdematch(args):
+ """Match function ranges between two libraries by unwind-table (FDE)
+ start/size, using the common exports as local address anchors.
+
+ A single global address shift is not enough across patch versions: the
+ library is partly re-laid-out (function insertions/removals drift the
+ addresses locally, and the generated wrapper region is regenerated). The
+ matcher therefore uses the nearest common export by address as the local
+ anchor and looks for a B function of the same FDE size within a small
+ window around old_start + anchor_delta. This measures how transferable a
+ name map is between two driver versions, and the --names breakdown shows
+ which parts of the map (exports, generated wrappers, internals) survive."""
+ import bisect
+ from collections import Counter, defaultdict
+
+ a, b = ELF(args.libA), ELF(args.libB)
+ fa = {s: (e - s if e is not None else None)
+ for s, e in a.function_ranges()}
+ fb = {s: (e - s if e is not None else None)
+ for s, e in b.function_ranges()}
+
+ ea = {s["name"]: s["value"] for s in a.exports()}
+ eb = {s["name"]: s["value"] for s in b.exports()}
+ common = sorted(set(ea) & set(eb))
+ anchors = sorted((ea[n], eb[n] - ea[n]) for n in common)
+ anchor_addrs = [x for x, _ in anchors]
+ deltas = [d for _, d in anchors]
+ d_counter = Counter(d // 0x1000 * 0x1000 for d in deltas)
+
+ def anchor_delta(v):
+ i = bisect.bisect_left(anchor_addrs, v)
+ cand = []
+ if i < len(anchors):
+ cand.append(anchors[i])
+ if i > 0:
+ cand.append(anchors[i - 1])
+ if not cand:
+ return 0
+ return min(cand, key=lambda t: abs(t[0] - v))[1]
+
+ size_b = defaultdict(list)
+ for s, sz in fb.items():
+ size_b[sz].append(s)
+ for k in size_b:
+ size_b[k].sort()
+
+ def window_matches(v, size, w):
+ if size is None or size not in size_b:
+ return []
+ target = v + anchor_delta(v)
+ starts = size_b[size]
+ lo = bisect.bisect_left(starts, target - w)
+ hi = bisect.bisect_right(starts, target + w)
+ return starts[lo:hi]
+
+ exact = window = unique = unmatched = 0
+ for v, size in fa.items():
+ m = window_matches(v, size, args.window)
+ if not m:
+ unmatched += 1
+ elif v + anchor_delta(v) in m:
+ exact += 1
+ if len(m) == 1:
+ unique += 1
+ else:
+ window += 1
+ if len(m) == 1:
+ unique += 1
+ matched = exact + window
+ total = len(fa)
+ rate = matched / total if total else 0.0
+
+ transfer = None
+ if args.names:
+ transfer = defaultdict(lambda: dict(entries=0, exact=0, window=0,
+ missing=0, not_in_a=0))
+ with open(args.names) as f:
+ for line in f:
+ line = line.strip()
+ if not line or line.startswith("#"):
+ continue
+ p = line.split("\t")
+ if len(p) < 2:
+ continue
+ try:
+ v = int(p[0], 16)
+ except ValueError:
+ continue
+ src = p[2] if len(p) > 2 else "?"
+ t = transfer[src]
+ t["entries"] += 1
+ if v not in fa:
+ t["not_in_a"] += 1
+ continue
+ m = window_matches(v, fa[v], args.window)
+ if not m:
+ t["missing"] += 1
+ elif v + anchor_delta(v) in m:
+ t["exact"] += 1
+ else:
+ t["window"] += 1
+
+ if args.json:
+ print(json.dumps(dict(
+ lib_a=args.libA, lib_b=args.libB,
+ functions_a=total, functions_b=len(fb),
+ anchors=len(anchors),
+ delta_median=hex(deltas[len(deltas) // 2]) if deltas else "0x0",
+ delta_clusters=[[hex(k), v] for k, v in d_counter.most_common(6)],
+ window=hex(args.window),
+ exact=exact, window_matches=window, unique=unique,
+ unmatched=unmatched, match_rate=round(rate, 4),
+ name_map=transfer), indent=1))
+ return
+
+ print(f"fdematch A: {args.libA}")
+ print(f" B: {args.libB}")
+ print(f"functions : A={total} B={len(fb)} "
+ f"(ranges from .eh_frame_hdr)")
+ print(f"anchors : {len(anchors)} common exports")
+ if deltas:
+ print(f"delta clusters : " + ", ".join(
+ f"{'+' if k >= 0 else '-'}0x{abs(k):x} ({v})"
+ for k, v in d_counter.most_common(5)))
+ print(f"matching window : +/-0x{args.window:x} around "
+ f"nearest-anchor-shifted address, same FDE size")
+ print(f"exact match : {exact} ({exact/total*100:.1f}%)"
+ if total else "exact match : 0")
+ print(f"window match : {window} ({window/total*100:.1f}%)"
+ if total else "window match : 0")
+ print(f"unique match : {unique} ({unique/total*100:.1f}%)"
+ if total else "unique match : 0")
+ print(f"unmatched A : {unmatched}")
+ print(f"match rate : {rate*100:.1f}% of A's {total} functions")
+ if transfer:
+ print(f"name map : {args.names} (per source column)")
+ for src in sorted(transfer):
+ t = transfer[src]
+ got = t["exact"] + t["window"]
+ pct = got / t["entries"] * 100 if t["entries"] else 0.0
+ print(f" {src:<14} : {got}/{t['entries']} ({pct:.1f}%) "
+ f"exact={t['exact']} window={t['window']} "
+ f"missing={t['missing']} not-in-A={t['not_in_a']}")
+
+
def cmd_note(args):
elf = ELF(args.lib)
print(json.dumps(elf.cuda_note(), indent=2))
@@ -502,6 +648,14 @@ def main():
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)
+ p = sub.add_parser("fdematch"); p.add_argument("libA"); p.add_argument("libB")
+ p.add_argument("--names", default=None,
+ help="names.tsv to report transferability for")
+ p.add_argument("--window", type=lambda x: int(x, 0), default=0x100,
+ help="same-size match window around the anchored address "
+ "(default 0x100)")
+ p.add_argument("--json", action="store_true")
+ p.set_defaults(fn=cmd_fdematch)
args = ap.parse_args()
try: