aboutsummaryrefslogtreecommitdiff
path: root/tools/nx2
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-27 11:46:04 -0400
committerTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-27 11:46:04 -0400
commit11aac324010eb2fe3c0fc9dca3c196f33aaef10c (patch)
tree92a5fbba6b08ea26c3628b1b7991fcd5ec1b55fc /tools/nx2
parenta05231f8782178d5a074de16aaba2ae0b118ccd4 (diff)
dynarmic: decode ARMv8.1-8.5 instructions used by Switch 2 code
Switch 2 titles are built for a Cortex-A78C class core. Scanning one title's .text with dynarmic's own decoder table found ~115k RETAB, ~37k LDAPR, ~25k LSE atomics, ~6.6k CAS, ~2.5k SWP plus a few CASP/XPAC/PAC/STG, none of which dynarmic decoded (only the declarations existed). - LSE atomics, SWP, CAS, CASP: exclusive load + op + exclusive store; on store failure no register is written and the instruction is retried (CheckBit terminal), so it stays atomic across cores. - LDAPR: ordered load, like LDAR. - Pointer authentication: identity (nothing is ever signed). RETAA/RETAB, BRA*/BLRA*, LDRAA/LDRAB, PAC*/AUT*/XPAC*, PACGA. - MTE: tags are always 0; STG/ST2G only write back, STZG/STZ2G zero the granules, STGP stores the pair, IRG/GMI/ADDG/SUBG ignore tags. tools/nx2/a64_coverage.py reports which encodings in an NSO's .text the decoder table does not match.
Diffstat (limited to 'tools/nx2')
-rw-r--r--tools/nx2/a64_coverage.py153
1 files changed, 153 insertions, 0 deletions
diff --git a/tools/nx2/a64_coverage.py b/tools/nx2/a64_coverage.py
new file mode 100644
index 0000000000..4ed04853b9
--- /dev/null
+++ b/tools/nx2/a64_coverage.py
@@ -0,0 +1,153 @@
+#!/usr/bin/env python3
+# Report which AArch64 encodings in NSO .text segments are not decoded by dynarmic.
+# Usage: a64_coverage.py <a64.inc> <nso> [<nso> ...]
+# The NSO files are only read.
+import struct
+import sys
+from collections import Counter
+
+import numpy as np
+
+
+def lz4_block_decompress(src, out_size):
+ dst = bytearray(out_size)
+ si = di = 0
+ n = len(src)
+ while si < n:
+ tok = src[si]
+ si += 1
+ lit = tok >> 4
+ if lit == 15:
+ while True:
+ b = src[si]
+ si += 1
+ lit += b
+ if b != 255:
+ break
+ dst[di:di + lit] = src[si:si + lit]
+ si += lit
+ di += lit
+ if si >= n:
+ break
+ off = src[si] | (src[si + 1] << 8)
+ si += 2
+ ml = tok & 15
+ if ml == 15:
+ while True:
+ b = src[si]
+ si += 1
+ ml += b
+ if b != 255:
+ break
+ ml += 4
+ start = di - off
+ if off >= ml:
+ dst[di:di + ml] = dst[start:start + ml]
+ else:
+ for k in range(ml):
+ dst[di + k] = dst[start + k]
+ di += ml
+ return bytes(dst)
+
+
+def nso_text(path):
+ data = open(path, "rb").read()
+ assert data[:4] == b"NSO0", path
+ flags = struct.unpack_from("<I", data, 0xC)[0]
+ file_off, mem_off, size = struct.unpack_from("<III", data, 0x10)
+ csize = struct.unpack_from("<I", data, 0x60)[0]
+ raw = data[file_off:file_off + (csize if flags & 1 else size)]
+ return lz4_block_decompress(raw, size) if flags & 1 else raw
+
+
+def load_patterns(inc):
+ pats = []
+ for line in open(inc, encoding="utf-8"):
+ line = line.strip()
+ if not line.startswith("INST("):
+ continue
+ parts = line.split('"')
+ name, bits = parts[1], parts[3]
+ assert len(bits) == 32, line
+ mask = expect = 0
+ for c in bits:
+ mask <<= 1
+ expect <<= 1
+ if c in "01":
+ mask |= 1
+ expect |= int(c)
+ pats.append((name, mask, expect))
+ return pats
+
+
+# Named ARMv8.1+ encodings, for labelling what dynarmic rejects.
+NAMED = [
+ ("LSE atomic (LDADD/LDCLR/LDEOR/LDSET/LD[SU]MAX/MIN)", 0x3F208C00, 0x38200000),
+ ("LSE SWP", 0x3F20FC00, 0x38208000),
+ ("LDAPR (RCpc)", 0x3FFFFC00, 0x38BFC000),
+ ("CAS/CASA/CASL/CASAL", 0x3FA07C00, 0x08A07C00),
+ ("CASP", 0xBFA07C00, 0x08207C00),
+ ("LDAPUR/STLUR (RCpc2)", 0x3F200C00, 0x19000000),
+ ("RETAA/RETAB", 0xFFFFFBFF, 0xD65F0BFF),
+ ("BRAA/BRAB/BLRAA/BLRAB(+Z)", 0xFEDFF800, 0xD61F0800),
+ ("ERETAA/ERETAB", 0xFFFFFBFF, 0xD69F0BFF),
+ ("LDRAA/LDRAB", 0xFF200400, 0xF8200400),
+ ("PAC*/AUT* (register)", 0xFFFFC000, 0xDAC10000),
+ ("XPACI/XPACD", 0xFFFFFBE0, 0xDAC143E0),
+ ("PACGA", 0xFFE0FC00, 0x9AC03000),
+ ("MTE STG/STZG/ST2G/STZ2G/LDG", 0xFF200000, 0xD9200000),
+ ("MTE STGP", 0xFE400000, 0x68000000),
+ ("MTE IRG/GMI", 0xFFE0E800, 0x9AC01000),
+ ("MTE ADDG/SUBG", 0xBFC0C000, 0x91800000),
+ ("SDOT/UDOT (vector)", 0xBF20FC00, 0x0E009400),
+ ("SDOT/UDOT (element)", 0xBF00F400, 0x0F00E000),
+ ("FJCVTZS", 0xFFFFFC00, 0x1E7E0000),
+ ("FCMLA/FCADD", 0xBF20E400, 0x2E00C400),
+ ("SQRDMLAH/SQRDMLSH (vector)", 0xBF20F400, 0x2E008400),
+ ("SQRDMLAH/SQRDMLSH (element)", 0xBF00D400, 0x2F00D000),
+ ("RMIF/SETF (flagm)", 0xFFE07C10, 0xBA000400),
+ ("CFINV/XAFLAG/AXFLAG", 0xFFFFF0FF, 0xD500401F),
+ ("DC CVAP/CVADP etc", 0xFFF80000, 0xD5080000),
+]
+
+
+def main():
+ pats = load_patterns(sys.argv[1])
+ by_mask = {}
+ for _, m, e in pats:
+ by_mask.setdefault(m, set()).add(e)
+ total_unknown = Counter()
+ for path in sys.argv[2:]:
+ text = nso_text(path)
+ w = np.frombuffer(text[: len(text) // 4 * 4], dtype="<u4")
+ known = np.zeros(w.shape, dtype=bool)
+ for m, es in by_mask.items():
+ known |= np.isin(w & np.uint32(m), np.array(sorted(es), dtype=np.uint32))
+ unk = w[~known]
+ named = np.zeros(unk.shape, dtype=bool)
+ print(f"== {path}: {len(w)} words, {len(unk)} not decoded by dynarmic")
+ for name, m, e in NAMED:
+ hit = (unk & np.uint32(m)) == np.uint32(e)
+ hit &= ~named
+ named |= hit
+ c = int(hit.sum())
+ if c:
+ total_unknown[name] += c
+ ex = int(unk[hit][0])
+ print(f" {c:9d} {name} (e.g. {ex:08x})")
+ rest = unk[~named]
+ # Group the remainder by the top 11 bits (rough instruction class).
+ groups = Counter((int(x) >> 21) for x in rest)
+ examples = {}
+ for x in rest:
+ examples.setdefault(int(x) >> 21, int(x))
+ print(f" {len(rest):9d} unnamed (mostly data in .text or zero padding); top groups:")
+ for g, c in groups.most_common(15):
+ print(f" {c:8d} top11={g:03x} e.g. {examples[g]:08x}")
+ print("== total named:")
+ for name, c in total_unknown.most_common():
+ print(f" {c:9d} {name}")
+
+
+if __name__ == "__main__":
+ main()