mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-14 23:10:33 +02:00
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>
132 lines
4.5 KiB
Python
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())
|