Files
AnyPS5/tools/tests/test_annotate_module.py
T

227 lines
11 KiB
Python

import contextlib
import io
import re
import shutil
import struct
import sys
import tempfile
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import annotate_module
import nid_names
IMPORT_NAME = "sceKernelGetProcessTime"
EXPORT_NAME = "sceKernelUsleep"
SECOND_IMPORT_NAME = "sceKernelGetTscFrequency"
TLS_NAME = "sceKernelGetCurrentCpu"
GNU_HASH = 0x6FFFFEF5
CODE_ADDRESS = 0x1000
DATA_ADDRESS = 0x2000
SLOT = 0x2800
CALL_ADDRESS = 0x1006
EXPORT_ADDRESS = 0x100B
def nid(name):
return nid_names.compute_nid(name)
def symbol_name(name):
return nid(name) + "#A#A"
def code_bytes(prefix=b""):
stub = prefix + b"\xff\x25" + struct.pack("<i", SLOT - (CODE_ADDRESS + len(prefix) + 6))
call_address = CODE_ADDRESS + len(stub)
call = b"\xe8" + struct.pack("<i", CODE_ADDRESS - (call_address + 5))
return stub + call + b"\xc3"
DEFAULT_SYMBOLS = [(IMPORT_NAME, 0, 0x12, 0), (EXPORT_NAME, 1, 0x12, EXPORT_ADDRESS)]
DEFAULT_TAGS = (4, 5, 6, 10, 11, 23, 2, 20)
def hash_bytes(kind, count):
if kind == "sysv":
return struct.pack("<II", 1, count) + bytes(4 + 4 * count)
chains = [0] * (count - 1)
chains[-1] = 1
return struct.pack("<IIII", 1, 1, 1, 0) + bytes(8) + struct.pack("<I", 1) + struct.pack("<%dI" % len(chains), *chains)
def data_bytes(tags, values, symbols, relocations, glob_dats, kind):
strings = b"\0"
entries = struct.pack("<IBBHQQ", 0, 0, 0, 0, 0, 0)
for name, shndx, info, value in symbols:
entries += struct.pack("<IBBHQQ", len(strings), info, 0, shndx, value, 1)
strings += symbol_name(name).encode() + b"\0"
resolved = {4: DATA_ADDRESS, GNU_HASH: DATA_ADDRESS, 5: DATA_ADDRESS + 0x100, 6: DATA_ADDRESS + 0x40, 10: len(strings), 11: 24,
7: DATA_ADDRESS + 0x200, 8: 24 * len(glob_dats), 23: DATA_ADDRESS + 0x280, 2: 24 * len(relocations), 20: 7,
9: 24, 17: DATA_ADDRESS + 0x200}
resolved.update(values)
dynamic = b"".join(struct.pack("<qQ", tag, resolved[tag]) for tag in tags) + struct.pack("<qQ", 0, 0)
image = bytearray(0x810)
pack = lambda rows, kind_code: b"".join(struct.pack("<QQq", slot, (index << 32) | kind_code, 0) for slot, index in rows)
for offset, blob in ((0, hash_bytes(kind, len(symbols) + 1)), (0x40, entries), (0x100, strings), (0x200, pack(glob_dats, 6)),
(0x280, pack(relocations, 7)), (0x300, dynamic)):
image[offset:offset + len(blob)] = blob
return bytes(image), len(dynamic)
def build_module(tags=DEFAULT_TAGS, values=None, symbols=DEFAULT_SYMBOLS, relocations=((SLOT, 1),), glob_dats=(), code=None,
code_flags=5, extra_code=None, kind="sysv", machine=0x3E):
code = code_bytes() if code is None else code
data, dynamic_size = data_bytes(tags, values or {}, symbols, relocations, glob_dats, kind)
headers = [(1, code_flags, 0x1000, CODE_ADDRESS, len(code)), (1, 6, 0x2000, DATA_ADDRESS, len(data)),
(2, 6, 0x2300, DATA_ADDRESS + 0x300, dynamic_size)]
if extra_code is not None:
headers.append((1, 7, 0x3000, 0x3000, len(extra_code)))
image = bytearray(0x3000 + len(extra_code or b""))
image[:64] = struct.pack("<16sHHIQQQIHHHHHH", b"\x7fELF\x02\x01\x01" + bytes(9), 3, machine, 1, 0, 64, 0, 0, 64, 56, len(headers), 0, 0, 0)
for index, (kind_code, flags, offset, vaddr, size) in enumerate(headers):
image[64 + index * 56:120 + index * 56] = struct.pack("<IIQQQQQQ", kind_code, flags, offset, vaddr, vaddr, size, size, 0x1000)
image[0x1000:0x1000 + len(code)] = code
image[0x2000:0x2000 + len(data)] = data
if extra_code is not None:
image[0x3000:] = extra_code
return bytes(image)
def instruction(listing, mnemonic):
pattern = re.compile(r"^\s*[0-9a-f]+:\s+(?:\w+\s+)?%s\b" % mnemonic)
return next(line for line in listing if pattern.search(line))
class AnnotateModuleTests(unittest.TestCase):
def setUp(self):
self.assertIsNotNone(shutil.which("objdump"), "binutils objdump is required to run these tests")
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.root = Path(self.directory.name)
self.database = self.root / "db.csv"
names = (IMPORT_NAME, SECOND_IMPORT_NAME, TLS_NAME, EXPORT_NAME)
self.database.write_text("".join("%s %s%s\n" % (nid(name), name, "_nid_postfix" if name == EXPORT_NAME else "#A#A") for name in names),
encoding="utf-8")
def run_tool(self, module, *extra):
path = self.root / "module.prx"
path.write_bytes(module)
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
annotate_module.main([str(path), "--out", str(self.root / "out")] + list(extra))
return stdout.getvalue()
def annotate(self, module):
return self.run_tool(module, "--db", str(self.database))
def read(self, suffix):
return (self.root / ("out" + suffix)).read_text(encoding="utf-8")
def assert_fails(self, module, message):
with self.assertRaises(SystemExit) as raised:
self.run_tool(module, "--no-names")
self.assertIn(message, str(raised.exception))
def test_names_imports_stubs_calls_and_exports(self):
summary = self.annotate(build_module())
self.assertIn("1 imports, 1 exports, 0 tls, 1 stubs", summary)
self.assertEqual("%#x %s %s\n" % (SLOT, symbol_name(IMPORT_NAME), IMPORT_NAME), self.read(".imports.txt"))
self.assertEqual("%#x %s %s\n" % (EXPORT_ADDRESS, symbol_name(EXPORT_NAME), EXPORT_NAME), self.read(".exports.txt"))
listing = self.read(".asm").splitlines()
self.assertTrue(instruction(listing, "jmp").endswith("; GOT " + IMPORT_NAME))
self.assertTrue(instruction(listing, "call").endswith("; -> " + IMPORT_NAME))
header = listing.index("; ===== export " + EXPORT_NAME)
self.assertEqual(instruction(listing, "ret"), listing[header + 1])
self.assertEqual([], [path.name for path in self.root.iterdir() if path.suffix == ".bin"])
def test_unknown_nid_prints_the_nid(self):
empty = self.root / "empty.csv"
empty.write_text("", encoding="utf-8")
self.run_tool(build_module(), "--db", str(empty))
self.assertEqual("%#x %s %s\n" % (SLOT, symbol_name(IMPORT_NAME), nid(IMPORT_NAME)), self.read(".imports.txt"))
self.assertIn("; -> " + nid(IMPORT_NAME), self.read(".asm"))
def test_no_names_skips_the_database(self):
self.run_tool(build_module(), "--no-names")
self.assertIn("; -> " + nid(IMPORT_NAME), self.read(".asm"))
def test_mismatching_database_name_is_not_used(self):
wrong = self.root / "wrong.csv"
wrong.write_text("%s sceKernelOther\n" % nid(IMPORT_NAME), encoding="utf-8")
self.run_tool(build_module(), "--db", str(wrong))
self.assertNotIn("sceKernelOther", self.read(".asm"))
def test_gnu_hash_gives_the_symbol_count(self):
symbols = [(IMPORT_NAME, 0, 0x12, 0), (SECOND_IMPORT_NAME, 0, 0x12, 0), (EXPORT_NAME, 1, 0x12, EXPORT_ADDRESS)]
self.annotate(build_module(tags=(GNU_HASH, 5, 6, 10, 11, 23, 2, 20), symbols=symbols, kind="gnu"))
self.assertIn(EXPORT_NAME, self.read(".exports.txt"))
def test_glob_dat_slot_referenced_by_an_indirect_call(self):
code = b"\xff\x15" + struct.pack("<i", SLOT + 8 - (CODE_ADDRESS + 6)) + b"\xc3"
symbols = [(IMPORT_NAME, 0, 0x12, 0), (SECOND_IMPORT_NAME, 0, 0x12, 0)]
self.annotate(build_module(tags=(4, 5, 6, 10, 11, 7, 8, 23, 2, 20), symbols=symbols, relocations=(), glob_dats=((SLOT + 8, 2),), code=code))
self.assertIn("%#x %s %s\n" % (SLOT + 8, symbol_name(SECOND_IMPORT_NAME), SECOND_IMPORT_NAME), self.read(".imports.txt"))
self.assertTrue(instruction(self.read(".asm").splitlines(), "call").endswith("; GOT " + SECOND_IMPORT_NAME))
def test_prefixed_stub_is_recognised(self):
self.annotate(build_module(code=code_bytes(b"\xf2")))
self.assertTrue(instruction(self.read(".asm").splitlines(), "call").endswith("; -> " + IMPORT_NAME))
def test_tls_symbol_is_listed_but_not_an_address(self):
symbols = DEFAULT_SYMBOLS + [(TLS_NAME, 1, 0x16, EXPORT_ADDRESS)]
summary = self.annotate(build_module(symbols=symbols))
self.assertIn("1 exports, 1 tls", summary)
self.assertIn("tls+%#x %s %s\n" % (EXPORT_ADDRESS, symbol_name(TLS_NAME), TLS_NAME), self.read(".exports.txt"))
self.assertNotIn("export " + TLS_NAME, self.read(".asm"))
def test_export_at_address_zero_is_kept(self):
symbols = [(IMPORT_NAME, 0, 0x12, 0), (EXPORT_NAME, 1, 0x12, 0)]
self.annotate(build_module(symbols=symbols))
self.assertEqual("0x0 %s %s\n" % (symbol_name(EXPORT_NAME), EXPORT_NAME), self.read(".exports.txt"))
def test_every_executable_segment_is_disassembled(self):
self.annotate(build_module(extra_code=b"\xc3"))
listing = self.read(".asm").splitlines()
first, second = listing.index("; ===== segment 0x1000"), listing.index("; ===== segment 0x3000")
self.assertLess(first, second)
self.assertTrue(any(re.match(r"^\s*3000:\s+ret\b", line) for line in listing[second:]))
def test_missing_hash_fails(self):
self.assert_fails(build_module(tags=(5, 6, 10, 11, 23, 2, 20)), "neither DT_HASH nor DT_GNU_HASH")
def test_missing_symtab_fails(self):
self.assert_fails(build_module(tags=(4, 5, 10, 11, 23, 2, 20)), "DT_SYMTAB")
def test_missing_strsz_fails(self):
self.assert_fails(build_module(tags=(4, 5, 6, 11, 23, 2, 20)), "DT_STRSZ")
def test_missing_pltrelsz_fails(self):
self.assert_fails(build_module(tags=(4, 5, 6, 10, 11, 23, 20)), "DT_PLTRELSZ")
def test_missing_relasz_fails(self):
self.assert_fails(build_module(tags=(4, 5, 6, 10, 11, 7)), "DT_RELASZ")
def test_pltrel_must_be_rela(self):
self.assert_fails(build_module(values={20: 17}), "DT_PLTREL is not DT_RELA")
def test_dt_rel_is_rejected(self):
self.assert_fails(build_module(tags=DEFAULT_TAGS + (17,)), "DT_REL relocations are not supported")
def test_relaent_must_be_24(self):
self.assert_fails(build_module(tags=DEFAULT_TAGS + (9,), values={9: 16}), "DT_RELAENT")
def test_name_offset_past_strsz_fails(self):
self.assert_fails(build_module(values={10: 4}), "outside DT_STRSZ")
def test_no_executable_segment_fails(self):
self.assert_fails(build_module(code_flags=6), "no executable PT_LOAD segment")
def test_wrong_machine_fails(self):
self.assert_fails(build_module(machine=0xB7), "not an x86-64 ELF")
if __name__ == "__main__":
unittest.main()