Files
Valiantsin DzerakhandClaude Opus 5.5 e603095ffa Refuse instructions above x86-64-v2 in the runtime objects of a v2 build
-march=x86-64-v2 does not reach inline assembly, target attributes or intrinsics, which is how kern_ap_stub's YMM
saves got into libtotkrt-v2.a (issue #454). tools/check_isa.py disassembles objects and lists every function with
VEX/EVEX, BMI1/2, LZCNT, MOVBE, ADX, SHA, AES, PCLMUL or RDRAND/RDSEED code or YMM/ZMM/mask registers, except
functions in its DISPATCHED list (sha256.c blocks_ni, behind __builtin_cpu_supports). build_game.py --cpu v2 runs it
over the runtime objects after compiling and before linking or archiving, so make_release.py's v2 library build fails
too. Objects unchanged since the last clean scan are skipped; a full scan of the runtime takes about a second. The
release ships the script, and the installer's runtime fingerprint includes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:06:40 +03:00

155 lines
6.2 KiB
Python

"""Refuse instructions above x86-64-v2 in objects built for --cpu v2.
The v2 build is for CPUs without AVX, AVX2, FMA, BMI1/2, F16C, LZCNT and MOVBE (a Pentium Gold G6400, an older Core
i7). The compiler keeps generated code inside that set by -march=x86-64-v2, but inline assembly, target attributes and
intrinsics pass through: kern_ap_stub (runtime/kernel.c) saved YMM registers with vmovdqa in the v2 library of 0.3.1
and crashed such a CPU with an illegal instruction the first time a core was interrupted. tools/build_game.py --cpu v2
runs this over every runtime object before it links or archives them.
A function that holds such an instruction behind a runtime CPU check goes in DISPATCHED with that check named.
python tools/check_isa.py [--objdump PATH] FILE... (objects, archives or executables)
Standard library only.
"""
import argparse
import collections
import concurrent.futures
import json
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
# Mnemonics outside x86-64-v2 that are not VEX/EVEX encoded, or VEX encoded but without a v prefix (BMI1/2).
# lzcnt and tzcnt decode as bsr and bsf on older CPUs and give other results; tzcnt is left out because the
# compiler emits it on any x86-64 level where bsf would do (a nonzero input, or one it checks first).
NON_V2 = {'andn', 'bextr', 'blsi', 'blsmsk', 'blsr', 'bzhi', 'shlx', 'shrx', 'sarx', 'rorx', 'pdep', 'pext', 'mulx',
'lzcnt', 'movbe', 'adcx', 'adox', 'sha1rnds4', 'sha1nexte', 'sha1msg1', 'sha1msg2', 'sha256rnds2',
'sha256msg1', 'sha256msg2', 'aesenc', 'aesenclast', 'aesdec', 'aesdeclast', 'aesimc', 'aeskeygenassist',
'pclmulqdq', 'rdrand', 'rdseed'}
PREFIXES = {'rep', 'repe', 'repz', 'repne', 'repnz', 'lock', 'notrack', 'data16', 'cs', 'ds', 'es', 'ss', 'fs', 'gs'}
WIDE = re.compile(r'%[yz]mm\d|%k[0-7]\b')
# Function -> the runtime check that keeps it off CPUs without the instructions.
DISPATCHED = {
'blocks_ni': 'runtime/sha256.c sha256_blocks: __builtin_cpu_supports("sha") && __builtin_cpu_supports("sse4.1")',
}
def find_objdump(hint=None):
"""llvm-objdump next to the compiler in use, else on PATH, else GNU objdump; None when there is none."""
for p in ([Path(hint)] if hint else []):
if p.is_file():
return str(p)
for name in ('llvm-objdump', 'objdump'):
found = shutil.which(name)
if found:
return found
return None
def mnemonic(line):
"""The mnemonic of one disassembly line (llvm-objdump or GNU objdump, AT&T syntax), or None."""
s = line.strip()
m = re.match(r'^[0-9a-f]+:\s*', s)
if m:
s = s[m.end():]
words = s.split()
while words and words[0] in PREFIXES:
words = words[1:]
return words[0] if words else None
def is_non_v2(line, mn):
if mn.startswith('v') and mn not in ('verr', 'verw'):
return True # VEX or EVEX: AVX, AVX2, FMA, F16C, AVX-512
if mn.startswith('k') and WIDE.search(line):
return True # AVX-512 mask registers
return mn in NON_V2 or bool(WIDE.search(line))
def scan_one(path, objdump):
"""{(file, function): Counter of mnemonics} of one file."""
hits = collections.defaultdict(collections.Counter)
r = subprocess.run([objdump, '-d', '--no-show-raw-insn', str(path)], capture_output=True, text=True,
errors='replace')
if r.returncode:
sys.exit(f'check_isa: {objdump} failed on {path}: {r.stderr.strip()[:300]}')
obj = str(path)
func = None
for line in r.stdout.splitlines():
if 'file format' in line:
obj = line.split(':\tfile format')[0].split(': file format')[0].strip()
continue
m = re.match(r'^[0-9a-f]*\s*<(.+)>:$', line)
if m:
func = m.group(1)
continue
if not line[:1].isspace():
continue
mn = mnemonic(line)
if mn and func not in DISPATCHED and is_non_v2(line, mn):
hits[(obj, func)][mn] += 1
return hits
def scan(paths, objdump):
"""{(file, function): Counter of mnemonics} for every function outside DISPATCHED with a non-v2 instruction."""
hits = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=min(16, os.cpu_count() or 1)) as pool:
for h in pool.map(lambda p: scan_one(p, objdump), paths):
hits.update(h)
return hits
def check_objects(paths, objdump, record):
"""Scan the objects changed since the last clean scan (their sizes and mtimes in the JSON file record); return
the failure text, or None. Without an objdump it warns and passes: the check guards the build, it is no part
of it."""
if not objdump:
print('check_isa: no llvm-objdump or objdump; the v2 instruction check is skipped', file=sys.stderr)
return None
try:
seen = json.loads(record.read_text())
except (OSError, ValueError):
seen = {}
stamp = {str(p): [p.stat().st_size, p.stat().st_mtime_ns] for p in paths}
todo = [p for p in paths if seen.get(str(p)) != stamp[str(p)]]
hits = scan(todo, objdump)
if hits:
record.unlink(missing_ok=True)
return report(hits)
record.write_text(json.dumps(stamp))
print(f'check_isa: {len(todo)} of {len(paths)} runtime objects scanned, none above x86-64-v2')
return None
def report(hits):
lines = [f' {f} in {o}: ' + ', '.join(f'{k} x{v}' for k, v in c.most_common(6))
for (o, f), c in sorted(hits.items())]
return ('instructions above x86-64-v2 in a --cpu v2 build (a CPU without AVX/BMI stops on them with an illegal '
'instruction); use SSE4.2 code under #ifndef __AVX__, or a runtime CPU check and an entry in '
'tools/check_isa.py DISPATCHED:\n' + '\n'.join(lines))
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument('files', nargs='+', type=Path)
ap.add_argument('--objdump', help='llvm-objdump or objdump to use (default: PATH)')
args = ap.parse_args()
objdump = find_objdump(args.objdump)
if not objdump:
sys.exit('check_isa: no llvm-objdump or objdump found')
hits = scan(args.files, objdump)
if hits:
sys.exit(report(hits))
print(f'check_isa: {len(args.files)} files, no instructions above x86-64-v2')
if __name__ == '__main__':
main()