mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
222 lines
8.9 KiB
Python
222 lines
8.9 KiB
Python
"""Differential test of exclusives and atomics against Unicorn (max CPU model, which has the ARMv8.1 LSE atomics).
|
|
|
|
Single steps cannot cover these: a store exclusive only succeeds after a load exclusive, and the A57 model rejects
|
|
LSE. So this test enumerates every variant and runs short sequences, with the monitor cleared before each case:
|
|
|
|
- LDXR/LDAXR then STXR/STLXR to the same address (succeeds), all sizes;
|
|
- LDXP/LDAXP then STXP/STLXP to the same address (succeeds) or 16 bytes further (fails), W and X pairs;
|
|
- STXR and STXP alone (fail without a monitor);
|
|
- LDADD, LDCLR, LDEOR, LDSET, LDSMAX, LDSMIN, LDUMAX, LDUMIN and SWP, every size and ordering;
|
|
- CAS and CASP, every size and ordering, with the compare value equal to memory in half of the cases.
|
|
|
|
Needs no game files. Usage: atomic.py [--cases K] [--seed S] [--jobs J]
|
|
"""
|
|
import argparse
|
|
import os
|
|
import platform
|
|
import random
|
|
import struct
|
|
import sys
|
|
import time
|
|
import zlib
|
|
|
|
import unicorn as uc
|
|
from unicorn import arm64_const as A
|
|
|
|
from common import BUF, BUF_SIZE, ROOT, Emu, build_harness, diff_state, dis, rand_fpcr, rand_v, rand_x, run_harness
|
|
from lifter import Lifter
|
|
|
|
BUF_MID = BUF + 0x10000
|
|
CODE = 0x7220000000
|
|
SLOT = 16 # bytes of code per test
|
|
CLREX = 0xD5033F5F
|
|
|
|
|
|
def lse(size, a, r, rs, o3, opc, rn, rt):
|
|
return 0x38200000 | size << 30 | a << 23 | r << 22 | rs << 16 | o3 << 15 | opc << 12 | rn << 5 | rt
|
|
|
|
|
|
def cas(size, l, rs, o0, rn, rt):
|
|
return 0x08A07C00 | size << 30 | l << 22 | rs << 16 | o0 << 15 | rn << 5 | rt
|
|
|
|
|
|
def casp(sz, l, rs, o0, rn, rt):
|
|
return 0x08207C00 | sz << 30 | l << 22 | rs << 16 | o0 << 15 | rn << 5 | rt
|
|
|
|
|
|
def ldxp(sz, o0, rt2, rn, rt):
|
|
return 0x887F0000 | sz << 30 | o0 << 15 | rt2 << 10 | rn << 5 | rt
|
|
|
|
|
|
def stxp(sz, o0, rs, rt2, rn, rt):
|
|
return 0x88200000 | sz << 30 | rs << 16 | o0 << 15 | rt2 << 10 | rn << 5 | rt
|
|
|
|
|
|
def ldxr(size, o0, rn, rt):
|
|
return 0x085F7C00 | size << 30 | o0 << 15 | rn << 5 | rt
|
|
|
|
|
|
def stxr(size, o0, rs, rn, rt):
|
|
return 0x08007C00 | size << 30 | rs << 16 | o0 << 15 | rn << 5 | rt
|
|
|
|
|
|
def regs(r, n, exclude=()):
|
|
"""n distinct general registers 0-30 outside exclude."""
|
|
return r.sample([i for i in range(31) if i not in exclude], n)
|
|
|
|
|
|
def tests(r):
|
|
"""[(words, rn, kind)]: kind 'cas'/'casp' marks compare-and-swap tests (state gets a matching compare value)."""
|
|
out = []
|
|
for size in range(4):
|
|
for a in (0, 1):
|
|
for rel in (0, 1):
|
|
for o3, opc in [(0, k) for k in range(8)] + [(1, 0)]:
|
|
rn = r.choice(regs(r, 1) + [31])
|
|
rs, rt = r.randrange(32), r.randrange(32)
|
|
out.append(([lse(size, a, rel, rs, o3, opc, rn, rt)], rn, 'lse'))
|
|
rn = r.choice(regs(r, 1) + [31])
|
|
rs, rt = regs(r, 2, {rn})
|
|
out.append(([cas(size, a, rs, rel, rn, rt)], rn, 'cas'))
|
|
for sz in (0, 1):
|
|
for l in (0, 1):
|
|
for o0 in (0, 1):
|
|
rn = r.choice(regs(r, 1) + [31])
|
|
rs, rt = (2 * i for i in r.sample(range(15), 2))
|
|
if rn in (rs, rs + 1):
|
|
rs = 30 if rn < 30 else 0 # Rs+1 = XZR or away from Rn
|
|
out.append(([casp(sz, l, rs, o0, rn, rt)], rn, 'casp'))
|
|
for size in range(4):
|
|
for o0 in (0, 1):
|
|
rn = r.choice(regs(r, 1) + [31])
|
|
rt, rs, rt_st = regs(r, 3, {rn})
|
|
out.append(([ldxr(size, o0, rn, rt), stxr(size, o0, rs, rn, rt_st)], rn, 'excl'))
|
|
out.append(([stxr(size, o0, rs, rn, rt_st)], rn, 'excl'))
|
|
for sz in (0, 1):
|
|
for o0 in (0, 1):
|
|
rn = r.choice(regs(r, 1) + [31])
|
|
rt, rt2, rs, d1, d2 = regs(r, 5, {rn})
|
|
out.append(([ldxp(sz, o0, rt2, rn, rt)], rn, 'excl'))
|
|
out.append(([ldxp(sz, o0, rt2, rn, rt), stxp(sz, o0, rs, d2, rn, d1)], rn, 'excl'))
|
|
out.append(([stxp(sz, o0, rs, d2, rn, d1)], rn, 'excl'))
|
|
# A store exclusive to another address fails; Rn is advanced between the two by an ADD.
|
|
if rn != 31:
|
|
add = 0x91004000 | rn << 5 | rn # add xn, xn, #16
|
|
out.append(([ldxp(sz, o0, rt2, rn, rt), add, stxp(sz, o0, rs, d2, rn, d1)], rn, 'excl'))
|
|
return out
|
|
|
|
|
|
def unicorn_wrong(w):
|
|
"""LDSMAX/LDSMIN on bytes, halfwords and words: Unicorn 2.1 (QEMU 5.0) compares them unsigned."""
|
|
return (w & 0x3F200C00) == 0x38200000 and (w >> 30) < 3 and not (w >> 15) & 1 and (w >> 12) & 7 in (4, 5)
|
|
|
|
|
|
def model_signed_minmax(w, x, sp, nzcv, v, buf0):
|
|
"""Reference for unicorn_wrong(w) words, from the ARM pseudocode: returns the expected state and memory CRC."""
|
|
nb = 1 << (w >> 30)
|
|
rs, rn, rt = (w >> 16) & 31, (w >> 5) & 31, w & 31
|
|
addr = sp if rn == 31 else x[rn]
|
|
off = addr - BUF
|
|
old = int.from_bytes(buf0[off:off + nb], 'little')
|
|
operand = (x[rs] if rs != 31 else 0) & ((1 << 8 * nb) - 1)
|
|
sign = lambda u: u - (1 << 8 * nb) if u >> (8 * nb - 1) else u
|
|
pick = max if (w >> 12) & 7 == 4 else min
|
|
new = old if pick(sign(old), sign(operand)) == sign(old) else operand
|
|
mem = bytearray(buf0)
|
|
mem[off:off + nb] = new.to_bytes(nb, 'little')
|
|
ox = list(x[:31])
|
|
if rt != 31:
|
|
ox[rt] = old
|
|
return (ox, sp, nzcv, 0, list(v), zlib.crc32(bytes(mem)))
|
|
|
|
|
|
def make_case(r, words, rn, kind, buf0):
|
|
addr = BUF_MID + r.randrange(0, 0x100, 16)
|
|
x = [rand_x(r) for _ in range(31)]
|
|
sp = addr if rn == 31 else BUF_MID + r.randrange(0, 0x100, 16)
|
|
if rn != 31:
|
|
x[rn] = addr
|
|
w = words[0]
|
|
if kind in ('cas', 'casp') and r.random() < 0.5:
|
|
off = addr - BUF
|
|
rs = (w >> 16) & 31
|
|
if kind == 'cas':
|
|
nb = 1 << (w >> 30)
|
|
if rs != 31:
|
|
x[rs] = int.from_bytes(buf0[off:off + nb], 'little')
|
|
else:
|
|
nb = 8 if (w >> 30) & 1 else 4
|
|
for i in (0, 1):
|
|
if rs + i != 31:
|
|
x[rs + i] = int.from_bytes(buf0[off + i * nb:off + (i + 1) * nb], 'little')
|
|
return rand_fpcr(r), x, sp, r.getrandbits(4) << 28, [rand_v(r) for _ in range(32)]
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
ap.add_argument('--cases', type=int, default=64)
|
|
ap.add_argument('--seed', type=int, default=1)
|
|
ap.add_argument('--jobs', type=int, default=os.cpu_count() or 4)
|
|
ap.add_argument('--cflags', default='-march=x86-64-v3' if platform.machine().lower() in ('amd64', 'x86_64') else '')
|
|
args = ap.parse_args()
|
|
r = random.Random(args.seed)
|
|
t0 = time.time()
|
|
|
|
ts = tests(r)
|
|
code = [CLREX, 0, 0, 0]
|
|
bodies = []
|
|
lifter = Lifter(0, 'c')
|
|
for i, (words, _, _) in enumerate(ts):
|
|
pc = CODE + SLOT * (i + 1)
|
|
code += words + [0] * (SLOT // 4 - len(words))
|
|
bodies.append(' '.join(lifter.lift(w, pc + 4 * j).code for j, w in enumerate(words)))
|
|
code_bytes = struct.pack(f'<{len(code)}I', *code)
|
|
work = ROOT / 'build' / 'test' / 'atomic'
|
|
work.mkdir(parents=True, exist_ok=True)
|
|
code_path = work / 'code.bin'
|
|
code_path.write_bytes(code_bytes)
|
|
exe = build_harness(work, bodies, args.jobs, args.cflags)
|
|
|
|
emu = Emu([(CODE, code_bytes)], A.UC_CPU_ARM64_MAX)
|
|
mu = emu.mu
|
|
buf0 = bytes(r.getrandbits(8) for _ in range(4096)) * (BUF_SIZE // 4096)
|
|
mu.mem_write(BUF, buf0)
|
|
cases, expect = [], []
|
|
for k, (words, rn, kind) in enumerate(ts):
|
|
pc = CODE + SLOT * (k + 1)
|
|
for _ in range(args.cases):
|
|
fpcr, x, sp, nzcv, v = make_case(r, words, rn, kind, buf0)
|
|
if unicorn_wrong(words[0]):
|
|
cases.append((k, fpcr, x, sp, nzcv, v, True))
|
|
expect.append(model_signed_minmax(words[0], x, sp, nzcv, v, buf0))
|
|
continue
|
|
mu.emu_start(CODE, CODE + 4, count=1) # clear the exclusive monitor
|
|
try:
|
|
ox, osp, onz, opc, ov = emu.run(pc, fpcr, x, sp, nzcv, v, count=len(words))
|
|
except uc.UcError as e:
|
|
raise SystemExit(f'unicorn rejected {" ; ".join(dis(w) for w in words)}: {e}')
|
|
mh = zlib.crc32(bytes(mu.mem_read(BUF, BUF_SIZE)))
|
|
mu.mem_write(BUF, buf0)
|
|
cases.append((k, fpcr, x, sp, nzcv, v, True))
|
|
expect.append((ox, osp, onz, opc, ov, mh))
|
|
|
|
recs, err = run_harness(exe, work, buf0, cases, [(CODE, code_path)])
|
|
if err:
|
|
k = cases[len(recs)][0] if len(recs) < len(cases) else None
|
|
print(f'harness failed: {err}' + (f', stopped at {" ; ".join(dis(w) for w in ts[k][0])}' if k is not None else ''))
|
|
fails, nfail = {}, 0
|
|
for case, want, got in zip(cases, expect, recs):
|
|
d = diff_state(got, want, check_pc=False, check_mem=True)
|
|
if d:
|
|
nfail += 1
|
|
fails.setdefault(case[0], d[:4])
|
|
print(f'{len(ts)} tests, {len(cases)} cases: {nfail} failing cases, {len(fails)} failing tests '
|
|
f'({time.time() - t0:.0f}s)')
|
|
for k, d in fails.items():
|
|
print(f' {" ; ".join(dis(w) for w in ts[k][0]):70} {"; ".join(d)}')
|
|
return 1 if fails or err else 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|