aboutsummaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-27 12:58:04 -0400
committerTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-27 12:58:04 -0400
commit5da171e8766184678be8204a2295e9b52be3bf9c (patch)
treea47dddef9e09797adebd840a1e131034f12439c3 /tools
parentc23465838c22e9092d5b1f00be08688f7965dd7c (diff)
Add NX2_NOTES.md and Switch 2 analysis helpers
npdm_info.py dumps META/ACI0 and kernel caps; nso_dis.py disassembles part of an NSO with clang + llvm-objdump.
Diffstat (limited to 'tools')
-rw-r--r--tools/nx2/.gitignore1
-rw-r--r--tools/nx2/npdm_info.py61
-rw-r--r--tools/nx2/nso_dis.py26
3 files changed, 88 insertions, 0 deletions
diff --git a/tools/nx2/.gitignore b/tools/nx2/.gitignore
new file mode 100644
index 0000000000..c18dd8d83c
--- /dev/null
+++ b/tools/nx2/.gitignore
@@ -0,0 +1 @@
+__pycache__/
diff --git a/tools/nx2/npdm_info.py b/tools/nx2/npdm_info.py
new file mode 100644
index 0000000000..20fe713163
--- /dev/null
+++ b/tools/nx2/npdm_info.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+# Print the META header and kernel capabilities of a main.npdm (read only).
+import struct
+import sys
+
+d = open(sys.argv[1], "rb").read()
+assert d[:4] == b"META"
+flags = d[0xC]
+print(f"META flags=0x{flags:02x} is64={flags & 1} address_space_type={(flags >> 1) & 7} "
+ f"prio={d[0xE]} core={d[0xF]} sys_res=0x{struct.unpack_from('<I', d, 0x10)[0]:x} "
+ f"version=0x{struct.unpack_from('<I', d, 0x14)[0]:x} stack=0x{struct.unpack_from('<I', d, 0x1C)[0]:x} "
+ f"name={d[0x20:0x30].rstrip(bytes(1)).decode(errors='replace')}")
+aci_off, aci_sz, acid_off, acid_sz = struct.unpack_from("<IIII", d, 0x70)
+print(f"ACI0 @0x{aci_off:x}+0x{aci_sz:x} ACID @0x{acid_off:x}+0x{acid_sz:x}")
+a = d[aci_off:aci_off + aci_sz]
+assert a[:4] == b"ACI0", a[:4]
+pid = struct.unpack_from("<Q", a, 0x10)[0]
+fs_off, fs_sz, sac_off, sac_sz, kc_off, kc_sz = struct.unpack_from("<IIIIII", a, 0x20)
+print(f"program_id={pid:016x} fs=0x{fs_sz:x} sac=0x{sac_sz:x} kc=0x{kc_sz:x}")
+sac = a[sac_off:sac_off + sac_sz]
+i, names = 0, []
+while i < len(sac):
+ ctl = sac[i]
+ n = (ctl & 7) + 1
+ names.append(("S:" if ctl & 0x80 else "") + sac[i + 1:i + 1 + n].decode(errors="replace"))
+ i += 1 + n
+print("services:", " ".join(names))
+kc = a[kc_off:kc_off + kc_sz]
+svcs = set()
+for (cap,) in struct.iter_unpack("<I", kc):
+ if cap == 0xFFFFFFFF:
+ continue
+ t = (~cap & (cap + 1)).bit_length() - 1 # number of trailing ones
+ if t == 3:
+ lo, hi = cap >> 4 & 0x3F, cap >> 10 & 0x3F
+ cl, ch = cap >> 16 & 0xFF, cap >> 24
+ print(f" ThreadInfo prio {lo}..{hi} cores {cl}..{ch}")
+ elif t == 4:
+ idx, mask = cap >> 29, cap >> 5 & 0xFFFFFF
+ for b in range(24):
+ if mask >> b & 1:
+ svcs.add(idx * 24 + b)
+ elif t == 6:
+ print(f" MapRange 0x{cap:08x}")
+ elif t == 7:
+ print(f" MapIoPage 0x{cap:08x}")
+ elif t == 10:
+ print(f" MapRegion 0x{cap:08x}")
+ elif t == 11:
+ print(f" EnableInterrupts 0x{cap:08x}")
+ elif t == 13:
+ print(f" ProgramType {cap >> 14 & 7}")
+ elif t == 14:
+ print(f" KernelVersion {cap >> 19 & 0x1FFF}.{cap >> 15 & 0xF}")
+ elif t == 15:
+ print(f" HandleTableSize {cap >> 16 & 0x3FF}")
+ elif t == 16:
+ print(f" DebugFlags 0x{cap:08x}")
+ else:
+ print(f" unknown cap type {t}: 0x{cap:08x}")
+print("SVCs:", " ".join(f"{s:02x}" for s in sorted(svcs)))
diff --git a/tools/nx2/nso_dis.py b/tools/nx2/nso_dis.py
new file mode 100644
index 0000000000..5e899b0264
--- /dev/null
+++ b/tools/nx2/nso_dis.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+# Disassemble a range of an NSO's .text using clang + llvm-objdump.
+# Usage: nso_dis.py <llvm bin dir> <nso> <text_offset_hex> <count> <work dir>
+# The NSO is only read; scratch files go to <work dir>.
+import subprocess
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+from a64_coverage import nso_text # noqa: E402
+
+llvm, nso, off, count, work = sys.argv[1], sys.argv[2], int(sys.argv[3], 16), int(sys.argv[4]), Path(sys.argv[5])
+work.mkdir(parents=True, exist_ok=True)
+text = nso_text(nso)
+words = [int.from_bytes(text[i:i + 4], "little") for i in range(off, off + count * 4, 4)]
+# .inst (not .incbin) so the bytes get a code mapping symbol and objdump decodes them.
+(work / "range.s").write_text(".text\n" + "".join(f".inst 0x{w:08x}\n" for w in words))
+subprocess.run([str(Path(llvm) / "clang.exe"), "--target=aarch64-none-elf", "-c", "range.s", "-o", "range.o"],
+ cwd=work, check=True)
+out = subprocess.run([str(Path(llvm) / "llvm-objdump.exe"), "-d", "-j", ".text", "--no-show-raw-insn",
+ "--mattr=+v8.5a,+mte,+lse,+rcpc,+pauth,+crc,+sha2,+dotprod,+fullfp16", "range.o"],
+ cwd=work, capture_output=True, text=True, check=True).stdout
+for line in out.splitlines():
+ parts = line.split(":", 1)
+ if len(parts) == 2 and parts[0].strip() and all(c in "0123456789abcdef" for c in parts[0].strip()):
+ print(f"{off + int(parts[0], 16):08x}:{parts[1].strip()}")