objdiff stuff

This commit is contained in:
Mc-muffin
2026-08-09 17:17:01 -05:00
committed by Timothy O'Barr
parent b4b1ea26eb
commit 4f0a3e76b9
20 changed files with 687 additions and 3111 deletions
+1
View File
@@ -17,6 +17,7 @@ config/**/tmp_*/
config/**/objdiff.json config/**/objdiff.json
# ignore elf files # ignore elf files
config/**/[A-Z][A-Z][A-Z][A-Z]_[0-9][0-9][0-9].[0-9][0-9] config/**/[A-Z][A-Z][A-Z][A-Z]_[0-9][0-9][0-9].[0-9][0-9]
config/**/[A-Z][A-Z][A-Z][A-Z]_[0-9][0-9][0-9].[0-9][0-9].rom
.idea/ .idea/
.vscode/ .vscode/
SLUS_210.07 SLUS_210.07
-3
View File
@@ -112,9 +112,6 @@ build: ## Build project
extract-data: ## Extract variables from .data in config directory extract-data: ## Extract variables from .data in config directory
@python3 tools/python/parse_data.py @python3 tools/python/parse_data.py
make-asm: ## Create expected asm folder in config directory
@python3 configure.py config/kuon.yaml --make-asm
map-mismatch: ## Check for mismatches in mapfile map-mismatch: ## Check for mismatches in mapfile
@python3 tools/python/map_mismatch.py --language @python3 tools/python/map_mismatch.py --language
+1 -1
View File
@@ -1 +1 @@
7124b436f61ebbe5944e4b50b7d4c04475711c11 build/SLUS_210.07 294a1d49a96589c9caf68837b8d914b7a1ba2ce1 build/SLUS_210.07
+4 -18
View File
@@ -28,14 +28,6 @@ options:
make_full_disasm_for_code: true make_full_disasm_for_code: true
asm_nonmatching_label_macro: "" asm_nonmatching_label_macro: ""
create_bss_pads: False create_bss_pads: False
# custom field used by fix_linkerscript.py to fix linkerscript alignments
_section_subalign:
elf_header: 8
main: 2
main_bss: 1
reg_info: 4
mips_debug: 4
rest: 4
segments: segments:
- [0, databin, elf_header] - [0, databin, elf_header]
- name: main - name: main
@@ -43,12 +35,6 @@ segments:
start: 0x80 start: 0x80
vram: 0x100000 vram: 0x100000
bss_size: 0x80BB00 # size of .sbss + size of .bss bss_size: 0x80BB00 # size of .sbss + size of .bss
align: 1
subalign: 2 # safe subalign value. proper values are applied by fix_linkerscript.py where necessary
# text has align 8 but other sections may have a different alignment ...
# explicit align correction is performed by fix_linkerscript.py during
# project configuration. vram address is used, when reported, in non text
# sections to calculate the correct align value.
subsegments: subsegments:
- { start: 0x000080, type: asmtu, name: sdk/sce/ee/crt0, ld_align_segment_start: 0x8 } - { start: 0x000080, type: asmtu, name: sdk/sce/ee/crt0, ld_align_segment_start: 0x8 }
- [0x0002B0, asmtu, sdk/sce/ee/libgraph/graph001] - [0x0002B0, asmtu, sdk/sce/ee/libgraph/graph001]
@@ -249,10 +235,10 @@ segments:
- [0x032110, asmtu, cs4/Seimei/src/Camera] - [0x032110, asmtu, cs4/Seimei/src/Camera]
- [0x034930, asmtu, cs4/Seimei/src/Debug] - [0x034930, asmtu, cs4/Seimei/src/Debug]
- [0x034D10, asmtu, cs4/Seimei/src/Draw] - [0x034D10, asmtu, cs4/Seimei/src/Draw]
- [0x038710, asmtu, cs4/Seimei/src/File] - [0x038710, c, cs4/Seimei/src/File]
- [0x038D00, asmtu, cs4/Seimei/src/Game] - [0x038D00, asmtu, cs4/Seimei/src/Game]
- [0x039A00, asmtu, cs4/Seimei/src/Load] - [0x039A00, asmtu, cs4/Seimei/src/Load]
- [0x03A380, asmtu, cs4/Seimei/src/Main] - [0x03A380, c, cs4/Seimei/src/Main]
- [0x03AFE0, asmtu, cs4/Seimei/src/MathUtil] - [0x03AFE0, asmtu, cs4/Seimei/src/MathUtil]
- [0x03BCE0, asmtu, cs4/Seimei/src/MdlLink] - [0x03BCE0, asmtu, cs4/Seimei/src/MdlLink]
- [0x03BDC0, asmtu, cs4/Seimei/src/Mic] - [0x03BDC0, asmtu, cs4/Seimei/src/Mic]
@@ -283,7 +269,7 @@ segments:
- [0x059C60, asmtu, cs4/Seimei/src/Dss] - [0x059C60, asmtu, cs4/Seimei/src/Dss]
- [0x05ACB0, asmtu, cs4/Seimei/src/Damage] - [0x05ACB0, asmtu, cs4/Seimei/src/Damage]
- [0x05B3B0, asmtu, cs4/Seimei/src/Hmd] - [0x05B3B0, asmtu, cs4/Seimei/src/Hmd]
- [0x05E360, asmtu, cs4/Seimei/src/Binder] - [0x05E360, c, cs4/Seimei/src/Binder]
- [0x05E980, asmtu, cs4/Seimei/src/Ene] - [0x05E980, asmtu, cs4/Seimei/src/Ene]
- [0x060DF0, asmtu, cs4/Seimei/src/EneAct] - [0x060DF0, asmtu, cs4/Seimei/src/EneAct]
- [0x062F40, asmtu, cs4/Seimei/src/EneThk] - [0x062F40, asmtu, cs4/Seimei/src/EneThk]
@@ -421,7 +407,7 @@ segments:
- [0x105150, asmtu, cs4/Seimei/src/E044Act] - [0x105150, asmtu, cs4/Seimei/src/E044Act]
- [0x105560, asmtu, cs4/Seimei/src/sfx/SfxFog] - [0x105560, asmtu, cs4/Seimei/src/sfx/SfxFog]
- [0x105A00, asmtu, cs4/Seimei/fslib/lib/fsspummng] - [0x105A00, asmtu, cs4/Seimei/fslib/lib/fsspummng]
- [0x106520, asmtu, cs4/Seimei/src/Event] - [0x106520, c, cs4/Seimei/src/Event]
- [0x106A90, asmtu, cs4/Seimei/src/Event0031] - [0x106A90, asmtu, cs4/Seimei/src/Event0031]
- [0x107380, asmtu, cs4/Seimei/src/Event0051] - [0x107380, asmtu, cs4/Seimei/src/Event0051]
- [0x107E90, asmtu, cs4/Seimei/src/MiniGame/sgh/sgh] - [0x107E90, asmtu, cs4/Seimei/src/MiniGame/sgh/sgh]
+47 -350
View File
@@ -5,23 +5,15 @@
from __future__ import annotations from __future__ import annotations
import os import os
import re
import sys import sys
import json import json
import yaml
import copy
import splat import splat
import shutil import shutil
import argparse import argparse
import tempfile
import subprocess import subprocess
import ninja_syntax import ninja_syntax
from dataclasses import dataclass, asdict from splat.segtypes.linker_entry import LinkerEntry
#from utils import ensure_path_and_write, normalize_object_path, to_expected_path
import splat.util.options as splat_options
from splat.segtypes.linker_entry import LinkerEntry, clean_up_path
from spimdisasm.common.CompilerConfig import compilerOptions from spimdisasm.common.CompilerConfig import compilerOptions
from typing import Any, Union, Protocol, Literal, cast from typing import Any, Union, Protocol, Literal, cast
@@ -30,16 +22,6 @@ from contextlib import contextmanager
from splat.scripts import split from splat.scripts import split
from splat.util.conf import load as splat_load_yaml from splat.util.conf import load as splat_load_yaml
from splat.segtypes.linker_entry import LinkerEntry
# from tools.python.fix_gp import fix_gp
from tools.python.fix_assets import fix_assets
from tools.python.fix_linkerscript import fix_linkerscript
from sys import stdout
from subprocess import run
from dataclasses import dataclass
from struct import unpack
@contextmanager @contextmanager
def suppress_stdout_stderr(): def suppress_stdout_stderr():
@@ -110,7 +92,7 @@ def make_compiler_cmd(config_dir: Path, src_path: Path, language: str):
def exec_shell(command: list[str]) -> str: def exec_shell(command: list[str]) -> str:
ret = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) ret = subprocess.run(command, capture_output=True, text=True, check=False)
return ret.stdout return ret.stdout
@@ -196,6 +178,21 @@ def build_stuff(
object_strs = [str(obj) for obj in object_paths] object_strs = [str(obj) for obj in object_paths]
expected_strs = []
for obj in object_paths:
if obj.name.endswith(".c.o"):
obj = obj.with_name(obj.name.removesuffix(".c.o") + ".o")
elif obj.name.endswith(".s.o"):
obj = obj.with_name(obj.name.removesuffix(".s.o") + ".o")
if obj.parts[:2] == ("build", "asm"):
p = obj.relative_to("build/asm")
elif obj.parts[:2] == ("build", "src"):
p = obj.relative_to("build/src")
else:
p = obj
expected_strs.append(str(Path("build/expected") / p))
for object_path in object_paths: for object_path in object_paths:
if object_path.name == "elf_header.s.o": if object_path.name == "elf_header.s.o":
continue continue
@@ -211,6 +208,14 @@ def build_stuff(
implicit_outputs=implicit_outputs, implicit_outputs=implicit_outputs,
) )
ninja.build(
outputs=expected_strs,
rule="as",
inputs=[str((Path("asm") / s.relative_to("../src") if s.parts[:2] == ("..", "src") else s).with_suffix(".s")) for s in src_paths],
variables=variables,
implicit_outputs=implicit_outputs,
)
ninja = ninja_syntax.Writer(open(str(ROOT / config_dir / "build.ninja"), "w"), width=9999) ninja = ninja_syntax.Writer(open(str(ROOT / config_dir / "build.ninja"), "w"), width=9999)
# Rules # Rules
@@ -295,16 +300,15 @@ def build_stuff(
else: else:
build(entry.object_path, entry.src_paths, "cc") build(entry.object_path, entry.src_paths, "cc")
elif isinstance(seg, splat.segtypes.common.databin.CommonSegDatabin): elif isinstance(
build(entry.object_path, entry.src_paths, "as") seg,
(
elif isinstance(seg, splat.segtypes.common.rodatabin.CommonSegRodatabin): splat.segtypes.common.databin.CommonSegDatabin,
build(entry.object_path, entry.src_paths, "as") splat.segtypes.common.rodatabin.CommonSegRodatabin,
splat.segtypes.common.textbin.CommonSegTextbin,
elif isinstance(seg, splat.segtypes.common.textbin.CommonSegTextbin): splat.segtypes.common.sbss.CommonSegSbss,
build(entry.object_path, entry.src_paths, "as") ),
):
elif isinstance(seg, splat.segtypes.common.sbss.CommonSegSbss):
build(entry.object_path, entry.src_paths, "as") build(entry.object_path, entry.src_paths, "as")
else: else:
@@ -338,288 +342,6 @@ def build_stuff(
implicit=[elf_path], implicit=[elf_path],
) )
def get_line_file_path(args: AnnotationArgs):
if args.line_file_path is not None:
return args.line_file_path
# if args.elf_path.name == SH2_SERIAL and "Event/stage" in args.asm_path.as_posix():
# return Path(f"{TOOLS}/alessatool/dwarf") / Path(args.asm_path.name).with_suffix(".line")
return None
def line_has_vram_addr(line: str, addr_str: str) -> bool:
if addr_str not in line or "*/" not in line:
return False
return line.index("*/") > line.index(addr_str)
def annotate_asm(args: AnnotationArgs):
with open(args.asm_path, "r") as asm_file:
asm_contents = asm_file.read()
asm_lines = asm_contents.splitlines()
asm_line_index = 0
vram_start = args.vram_start
vram_end = args.vram_end
if vram_start is None or vram_end is None:
vram_start, vram_end = find_vram_bounds(asm_lines)
line_file_path = get_line_file_path(args)
if line_file_path is None or not line_file_path.exists():
addresses = (f"0x{v:X}" for v in range(vram_start, vram_end, 0x4))
proc = run([args.addr2line_path, "-e", args.elf_path, *addresses], capture_output=True, encoding=args.encoding)
addr2line_output_lines = proc.stdout.splitlines()
else:
# parse the binary line number file.
# the format is a list of u_shorts, one per line number.
# there should be one line number per vram address, and each vram
# address should be exactly 4 bytes apart, mirroring how the addr2line
# output is formatted
with open(line_file_path, "rb") as line_file:
line_data = line_file.read()
line_numbers = unpack(f"<{len(line_data) // 2}H", line_data)
compile_unit = args.asm_path.with_suffix(".c").name
addr2line_output_lines = list(map(lambda n : to_addr2line_format(compile_unit, n), line_numbers))
main_tu_name = None
prev_tu_name = None
prev_line_number = -1
function_count = 0
is_in_function_label = False
current_vram_addr = vram_start
annotated_asm_lines = []
for addr_index in range(0, len(addr2line_output_lines) - 1):
line = addr2line_output_lines[addr_index]
if line.startswith("?"):
current_vram_addr += 0x4
continue
separator_index = line.rfind(":")
current_line_number = int(line[separator_index+1:])
if current_line_number == prev_line_number:
current_vram_addr += 0x4
continue
vram_addr_str = f"{current_vram_addr:X}"
current_tu_name = line[0:separator_index]
if current_tu_name != main_tu_name and current_tu_name != prev_tu_name:
if main_tu_name:
print(f"[warn] alessatool/annotate: address at {vram_addr_str} belongs to {current_tu_name}")
else:
main_tu_name = current_tu_name
prev_tu_name = current_tu_name
while True:
if asm_line_index >= len(asm_lines):
raise AssertionError(
f"address 0x{current_vram_addr:X} not found in asm"
)
asm_line = asm_lines[asm_line_index]
if line_has_vram_addr(asm_line, vram_addr_str):
break
should_append_asm_line = True
if args.tu:
asm_line_trimmed = asm_line.strip()
# track when we go in & out of function symbols
if asm_line_trimmed.startswith(FUNCTION_SYMBOL_LABEL):
function_count += 1
is_in_function_label = True
annotated_asm_lines.append(f"{UNIQUE_TEXT_SECTION_DIRECTIVE}{function_count}")
annotated_asm_lines.append("")
elif asm_line_trimmed.startswith(END_FUNCTION_SYMBOL_LABEL):
is_in_function_label = False
# remove `nop`s
if not is_in_function_label and asm_line_trimmed.endswith("nop"):
should_append_asm_line = False
# remove `macro.inc` include directive
elif asm_line_trimmed == INCLUDE_MACRO_INC_DIRECTIVE:
should_append_asm_line = False
if should_append_asm_line:
annotated_asm_lines.append(asm_line)
asm_line_index += 1
annotated_asm_lines.append(f"\t.loc 1 {current_line_number}")
annotated_asm_lines.append(asm_line)
asm_line_index += 1
prev_line_number = current_line_number
current_vram_addr += 0x4
while asm_line_index < len(asm_lines) - 1:
annotated_asm_lines.append(asm_lines[asm_line_index])
asm_line_index += 1
assert main_tu_name, "no valid compilation unit found"
annotated_asm_lines = [
".section .debug",
".previous",
".text",
f".file 1 \"{main_tu_name}\"",
*annotated_asm_lines
]
append_final_new_line(annotated_asm_lines)
annotated_asm_contents = "\n".join(annotated_asm_lines)
if not args.stdout and args.out_path:
with open(args.out_path, "w") as out_file:
out_file.write(annotated_asm_contents)
if args.verbose:
print(f"alessatool/annotate: wrote asm to {args.out_path}")
else:
stdout.write(annotated_asm_contents)
def append_final_new_line(lines: list[str]):
if lines[-1] != "":
lines.append("")
def make_asm(config_path: Path, config: dict[str, Any]):
"""
Extracts assembly for each function into 'expected/asm/' subfolder.
The extraction is done following these steps:
1. create temporary directory in project folder
2. copy yaml to temporary directory
3. modify yaml to have c subsegments instead of asm subsegments
4. remove data, bss, etc., subsegments
5. run splat on the new config yaml
6. splat extracts assembly for each function into '<temp_dir>/asm/nonmatchings/'
7. copy '<temp_dir>/asm/nonmatchings/' to '<project_dir>/expected/asm/'
"""
with tempfile.TemporaryDirectory(dir=config_path, prefix="tmp_") as tmp_dir:
tmp_path = Path(tmp_dir)
yaml_path = tmp_path / "config.yaml"
asm_path = tmp_path / "asm" / "nonmatchings"
dst_path = tmp_path / ".." / "expected" / "asm"
dst_path = dst_path.resolve().relative_to(ROOT)
if dst_path.exists():
print(f"expected asm dir '{dst_path}' already exists")
return
config = copy.deepcopy(config)
config["options"]["target_path"] = "../" + config["options"]["target_path"]
config["options"]["asm_path"] = "asm"
config["options"]["src_path"] = "src"
config["options"]["build_path"] = "build"
config["options"]["asset_path"] = "assets"
config["options"]["undefined_funcs_auto_path"] = "../" + config["options"]["undefined_funcs_auto_path"]
config["options"]["undefined_syms_auto_path"] = "../" + config["options"]["undefined_syms_auto_path"]
config["options"]["symbol_addrs_path"] = "../" + config["options"]["symbol_addrs_path"]
config["options"]["extensions_path"] = "../" + config["options"]["extensions_path"]
new_segments: list[Any] = []
segments: list[Any] = config["segments"]
for segment in segments:
if isinstance(segment, list):
new_segments.append(segment)
elif isinstance(segment, dict) and segment["name"] == "main":
new_subsegments: list[Any] = []
subsegments = cast(list[Any], segment["subsegments"])
for subsegment in subsegments:
if isinstance(subsegment, list):
if subsegment[1] == "asm":
subsegment[1] = "c"
new_subsegments.append(subsegment)
elif isinstance(subsegment, dict):
subsegment["type"] = subsegment["type"].strip(".")
if subsegment["type"] == "rodata":
# splat now requires rodata to be always prefixed with a dot (.)
# for the rodata migration to work properly
subsegment["type"] = ".rodata"
new_subsegments.append(subsegment)
segment["subsegments"] = new_subsegments
new_segments.append(segment)
config["segments"] = new_segments
def rename_locals(base_path: Path):
for asm_file in base_path.rglob("*.s"):
data = asm_file.read_text()
data = re.sub(r"__local_\d+", "", data)
asm_file.write_text(data)
with yaml_path.open(mode="w") as yaml_file:
yaml.dump(config, yaml_file, default_flow_style=False)
with suppress_stdout_stderr():
split.main([yaml_path], modes=["all"], verbose=False)
# remove '__local_#' from asm
rename_locals(asm_path)
dst_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(asm_path, dst_path, dirs_exist_ok=True)
print(f"expected asm extracted to '{dst_path}'")
# make expected objs
for subseg in new_segments[1]["subsegments"]:
if isinstance(subseg, list) and subseg[1] == "c":
subseg[1] = "asm"
subseg[2] += ".c"
config["options"]["asm_jtbl_label_macro"] = "llabel"
with yaml_path.open(mode="w") as yaml_file:
yaml.dump(config, yaml_file, default_flow_style=False)
shutil.rmtree(tmp_path / "asm")
(tmp_path / ".splache").unlink()
with suppress_stdout_stderr():
split.main([yaml_path], modes=["all"], verbose=False)
# remove '__local_#' from asm
rename_locals(asm_path)
dst_path = dst_path.parent / "obj"
tmp_obj_path = tmp_path / "obj"
tmp_asm_dir = tmp_path / "asm"
cpp = Path("..", "..", "..", (Path("tools") / "cc" / COMPILER))
for asm_file in tmp_asm_dir.rglob("*.c.s"):
asm_file_rel = asm_file.relative_to(tmp_path)
obj_file_rel = Path("obj") / asm_file.relative_to(tmp_asm_dir).with_suffix(".o")
obj_file = tmp_obj_path / obj_file_rel.relative_to("obj")
obj_file.parent.mkdir(parents=True, exist_ok=True)
subprocess.run(
f"{cpp} -I../src -I../include -Iinclude -Iinclude/sdk/ee -I../.. '{asm_file_rel}' -o - | "
#f"iconv -f=UTF-8 -t=EUC-JP '{asm_file_rel}' | "
f"mips-linux-gnu-as -no-pad-sections -EL -march=5900 -mabi=eabi -I../include -o {obj_file_rel} {asm_file_rel}",
shell=True,
cwd=tmp_path,
)
shutil.copytree(tmp_obj_path, dst_path, dirs_exist_ok=True)
print(f"expected obj built to '{dst_path}'")
def generate_objdiff_configuration(config_path: Path, config: dict[str, Any], language: str): def generate_objdiff_configuration(config_path: Path, config: dict[str, Any], language: str):
""" """
Generate `objdiff.json` configuration from splat YAML config. Generate `objdiff.json` configuration from splat YAML config.
@@ -633,7 +355,7 @@ def generate_objdiff_configuration(config_path: Path, config: dict[str, Any], la
""" """
segments: list[Any] = config["segments"] segments: list[Any] = config["segments"]
tu_to_diff: list[tuple[Literal["asm", "c"], str]] = [] tu_to_diff: list[tuple[Literal["asmtu", "c"], str]] = []
for segment in segments: for segment in segments:
if not (isinstance(segment, dict) and segment["name"] == "main"): if not (isinstance(segment, dict) and segment["name"] == "main"):
@@ -653,7 +375,7 @@ def generate_objdiff_configuration(config_path: Path, config: dict[str, Any], la
else: else:
raise RuntimeError("invalid subsegment type") raise RuntimeError("invalid subsegment type")
if subs_type in ("asm", "c"): if subs_type in ("asmtu", "c"):
if subs_name in ( if subs_name in (
"crt0", "crt0",
"main/glob", "main/glob",
@@ -678,7 +400,7 @@ def generate_objdiff_configuration(config_path: Path, config: dict[str, Any], la
units: list[dict[str, Any]] = [] units: list[dict[str, Any]] = []
for tu_type, tu_name in tu_to_diff: for tu_type, tu_name in tu_to_diff:
target_path = Path("expected", "obj", tu_name).with_suffix(".c.o") target_path = Path("build", "expected", tu_name).with_suffix(".o")
# since we only compile fully decompiled TUs, the # since we only compile fully decompiled TUs, the
# "c" type implies that the TU is complete # "c" type implies that the TU is complete
@@ -695,7 +417,10 @@ def generate_objdiff_configuration(config_path: Path, config: dict[str, Any], la
"name": tu_name, "name": tu_name,
"target_path": str(target_path), "target_path": str(target_path),
"base_path": str(base_path) if base_path else None, "base_path": str(base_path) if base_path else None,
"metadata": {"progress_categories": [language]}, "metadata": {
"source_path": str(Path("..", "src", tu_name).with_suffix(".c")),
"progress_categories": [language]
},
} }
if not base_path: if not base_path:
@@ -717,10 +442,10 @@ def generate_objdiff_configuration(config_path: Path, config: dict[str, Any], la
objdiff_json: dict[str, Any] = { objdiff_json: dict[str, Any] = {
"$schema": "https://raw.githubusercontent.com/encounter/objdiff/main/config.schema.json", "$schema": "https://raw.githubusercontent.com/encounter/objdiff/main/config.schema.json",
"custom_make": "true", "custom_make": "ninja",
"custom_args": [], "custom_args": [],
"build_target": False, "build_target": True,
"build_base": False, "build_base": True,
"watch_patterns": [], "watch_patterns": [],
"units": units, "units": units,
"progress_categories": progress_categories, "progress_categories": progress_categories,
@@ -858,7 +583,7 @@ def main():
cwd = Path(os.getcwd()).resolve() cwd = Path(os.getcwd()).resolve()
if not ROOT.samefile(cwd): if not ROOT.samefile(cwd):
print("ERROR: this script must be run from it's directory!") print("ERROR: this script must be run from it's directory!")
exit(1) sys.exit(1)
compilerOptions["MWCCPS2"].value.bigAddendWorkaroundForMigratedFunctions = False compilerOptions["MWCCPS2"].value.bigAddendWorkaroundForMigratedFunctions = False
@@ -876,9 +601,9 @@ def main():
if basename not in LANGUAGES: if basename not in LANGUAGES:
supported_elfs = f"{set(f'{elf} ({lang})' for elf, lang in LANGUAGES.items())}".replace("'", "") supported_elfs = f"{ {f'{elf} ({lang})' for elf, lang in LANGUAGES.items()} }".replace("'", "")
print(f"unsupported game ELF. Supported versions are: {supported_elfs}") print(f"unsupported game ELF. Supported versions are: {supported_elfs}")
exit(1) sys.exit(1)
language = LANGUAGES[basename] language = LANGUAGES[basename]
@@ -886,10 +611,6 @@ def main():
clean(config_dir, config) clean(config_dir, config)
return return
if args.make_asm:
make_asm(config_dir, config)
return
if args.clean: if args.clean:
clean(config_dir, config) clean(config_dir, config)
@@ -906,36 +627,12 @@ def main():
generate_lcf() generate_lcf()
# fix asset .incbin path from relative to root to relative to config dir:
# e.g., change:
# .incbin "config/assets/name.section.bin"
# to:
# .incbin "assets/name.section.bin"
fix_assets(asm_data_path, asset_rel_path)
linker_entries = split.linker_writer.entries linker_entries = split.linker_writer.entries
build_stuff(config_dir, split.config, linker_entries, language) build_stuff(config_dir, split.config, linker_entries, language)
write_permuter_settings(config_dir, src_path, language) write_permuter_settings(config_dir, src_path, language)
# # replace gp_rel assembler macro with explicit offset as the gcc used
# # to compile the code does not support it
# gp_value = split.config["options"]["gp_value"]
# symbol_addrs_path = Path(split.config["options"]["symbol_addrs_path"])
# asm_rel_path = (config_dir / asm_path).resolve().relative_to(ROOT)
# symbol_addrs_rel_path = (config_dir / symbol_addrs_path).resolve().relative_to(ROOT)
# assert asm_rel_path.is_dir(), f"{asm_rel_path} not found or not a directory"
# assert symbol_addrs_rel_path.is_file(), f"{symbol_addrs_rel_path} not found"
# fix_gp(asm_rel_path, gp_value, symbol_addrs_rel_path)
# fix linkerscript by applying explicit alignments as
# specified in the config yaml
linkerscript_path = (config_dir / f"{basename}.ld").resolve().relative_to(ROOT)
assert linkerscript_path.is_file(), f"{linkerscript_path} not found"
fix_linkerscript(split.config, linkerscript_path)
generate_objdiff_configuration(config_dir, split.config, language) generate_objdiff_configuration(config_dir, split.config, language)
+634
View File
@@ -0,0 +1,634 @@
diff --git a/.gitignore b/.gitignore
index 26dc792..4b8c8a3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@ config/**/tmp_*/
config/**/objdiff.json
# ignore elf files
config/**/[A-Z][A-Z][A-Z][A-Z]_[0-9][0-9][0-9].[0-9][0-9]
+config/**/[A-Z][A-Z][A-Z][A-Z]_[0-9][0-9][0-9].[0-9][0-9].rom
.idea/
.vscode/
SLUS_210.07
diff --git a/Makefile b/Makefile
index 260e6df..4a4a30d 100644
--- a/Makefile
+++ b/Makefile
@@ -96,7 +96,7 @@ help: $(WITH_ENV) ## Show this help
##
## Commands:
configure: ## Configure project (needs SLUS_210.07)
- @python3 configure.py config/kuon.yaml -c
+ @uv run configure.py config/kuon.yaml -c
.build-only:
@cd config/; \
@@ -111,9 +111,6 @@ build: ## Build project
extract-data: ## Extract variables from .data in config directory
@python3 tools/python/parse_data.py
-
-make-asm: ## Create expected asm folder in config directory
- @python3 configure.py config/kuon.yaml --make-asm
map-mismatch: ## Check for mismatches in mapfile
@python3 tools/python/map_mismatch.py --language
diff --git a/config/checksum.sha1 b/config/checksum.sha1
index f58e64e..3278653 100644
--- a/config/checksum.sha1
+++ b/config/checksum.sha1
@@ -1 +1 @@
-7124b436f61ebbe5944e4b50b7d4c04475711c11 build/SLUS_210.07
+294a1d49a96589c9caf68837b8d914b7a1ba2ce1 build/SLUS_210.07
diff --git a/config/kuon.yaml b/config/kuon.yaml
index 63770ca..564ff60 100644
--- a/config/kuon.yaml
+++ b/config/kuon.yaml
@@ -28,14 +28,6 @@ options:
make_full_disasm_for_code: true
asm_nonmatching_label_macro: ""
create_bss_pads: False
-# custom field used by fix_linkerscript.py to fix linkerscript alignments
-_section_subalign:
- elf_header: 8
- main: 2
- main_bss: 1
- reg_info: 4
- mips_debug: 4
- rest: 4
segments:
- [0, databin, elf_header]
- name: main
@@ -43,12 +35,6 @@ segments:
start: 0x80
vram: 0x100000
bss_size: 0x80BB00 # size of .sbss + size of .bss
- align: 1
- subalign: 2 # safe subalign value. proper values are applied by fix_linkerscript.py where necessary
- # text has align 8 but other sections may have a different alignment ...
- # explicit align correction is performed by fix_linkerscript.py during
- # project configuration. vram address is used, when reported, in non text
- # sections to calculate the correct align value.
subsegments:
- { start: 0x000080, type: asmtu, name: sdk/sce/ee/crt0, ld_align_segment_start: 0x8 }
- [0x0002B0, asmtu, sdk/sce/ee/libgraph/graph001]
@@ -249,10 +235,10 @@ segments:
- [0x032110, asmtu, cs4/Seimei/src/Camera]
- [0x034930, asmtu, cs4/Seimei/src/Debug]
- [0x034D10, asmtu, cs4/Seimei/src/Draw]
- - [0x038710, asmtu, cs4/Seimei/src/File]
+ - [0x038710, c, cs4/Seimei/src/File]
- [0x038D00, asmtu, cs4/Seimei/src/Game]
- [0x039A00, asmtu, cs4/Seimei/src/Load]
- - [0x03A380, asmtu, cs4/Seimei/src/Main]
+ - [0x03A380, c, cs4/Seimei/src/Main]
- [0x03AFE0, asmtu, cs4/Seimei/src/MathUtil]
- [0x03BCE0, asmtu, cs4/Seimei/src/MdlLink]
- [0x03BDC0, asmtu, cs4/Seimei/src/Mic]
@@ -283,7 +269,7 @@ segments:
- [0x059C60, asmtu, cs4/Seimei/src/Dss]
- [0x05ACB0, asmtu, cs4/Seimei/src/Damage]
- [0x05B3B0, asmtu, cs4/Seimei/src/Hmd]
- - [0x05E360, asmtu, cs4/Seimei/src/Binder]
+ - [0x05E360, c, cs4/Seimei/src/Binder]
- [0x05E980, asmtu, cs4/Seimei/src/Ene]
- [0x060DF0, asmtu, cs4/Seimei/src/EneAct]
- [0x062F40, asmtu, cs4/Seimei/src/EneThk]
@@ -421,7 +407,7 @@ segments:
- [0x105150, asmtu, cs4/Seimei/src/E044Act]
- [0x105560, asmtu, cs4/Seimei/src/sfx/SfxFog]
- [0x105A00, asmtu, cs4/Seimei/fslib/lib/fsspummng]
- - [0x106520, asmtu, cs4/Seimei/src/Event]
+ - [0x106520, c, cs4/Seimei/src/Event]
- [0x106A90, asmtu, cs4/Seimei/src/Event0031]
- [0x107380, asmtu, cs4/Seimei/src/Event0051]
- [0x107E90, asmtu, cs4/Seimei/src/MiniGame/sgh/sgh]
diff --git a/configure.py b/configure.py
index cce9d9f..9d31656 100755
--- a/configure.py
+++ b/configure.py
@@ -5,23 +5,15 @@
from __future__ import annotations
import os
-import re
import sys
import json
-import yaml
-import copy
import splat
import shutil
import argparse
-import tempfile
import subprocess
import ninja_syntax
-from dataclasses import dataclass, asdict
-#from utils import ensure_path_and_write, normalize_object_path, to_expected_path
-
-import splat.util.options as splat_options
-from splat.segtypes.linker_entry import LinkerEntry, clean_up_path
+from splat.segtypes.linker_entry import LinkerEntry
from spimdisasm.common.CompilerConfig import compilerOptions
from typing import Any, Union, Protocol, Literal, cast
@@ -30,16 +22,6 @@ from contextlib import contextmanager
from splat.scripts import split
from splat.util.conf import load as splat_load_yaml
-from splat.segtypes.linker_entry import LinkerEntry
-
-# from tools.python.fix_gp import fix_gp
-from tools.python.fix_assets import fix_assets
-from tools.python.fix_linkerscript import fix_linkerscript
-
-from sys import stdout
-from subprocess import run
-from dataclasses import dataclass
-from struct import unpack
@contextmanager
def suppress_stdout_stderr():
@@ -110,7 +92,7 @@ def make_compiler_cmd(config_dir: Path, src_path: Path, language: str):
def exec_shell(command: list[str]) -> str:
- ret = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+ ret = subprocess.run(command, capture_output=True, text=True, check=False)
return ret.stdout
@@ -196,6 +178,21 @@ def build_stuff(
object_strs = [str(obj) for obj in object_paths]
+ expected_strs = []
+ for obj in object_paths:
+ if obj.name.endswith(".c.o"):
+ obj = obj.with_name(obj.name.removesuffix(".c.o") + ".o")
+ elif obj.name.endswith(".s.o"):
+ obj = obj.with_name(obj.name.removesuffix(".s.o") + ".o")
+
+ if obj.parts[:2] == ("build", "asm"):
+ p = obj.relative_to("build/asm")
+ elif obj.parts[:2] == ("build", "src"):
+ p = obj.relative_to("build/src")
+ else:
+ p = obj
+ expected_strs.append(str(Path("build/expected") / p))
+
for object_path in object_paths:
if object_path.name == "elf_header.s.o":
continue
@@ -211,6 +208,14 @@ def build_stuff(
implicit_outputs=implicit_outputs,
)
+ ninja.build(
+ outputs=expected_strs,
+ rule="as",
+ inputs=[str((Path("asm") / s.relative_to("../src") if s.parts[:2] == ("..", "src") else s).with_suffix(".s")) for s in src_paths],
+ variables=variables,
+ implicit_outputs=implicit_outputs,
+ )
+
ninja = ninja_syntax.Writer(open(str(ROOT / config_dir / "build.ninja"), "w"), width=9999)
# Rules
@@ -295,16 +300,15 @@ def build_stuff(
else:
build(entry.object_path, entry.src_paths, "cc")
- elif isinstance(seg, splat.segtypes.common.databin.CommonSegDatabin):
- build(entry.object_path, entry.src_paths, "as")
-
- elif isinstance(seg, splat.segtypes.common.rodatabin.CommonSegRodatabin):
- build(entry.object_path, entry.src_paths, "as")
-
- elif isinstance(seg, splat.segtypes.common.textbin.CommonSegTextbin):
- build(entry.object_path, entry.src_paths, "as")
-
- elif isinstance(seg, splat.segtypes.common.sbss.CommonSegSbss):
+ elif isinstance(
+ seg,
+ (
+ splat.segtypes.common.databin.CommonSegDatabin,
+ splat.segtypes.common.rodatabin.CommonSegRodatabin,
+ splat.segtypes.common.textbin.CommonSegTextbin,
+ splat.segtypes.common.sbss.CommonSegSbss,
+ ),
+ ):
build(entry.object_path, entry.src_paths, "as")
else:
@@ -338,288 +342,6 @@ def build_stuff(
implicit=[elf_path],
)
-
-def get_line_file_path(args: AnnotationArgs):
- if args.line_file_path is not None:
- return args.line_file_path
-
- # if args.elf_path.name == SH2_SERIAL and "Event/stage" in args.asm_path.as_posix():
-# return Path(f"{TOOLS}/alessatool/dwarf") / Path(args.asm_path.name).with_suffix(".line")
-
- return None
-
-
-def line_has_vram_addr(line: str, addr_str: str) -> bool:
- if addr_str not in line or "*/" not in line:
- return False
-
- return line.index("*/") > line.index(addr_str)
-
-
-def annotate_asm(args: AnnotationArgs):
- with open(args.asm_path, "r") as asm_file:
- asm_contents = asm_file.read()
-
- asm_lines = asm_contents.splitlines()
- asm_line_index = 0
-
- vram_start = args.vram_start
- vram_end = args.vram_end
-
- if vram_start is None or vram_end is None:
- vram_start, vram_end = find_vram_bounds(asm_lines)
-
- line_file_path = get_line_file_path(args)
-
- if line_file_path is None or not line_file_path.exists():
- addresses = (f"0x{v:X}" for v in range(vram_start, vram_end, 0x4))
- proc = run([args.addr2line_path, "-e", args.elf_path, *addresses], capture_output=True, encoding=args.encoding)
- addr2line_output_lines = proc.stdout.splitlines()
- else:
- # parse the binary line number file.
-
- # the format is a list of u_shorts, one per line number.
- # there should be one line number per vram address, and each vram
- # address should be exactly 4 bytes apart, mirroring how the addr2line
- # output is formatted
-
- with open(line_file_path, "rb") as line_file:
- line_data = line_file.read()
- line_numbers = unpack(f"<{len(line_data) // 2}H", line_data)
- compile_unit = args.asm_path.with_suffix(".c").name
- addr2line_output_lines = list(map(lambda n : to_addr2line_format(compile_unit, n), line_numbers))
-
- main_tu_name = None
- prev_tu_name = None
- prev_line_number = -1
- function_count = 0
- is_in_function_label = False
- current_vram_addr = vram_start
- annotated_asm_lines = []
-
- for addr_index in range(0, len(addr2line_output_lines) - 1):
- line = addr2line_output_lines[addr_index]
-
- if line.startswith("?"):
- current_vram_addr += 0x4
- continue
-
- separator_index = line.rfind(":")
- current_line_number = int(line[separator_index+1:])
-
- if current_line_number == prev_line_number:
- current_vram_addr += 0x4
- continue
-
- vram_addr_str = f"{current_vram_addr:X}"
-
- current_tu_name = line[0:separator_index]
- if current_tu_name != main_tu_name and current_tu_name != prev_tu_name:
- if main_tu_name:
- print(f"[warn] alessatool/annotate: address at {vram_addr_str} belongs to {current_tu_name}")
- else:
- main_tu_name = current_tu_name
- prev_tu_name = current_tu_name
-
- while True:
- if asm_line_index >= len(asm_lines):
- raise AssertionError(
- f"address 0x{current_vram_addr:X} not found in asm"
- )
-
- asm_line = asm_lines[asm_line_index]
-
- if line_has_vram_addr(asm_line, vram_addr_str):
- break
-
- should_append_asm_line = True
- if args.tu:
- asm_line_trimmed = asm_line.strip()
-
- # track when we go in & out of function symbols
- if asm_line_trimmed.startswith(FUNCTION_SYMBOL_LABEL):
- function_count += 1
- is_in_function_label = True
- annotated_asm_lines.append(f"{UNIQUE_TEXT_SECTION_DIRECTIVE}{function_count}")
- annotated_asm_lines.append("")
- elif asm_line_trimmed.startswith(END_FUNCTION_SYMBOL_LABEL):
- is_in_function_label = False
-
- # remove `nop`s
- if not is_in_function_label and asm_line_trimmed.endswith("nop"):
- should_append_asm_line = False
-
- # remove `macro.inc` include directive
- elif asm_line_trimmed == INCLUDE_MACRO_INC_DIRECTIVE:
- should_append_asm_line = False
-
- if should_append_asm_line:
- annotated_asm_lines.append(asm_line)
-
- asm_line_index += 1
-
- annotated_asm_lines.append(f"\t.loc 1 {current_line_number}")
- annotated_asm_lines.append(asm_line)
- asm_line_index += 1
-
- prev_line_number = current_line_number
- current_vram_addr += 0x4
-
- while asm_line_index < len(asm_lines) - 1:
- annotated_asm_lines.append(asm_lines[asm_line_index])
- asm_line_index += 1
-
- assert main_tu_name, "no valid compilation unit found"
- annotated_asm_lines = [
- ".section .debug",
- ".previous",
- ".text",
- f".file 1 \"{main_tu_name}\"",
- *annotated_asm_lines
- ]
-
- append_final_new_line(annotated_asm_lines)
- annotated_asm_contents = "\n".join(annotated_asm_lines)
-
- if not args.stdout and args.out_path:
- with open(args.out_path, "w") as out_file:
- out_file.write(annotated_asm_contents)
- if args.verbose:
- print(f"alessatool/annotate: wrote asm to {args.out_path}")
- else:
- stdout.write(annotated_asm_contents)
-
-
-def append_final_new_line(lines: list[str]):
- if lines[-1] != "":
- lines.append("")
-
-
-def make_asm(config_path: Path, config: dict[str, Any]):
- """
- Extracts assembly for each function into 'expected/asm/' subfolder.
-
- The extraction is done following these steps:
- 1. create temporary directory in project folder
- 2. copy yaml to temporary directory
- 3. modify yaml to have c subsegments instead of asm subsegments
- 4. remove data, bss, etc., subsegments
- 5. run splat on the new config yaml
- 6. splat extracts assembly for each function into '<temp_dir>/asm/nonmatchings/'
- 7. copy '<temp_dir>/asm/nonmatchings/' to '<project_dir>/expected/asm/'
- """
- with tempfile.TemporaryDirectory(dir=config_path, prefix="tmp_") as tmp_dir:
- tmp_path = Path(tmp_dir)
-
- yaml_path = tmp_path / "config.yaml"
- asm_path = tmp_path / "asm" / "nonmatchings"
- dst_path = tmp_path / ".." / "expected" / "asm"
- dst_path = dst_path.resolve().relative_to(ROOT)
-
- if dst_path.exists():
- print(f"expected asm dir '{dst_path}' already exists")
- return
-
- config = copy.deepcopy(config)
- config["options"]["target_path"] = "../" + config["options"]["target_path"]
- config["options"]["asm_path"] = "asm"
- config["options"]["src_path"] = "src"
- config["options"]["build_path"] = "build"
- config["options"]["asset_path"] = "assets"
- config["options"]["undefined_funcs_auto_path"] = "../" + config["options"]["undefined_funcs_auto_path"]
- config["options"]["undefined_syms_auto_path"] = "../" + config["options"]["undefined_syms_auto_path"]
- config["options"]["symbol_addrs_path"] = "../" + config["options"]["symbol_addrs_path"]
- config["options"]["extensions_path"] = "../" + config["options"]["extensions_path"]
-
- new_segments: list[Any] = []
- segments: list[Any] = config["segments"]
- for segment in segments:
- if isinstance(segment, list):
- new_segments.append(segment)
- elif isinstance(segment, dict) and segment["name"] == "main":
- new_subsegments: list[Any] = []
- subsegments = cast(list[Any], segment["subsegments"])
- for subsegment in subsegments:
- if isinstance(subsegment, list):
- if subsegment[1] == "asm":
- subsegment[1] = "c"
- new_subsegments.append(subsegment)
- elif isinstance(subsegment, dict):
- subsegment["type"] = subsegment["type"].strip(".")
- if subsegment["type"] == "rodata":
- # splat now requires rodata to be always prefixed with a dot (.)
- # for the rodata migration to work properly
- subsegment["type"] = ".rodata"
- new_subsegments.append(subsegment)
- segment["subsegments"] = new_subsegments
- new_segments.append(segment)
- config["segments"] = new_segments
-
- def rename_locals(base_path: Path):
- for asm_file in base_path.rglob("*.s"):
- data = asm_file.read_text()
- data = re.sub(r"__local_\d+", "", data)
- asm_file.write_text(data)
-
- with yaml_path.open(mode="w") as yaml_file:
- yaml.dump(config, yaml_file, default_flow_style=False)
-
- with suppress_stdout_stderr():
- split.main([yaml_path], modes=["all"], verbose=False)
-
- # remove '__local_#' from asm
- rename_locals(asm_path)
-
- dst_path.parent.mkdir(parents=True, exist_ok=True)
- shutil.copytree(asm_path, dst_path, dirs_exist_ok=True)
-
- print(f"expected asm extracted to '{dst_path}'")
-
- # make expected objs
-
- for subseg in new_segments[1]["subsegments"]:
- if isinstance(subseg, list) and subseg[1] == "c":
- subseg[1] = "asm"
- subseg[2] += ".c"
-
- config["options"]["asm_jtbl_label_macro"] = "llabel"
-
- with yaml_path.open(mode="w") as yaml_file:
- yaml.dump(config, yaml_file, default_flow_style=False)
-
- shutil.rmtree(tmp_path / "asm")
- (tmp_path / ".splache").unlink()
-
- with suppress_stdout_stderr():
- split.main([yaml_path], modes=["all"], verbose=False)
-
- # remove '__local_#' from asm
- rename_locals(asm_path)
-
- dst_path = dst_path.parent / "obj"
- tmp_obj_path = tmp_path / "obj"
- tmp_asm_dir = tmp_path / "asm"
-
- cpp = Path("..", "..", "..", (Path("tools") / "cc" / COMPILER))
-
- for asm_file in tmp_asm_dir.rglob("*.c.s"):
- asm_file_rel = asm_file.relative_to(tmp_path)
- obj_file_rel = Path("obj") / asm_file.relative_to(tmp_asm_dir).with_suffix(".o")
- obj_file = tmp_obj_path / obj_file_rel.relative_to("obj")
- obj_file.parent.mkdir(parents=True, exist_ok=True)
- subprocess.run(
- f"{cpp} -I../src -I../include -Iinclude -Iinclude/sdk/ee -I../.. '{asm_file_rel}' -o - | "
- #f"iconv -f=UTF-8 -t=EUC-JP '{asm_file_rel}' | "
- f"mips-linux-gnu-as -no-pad-sections -EL -march=5900 -mabi=eabi -I../include -o {obj_file_rel} {asm_file_rel}",
- shell=True,
- cwd=tmp_path,
- )
-
- shutil.copytree(tmp_obj_path, dst_path, dirs_exist_ok=True)
-
- print(f"expected obj built to '{dst_path}'")
-
-
def generate_objdiff_configuration(config_path: Path, config: dict[str, Any], language: str):
"""
Generate `objdiff.json` configuration from splat YAML config.
@@ -633,7 +355,7 @@ def generate_objdiff_configuration(config_path: Path, config: dict[str, Any], la
"""
segments: list[Any] = config["segments"]
- tu_to_diff: list[tuple[Literal["asm", "c"], str]] = []
+ tu_to_diff: list[tuple[Literal["asmtu", "c"], str]] = []
for segment in segments:
if not (isinstance(segment, dict) and segment["name"] == "main"):
@@ -653,7 +375,7 @@ def generate_objdiff_configuration(config_path: Path, config: dict[str, Any], la
else:
raise RuntimeError("invalid subsegment type")
- if subs_type in ("asm", "c"):
+ if subs_type in ("asmtu", "c"):
if subs_name in (
"crt0",
"main/glob",
@@ -678,7 +400,7 @@ def generate_objdiff_configuration(config_path: Path, config: dict[str, Any], la
units: list[dict[str, Any]] = []
for tu_type, tu_name in tu_to_diff:
- target_path = Path("expected", "obj", tu_name).with_suffix(".c.o")
+ target_path = Path("build", "expected", tu_name).with_suffix(".o")
# since we only compile fully decompiled TUs, the
# "c" type implies that the TU is complete
@@ -695,7 +417,10 @@ def generate_objdiff_configuration(config_path: Path, config: dict[str, Any], la
"name": tu_name,
"target_path": str(target_path),
"base_path": str(base_path) if base_path else None,
- "metadata": {"progress_categories": [language]},
+ "metadata": {
+ "source_path": str(Path("..", "src", tu_name).with_suffix(".c")),
+ "progress_categories": [language]
+ },
}
if not base_path:
@@ -717,10 +442,10 @@ def generate_objdiff_configuration(config_path: Path, config: dict[str, Any], la
objdiff_json: dict[str, Any] = {
"$schema": "https://raw.githubusercontent.com/encounter/objdiff/main/config.schema.json",
- "custom_make": "true",
+ "custom_make": "ninja",
"custom_args": [],
- "build_target": False,
- "build_base": False,
+ "build_target": True,
+ "build_base": True,
"watch_patterns": [],
"units": units,
"progress_categories": progress_categories,
@@ -858,7 +583,7 @@ def main():
cwd = Path(os.getcwd()).resolve()
if not ROOT.samefile(cwd):
print("ERROR: this script must be run from it's directory!")
- exit(1)
+ sys.exit(1)
compilerOptions["MWCCPS2"].value.bigAddendWorkaroundForMigratedFunctions = False
@@ -876,9 +601,9 @@ def main():
if basename not in LANGUAGES:
- supported_elfs = f"{set(f'{elf} ({lang})' for elf, lang in LANGUAGES.items())}".replace("'", "")
+ supported_elfs = f"{ {f'{elf} ({lang})' for elf, lang in LANGUAGES.items()} }".replace("'", "")
print(f"unsupported game ELF. Supported versions are: {supported_elfs}")
- exit(1)
+ sys.exit(1)
language = LANGUAGES[basename]
@@ -886,10 +611,6 @@ def main():
clean(config_dir, config)
return
- if args.make_asm:
- make_asm(config_dir, config)
- return
-
if args.clean:
clean(config_dir, config)
@@ -906,36 +627,12 @@ def main():
generate_lcf()
-
- # fix asset .incbin path from relative to root to relative to config dir:
- # e.g., change:
- # .incbin "config/assets/name.section.bin"
- # to:
- # .incbin "assets/name.section.bin"
- fix_assets(asm_data_path, asset_rel_path)
-
linker_entries = split.linker_writer.entries
build_stuff(config_dir, split.config, linker_entries, language)
write_permuter_settings(config_dir, src_path, language)
- # # replace gp_rel assembler macro with explicit offset as the gcc used
- # # to compile the code does not support it
- # gp_value = split.config["options"]["gp_value"]
- # symbol_addrs_path = Path(split.config["options"]["symbol_addrs_path"])
- # asm_rel_path = (config_dir / asm_path).resolve().relative_to(ROOT)
- # symbol_addrs_rel_path = (config_dir / symbol_addrs_path).resolve().relative_to(ROOT)
- # assert asm_rel_path.is_dir(), f"{asm_rel_path} not found or not a directory"
- # assert symbol_addrs_rel_path.is_file(), f"{symbol_addrs_rel_path} not found"
- # fix_gp(asm_rel_path, gp_value, symbol_addrs_rel_path)
-
- # fix linkerscript by applying explicit alignments as
- # specified in the config yaml
- linkerscript_path = (config_dir / f"{basename}.ld").resolve().relative_to(ROOT)
- assert linkerscript_path.is_file(), f"{linkerscript_path} not found"
- fix_linkerscript(split.config, linkerscript_path)
-
generate_objdiff_configuration(config_dir, split.config, language)
-265
View File
@@ -1,265 +0,0 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright 2025 karas84 (https://github.com/karas84)
# SPDX-License-Identifier: MIT
#
# This script inserts source line debug information (.loc directives) into assembly files
# using data extracted from a JSON "stdump" file generated by the ccc tool (version 2.1),
# available at https://github.com/chaoticgd/ccc.
#
# Original concept by Mc-muffin (https://github.com/Mc-muffin).
from __future__ import annotations
import re
import io
import sys
import json
import argparse
from typing import Callable, Protocol, cast
from pathlib import Path
class STDUMPJson:
def __init__(self, json_path: Path):
self._json = json.loads(json_path.read_text())
self._line_cache = self._build_line_cache()
self._validate_sub_source_files()
def _build_line_cache(self):
line_cache: dict[int, int] = {}
functions = self._json["functions"]
for fun in functions:
if "line_numbers" not in fun:
continue
for addr, num in fun["line_numbers"]:
# there may be more lines for the same address. Ghidra seems to
# keep the last one, so that's what we are also doing
line_cache[addr] = num
return line_cache
def _validate_sub_source_files(self):
functions = self._json["functions"]
for fun in functions:
if (line_numbers := fun.get("line_numbers")) is None:
continue
if (sub_source_files := fun.get("sub_source_files")) is None:
continue
line_numbers = cast(list[tuple[int, int]], line_numbers)
sub_source_files = cast(list[tuple[int, str]], sub_source_files)
# # some functions may have "unterminated" inlines, but they usually have just one line number
# if len(sub_source_files) % 2 != 0: # and len(line_numbers) != 1:
# print(fun["name"], len(line_numbers))
# for addr, source in sub_source_files:
def find_function(self, name: str):
functions = self._json["functions"]
fun = next((f for f in functions if f["name"] == name), None)
return fun
def get_line(self, addr: int):
return self._line_cache.get(addr)
def make_range_checker(lst: list[tuple[int, str]], delimiter: str) -> Callable[[int], bool]:
"""
Given a sorted list of (number, label) where number increases,
build a checker that returns True if x is in a valid interval.
Interpretation:
- Each entry (n, label) marks the interval starting at n and going
up to the next entry's n (exclusive). The last entry's interval
goes to +inf.
- An interval starting at n is valid iff label == delimiter.
- If the first entry's label != delimiter, everything before the first n is valid.
"""
if not lst:
# no markers -> everything valid
return lambda x: True
# ensure sorted by number
lst_sorted = sorted(lst, key=lambda t: t[0])
assert lst == lst_sorted
nums = [t[0] for t in lst_sorted]
labels = [t[1] for t in lst_sorted]
# Precompute intervals as (start, end_exclusive, is_valid)
intervals: list[tuple[int, int | None, bool]] = []
n_items = len(nums)
for i in range(n_items):
start = nums[i]
end_exclusive: int | None
if i + 1 < n_items:
end_exclusive = nums[i + 1]
else:
end_exclusive = None # means to +inf
is_valid = labels[i] == delimiter
intervals.append((start, end_exclusive, is_valid))
first_before_is_valid = labels[0] != delimiter
def is_valid_fn(x: int) -> bool:
# before first number
if x < nums[0]:
return first_before_is_valid
# find the interval that contains x
for start, end_exclusive, valid_flag in intervals:
if end_exclusive is None:
if x >= start:
return valid_flag
else:
if start <= x < end_exclusive:
return valid_flag
# fallback (shouldn't happen)
return False
return is_valid_fn
def is_always_valid_fn(addr: int):
return True
def add_lines_to_asm(
asm_path: Path,
stdump_json_path: Path,
fun_start_offset: int,
keep_original_numbers: bool,
asm_out: Path | None,
):
stdump_json = STDUMPJson(stdump_json_path)
function_name = asm_path.stem
if (fun := stdump_json.find_function(function_name)) is None:
raise RuntimeError(f"Cannot find function '{function_name}' in ccc's JSON")
sub_source_files = fun.get("sub_source_files")
# print(len(sub_source_files) if sub_source_files is not None else None)
asm_lines = asm_path.read_text().splitlines()
re_instr = re.compile(r"^\s*\/\* [A-Z0-9]+ ([A-Z0-9]{8}) [A-Z0-9]{8} \*\/ .*$")
line_dict: dict[int, int] = {}
relative_path: str = fun["relative_path"]
non_func_addrs: list[int] = []
if sub_source_files:
checker = make_range_checker(sub_source_files, relative_path)
else:
checker = is_always_valid_fn
start_line_num: int = sys.maxsize
for line in asm_lines:
if m := re_instr.match(line):
instr_addr = int(m.group(1), 16)
line_num = stdump_json.get_line(instr_addr)
if line_num:
line_dict[instr_addr] = line_num
if not checker(instr_addr):
non_func_addrs.append(instr_addr)
elif line_num:
start_line_num = min(start_line_num, line_num)
if start_line_num == sys.maxsize:
start_line_num = 1
min_line_num = 0 if keep_original_numbers else start_line_num - 1
new_asm_lines: list[str] = []
asm_n: int = 0
for line in asm_lines:
if m := re_instr.match(line):
asm_n += 1
instr_addr = int(m.group(1), 16)
line_num = line_dict.get(instr_addr)
if line_num is not None and instr_addr in non_func_addrs:
new_asm_lines.append(f" .loc 1 {line_num} # inline")
new_asm_lines.append(line)
continue
if asm_n == 1 and line_num is None:
# sometimes we don't have a number for the first line of assembly,
# so we reuse the first known line among the ones we have
line_num = start_line_num
if line_num is not None:
new_line_num = (line_num - min_line_num) + fun_start_offset
new_asm_lines.append(f" .loc 1 {new_line_num} ")
new_asm_lines.append(line)
stream = io.StringIO()
stream.write(""".section .debug
.previous
.text
.file 1 "source.c"
.set noat
.set noreorder
""")
stream.write("\n".join(new_asm_lines))
if asm_out is not None:
asm_out.write_text(stream.getvalue())
else:
print(stream.getvalue())
def main():
class ArgProtocol(Protocol):
asm_path: Path
stdump_json_path: Path
asm_out: Path | None
offset: int
keep_original: bool
parser = argparse.ArgumentParser(description="Add line debug info to assembly files")
parser.add_argument("--asm-path", required=True, type=Path, help="Path to the assembly file to add lines to")
parser.add_argument("--stdump-json-path", required=True, type=Path, help="Path to ccc's json stdump")
parser.add_argument("--asm-out", type=Path, required=False, help="Path to output asm (defaults to stdout)")
parser.add_argument(
"--offset", type=int, required=False, default=0, help="Offset to apply to line numbers (default: 0)"
)
parser.add_argument(
"--keep-original",
action="store_true",
help="Don't start line numbers from 1 (+ offset) but keep the original line numbers (+offset) instead",
)
args = cast(ArgProtocol, parser.parse_args())
fun_start_num = args.offset
add_lines_to_asm(
args.asm_path,
args.stdump_json_path,
fun_start_num,
args.keep_original,
args.asm_out,
)
if __name__ == "__main__":
main()
-351
View File
@@ -1,351 +0,0 @@
from __future__ import annotations
import pydantic
from typing import Optional, Literal, Any
class AddressRange(pydantic.BaseModel):
low: int
high: int
class ValueType(pydantic.BaseModel):
descriptor: Optional[str] = None
source: Optional[str] = None
type_name: Optional[str] = None
referenced_file_index: Optional[int] = None
referenced_stabs_type_number: Optional[int] = None
value_type: Optional[ValueType] = None
return_type: Optional[ReturnType] = None
modifier: Optional[str] = None
vtable_index: Optional[int] = None
is_constructor: Optional[bool] = None
element_type: Optional[ElementType] = None
element_count: Optional[int] = None
class DeduplicatedTypeValueType(pydantic.BaseModel):
descriptor: Literal["function_type", "type_name"]
source: Optional[str] = None
type_name: Optional[str] = None
referenced_file_index: Optional[int] = None
referenced_stabs_type_number: Optional[int] = None
value_type: Optional[ValueType] = None
return_type: Optional[ReturnType] = None
modifier: Optional[str] = None
vtable_index: Optional[int] = None
is_constructor: Optional[bool] = None
element_type: Optional[ElementType] = None
element_count: Optional[int] = None
class ParameterType(pydantic.BaseModel):
descriptor: str
source: Optional[str] = None
type_name: Optional[str] = None
referenced_file_index: Optional[int] = None
referenced_stabs_type_number: Optional[int] = None
value_type: Optional[ValueType] = None
class Parameter(pydantic.BaseModel):
descriptor: str
name: str
class_: str = pydantic.Field(..., alias="class")
storage: Storage
block_low: int
block_high: int
type: ParameterType
class ReturnType(pydantic.BaseModel):
descriptor: str
source: Optional[str] = None
type_name: Optional[str] = None
referenced_file_index: Optional[int] = None
referenced_stabs_type_number: Optional[int] = None
value_type: Optional[ValueType] = None
class FunctionType(pydantic.BaseModel):
descriptor: str
parameters: list[Parameter]
modifier: str
vtable_index: int
is_constructor: bool
return_type: Optional[ReturnType] = None
class Storage(pydantic.BaseModel):
type: str
register_: Optional[str] = pydantic.Field(None, alias="register")
register_class: Optional[str] = None
dbx_register_number: Optional[int] = None
register_index: Optional[int] = None
is_by_reference: Optional[bool] = None
stack_offset: Optional[int] = None
global_location: Optional[str] = None
global_address: Optional[int] = None
class Constant(pydantic.BaseModel):
value: int
name: str
class ElementType(pydantic.BaseModel):
descriptor: Literal["array", "pointer", "type_name", "enum"]
source: Optional[str] = None
type_name: Optional[str] = None
referenced_file_index: Optional[int] = None
referenced_stabs_type_number: Optional[int] = None
element_type: Optional[ElementType] = None
element_count: Optional[int] = None
value_type: Optional[ValueType] = None
constants: Optional[list[Constant]] = None
def parsed_size(self) -> tuple[int, str]:
if self.descriptor == "pointer":
return 1, "pointer"
elif self.descriptor == "array":
assert self.element_type
assert self.element_count is not None
if self.element_count == 0:
# implicit size array
_, type_name = self.element_type.parsed_size()
return 0, type_name
n, type_name = self.element_type.parsed_size()
return self.element_count * n, type_name
elif self.descriptor == "enum":
return 1, "enum"
else: # "type_name"
assert self.type_name
return 1, self.type_name
class Local(pydantic.BaseModel):
descriptor: str
name: str
class_: str = pydantic.Field(..., alias="class")
storage: Storage
block_low: int
block_high: int
type: ElementType
storage_class: Optional[str] = None
@property
def parsed_size(self) -> tuple[int, str]:
return self.type.parsed_size()
class SubSourceFile(pydantic.BaseModel):
address: int
path: str
class Function(pydantic.BaseModel):
descriptor: str
name: str
address_range: AddressRange
type: FunctionType
locals: list[Local]
line_numbers: list[list[int]]
sub_source_files: list[SubSourceFile]
storage_class: Optional[str] = None
relative_path: Optional[str] = None
class Global(pydantic.BaseModel):
descriptor: str
name: str
class_: str = pydantic.Field(..., alias="class")
storage: Storage
block_low: int
block_high: int
type: ElementType
storage_class: Optional[str] = None
@property
def parsed_size(self) -> tuple[int, str]:
return self.type.parsed_size()
class File(pydantic.BaseModel):
descriptor: str
path: str
relative_path: str
text_address: int
types: list[Any]
functions: list[Function]
globals: list[Global]
stabs_type_number_to_deduplicated_type_index: dict[str, int]
class UnderlyingType(pydantic.BaseModel):
descriptor: str
source: str
type_name: str
referenced_file_index: int
referenced_stabs_type_number: int
class Field(pydantic.BaseModel):
descriptor: str
name: str
relative_offset_bytes: int
absolute_offset_bytes: int
size_bits: int
bitfield_offset_bits: Optional[int] = None
underlying_type: Optional[UnderlyingType] = None
source: Optional[str] = None
type_name: Optional[str] = None
referenced_file_index: Optional[int] = None
referenced_stabs_type_number: Optional[int] = None
value_type: Optional[ValueType] = None
base_classes: Optional[list[Any]] = None
fields: Optional[list[Field]] = None
member_functions: Optional[list[Any]] = None
element_type: Optional[ElementType] = None
element_count: Optional[int] = None
class FieldModel(pydantic.BaseModel):
descriptor: str
name: str
relative_offset_bytes: int
absolute_offset_bytes: int
size_bits: int
value_type: Optional[ValueType] = None
source: Optional[str] = None
type_name: Optional[str] = None
referenced_file_index: Optional[int] = None
referenced_stabs_type_number: Optional[int] = None
element_type: Optional[ElementType] = None
element_count: Optional[int] = None
bitfield_offset_bits: Optional[int] = None
underlying_type: Optional[UnderlyingType] = None
fields: Optional[list[Field]] = None
member_functions: Optional[list[Any]] = None
base_classes: Optional[list[Any]] = None
class DeduplicatedType(pydantic.BaseModel):
descriptor: Literal["array", "builtin", "enum", "pointer", "struct", "type_name", "union"]
name: Optional[str] = None
storage_class: Optional[Literal["typedef"]] = None
stabs_type_number: int
files: list[int]
class_: Optional[str] = pydantic.Field(None, alias="class")
size_bits: Optional[int] = None
base_classes: Optional[list[Any]] = None
fields: Optional[list[FieldModel]] = None
member_functions: Optional[list[Any]] = None
source: Optional[str] = None
type_name: Optional[str] = None
referenced_file_index: Optional[int] = None
referenced_stabs_type_number: Optional[int] = None
value_type: Optional[DeduplicatedTypeValueType] = None
conflict: Optional[bool] = None
element_type: Optional[ElementType] = None
element_count: Optional[int] = None
constants: Optional[list[Constant]] = None
class CCCJSONv7Model(pydantic.BaseModel):
version: Literal[7]
files: list[File]
deduplicated_types: list[DeduplicatedType]
# def test(stdump_json_path: str):
# import json
#
# with open(stdump_json_path, mode="r") as fh:
# json_data = fh.read()
#
# model = CCCJSONv7Model.model_validate_json(json_data)
#
# type_map: dict[str, int] = {}
#
# for n, dt in enumerate(model.deduplicated_types):
# if dt.descriptor == "builtin":
# assert dt.name and dt.name not in type_map
# assert dt.class_ is not None
# size_bits = int(dt.class_.split("-", maxsplit=1)[0])
# assert size_bits % 8 == 0
# type_map[dt.name] = size_bits // 8
#
# elif dt.descriptor == "type_name":
# assert dt.name is not None
# assert dt.type_name is not None
# if dt.name in type_map:
# if dt.size_bits:
# assert type_map[dt.name] == dt.size_bits // 8
# if dt.name == dt.type_name == "void":
# type_map["void"] = type_map["int"]
# continue
# assert dt.type_name in type_map, (n, dt.type_name)
# type_map[dt.name] = type_map[dt.type_name]
#
# elif dt.descriptor == "pointer":
# assert dt.name is not None
# assert dt.value_type is not None
# if dt.value_type.descriptor == "function_type":
# assert dt.name not in type_map
# type_map[dt.name] = type_map["int"]
# elif dt.value_type.descriptor == "type_name":
# assert dt.value_type.type_name
# assert dt.value_type.type_name in type_map
# type_map[dt.name] = type_map[dt.value_type.type_name]
#
# elif dt.descriptor == "struct":
# assert dt.name is not None
# if not dt.conflict:
# assert dt.name not in type_map, n
# else:
# if dt.name in type_map:
# continue
# assert dt.size_bits is not None
# assert dt.size_bits % 8 == 0
# type_map[dt.name] = dt.size_bits // 8
#
# elif dt.descriptor == "array":
# assert dt.name is not None
# element_count = 1
# element_type = dt.element_type
# type_name = None
# while element_type:
# if element_type.element_count is not None:
# element_count *= element_type.element_count
# if element_type.type_name is not None:
# type_name = element_type.type_name
# assert type_name in type_map
# element_count *= type_map[type_name]
# element_type = element_type.element_type
# assert type_name and type_name in type_map, n
# type_map[dt.name] = type_map[type_name] * element_count
#
# elif dt.descriptor == "enum":
# if dt.name:
# if not dt.conflict:
# assert dt.name not in type_map, n
# type_map[dt.name] = type_map["int"]
#
# elif dt.descriptor == "union":
# assert dt.name
# assert dt.size_bits
# assert dt.size_bits % 8 == 0
# type_map[dt.name] = dt.size_bits // 8
#
# else:
# assert False, f"unknown {n}"
#
# print(json.dumps(type_map, indent=2))
# if __name__ == "__main__":
# test(path-to-stdump-json)
-322
View File
@@ -1,322 +0,0 @@
from __future__ import annotations
import io
import sys
import math
import ctypes
import numpy as np
import numpy.typing as npt
from collections.abc import Sized, Sequence
from typing import Type, TypeVar, Any, cast, TextIO, BinaryIO, Optional
from ctypes import LittleEndianStructure, Union, Array, c_uint32
_G = TypeVar("_G")
CTypeType = (
type[ctypes.c_int8]
| type[ctypes.c_uint8]
| type[ctypes.c_int16]
| type[ctypes.c_uint16]
| type[ctypes.c_int32]
| type[ctypes.c_uint32]
| type[ctypes.c_float]
| type[ctypes.c_double]
)
ctypes_types: dict[str, CTypeType] = {
"char": ctypes.c_int8,
"u_char": ctypes.c_uint8,
"short": ctypes.c_int16,
"u_short": ctypes.c_uint16,
"int": ctypes.c_int32,
"u_int": ctypes.c_uint32,
"float": ctypes.c_float,
"double": ctypes.c_double,
}
class c_addr(c_uint32):
def __str__(self):
# if self.value == 0:
# return "NULL"
# return f"0x{self.value:08x}"
return f"0x{self.value:x}"
class c_str(c_uint32):
def to_str(self, elf: BinaryIO):
if self.value == 0:
return "NULL"
elf.seek(self.value)
buf = io.BytesIO()
while (c := elf.read(1)) != b"\0":
buf.write(c)
return '"' + buf.getvalue().decode("ASCII") + '"'
class c_addr_ptr(c_uint32):
_addresses: dict[int, str] | None = None
@classmethod
def set_addresses(cls, addresses: dict[int, str] | None):
cls._addresses = addresses
def __str__(self):
if self.value == 0:
return "NULL"
if self._addresses and self.value in self._addresses:
return self._addresses[self.value]
print(f"warning: no address for pointer 0x{self.value:x}")
return f"0x{self.value:x}"
def print_arr(arr: npt.NDArray[np.int_], lst: list[Any], file: TextIO):
if arr.ndim > 1:
for x in arr:
file.write("{")
print_arr(x, lst, file)
file.write("},")
else:
file.write(",".join(str(lst[i]) for i in arr))
def print_carr(arr: Sequence[Any], file: TextIO):
if hasattr(arr[0], "_length_"):
for a in arr:
file.write("{")
print_carr(a, file)
file.write("},")
else:
v = str(str([x for x in arr])).replace("[", "").replace("]", "")
file.write(v)
def chunks(lst: Sequence[_G], n: int):
"""Yield successive n-sized chunks from lst."""
for i in range(0, len(lst), n):
yield lst[i : i + n]
def format_array(lst: Sequence[_G], dims: Sequence[int], file: TextIO):
if len(dims) > 1:
for ll in chunks(lst, len(lst) // dims[0]):
file.write("{")
format_array(ll, dims[1:], file)
file.write("},")
else:
for n, x in enumerate(lst):
sep = ", " if n < len(lst) - 1 else ""
file.write(f"{x}{sep}")
def resolve_annotations(namespace: dict[str, Any], annotations: dict[str, Any]):
module = sys.modules.get(namespace.get("__module__", ""))
globals_ = vars(module) if module else {}
resolved: dict[str, Any] = {}
for k, v in annotations.items():
if isinstance(v, str):
try:
resolved[k] = eval(v, globals_, namespace)
except Exception:
resolved[k] = v # keep as string if can't resolve
else:
resolved[k] = v
return resolved
class LittleEndianStructureFieldsFromTypeHints(type(LittleEndianStructure)): # pyright: ignore
def __new__(
cls: Type[type],
name: str,
bases: tuple[type, ...],
namespace: dict[str, Any],
/,
*,
align: Optional[int] = None,
pack: Optional[int] = None,
) -> LittleEndianStructureFieldsFromTypeHints:
annotations = namespace.get("__annotations__", {})
annotations = resolve_annotations(namespace, annotations)
if "__elf__" in annotations:
annotations.pop("__elf__")
if align is not None:
namespace["_align_"] = align
if pack is not None:
namespace["_pack_"] = pack
namespace["_layout_"] = "ms"
if fields := list(annotations.items()):
namespace["_fields_"] = fields
return type(LittleEndianStructure).__new__(cls, name, bases, namespace) # pyright: ignore
class CStructure(LittleEndianStructure, metaclass=LittleEndianStructureFieldsFromTypeHints):
__elf__: BinaryIO
@classmethod
def sizeof(cls) -> int:
align = getattr(cls, "_align_", 0)
c_size = ctypes.sizeof(cls)
if align > 0:
rem = c_size % align
if rem:
rem = align - rem
return c_size + rem
return c_size
@classmethod
def parse(cls, data: bytes):
cstruct_size = sizeof(cls)
assert len(data) % cstruct_size == 0, f"{len(data)}, {cstruct_size}"
cstruct_num = len(data) // cstruct_size
stream = io.BytesIO(data)
cstructs = [cls.from_buffer_copy(stream.read(cstruct_size)) for _ in range(cstruct_num)]
return cstructs
@classmethod
def dumps(
cls,
name: str,
data: bytes,
numel: int | list[int],
static: bool = False,
nosize: bool = False,
noarray: bool = False,
):
cstructs = cls.parse(data)
stream = io.StringIO()
if static:
stream.write("static ")
stream.write(f"{cls.__name__} {name}") # pyright: ignore
assert not (noarray and isinstance(numel, list))
if not noarray:
if isinstance(numel, int):
assert len(cstructs) == numel, (len(cstructs), numel)
numel_str = f"{len(cstructs)}" if not nosize else ""
stream.write(f"[{numel_str}]")
else:
numel_str = "".join([f"[{num if n > 0 or not nosize else ''}]" for n, num in enumerate(numel)])
stream.write(numel_str)
tot_numel = max(1, numel) if isinstance(numel, int) else math.prod(numel)
# nmdim = 1 if isinstance(numel, int) else len(numel)
assert len(cstructs) == tot_numel, (len(cstructs), tot_numel)
stream.write(" = ")
if not noarray:
stream.write("{\n")
if isinstance(numel, int):
for n, s in enumerate(cstructs):
is_last = n == len(cstructs) - 1
if is_last and noarray:
stream.write(f"{s}\n")
else:
stream.write(f"{s},\n")
else:
idxs = np.arange(tot_numel).reshape(numel)
print_arr(idxs, cstructs, stream)
if not noarray:
stream.write("}")
stream.write(";\n\n")
return stream.getvalue()
# def to_str(self, elf: BinaryIO):
def __str__(self):
stream = io.StringIO()
stream.write(" {\n")
for f, *_ in self._fields_: # pyright: ignore
if f.startswith("_pad_"):
continue
v = getattr(self, f) # pyright: ignore
if f in ("_in", "_pass"):
f = f[1:] # pyright: ignore
if isinstance(v, c_str):
stream.write(f" .{f} = {v.to_str(self.__elf__)},\n")
elif not isinstance(v, Array):
stream.write(f" .{f} = {v},\n")
else:
arr = cast(Sized, v)
if len(arr) and isinstance(v[0], CStructure):
arr = cast(list[CStructure], arr)
stream.write(f" .{f} = {{\n")
for elem in arr:
stream.write(f" {elem},\n")
stream.write(" },\n")
else:
arr = cast(Sequence[Any], arr)
if isinstance(arr[0], Array):
# multidimensional ctypes array
stream.write(f" .{f} = {{")
print_carr(arr, stream)
stream.write("},\n")
else:
stream.write(f" .{f} = {{")
dims = [len(arr)]
format_array(arr, dims, stream)
stream.write("},\n")
stream.write(" }")
return stream.getvalue()
class UnionFieldsFromTypeHints(type(Union)): # pyright: ignore
def __new__(
cls: Type[type],
name: str,
bases: tuple[type, ...],
namespace: dict[str, Any],
/,
*,
align: Optional[int] = None,
pack: Optional[int] = None,
) -> UnionFieldsFromTypeHints:
annotations = namespace.get("__annotations__", {})
if align is not None:
namespace["_align_"] = align
if pack is not None:
namespace["_pack_"] = pack
if fields := list(annotations.items()):
namespace["_fields_"] = fields
return type(Union).__new__(cls, name, bases, namespace) # pyright: ignore
class CUnion(Union, metaclass=UnionFieldsFromTypeHints):
pass
_T = TypeVar("_T", bound=CStructure)
def sizeof(cstruct_type: Type[_T]) -> int:
align = getattr(cstruct_type, "_align_", 0)
c_size = ctypes.sizeof(cstruct_type)
if align > 0:
rem = c_size % align
if rem:
rem = align - rem
return c_size + rem
return c_size
def parse_cstruct(cstruct_type: Type[_T], data: bytes) -> list[_T]:
cstruct_size = sizeof(cstruct_type)
assert len(data) % cstruct_size == 0, f"{len(data)}, {cstruct_size}"
cstruct_num = len(data) // cstruct_size
stream = io.BytesIO(data)
cstructs = [cstruct_type.from_buffer_copy(stream.read(cstruct_size)) for _ in range(cstruct_num)]
return cstructs
def print_cstruct(name: str, cstruct_type: Type[CStructure], data: bytes):
cstructs = parse_cstruct(cstruct_type, data)
print(f"{cstruct_type.__name__} {name}[{len(cstructs)}] = {{") # pyright: ignore
for s in cstructs:
print(s)
print("};\n")
-56
View File
@@ -1,56 +0,0 @@
import argparse
registers = {
"$0": "zero",
"$1": "at",
"$2": "v0",
"$3": "v1",
"$4": "a0",
"$5": "a1",
"$6": "a2",
"$7": "a3",
"$8": "t0",
"$9": "t1",
"$10": "t2",
"$11": "t3",
"$12": "t4",
"$13": "t5",
"$14": "t6",
"$15": "t7",
"$16": "s0",
"$17": "s1",
"$18": "s2",
"$19": "s3",
"$20": "s4",
"$21": "s5",
"$22": "s6",
"$23": "s7",
"$24": "t8",
"$25": "t9",
"$26": "k0",
"$27": "k1",
"$28": "gp",
"$29": "sp",
"$30": "fp",
"$31": "ra",
}
def fix_asm(asm_file: str):
with open(asm_file, mode="r") as fh:
for line in fh:
for reg_num, reg_mnem in reversed(registers.items()):
line = line.replace(reg_num, reg_mnem)
print(line, end="")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("asm_file", help="source assembly file path")
args = parser.parse_args()
fix_asm(args.asm_file)
if __name__ == "__main__":
main()
-125
View File
@@ -1,125 +0,0 @@
import os
import re
import argparse
import pydantic
from pathlib import Path
from typing import Protocol, cast
CONFIG_ROOT = Path(__file__).parent.parent.parent.resolve() / "config"
# enumerate languages from config dir
LANGUAGES = [Path(f.path).name for f in os.scandir(CONFIG_ROOT) if f.is_dir()]
RE_INSTR = re.compile(r"^\t(?:[^.]|\.p2align \d+)")
class InstructionPatch(pydantic.RootModel[tuple[int, str, str]]):
root: tuple[int, str, str]
@property
def instr_no(self):
return self.root[0]
@property
def instr_org(self):
return self.root[1]
@property
def instr_patch(self):
return self.root[2]
class ASMPatch(pydantic.RootModel[dict[str, list[InstructionPatch]]]):
root: dict[str, list[InstructionPatch]]
class PatchDB(pydantic.RootModel[dict[str, ASMPatch]]):
root: dict[str, ASMPatch]
def fix_asm(asm_file: Path, asm_patch: ASMPatch):
lines = asm_file.read_text().splitlines()
def find_func(func: str):
for i, line in enumerate(lines):
if re.match(rf"^{func}:", line):
return i
return -1
def find_line(start: int, num: int):
offset = 0
instr_no = -1
for line in lines[start:]:
if RE_INSTR.match(line):
instr_no += 1
if instr_no == num:
return start + offset
offset += 1
raise RuntimeError("cannot find instruction!")
for func, patch_lst in asm_patch.root.items():
n = find_func(func)
if n == -1:
print(f"WARNING: cannot find function {func} in {asm_file.name}")
continue
for patch in patch_lst:
line_no = find_line(n, patch.instr_no)
line_org = lines[line_no]
line_org_clean = re.sub(r"\s+", " ", line_org).strip()
instr_org_clean = re.sub(r"\s+", " ", patch.instr_org).strip()
instr_patch_clean = re.sub(r"\s+", " ", patch.instr_patch).strip()
if line_org_clean != instr_org_clean:
print(f"WARNING: wrong line: {asm_file.name}:{func}:{line_no + 1}: {line_org} != {patch.instr_org}")
continue
lines[line_no] = f"\t{instr_patch_clean}"
asm_file.write_text("\n".join(lines))
def main():
class ArgsProtocol(Protocol):
language: str
asm_file: Path
parser = argparse.ArgumentParser(description="apply asm patches to assembly files")
parser.add_argument("language", type=str, choices=LANGUAGES, help="language of the asm that is being patched")
parser.add_argument("asm_file", type=Path, help="generated assembly file to patch (relative to build dir)")
args = cast(ArgsProtocol, parser.parse_args())
asm_file = CONFIG_ROOT / args.language / args.asm_file
if not asm_file.exists():
print(f"ERROR: cannot find assembly file {asm_file}")
exit(1)
patch_db_path = CONFIG_ROOT / args.language / "asm_patches.json"
if not patch_db_path.exists():
print("ERROR: patch db file doesn't exist. aborting patching asm")
exit(1)
patch_db = PatchDB.model_validate_json(patch_db_path.read_text())
if asm_file.name not in patch_db.root:
print(f"ERROR: no patches found for file {asm_file.name}")
exit(1)
asm_patch = patch_db.root[asm_file.name]
fix_asm(asm_file, asm_patch)
if __name__ == "__main__":
main()
-38
View File
@@ -1,38 +0,0 @@
import re
import glob
import tqdm
import argparse
from typing import Protocol, cast
from pathlib import Path
def fix_assets(asm_data_path: Path, asset_rel_path: Path):
asm_files = glob.glob(str(asm_data_path / "**/*.s"), recursive=True)
for asm_file in tqdm.tqdm(asm_files, desc="Fixing data asm"):
n: int = 0
with open(asm_file, mode="r") as fh:
data_asm: str = fh.read()
data_asm, n = re.subn(rf'\.incbin "{asset_rel_path}/', '.incbin "assets/', data_asm)
if n > 0:
with open(asm_file, mode="w") as wh:
wh.write(data_asm)
def main():
class ArgsProtocol(Protocol):
asm_data_path: Path
asset_rel_path: Path
parser = argparse.ArgumentParser(description="fixes data asm include path")
parser.add_argument("asm_path", metavar="asm-path", type=Path, help="data path in assembly root to patch")
parser.add_argument("asset_rel_path", metavar="asset-path", type=Path, help="relative asset path")
args = cast(ArgsProtocol, parser.parse_args())
fix_assets(args.asm_data_path, args.asset_rel_path)
if __name__ == "__main__":
main()
-79
View File
@@ -1,79 +0,0 @@
from __future__ import annotations
import os
import argparse
import pydantic
from pathlib import Path
from typing import Protocol, cast
CONFIG_ROOT = Path(__file__).parent.parent.parent.resolve() / "config"
# enumerate languages from config dir
LANGUAGES = [Path(f.path).name for f in os.scandir(CONFIG_ROOT) if f.is_dir()]
class BytePatch(pydantic.RootModel[tuple[int, int]]):
root: tuple[int, int]
@property
def address(self):
return self.root[0]
@property
def size(self):
return self.root[1]
class BINPatch(pydantic.RootModel[dict[str, BytePatch]]):
root: dict[str, BytePatch]
class PatchDB(pydantic.RootModel[dict[str, BINPatch]]):
root: dict[str, BINPatch]
def fix_elf(orig_elf_path: Path, built_elf_path: Path, bin_patch: BytePatch):
with orig_elf_path.open("rb") as fh:
fh.seek(bin_patch.address)
data = fh.read(bin_patch.size)
with built_elf_path.open(mode="r+b") as wh:
wh.seek(bin_patch.address)
wh.write(data)
def main():
class ArgsProtocol(Protocol):
language: str
elf_file: Path
parser = argparse.ArgumentParser(description="apply asm patches to assembly files")
parser.add_argument("language", type=str, choices=LANGUAGES, help="language of the elf that is being patched")
parser.add_argument("elf_file", type=Path, help="elf file to patch (relative to build dir)")
args = cast(ArgsProtocol, parser.parse_args())
built_elf_file = CONFIG_ROOT / args.language / args.elf_file
if not built_elf_file.exists():
print(f"ERROR: cannot find elf file {built_elf_file}")
exit(1)
orig_elf_path = CONFIG_ROOT / args.language / args.elf_file.name
patch_db_path = CONFIG_ROOT / args.language / "bin_patches.json"
if not patch_db_path.exists():
exit(0)
patch_db = PatchDB.model_validate_json(patch_db_path.read_text())
for _tu_name, bin_patch in patch_db.root.items():
for _func_name, byte_patch in bin_patch.root.items():
fix_elf(orig_elf_path, built_elf_file, byte_patch)
if __name__ == "__main__":
main()
-75
View File
@@ -1,75 +0,0 @@
import re
import glob
import tqdm
import argparse
import functools
from typing import Protocol, cast
from pathlib import Path
@functools.lru_cache(maxsize=1024) # pyright: ignore[reportUntypedFunctionDecorator]
def get_symbol_address(symbol_addrs: Path, symbol: str):
if match := re.match(r"^ *([^+ ]+) *\+ *(0x[0-9a-fA-F]+)$", symbol):
symbol = match.group(1)
offset = int(match.group(2), 16)
else:
offset = 0
with open(symbol_addrs, mode="r") as fh:
for line in fh:
if line.startswith(f"{symbol} "):
addr = int(line.split("=")[1].split(";")[0].strip(), 16)
return addr + offset
assert False, f"{symbol} not found"
def fix_gp(asm_path: Path, gp_value: int, symbol_addrs: Path):
if gp_value <= 0:
return
asm_files = glob.glob(str(asm_path / "**/*.s"), recursive=True)
for asm_file in tqdm.tqdm(asm_files, desc="Fixing gp_rel"):
lines: list[str] = []
with open(asm_file, mode="r") as fh:
for line in fh:
if match := re.match(r"^(.*)%gp_rel\(([^)]+)\)(.*)$", line):
# ol = line
instr_pre = match.group(1)
instr_post = match.group(3)
address_str = match.group(2)
if address_str.startswith("D_"):
address_str = address_str.replace("D_", "0x")
res = eval(address_str)
address = res
else:
address = get_symbol_address(symbol_addrs, address_str)
gp_rel = address - gp_value
line = f"{instr_pre}{hex(gp_rel)}{instr_post}\n"
lines.append(line)
with open(asm_file, mode="w") as wh:
wh.writelines(lines)
def main():
class ArgsProtocol(Protocol):
asm_path: Path
gp_value: int
symbol_addrs: Path
def hex_int(x: str):
return int(x, 16)
parser = argparse.ArgumentParser(description="fixes asm removing gp_rel macro")
parser.add_argument("asm_path", metavar="asm-path", type=Path, help="assembly root path to patch")
parser.add_argument("gp_value", metavar="gp", type=hex_int, help="gp value in hex")
parser.add_argument("symbol_addrs", metavar="symbol-addrs", type=Path, help="path of symbol_addrs.txt")
args = cast(ArgsProtocol, parser.parse_args())
fix_gp(args.asm_path, args.gp_value, args.symbol_addrs)
if __name__ == "__main__":
main()
-139
View File
@@ -1,139 +0,0 @@
import re
import sys
import yaml
import tqdm
from typing import cast, Any
from pathlib import Path
# e.g.: build/src/main/main.c.o(.text);
# build/asm/data/rodata_4.rodata.s.o(.rodata);
# ...
# NOTE: also account for optional '/data/' in path as chunks of data
# not belonging to a c file are put into 'build/asm/data/' folder.
re_subsegment_line = re.compile(
r"^(?P<indent> +)build/(?:asm|src)(?:/data)?/(?P<name>.*)\.[sc]\.o\(\.(?P<section>.+)\);$"
)
# e.g.: .main 0x100000 : AT(main_ROM_START) SUBALIGN(2)
# .main_bss (NOLOAD) : SUBALIGN(4)
# ...
re_section_line = re.compile(r"^(?P<indent> +)\.(?P<section>[^ ]+) .* SUBALIGN\((?P<subalign>[0-9]+)\)$")
def get_align(address: int):
return (
128 * ((address % 128) == 0)
or 64 * ((address % 64) == 0)
or 32 * ((address % 32) == 0)
or 16 * ((address % 16) == 0)
or 8 * ((address % 8) == 0)
or 4 * ((address % 4) == 0)
or 2 * ((address % 2) == 0)
or 1
)
def make_align_map(config: dict[str, Any]):
segments = cast(list[dict[str, Any] | list[Any]] | None, config["segments"])
assert segments
main_segment = next(
(segment for segment in segments if isinstance(segment, dict) and segment.get("name") == "main"), None
)
assert main_segment, "cannot find main segment"
subsegments = cast(list[dict[str, Any] | list[Any]] | None, main_segment["subsegments"])
assert subsegments, "cannot find main subsegments"
align_map: dict[str, int] = {}
for subsegment in subsegments:
if not isinstance(subsegment, dict):
continue
s_type = cast(str | None, subsegment.get("type"))
vram = cast(int | None, subsegment.get("vram"))
name = cast(str | None, subsegment.get("name"))
if not s_type or not vram or not name:
continue
if name.endswith("bin"):
continue
align = get_align(vram)
if not s_type.startswith("."):
name = f"{s_type}#{name}.{s_type}"
else:
name = f"{s_type[1:]}#{name}"
align_map[name] = align
return align_map
def fix_linkerscript(config: dict[str, Any], linkerscript_path: Path):
align_map = make_align_map(config)
section_subalign = cast(dict[str, int], config["_section_subalign"])
line_count = 0
with open(linkerscript_path, mode="r") as fh:
for line in fh:
line_count += 1
patched_lines: list[str] = []
with open(linkerscript_path, mode="r") as fh:
for line in tqdm.tqdm(fh, desc="Fixing linker script", total=line_count):
if match := re_subsegment_line.match(line):
indent = cast(str, match["indent"])
name = cast(str, match["name"])
section = cast(str, match["section"])
# force each subsegment in the following sections to have align 8
if section == "text":
patched_lines.append(f"{indent}. = ALIGN(., 8);\n")
key = f"{section}#{name}"
if align := align_map.get(key):
patched_lines.append(f"{indent}. = ALIGN(., {align});\n")
if match := re_section_line.match(line):
indent = cast(str, match["indent"])
section = cast(str, match["section"])
subalign = cast(str, match["subalign"])
# force each section to have the subalign specified in the yaml
if section in section_subalign:
current_subalign = f"SUBALIGN({subalign})"
fixed_subalign = f"SUBALIGN({section_subalign[section]})"
line = line.replace(current_subalign, fixed_subalign)
patched_lines.append(line)
with open(linkerscript_path, mode="w") as fh:
fh.writelines(patched_lines)
def main():
if len(sys.argv) != 3:
print("usage: fix_linkerscript.py CONFIG_YAML_PATH LINKERSCRIPT_PATH")
exit(1)
config_path = Path(sys.argv[1])
linkerscript_path = Path(sys.argv[2])
with open(config_path, mode="r") as fh:
try:
config = cast(dict[str, Any], yaml.safe_load(fh))
except yaml.YAMLError as e:
print(e)
raise e
fix_linkerscript(config, linkerscript_path)
if __name__ == "__main__":
main()
-153
View File
@@ -1,153 +0,0 @@
import json
import argparse
from typing import Protocol, Iterable, Any, cast
from pathlib import Path
def fix_unit(unit: dict[str, Any]):
# name: str = unit["name"]
fuzzy_match_percent: float | None = unit["measures"].get("fuzzy_match_percent")
assert fuzzy_match_percent and 0 < fuzzy_match_percent < 100
text_section = next(section for section in unit["sections"] if section["name"] == ".text")
# text_size: int = int(text_section["size"])
text_section_fuzzy_match_percent: float = text_section["fuzzy_match_percent"]
if 0 < text_section_fuzzy_match_percent < 100:
# fix text section fuzzy match percent
text_section["fuzzy_match_percent"] = 100.0
text_fuzzy_match_percent: float = unit["measures"]["fuzzy_match_percent"]
assert text_fuzzy_match_percent == fuzzy_match_percent
functions = unit["functions"]
total_code: int = int(unit["measures"]["total_code"])
matched_code: int = int(unit["measures"]["matched_code"])
matched_functions = unit["measures"]["matched_functions"]
computed_total_code: int = 0
computed_matched_code: int = 0
computed_matched_functions: int = 0
duplicate_functions = (
"Tim2CalcBufWidth__2",
"_ftoi0__2",
"ItemGetMain__2",
"setD3_CHCR__2",
"setD4_CHCR__2",
"setD4_CHCR__3",
"_fpadd_parts__2",
)
for function in functions:
function_size: int = int(function["size"])
try:
function_fuzzy_match_percent: float = function["fuzzy_match_percent"]
except KeyError:
# fix known function duplicates
if function["name"] in duplicate_functions:
function["fuzzy_match_percent"] = 100.0
function_fuzzy_match_percent = 100.0
matched_code += function_size
matched_functions += 1
else:
raise
if function_fuzzy_match_percent == 100.0:
computed_matched_code += function_size
computed_matched_functions += 1
# fix function fuzzy match percent
function["fuzzy_match_percent"] = 100.0
computed_total_code += function_size
assert total_code == computed_total_code
assert matched_code == computed_matched_code
assert matched_functions == computed_matched_functions
# fix unit measures
unit["measures"]["fuzzy_match_percent"] = 100.0
unit["measures"]["matched_code"] = unit["measures"]["total_code"]
unit["measures"]["matched_code_percent"] = 100.0
unit["measures"]["matched_functions"] = unit["measures"]["total_functions"]
unit["measures"]["matched_functions_percent"] = 100.0
def fix_report(report_path: Path):
report = json.loads(report_path.read_text())
units: Iterable[Any] = report["units"]
computed_total_code: int = 0
computed_matched_code: int = 0
computed_total_functions: int = 0
computed_matched_functions: int = 0
total_code: int = int(report["measures"]["total_code"])
total_functions: int = report["measures"]["total_functions"]
for unit in units:
# name: str = unit["name"]
unit_total_code: int = int(unit["measures"]["total_code"])
unit_total_functions: int = unit["measures"]["total_functions"]
fuzzy_match_percent: float | None = unit["measures"].get("fuzzy_match_percent")
if fuzzy_match_percent and 0 < fuzzy_match_percent < 100:
fix_unit(unit)
if fuzzy_match_percent and fuzzy_match_percent > 0:
computed_matched_code += unit_total_code
computed_matched_functions += unit_total_functions
computed_total_functions += unit_total_functions
computed_total_code += unit_total_code
assert total_code == computed_total_code
assert total_functions == computed_total_functions
# fix report measures
report["measures"]["fuzzy_match_percent"] = 100.0 * computed_matched_code / computed_total_code
report["measures"]["matched_code"] = str(computed_matched_code)
report["measures"]["matched_code_percent"] = report["measures"]["fuzzy_match_percent"]
report["measures"]["matched_functions"] = computed_matched_functions
report["measures"]["matched_functions_percent"] = 100.0 * computed_matched_functions / computed_total_functions
categories = report["categories"]
assert len(categories) == 1
assert categories[0]["measures"]["total_code"] == report["measures"]["total_code"]
assert categories[0]["measures"]["total_units"] == report["measures"]["total_units"]
categories[0]["measures"]["fuzzy_match_percent"] = report["measures"]["fuzzy_match_percent"]
categories[0]["measures"]["matched_code"] = report["measures"]["matched_code"]
categories[0]["measures"]["matched_code_percent"] = report["measures"]["matched_code_percent"]
categories[0]["measures"]["matched_functions"] = report["measures"]["matched_functions"]
categories[0]["measures"]["matched_functions_percent"] = report["measures"]["matched_functions_percent"]
# /path/to/report.json -> /path/to/report_fixed.json
# fixed_report_path = report_path.with_name(f"{report_path.stem}_fixed{report_path.suffix}")
# fixed_report_path.write_text(json.dumps(report))
report_path.write_text(json.dumps(report))
print(f"Wrote fixed report to {report_path}")
def main():
class ArgsProtocol(Protocol):
report_path: Path
parser = argparse.ArgumentParser(description="fixes objdiff report")
parser.add_argument("report_path", metavar="report-path", type=Path, help="path to the report generated by objdiff")
args = cast(ArgsProtocol, parser.parse_args())
fix_report(args.report_path)
if __name__ == "__main__":
main()
-32
View File
@@ -1,32 +0,0 @@
import re
import argparse
from pathlib import Path
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--language", required=True, choices=["us", "eu"])
args = parser.parse_args()
if args.language == "us":
map_path = Path("config/us/build/SLUS_203.88.map")
else:
map_path = Path("config/eu/build/SLES_508.21.map")
with open(map_path, mode="r") as fh:
for n, line in enumerate(fh):
line = line.rstrip("\n")
if match := re.match(r"^\s*0x([0-9a-fA-F]+)\s+[^ ]+?([0-9a-fA-F]{6,8})\s*$", line):
addr = match.group(1)
label = match.group(2)
if addr.upper() != label.upper():
print(f"{map_path}:{n+1} {line}")
return
print("no mismatches found")
if __name__ == "__main__":
main()
-438
View File
@@ -1,438 +0,0 @@
# pyright: reportInvalidTypeForm=false
from __future__ import annotations
import os
import re
import io
import enum
import math
import tqdm
import argparse
import pydantic
import subprocess
from ctypes import c_uint16, c_int16, c_uint64, c_uint32, c_int32, c_uint8, c_float, sizeof as c_sizeof
from typing import Protocol, BinaryIO, Type, Iterable, cast
from pathlib import Path
from cstruct import CStructure, ctypes_types, CTypeType, c_addr_ptr, c_str, print_carr
def vram2offset(vram: int):
return vram - 0xFF000
sceVu0FVECTOR = c_float * 4
sceVu0IVECTOR = c_int32 * 4
sceVu0FMATRIX = sceVu0FVECTOR * 4
qword = c_int32 * 4
class ANI_CODE(c_uint16):
def __str__(self):
return f"{self.value}"
class GS_REG(c_uint64):
def __str__(self):
return f"0x{self.value:x}"
class HEX_ADDR(c_uint32):
def __str__(self):
return f"0x{self.value:x}"
# CTypeTypeEX = CTypeType | type[sceVu0FVECTOR] | type[sceVu0IVECTOR]
###########################################################################
# struct _MNG_STATE_ST {
# // Members
# int state; // offset 0x0, size 0x4
# u_int flag; // offset 0x4, size 0x4
# u_int calc_total_time; // offset 0x8, size 0x4
# u_int total_time; // offset 0xC, size 0x4
# u_int get_time; // offset 0x10, size 0x4
# u_int dmy[3]; // offset 0x14, size 0xC
# };
class MNG_STATE_ST(CStructure):
state: c_int32
flag: c_uint32
calc_total_time: c_uint32
total_time: c_uint32
get_time: c_uint32
flag: c_uint32
dmy: c_uint32 * 3
# struct _TMP_MENU_MNG_ST {
# // Members
# struct _MNG_STATE_ST mng_state; // offset 0x0, size 0x20
# int DrawState; // offset 0x20, size 0x4
# u_char** pTmpMenuFileBuf; // offset 0x24, size 0x4
# float draw_up_rate; // offset 0x28, size 0x4
# u_char dmy[12]; // offset 0x2C, size 0xC
# };
class TMP_MENU_MNG_ST(CStructure):
mng_state: MNG_STATE_ST
DrawState: c_int32
pTmpMenuFileBuf: c_addr_ptr
draw_up_rate: c_float
dmy: c_uint8 * 12
###########################################################################
elf_names: dict[str, str] = {
"us": "SLUS_203.88",
"eu": "SLES_508.21",
}
class VRamElf(BinaryIO):
def __init__(self, elf: BinaryIO):
self.elf = elf
def read(self, n: int = -1):
return self.elf.read(n)
def seek(self, offset: int, whence: int = 0):
return self.elf.seek(vram2offset(offset), whence)
# matches "var_name = 0x12345678; // attr1:val1 attr2:val2 ... attrN:valN"
line_pattern = re.compile(r"^([^\s=]+)\s*=\s*(0x[0-9a-fA-F]+);\s*\/\/((?:\s*[0-9a-zA-Z_]+(:?:[0-9a-zA-Z_\*]+)?)+)\s*$")
# matches multiple attributes in the form of attr:val or attr
attr_pattern = re.compile(r"([0-9a-zA-Z_]+)(?::([0-9a-zA-Z_\*]+))?")
# match address lines: "0xaabbccdd = &var"
addr_pattern = re.compile(r"^\s*(0x[0-9a-fA-F]+)\s*=\s*(.*)\s*;\s*(\/\/.*)?$")
class DataVar(pydantic.BaseModel):
model_config = pydantic.ConfigDict(extra="forbid")
_elf: VRamElf = pydantic.PrivateAttr()
address: int
name: str
type: Type[CStructure] | CTypeType | type[sceVu0FVECTOR] | type[sceVu0IVECTOR] | type[qword] | type[c_str]
numel: int | list[int] = 0
nosize: bool = False
static: bool = False
num_ptr: int = 0
@pydantic.model_validator(mode="after")
def store_num_ptr(self):
# transfer class attribute to instance
if DataVar.num_ptr > 0:
self.num_ptr = DataVar.num_ptr
return self
@pydantic.field_validator("type", mode="before")
@classmethod
def type_from_str( # pyright: ignore
cls, v: str | Type[CStructure] | CTypeType
) -> Type[CStructure] | CTypeType | sceVu0FVECTOR | sceVu0IVECTOR | qword | ANI_CODE | GS_REG | HEX_ADDR:
if not isinstance(v, str):
return v
cls.num_ptr = v.count("*") # temporary store num_ptr in class attribute
v = v.rstrip("*") # remove pointer(s)
if v in ctypes_types:
return ctypes_types[v]
if v == "sceVu0FVECTOR":
return sceVu0FVECTOR
if v == "sceVu0IVECTOR":
return sceVu0IVECTOR
if v == "qword":
return qword
if v == "ANI_CODE":
return ANI_CODE
if v == "GS_REG":
return GS_REG
if v == "HEX_ADDR":
return HEX_ADDR
if v == "c_str":
return c_str
class_type = globals()[v]
if issubclass(class_type, CStructure):
return class_type
raise ValueError(f"{v} is unknown/not a valid type")
def data_var_dumps(self, addresses: dict[int, str]):
self._elf.seek(self.address, os.SEEK_SET)
if isinstance(self.numel, list):
numel = math.prod(self.numel)
ndims = len(self.numel)
else:
numel = max(1, self.numel)
ndims = 1
if self.num_ptr > 0:
var_data = (c_addr_ptr * numel).from_buffer_copy(self._elf.read(numel * c_sizeof(c_addr_ptr)))
if self.type == ANI_CODE: # pyright: ignore
type_str = "ANI_CODE" + ("*" * self.num_ptr)
if self.type == GS_REG: # pyright: ignore
type_str = "u_long" + ("*" * self.num_ptr)
if self.type == HEX_ADDR: # pyright: ignore
type_str = "u_int" + ("*" * self.num_ptr)
elif issubclass(self.type, CStructure): # pyright: ignore
type_str = self.type.__name__ + ("*" * self.num_ptr)
elif self.type == sceVu0FVECTOR: # pyright: ignore
type_str = "sceVu0FVECTOR" + ("*" * self.num_ptr)
elif self.type == sceVu0IVECTOR: # pyright: ignore
type_str = "sceVu0IVECTOR" + ("*" * self.num_ptr)
else:
type_str = next(
k
for k, v in ctypes_types.items()
if getattr(v, "_type_") == getattr(self.type, "_type_") # pyright: ignore
) + ("*" * self.num_ptr)
stream = io.StringIO()
if self.static:
stream.write("static ")
stream.write(f"{type_str} {self.name}")
if numel > 1:
numel = f"{self.numel}" if not self.nosize else ""
var_str = f"{{ {', '.join(str(var) for var in cast(Iterable[c_addr_ptr], var_data))}, }}" # pyright: ignore
stream.write(f"[{numel}]")
else:
# should have been parsed as c_float_Array_4_Array_1
assert len(var_data) == 1 and var_data[0].__class__ is c_addr_ptr
var_str = str(var_data[0])
stream.write(f" = {var_str};")
elif issubclass(self.type, CStructure): # pyright: ignore
data = self._elf.read(numel * self.type.sizeof())
return self.type.dumps(
self.name,
data,
numel=self.numel,
static=self.static,
nosize=self.nosize,
noarray=self.numel == 0,
)
elif self.type == sceVu0FVECTOR: # pyright: ignore[reportUnknownMemberType]
var_data = (sceVu0FVECTOR * numel).from_buffer_copy(self._elf.read(numel * c_sizeof(sceVu0FVECTOR)))
type_str = "sceVu0FVECTOR"
stream = io.StringIO()
if self.static:
stream.write("static ")
stream.write(f"{type_str} {self.name}")
def sceVu0FVECTOR_to_str(_v: sceVu0FVECTOR): # pyright: ignore
return f"{{ {', '.join(f'{flt}f' for flt in cast(Iterable[c_float], _v))} }}"
if numel > 1:
numel = f"{self.numel}" if not self.nosize else ""
var_str = (
f"{{ {', '.join(sceVu0FVECTOR_to_str(var) for var in cast(Iterable[sceVu0FVECTOR], var_data))}, }}" # pyright: ignore
)
stream.write(f"[{numel}]")
else:
# should have been parsed as c_float_Array_4_Array_1
assert len(var_data) == 1 and var_data[0].__class__ is sceVu0FVECTOR
var_str = sceVu0FVECTOR_to_str(var_data[0])
stream.write(f" = {var_str};")
elif self.type == sceVu0IVECTOR: # pyright: ignore[reportUnknownMemberType]
var_data = (sceVu0IVECTOR * numel).from_buffer_copy(self._elf.read(numel * c_sizeof(sceVu0IVECTOR)))
type_str = "sceVu0IVECTOR"
stream = io.StringIO()
if self.static:
stream.write("static ")
stream.write(f"{type_str} {self.name}")
def sceVu0IVECTOR_to_str(_v: sceVu0IVECTOR): # pyright: ignore
return f"{{ {', '.join(f'{int32}' for int32 in cast(Iterable[c_int32], _v))} }}"
if numel > 1:
numel = f"{self.numel}" if not self.nosize else ""
var_str = (
f"{{ {', '.join(sceVu0IVECTOR_to_str(var) for var in cast(Iterable[sceVu0IVECTOR], var_data))}, }}" # pyright: ignore
)
stream.write(f"[{numel}]")
else:
# should have been parsed as c_int_Array_4_Array_1
assert len(var_data) == 1 and var_data[0].__class__ is sceVu0IVECTOR
var_str = sceVu0IVECTOR_to_str(var_data[0])
stream.write(f" = {var_str};")
elif self.type == c_str: # pyright: ignore
assert numel > 1, "we only handle string arrays here, simple strings should be embedded in the source"
var_data = (self.type * numel).from_buffer_copy(self._elf.read(numel * c_sizeof(self.type))) # pyright: ignore
stream = io.StringIO()
if self.static:
stream.write("static ")
numel = f"{self.numel}" if not self.nosize else ""
stream.write(f"char *{self.name}[{numel}] = {{")
for var in cast(Iterable[c_str], var_data):
stream.write(f"{var.to_str(self._elf)},")
stream.write("};")
else:
typ = self.type # pyright: ignore
if ndims == 1:
typ *= numel # pyright: ignore
elif isinstance(self.numel, list):
for n in reversed(self.numel):
typ *= n # pyright: ignore
var_data = (typ).from_buffer_copy(self._elf.read(numel * c_sizeof(self.type))) # pyright: ignore
if self.type.__name__ == "ANI_CODE": # pyright: ignore
type_str = "ANI_CODE"
elif self.type.__name__ == "GS_REG": # pyright: ignore
type_str = "u_long"
elif self.type.__name__ == "HEX_ADDR": # pyright: ignore
type_str = "u_int"
else:
type_str = next(
k
for k, v in ctypes_types.items()
if getattr(v, "_type_") == getattr(self.type, "_type_") # pyright: ignore
)
stream = io.StringIO()
if self.static:
stream.write("static ")
stream.write(f"{type_str} {self.name}")
if ndims == 1:
numel = f"{self.numel}" if not self.nosize else ""
var_str = f"{{ {', '.join(f'{var}' for var in cast(Iterable[CTypeType], var_data))} }}"
stream.write(f"[{numel}]")
elif ndims > 1:
assert isinstance(self.numel, list)
numel = ""
for n, num in enumerate(self.numel):
numel += "[]" if n == 0 and self.nosize else f"[{num}]"
stream.write(numel)
tmpbuf = io.StringIO()
tmpbuf.write("{")
print_carr(var_data, tmpbuf) # pyright: ignore
tmpbuf.write("}")
var_str = tmpbuf.getvalue()
else:
var_str = f"{var_data.value}" # pyright: ignore
stream.write(f" = {var_str};")
return stream.getvalue()
def parse_data_vars(data_vars_txt: Path, strict: bool = True):
data_vars: list[DataVar] = []
addr_vals: dict[int, str] = {}
with open(data_vars_txt, mode="r") as fh:
for n, line in enumerate(fh):
line = line.strip()
# skip empty lines and comments
if not line or line.startswith("//"):
continue
valid = False
if addr_match := addr_pattern.match(line):
address = int(addr_match.group(1), 16)
value = addr_match.group(2)
addr_vals[address] = value
valid = True
elif line_match := line_pattern.match(line):
address = int(line_match.group(2), 16)
options = line_match.group(3)
name = line_match.group(1)
if attr_match := attr_pattern.findall(options):
attrs: dict[str, str | list[str] | bool] = {}
for attr_name, attr_val in cast(list[tuple[str, str]], attr_match):
if attr_name not in attrs:
attrs[attr_name] = attr_val
elif not isinstance(attrs[attr_name], list):
attrs[attr_name] = cast(list[str], [attrs[attr_name], attr_val])
else:
cast(list[str], attrs[attr_name]).append(attr_val)
for key, value in attrs.items():
if not isinstance(value, bool) and len(value) == 0:
attrs[key] = True
try:
data_var = DataVar(address=address, name=name, **attrs) # pyright: ignore[reportArgumentType]
data_vars.append(data_var)
if address not in addr_vals:
if (
isinstance(data_var.numel, list) or data_var.numel > 0 or data_var.type == sceVu0FVECTOR # type: ignore
):
addr_vals[address] = name
else:
addr_vals[address] = f"&{name}"
valid = True
except pydantic.ValidationError as ve:
print(f"syntax error at line {n}")
raise ve
if strict and not valid:
raise ValueError(f"syntax error at line {n}: {line}")
return data_vars, addr_vals
def parse_data(lang: str):
data_vars_txt = Path("config") / lang / "data_vars.txt"
data_vars, addr_vals = parse_data_vars(data_vars_txt)
if not data_vars:
return
if len(set(data_var.name for data_var in data_vars)) != len(data_vars):
raise RuntimeError("duplicate names in data_vars.txt")
elf_path = Path("config") / lang / elf_names[lang]
include_path = Path("config") / lang / "include" / "data"
include_path.mkdir(parents=True, exist_ok=True)
c_addr_ptr.set_addresses(addr_vals)
extract_filter: list[str] = []
extract_filter_str = os.environ.get("EXTRACT_DATA_FILTER", "").strip()
if extract_filter_str:
extract_filter: list[str] = extract_filter_str.split(",")
with open(elf_path, mode="rb") as elf:
vram_elf = VRamElf(elf)
CStructure.__elf__ = vram_elf
for data_var in tqdm.tqdm(data_vars, desc="Extracting data"):
header_path = include_path / f"{data_var.name}.h"
if extract_filter and data_var.name not in extract_filter:
continue
with header_path.open(mode="w") as fw:
data_var._elf = vram_elf # pyright: ignore[reportPrivateUsage]
fw.write(data_var.data_var_dumps(addr_vals))
try:
subprocess.run(["clang-format", "-i", "--style=Microsoft", header_path])
except Exception:
# formatting is best effort for now
pass
def main():
class Language(str, enum.Enum):
US = "us"
EU = "eu"
languages = [lang.value for lang in Language]
class ArgsProtocol(Protocol):
language: Language
parser = argparse.ArgumentParser(description="fixes asm removing gp_rel macro")
parser.add_argument("language", type=str, choices=languages, help="language to extract data for")
args = cast(ArgsProtocol, parser.parse_args())
parse_data(args.language)
if __name__ == "__main__":
main()
-288
View File
@@ -1,288 +0,0 @@
# pyright: reportUnknownMemberType=false
import re
import argparse
from typing import cast
from pathlib import Path
from elftools.elf.elffile import ELFFile
from elftools.elf.sections import SymbolTableSection
"""
matches variable declaration:
/* SECTION ADDRESS */ VAR_TYPE NAME[NUMEL];
"""
re_glob = re.compile(
r"^/\* (?P<section>.*?) (?P<address>.*?) \*/ (?P<var_type>[^(]*) (?P<name>.*?)(?:\[(?P<numel>.*?)\])?;"
)
"""
matches structs (or unions) with no typedef:
struct NAME { // SIZE
...
};
"""
re_struct = re.compile(
r"^(?:struct|union) (?P<name>.*?) \{ // (?P<size>0x[0-9a-f]+)\n.*?^\};", flags=re.MULTILINE | re.DOTALL
)
"""
matches structs (or unions) with typedef:
typedef struct { // SIZE
...
} NAME;
"""
re_typedef_struct = re.compile(
r"^typedef (?:struct|union) \{ // (?P<size>0x[0-9a-f]+)\n.*?^\} (?P<name>.*?);", flags=re.MULTILINE | re.DOTALL
)
sizes: dict[str, int] = {
"char": 1,
"u_char": 1,
"short": 2,
"short int": 2,
"u_short": 2,
"int": 4,
"u_int": 4,
"float": 4,
"u_long128": 8,
"sceVu0FMATRIX": 4 * 4 * 4,
"sceVu0FVECTOR": 4 * 4,
"sceSifClientData": 0x2C,
}
command_script_keywords = (
"VERSION",
"SECTIONS",
"ABSOLUTE",
"LOADADDR",
"ALIGN",
"DEFINED",
"NEXT",
"SIZEOF",
"SIZEOF_HEADERS",
"MAX",
"MIN",
"PHDRS",
"CREATE_OBJECT_SYMBOLS",
"BYTE",
"SHORT",
"LONG",
"SQUAD",
"FILL",
"BLOCK",
"NOLOAD",
"AT",
"OVERLAY",
"NOCROSSREFS",
"PT_NULL",
"PT_LOAD",
"PT_DYNAMIC",
"PT_INTERP",
"PT_NOTE",
"PT_SHLIB",
"PT_PHDR",
"ENTRY",
"FLOAT",
"NOFLOAT",
"FORCE_COMMON_ALLOCATION",
"INCLUDE",
"INPUT",
"GROUP",
"OUTPUT",
"OUTPUT_ARCH",
"OUTPUT_FORMAT",
"SEARCH_DIR",
"STARTUP",
"TARGET",
"NOCROSSREFS",
)
class GlobalVarLineMatch:
max_section_len: int = 0
max_address_len: int = 0
max_var_type_len: int = 0
max_name_len: int = 0
max_numel_len: int = 0
_name_cache: list[str] = []
@classmethod
def _get_unique_name(cls, name: str):
i = 1
unique_name = f"{name}__local_{i}"
while unique_name in cls._name_cache:
i += 1
unique_name = f"{name}__local_{i}"
cls._name_cache.append(unique_name)
return unique_name
def __init__(
self,
symtab: SymbolTableSection,
/,
*,
section: str,
address: str | int,
var_type: str,
name: str,
numel: str | int | None = None,
):
self.symtab: SymbolTableSection = symtab
self.section: str = section
self.address: int = address if isinstance(address, int) else int(address, 16)
self.var_type: str = var_type
self.name: str = name
self.numel: int
while self.name.startswith("*"):
self.name = self.name[1:]
self.var_type += "*"
# resolve function pointers (e.g., 'void (*SpecialEventInitTbl[0])...')
if match := re.match(r"\(\*(.*?)\[.*?\]\).*", self.name):
self.name = match.group(1)
self.name = self.name.replace("*", "")
if numel is not None:
if isinstance(numel, int):
self.numel = numel
else:
prod_numel = 1
numels = numel.split("][")
for n in numels:
prod_numel *= int(n)
self.numel = prod_numel
if numel is None:
self.numel = 1
# get symbol
symbol = self.symtab.get_symbol_by_name(self.name)
assert isinstance(symbol, list)
symbol = next(
(sym for sym in symbol if sym.name == self.name and sym.entry["st_value"] == self.address),
None,
)
assert symbol
self.symbol = symbol
if not self.is_global:
self.name = self._get_unique_name(self.name)
escaped_name_len = len(str(self.name))
if self.name in command_script_keywords:
escaped_name_len += 2
GlobalVarLineMatch.max_section_len = max(GlobalVarLineMatch.max_section_len, len(str(self.section)))
GlobalVarLineMatch.max_address_len = max(GlobalVarLineMatch.max_address_len, len(str(self.address)))
GlobalVarLineMatch.max_var_type_len = max(GlobalVarLineMatch.max_var_type_len, len(str(self.var_type)))
GlobalVarLineMatch.max_name_len = max(GlobalVarLineMatch.max_name_len, escaped_name_len)
GlobalVarLineMatch.max_numel_len = max(GlobalVarLineMatch.max_numel_len, len(str(self.numel)))
@property
def is_global(self):
return cast(str, self.symbol["st_info"]["bind"]) == "STB_GLOBAL"
@property
def is_hidden(self):
return cast(str, self.symbol["st_other"]["visibility"]) == "STV_HIDDEN"
@property
def size(self):
size = cast(int, self.symbol["st_size"])
if size == 0:
if "*" in self.var_type:
size = 4
elif self.var_type in sizes:
size = sizes[self.var_type] * self.numel
return size
def to_string(self, as_linker_command_file: bool):
name = self.name if self.name not in command_script_keywords else f'"{self.name}"'
cls_str = f"{name:{GlobalVarLineMatch.max_name_len}s} = 0x{self.address:08x};"
if not as_linker_command_file:
if self.size > 0:
cls_str = f"{cls_str} // size:0x{self.size:x}"
else:
cls_str = f"{cls_str} //0 {self.var_type} * {self.numel}"
# cls_str += f" bind:{'global' if self.is_global else 'local'}"
# cls_str += f" visibility:{'hidden' if self.is_hidden else 'visible'}"
return cls_str
def __str__(self):
return self.to_string(as_linker_command_file=False)
def parse_globals(elf_path: Path, globals_path: Path, types_path: Path, as_linker_command_file: bool):
with open(elf_path, mode="rb") as fh:
elf = ELFFile(fh)
# Find the symbol table.
symtab = elf.get_section_by_name(".symtab")
assert isinstance(symtab, SymbolTableSection)
with open(types_path, mode="r") as f:
types_data = f.read()
for struct_type, size_hex_str in re_struct.findall(types_data):
struct_type = cast(str, struct_type)
struct_size = int(cast(str, size_hex_str), 16)
# assert not (struct_type in sizes and sizes[struct_type] != struct_size)
sizes[struct_type] = struct_size
for size_hex_str, struct_type in re_typedef_struct.findall(types_data):
struct_type = cast(str, struct_type)
struct_size = int(cast(str, size_hex_str), 16)
# assert not (struct_type in sizes and sizes[struct_type] != struct_size), (
# struct_type,
# sizes[struct_type],
# struct_size,
# )
sizes[struct_type] = struct_size
if "tagSE_WRK" in sizes:
sizes["SE_WRK"] = sizes["tagSE_WRK"]
with open(globals_path, mode="r") as f:
lines = [line.strip() for line in f.readlines() if line.startswith("/*") and line.strip().endswith(";")]
matches = [GlobalVarLineMatch(symtab, **match.groupdict()) for line in lines if (match := re_glob.match(line))]
matches.sort(key=lambda match: match.address)
for match in matches:
print(match.to_string(as_linker_command_file=as_linker_command_file))
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--language", required=True, choices=["us", "eu"])
parser.add_argument("--as-linker-command-file", action="store_true")
args = parser.parse_args()
if args.language == "us":
elf_path = Path("config/us/SLUS_203.88")
globals_path = Path("ccc/SLUS_203.88/globals.h")
types_path = Path("ccc/SLUS_203.88/types.h")
else:
elf_path = Path("config/eu/SLES_508.21")
globals_path = Path("ccc/SLES_508.21/globals.h")
types_path = Path("ccc/SLES_508.21/types.h")
parse_globals(elf_path, globals_path, types_path, args.as_linker_command_file)
if __name__ == "__main__":
main()
-378
View File
@@ -1,378 +0,0 @@
# pyright: reportUnknownMemberType=false
from __future__ import annotations
import re
import json
import argparse
import itertools
from typing import Protocol, TextIO, cast
from pathlib import Path
from elftools.elf.elffile import ELFFile
from elftools.elf.sections import SymbolTableSection, Symbol
import ccc_json_v7
Range = tuple[int, int]
command_script_keywords = (
"VERSION",
"SECTIONS",
"ABSOLUTE",
"LOADADDR",
"ALIGN",
"DEFINED",
"NEXT",
"SIZEOF",
"SIZEOF_HEADERS",
"MAX",
"MIN",
"PHDRS",
"CREATE_OBJECT_SYMBOLS",
"BYTE",
"SHORT",
"LONG",
"SQUAD",
"FILL",
"BLOCK",
"NOLOAD",
"AT",
"OVERLAY",
"NOCROSSREFS",
"PT_NULL",
"PT_LOAD",
"PT_DYNAMIC",
"PT_INTERP",
"PT_NOTE",
"PT_SHLIB",
"PT_PHDR",
"ENTRY",
"FLOAT",
"NOFLOAT",
"FORCE_COMMON_ALLOCATION",
"INCLUDE",
"INPUT",
"GROUP",
"OUTPUT",
"OUTPUT_ARCH",
"OUTPUT_FORMAT",
"SEARCH_DIR",
"STARTUP",
"TARGET",
"NOCROSSREFS",
)
skip_symbols = (
"_fbss",
"_gp",
)
size_exceptions = {
"dorcon": 0x1C,
}
def parse_types(ccc_model_v7: ccc_json_v7.CCCJSONv7Model):
type_map: dict[str, int] = {}
for n, dt in enumerate(ccc_model_v7.deduplicated_types):
if dt.descriptor == "builtin":
assert dt.name and dt.name not in type_map
assert dt.class_ is not None
size_bits = int(dt.class_.split("-", maxsplit=1)[0])
assert size_bits % 8 == 0
type_map[dt.name] = size_bits // 8
elif dt.descriptor == "type_name":
assert dt.name is not None
assert dt.type_name is not None
if dt.name in type_map:
if dt.size_bits:
assert type_map[dt.name] == dt.size_bits // 8
if dt.name == dt.type_name == "void":
type_map["void"] = type_map["int"]
continue
assert dt.type_name in type_map, (n, dt.type_name)
type_map[dt.name] = type_map[dt.type_name]
elif dt.descriptor == "pointer":
assert dt.name is not None
assert dt.value_type is not None
if dt.value_type.descriptor == "function_type":
assert dt.name not in type_map
type_map[dt.name] = type_map["int"]
elif dt.value_type.descriptor == "type_name":
assert dt.value_type.type_name
assert dt.value_type.type_name in type_map
type_map[dt.name] = type_map[dt.value_type.type_name]
elif dt.descriptor == "struct":
assert dt.name is not None
if not dt.conflict:
assert dt.name not in type_map, n
else:
if dt.name in type_map:
continue
assert dt.size_bits is not None
assert dt.size_bits % 8 == 0
type_map[dt.name] = dt.size_bits // 8
elif dt.descriptor == "array":
assert dt.name is not None
element_count = 1
element_type = dt.element_type
type_name = None
while element_type:
if element_type.element_count is not None:
element_count *= element_type.element_count
if element_type.type_name is not None:
type_name = element_type.type_name
assert type_name in type_map
element_count *= type_map[type_name]
element_type = element_type.element_type
assert type_name and type_name in type_map, n
type_map[dt.name] = type_map[type_name] * element_count
elif dt.descriptor == "enum":
if dt.name:
if not dt.conflict:
assert dt.name not in type_map, n
type_map[dt.name] = type_map["int"]
elif dt.descriptor == "union":
assert dt.name
assert dt.size_bits
assert dt.size_bits % 8 == 0
type_map[dt.name] = dt.size_bits // 8
else:
assert False, f"unknown {n}"
return type_map
def in_range(ranges: list[Range], address: int):
if not ranges:
return True
in_range = any((ra[0] <= address <= ra[1]) for ra in ranges)
return in_range
def parse_ccc_model_v7(ccc_model_v7: ccc_json_v7.CCCJSONv7Model, ranges: list[Range]):
type_sizes = parse_types(ccc_model_v7)
if "pointer" not in type_sizes:
type_sizes["pointer"] = type_sizes["int"]
static_locals: list[ccc_json_v7.Local] = []
global_vars: list[ccc_json_v7.Global] = []
for file in ccc_model_v7.files:
for global_var in file.globals:
assert global_var.storage.global_address
if in_range(ranges, global_var.storage.global_address):
global_vars.append(global_var)
for function in file.functions:
for local in function.locals:
if local.storage_class == "static":
assert local.storage.global_address
if in_range(ranges, local.storage.global_address):
static_locals.append(local)
return type_sizes, global_vars, static_locals
class SymbolWithNoNameException(Exception): ...
class ParsedSymbol:
_name_map: dict[str, list[ParsedSymbol]] = {}
_max_name_len: int = 0
def __init__(self, symtab: SymbolTableSection, symbol: Symbol) -> None:
self.symtab = symtab
self.symbol = symbol
if not self.name:
raise SymbolWithNoNameException
if self.name not in ParsedSymbol._name_map:
ParsedSymbol._name_map[self.name] = []
ParsedSymbol._name_map[self.name].append(self)
ParsedSymbol._max_name_len = max(ParsedSymbol._max_name_len, len(self.name))
@property
def name(self) -> str:
return re.sub(r"^(.*?)(\.\d+)?$", r"\1", self.symbol.name)
@property
def size(self) -> int:
return cast(int, self.symbol["st_size"])
@property
def address(self) -> int:
return cast(int, self.symbol.entry["st_value"])
def in_range(self, range: Range):
return range[0] <= self.address <= range[1]
def to_undefined_syms(self) -> str:
homonyms = ParsedSymbol._name_map[self.name]
assert len(homonyms) > 0
if len(homonyms) == 1:
assert homonyms[0] == self
name = self.name
else:
idx = homonyms.index(self)
name = f"{self.name}__local_{idx + 1}"
name = name if name not in command_script_keywords else f'"{name}"'
# name_len_fmt = ParsedSymbol._max_name_len + len("__local_9999")
name_len_fmt = 40
return f"{name:{name_len_fmt}s} = 0x{self.address:08x};"
def to_symbol_addrs(
self,
type_sizes: dict[str, int],
global_vars: list[ccc_json_v7.Global],
static_locals: list[ccc_json_v7.Local],
) -> str:
homonyms = ParsedSymbol._name_map[self.name]
assert len(homonyms) > 0
if len(homonyms) == 1:
assert homonyms[0] == self
name = self.name
else:
idx = homonyms.index(self)
name = f"{self.name}__local_{idx + 1}"
# name_len_fmt = ParsedSymbol._max_name_len + len("__local_9999")
name_len_fmt = 40
size = size_exceptions.get(self.name, self.size)
if size == 0:
var = next(
(global_ for global_ in global_vars if global_.storage.global_address == self.address), None
) or next((local for local in static_locals if local.storage.global_address == self.address), None)
if not var:
print(f"cannot find {self.name} with size 0 in json")
else:
element_count, type_name = var.parsed_size
type_size = type_sizes.get(type_name)
assert type_size is not None, type_name
size = element_count * type_size
if not size:
print(f"size of {self.name} is also 0 using json")
size_str = f"size:0x{size:x}" if size else ""
return f"{name:{name_len_fmt}s} = 0x{self.address:08x}; // {size_str}"
def parse_symbols_safe(elf_path: Path, dest_path: Path, json_path: Path, ranges: list[Range]):
if not dest_path.is_dir():
raise RuntimeError(f"{dest_path} is not a directory")
symbol_addrs_path = dest_path / "symbols_addrs.txt"
undefined_syms_path = dest_path / "undefined_syms.txt"
if symbol_addrs_path.exists() or undefined_syms_path.exists():
raise RuntimeError("symbols_addrs.txt or undefined_syms.txt already exist in dest folder")
with open(elf_path, mode="rb") as elf_fh, open(json_path, mode="r") as json_fh:
elf = ELFFile(elf_fh)
json_data = json.load(json_fh)
ccc_model_v7 = ccc_json_v7.CCCJSONv7Model.model_validate(json_data)
type_sizes, global_vars, static_locals = parse_ccc_model_v7(ccc_model_v7, ranges)
with open(symbol_addrs_path, mode="w") as symbol_addrs, open(undefined_syms_path, mode="w") as undefined_syms:
parse_symbols(elf, symbol_addrs, undefined_syms, ranges, type_sizes, global_vars, static_locals)
def parse_symbols(
elf: ELFFile,
symbol_addrs: TextIO,
undefined_syms: TextIO,
ranges: list[Range],
type_sizes: dict[str, int],
global_vars: list[ccc_json_v7.Global],
static_locals: list[ccc_json_v7.Local],
):
# find symbol table
symtab = elf.get_section_by_name(".symtab")
assert isinstance(symtab, SymbolTableSection)
parsed_symbols: list[ParsedSymbol] = []
for symbol in symtab.iter_symbols():
try:
parsed_symbol = ParsedSymbol(symtab, symbol)
if parsed_symbol.name in skip_symbols:
continue
parsed_symbols.append(parsed_symbol)
except SymbolWithNoNameException:
pass
parsed_symbols.sort(key=lambda x: x.address)
for parsed_symbol in parsed_symbols:
if ranges:
in_range = any(parsed_symbol.in_range(ra) for ra in ranges)
if not in_range:
continue
parsed_symbol.address
symbol_addrs.write(parsed_symbol.to_symbol_addrs(type_sizes, global_vars, static_locals))
symbol_addrs.write("\n")
undefined_syms.write(parsed_symbol.to_undefined_syms())
undefined_syms.write("\n")
def main():
class ArgsProtocol(Protocol):
elf_path: Path
dest_path: Path
json_path: Path
ranges: list[Range]
def range_type(arg: str) -> Range:
if m := re.match(r"^([0-9a-f]+)-([0-9a-f]+)$", arg):
range_start, range_end = int(m.group(1), 16), int(m.group(2), 16)
if not (range_end > range_start):
raise argparse.ArgumentTypeError("non monotonic range")
return range_start, range_end
raise argparse.ArgumentTypeError("invalid range")
parser = argparse.ArgumentParser()
parser.add_argument("elf_path", metavar="ELF", type=Path, help="path to ELF file")
parser.add_argument("--json", dest="json_path", type=Path, help="path to CCC json (v7)")
parser.add_argument(
"--dest",
dest="dest_path",
type=Path,
required=True,
help="folder where symbol_addrs.txt and undefined_syms.txt will be created "
"(existing files will not be overwritten)",
)
parser.add_argument("--range", action="append", dest="ranges", nargs="+", type=range_type)
args = parser.parse_args()
args.ranges = list(itertools.chain(*args.ranges))
args = cast(ArgsProtocol, args)
parse_symbols_safe(args.elf_path, args.dest_path, args.json_path, args.ranges)
if __name__ == "__main__":
main()