Files
ppsspp/Tools/arm_disasm_check.py
T
Henrik RydgårdandClaude Opus 5.5 f7db56628c ARM disassembler: Rewrite, cover everything the emitter can emit, add a test
The old disarm handled the integer instructions but only a fraction of VFP
and NEON, and got several of those wrong. The new one decodes ARMv7-A ARM
state with VFPv4 and NEON, printing UAL with upper case mnemonics.

The new ArmDisasm test runs every ARM emitter function through it, two
examples each. Tools/arm_disasm_check.py compares the test's output with
llvm-mc; they agree on all of it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 22:23:14 +02:00

132 lines
4.5 KiB
Python

#!/usr/bin/env python3
# Cross-checks our ARM32 disassembler (ext/disarm.cpp) against LLVM's.
#
# Runs the ArmDisasm unit test with ARM_DISASM_DUMP set, which makes it print every instruction word
# along with our disassembly, then disassembles the same words with llvm-mc and compares the two after
# normalizing away the syntax differences: case, absolute branch targets, register ranges in lists
# ("{d8-d11}"), the hs/lo condition names and number formatting. What's left over is printed, so an
# empty report means the two agree on mnemonic, data types, registers and immediates for every
# instruction in the test.
#
# Usage: python Tools/arm_disasm_check.py [path/to/PPSSPPUnitTest] [--llvm-mc=llvm-mc]
import os
import re
import subprocess
import sys
from concurrent.futures import ThreadPoolExecutor
def run_test(exe):
env = dict(os.environ, ARM_DISASM_DUMP='1')
out = subprocess.run([exe, 'ArmDisasm'], env=env, capture_output=True, text=True).stdout
rows = []
for line in out.splitlines():
if line.startswith('DIS|'):
_, ln, addr, words, ours, expected, code = line.split('|', 6)
rows.append((int(ln), int(addr, 16), words.split(), ours.split('; '), code))
return rows
def llvm_dis(llvm_mc, word):
w = int(word, 16)
inp = '0x%02x,0x%02x,0x%02x,0x%02x' % (w & 0xff, (w >> 8) & 0xff, (w >> 16) & 0xff, w >> 24)
out = subprocess.run([llvm_mc, '--disassemble', '-triple=armv7a', '-mattr=+neon,+vfp4,+hwdiv-arm,+fp16,+mp'],
input=inp, capture_output=True, text=True).stdout
lines = [l.strip() for l in out.splitlines() if l.strip() and not l.strip().startswith('.text')]
return lines[0].split('@')[0].strip() if lines else 'INVALID'
CONDS = '(eq|ne|cs|cc|hs|lo|mi|pl|vs|vc|hi|ls|ge|lt|gt|le)?'
BRANCHES = re.compile(r'^(b|bl|blx)%s$' % CONDS)
# Data processing and MSR, where LLVM prints an immediate with a non-canonical rotation as "#imm8, #rot".
ROTATED_IMM = re.compile(r'^(and|eor|sub|rsb|add|adc|sbc|rsc|tst|teq|cmp|cmn|orr|mov|bic|mvn|msr)s?%s$' % CONDS)
def expand_ranges(text):
def expand(m):
kind, first, last = m.group(1), int(m.group(2)), int(m.group(3))
return ', '.join('%s%d' % (kind, i) for i in range(first, last + 1))
return re.sub(r'\b([sdq])(\d+)-[sdq](\d+)\b', expand, text)
def tokens(text):
text = expand_ranges(text.lower())
toks = re.findall(r'[a-z][a-z0-9_.]*|-?0x[0-9a-f]+|-?\d+\.\d+(?:e[+-]\d+)?|-?\d+|[\[\]{}!^]', text)
if not toks:
return toks
toks[0] = re.sub(r'(hs|lo)(?=\.|$)', lambda m: 'cs' if m.group(1) == 'hs' else 'cc', toks[0])
out = [toks[0]]
for t in toks[1:]:
if re.fullmatch(r'-?0x[0-9a-f]+', t):
t = int(t, 16)
elif re.fullmatch(r'-?\d+\.\d+(e[+-]\d+)?', t):
t = float(t)
elif re.fullmatch(r'-?\d+', t):
t = int(t)
if isinstance(t, int) and -(1 << 31) <= t < 0:
t &= 0xffffffff
out.append(t)
return out
def norm_ours(text):
t = tokens(text)
# We spell out the IA mode that LLVM leaves implicit.
if t:
t[0] = re.sub(r'^(ldm|stm)ia', r'\1', t[0])
return t
def norm_llvm(text, addr):
t = tokens(text)
if not t:
return t
m = t[0]
# Branch offsets are relative to the instruction plus 8; ours are absolute.
if BRANCHES.match(m) and len(t) == 2 and isinstance(t[1], int):
t[1] = (addr + 8 + t[1]) & 0xffffffff
elif ROTATED_IMM.match(m) and re.search(r'#\d+, #\d+$', text):
imm, rot = t[-2], t[-1]
t = t[:-2] + [((imm >> rot) | (imm << (32 - rot))) & 0xffffffff]
elif m == 'vorr' and len(t) == 4 and t[2] == t[3]:
t = ['vmov', t[1], t[2]]
elif m.startswith('vext.') and isinstance(t[-1], int):
# We always print bytes.
t[-1] *= int(m[5:]) // 8
t[0] = 'vext.8'
return t
def main():
exe = 'build/PPSSPPUnitTest'
llvm_mc = 'llvm-mc'
for arg in sys.argv[1:]:
if arg.startswith('--llvm-mc='):
llvm_mc = arg.split('=', 1)[1]
else:
exe = arg
rows = run_test(exe)
if not rows:
print('No output from the test - is %s built with the ArmDisasm test?' % exe)
return 1
words = [w for r in rows for w in r[2]]
with ThreadPoolExecutor(16) as ex:
ref = dict(zip(words, ex.map(lambda w: llvm_dis(llvm_mc, w), words)))
mismatches = 0
count = 0
for line, addr, ws, ours, code in rows:
for i, w in enumerate(ws):
count += 1
ours_text = ours[i] if i < len(ours) else ''
theirs = ref[w]
if norm_ours(ours_text) != norm_llvm(theirs, addr + i * 4):
mismatches += 1
print('line %d: %s %-40s llvm: %s (%s)' % (line, w, ours_text, theirs, code))
print('%d of %d instructions differ from llvm-mc' % (mismatches, count))
return 1 if mismatches else 0
if __name__ == '__main__':
sys.exit(main())