mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-17 14:00:25 +02:00
265 lines
9.9 KiB
Python
265 lines
9.9 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.
|
|
|
|
Usage: android_asm_convert.py input.s output.s
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
|
|
IDENT = re.compile(r'"[^"]*"|[A-Za-z_.$][\w.$]*')
|
|
|
|
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",
|
|
}
|
|
|
|
|
|
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.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
|
|
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()
|