mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-21 05:00:32 +02:00
On the x86 Android emulator every automated network test (debug.network_test
"join", "browse" or "host:<map>") stopped the app with SIGILL a few seconds in,
in hud_calculate_point. That was not an assertion: logcat's ndk_translation
line names the instruction, fcvtzs s0, s0, which clang makes of
(short)fast_ftol(point.x) (convert in the SIMD register, store its low half).
The emulator's ARM translation has none of the scalar conversions from
floating point to an integer in a SIMD register (fcvt{a,m,n,p,z}{s,u} s/d,
with or without fraction bits); the general-register and vector forms work.
The guest image had 141 of them, the HUD's the first to run once a game is
drawn. Devices run them, so only the emulator was affected.
tools/android_asm_convert.py now writes each as fmov to the destination, then
the vector form on it: the copy clears the other lanes, which convert 0.0 to
0, so the register and FPSR end as the scalar form leaves them (the game
reads FPSR through _statusfp).
Checked: on the emulator, each replacement against the general-register
form, 315 cases (every rounding mode, signed and unsigned, single and
double, saturating, NaN, infinity, denormal): the same register and FPSR,
except that the translation's own vector fixed-point form leaves out the
invalid flag on overflow (the guest has no fixed-point conversions). The
rebuilt image has none of the scalar forms left. debug.network_test =
"host:bloodgulch" now hosts and plays on the emulator (game started,
player states logged for 45 s, no signal); "join" searches for 150 s
without stopping. tools/test_android_asm_convert.py (new cases, and clang's
output through the pipeline), ci_build android debug and
tools/test_touch_layout.py pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
296 lines
12 KiB
Python
296 lines
12 KiB
Python
"""Convert arm64_32 (Apple ILP32) assembly into AArch64 ELF assembly.
|
|
|
|
The Android port runs the game as ILP32 code: native AArch64 instructions
|
|
with 32-bit pointers, so the game's data formats (which embed 32-bit
|
|
pointers, exactly as on the Xbox) keep their layout. Clang only offers such
|
|
a target as ``arm64_32-apple-watchos``, which emits Mach-O. The generated
|
|
*code* is ordinary AArch64 though; only the assembly syntax, section names,
|
|
symbol decoration and relocation operators are Darwin-specific. This script
|
|
rewrites them for the ELF assembler, so the objects can be linked by ld.lld
|
|
into an image that the Android loader maps into the low 4 GB of the process.
|
|
|
|
One kind of instruction is also replaced: a conversion from floating point
|
|
to an integer that stays in a SIMD register (``fcvtzs s0, s1``). The ARM
|
|
translation of the x86 Android emulator does not have these, and stops the
|
|
app with SIGILL at the first one (the HUD's, once a game is drawn).
|
|
|
|
Usage: android_asm_convert.py input.s output.s
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
|
|
# a quoted name (its escapes kept: \" does not end it) or a plain one
|
|
IDENT = re.compile(r'"(?:[^"\\]|\\.)*"|[A-Za-z_.$][\w.$]*')
|
|
|
|
# directives whose operands are string literals, the same in both
|
|
# syntaxes: they go through as they are, never renamed as symbols (data
|
|
# starting with "_" would lose that byte)
|
|
STRING_DIRECTIVES = {".ascii", ".asciz", ".string"}
|
|
|
|
DROP_DIRECTIVES = {
|
|
".subsections_via_symbols", ".build_version", ".watchos_version_min",
|
|
".ios_version_min", ".macosx_version_min", ".tvos_version_min", ".loh",
|
|
".data_region", ".end_data_region", ".linker_option", ".indirect_symbol",
|
|
".addrsig", ".addrsig_sym", ".no_dead_strip", ".alt_entry", ".ident",
|
|
".desc", ".cfi_startproc", ".cfi_endproc", ".cfi_def_cfa",
|
|
".cfi_offset", ".cfi_def_cfa_offset", ".cfi_def_cfa_register",
|
|
".cfi_personality", ".cfi_lsda", ".cfi_remember_state",
|
|
".cfi_restore_state", ".cfi_restore", ".cfi_adjust_cfa_offset",
|
|
".cfi_same_value", ".cfi_escape",
|
|
}
|
|
|
|
RODATA_SECTIONS = {
|
|
"__cstring", "__const", "__literal4", "__literal8", "__literal16",
|
|
"__ustring",
|
|
}
|
|
|
|
# the scalar conversions to an integer in a SIMD register, which the
|
|
# emulator's ARM translation lacks: fcvt{a,m,n,p,z}{s,u} sN/dN, sM/dM, with
|
|
# an optional number of fraction bits
|
|
SCALAR_FP_TO_INT = re.compile(r"^fcvt[amnpz][su]$")
|
|
SCALAR_FP_TO_INT_OPERANDS = re.compile(r"^([sd])(\d+)\s*,\s*([sd])(\d+)\s*(,\s*#\d+)?$")
|
|
|
|
|
|
class ConvertError(Exception):
|
|
pass
|
|
|
|
|
|
def strip_comment(line: str) -> str:
|
|
out = []
|
|
in_string = False
|
|
i = 0
|
|
while i < len(line):
|
|
c = line[i]
|
|
if in_string:
|
|
out.append(c)
|
|
if c == "\\" and i + 1 < len(line):
|
|
out.append(line[i + 1])
|
|
i += 2
|
|
continue
|
|
if c == '"':
|
|
in_string = False
|
|
else:
|
|
if c == '"':
|
|
in_string = True
|
|
elif c == ";" or line.startswith("//", i):
|
|
break
|
|
out.append(c)
|
|
i += 1
|
|
return "".join(out).rstrip()
|
|
|
|
|
|
class Converter:
|
|
def __init__(self, lines):
|
|
self.lines = lines
|
|
self.local_labels = set()
|
|
self.out = []
|
|
self.in_dropped_section = False
|
|
for line in lines:
|
|
text = strip_comment(line).strip()
|
|
m = re.match(r'^([A-Za-z_.$][\w.$]*|"[^"]*"):', text)
|
|
if m and m.group(1)[0] in "lL":
|
|
self.local_labels.add(m.group(1))
|
|
|
|
def sym(self, name: str) -> str:
|
|
if name.startswith('"'):
|
|
inner = name[1:-1]
|
|
if inner.startswith("_"):
|
|
inner = inner[1:]
|
|
return '"' + inner + '"'
|
|
if name.startswith("_"):
|
|
return name[1:]
|
|
if name in self.local_labels:
|
|
return ".L" + name
|
|
return name
|
|
|
|
def operands(self, text: str) -> str:
|
|
# relocation operators first
|
|
text = re.sub(r'([\w.$"]+)@GOTPAGE\b', r"\1", text)
|
|
text = re.sub(r'([\w.$"]+)@PAGE\b', r"\1", text)
|
|
text = re.sub(r'(\([^)]*\)|[\w.$"]+(?:[+-]\d+)?)@PAGEOFF\b', r":lo12:\1", text)
|
|
if "@" in text and not re.search(r'"[^"]*@[^"]*"', text):
|
|
raise ConvertError("unsupported relocation operator: " + text)
|
|
|
|
def repl(m):
|
|
tok = m.group(0)
|
|
if tok.startswith(".") and not tok.startswith(".L"):
|
|
return tok
|
|
if tok.startswith("lo12"):
|
|
return tok
|
|
return self.sym(tok)
|
|
# rename identifiers outside :lo12: markers
|
|
parts = re.split(r'(:lo12:)', text)
|
|
return "".join(p if p == ":lo12:" else IDENT.sub(repl, p) for p in parts)
|
|
|
|
def emit(self, s: str):
|
|
self.out.append(s)
|
|
|
|
def section(self, segment: str, name: str):
|
|
self.in_dropped_section = False
|
|
if segment == "__DWARF" or segment == "__LD" or name in ("__compact_unwind", "__eh_frame"):
|
|
self.in_dropped_section = True
|
|
return
|
|
if name == "__guest_header":
|
|
self.emit('\t.section .guest_header,"a",@progbits')
|
|
elif name == "__mod_init_func":
|
|
self.emit('\t.section .guest_init,"aw",@progbits')
|
|
elif name == "__mod_term_func":
|
|
self.emit('\t.section .guest_fini,"aw",@progbits')
|
|
elif segment == "__TEXT" and name == "__text":
|
|
self.emit("\t.text")
|
|
elif segment == "__TEXT" and name in RODATA_SECTIONS:
|
|
self.emit('\t.section .rodata,"a",@progbits')
|
|
elif segment == "__TEXT":
|
|
self.emit("\t.text")
|
|
elif name in ("__bss", "__common"):
|
|
self.emit("\t.bss")
|
|
else:
|
|
self.emit("\t.data")
|
|
|
|
def zerofill(self, args):
|
|
parts = [a.strip() for a in args.split(",")]
|
|
if len(parts) < 4:
|
|
return
|
|
segment, name, symbol, size = parts[:4]
|
|
align = int(parts[4]) if len(parts) > 4 else 0
|
|
symbol = self.sym(symbol)
|
|
# a definition in either section: tentative definitions (-fcommon)
|
|
# are emitted as .comm, and __common here only holds zero-initialized
|
|
# ones
|
|
self.emit("\t.pushsection .bss")
|
|
self.emit(f"\t.p2align {align}")
|
|
self.emit(f"{symbol}:")
|
|
self.emit(f"\t.zero {size}")
|
|
self.emit("\t.popsection")
|
|
|
|
def convert_line(self, raw: str):
|
|
text = strip_comment(raw)
|
|
stripped = text.strip()
|
|
if not stripped:
|
|
return
|
|
# labels
|
|
m = re.match(r'^([A-Za-z_.$][\w.$]*|"[^"]*"):(.*)$', stripped)
|
|
if m:
|
|
if not self.in_dropped_section:
|
|
self.emit(self.sym(m.group(1)) + ":")
|
|
rest = m.group(2).strip()
|
|
if rest:
|
|
self.convert_line(rest)
|
|
return
|
|
# symbol assignment (aliases)
|
|
m = re.match(r'^([A-Za-z_.$][\w.$]*)\s*=\s*(.+)$', stripped)
|
|
if m:
|
|
if not self.in_dropped_section:
|
|
self.emit(f"\t.set {self.sym(m.group(1))}, {self.operands(m.group(2))}")
|
|
return
|
|
parts = stripped.split(None, 1)
|
|
op = parts[0]
|
|
args = parts[1] if len(parts) > 1 else ""
|
|
|
|
if op == ".section":
|
|
seg, _, rest = args.partition(",")
|
|
name = rest.split(",")[0].strip()
|
|
self.section(seg.strip(), name)
|
|
return
|
|
shorthand = {
|
|
".text": ("__TEXT", "__text"), ".data": ("__DATA", "__data"),
|
|
".const": ("__TEXT", "__const"), ".cstring": ("__TEXT", "__cstring"),
|
|
".literal4": ("__TEXT", "__literal4"), ".literal8": ("__TEXT", "__literal8"),
|
|
".literal16": ("__TEXT", "__literal16"), ".const_data": ("__DATA", "__const"),
|
|
".static_data": ("__DATA", "__data"), ".mod_init_func": ("__DATA", "__mod_init_func"),
|
|
".mod_term_func": ("__DATA", "__mod_term_func"),
|
|
}
|
|
if op in shorthand:
|
|
self.section(*shorthand[op])
|
|
return
|
|
if op == ".zerofill":
|
|
self.zerofill(args)
|
|
return
|
|
if self.in_dropped_section:
|
|
return
|
|
if op in DROP_DIRECTIVES or op.endswith("_version_min"):
|
|
return
|
|
if op in (".tbss", ".tlv", ".tdata") or "@TLVP" in args:
|
|
raise ConvertError("thread-local storage is not supported: " + stripped)
|
|
if op == ".globl":
|
|
self.emit(f"\t.globl {self.sym(args.strip())}")
|
|
return
|
|
if op == ".private_extern":
|
|
s = self.sym(args.strip())
|
|
self.emit(f"\t.hidden {s}")
|
|
return
|
|
if op in (".weak_definition", ".weak_reference", ".weak_def_can_be_hidden", ".weak"):
|
|
self.emit(f"\t.weak {self.sym(args.strip())}")
|
|
return
|
|
if op == ".comm" or op == ".lcomm":
|
|
a = [x.strip() for x in args.split(",")]
|
|
s = self.sym(a[0])
|
|
align = 1 << int(a[2]) if len(a) > 2 else 1
|
|
if op == ".lcomm":
|
|
self.emit(f"\t.local {s}")
|
|
self.emit(f"\t.comm {s},{a[1]},{align}")
|
|
return
|
|
if op == ".align":
|
|
self.emit(f"\t.p2align {args}")
|
|
return
|
|
if op in STRING_DIRECTIVES:
|
|
self.emit(f"\t{op} {args}")
|
|
return
|
|
if op.startswith("."):
|
|
self.emit(f"\t{op} {self.operands(args)}" if args else f"\t{op}")
|
|
return
|
|
# instructions
|
|
if "@GOTPAGEOFF" in args:
|
|
# ldr wN/xN, [xM, sym@GOTPAGEOFF] -> add xN, xM, :lo12:sym (the
|
|
# image is linked statically below 4 GB, so every GOT load can be
|
|
# relaxed to the address itself)
|
|
gm = re.match(r'^\s*([wx])(\d+|zr)\s*,\s*\[\s*(x\d+|sp)\s*,\s*([\w.$"]+)@GOTPAGEOFF\s*\]\s*$', args)
|
|
if op != "ldr" or not gm:
|
|
raise ConvertError("unsupported GOT access: " + stripped)
|
|
self.emit(f"\tadd x{gm.group(2)}, {gm.group(3)}, :lo12:{self.sym(gm.group(4))}")
|
|
return
|
|
cm = SCALAR_FP_TO_INT_OPERANDS.match(args) if SCALAR_FP_TO_INT.match(op) else None
|
|
if cm and cm.group(1) == cm.group(3):
|
|
# the vector form on a copy of the operand: the copy clears the
|
|
# rest of the register, whose lanes then convert 0.0 to 0, so the
|
|
# register and the exception flags end as the scalar form leaves
|
|
# them
|
|
size, d, n = cm.group(1), cm.group(2), cm.group(4)
|
|
lanes = "2s" if size == "s" else "2d"
|
|
self.emit(f"\tfmov {size}{d}, {size}{n}")
|
|
self.emit(f"\t{op} v{d}.{lanes}, v{d}.{lanes}{cm.group(5) or ''}")
|
|
return
|
|
self.emit(f"\t{op} {self.operands(args)}" if args else f"\t{op}")
|
|
|
|
def run(self):
|
|
for i, raw in enumerate(self.lines):
|
|
try:
|
|
self.convert_line(raw)
|
|
except ConvertError as e:
|
|
raise ConvertError(f"line {i + 1}: {e}") from None
|
|
# a weak symbol is already global; a second directive only warns
|
|
weak = {line.split(None, 1)[1] for line in self.out if line.startswith("\t.weak ")}
|
|
out = [line for line in self.out
|
|
if not (line.startswith("\t.globl ") and line.split(None, 1)[1] in weak)]
|
|
return "\n".join(out) + "\n"
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) != 3:
|
|
print(__doc__)
|
|
sys.exit(2)
|
|
with open(sys.argv[1], "r", encoding="utf-8", errors="surrogateescape") as f:
|
|
lines = f.read().split("\n")
|
|
try:
|
|
result = Converter(lines).run()
|
|
except ConvertError as e:
|
|
print(f"{sys.argv[1]}: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
with open(sys.argv[2], "w", encoding="utf-8", errors="surrogateescape") as f:
|
|
f.write(result)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|