Skip to content

Commit

Permalink
Improve create_config.py for PSX (#316)
Browse files Browse the repository at this point in the history
* Improve create_config.py for PSX

* appease black

* Update create_config.py

Co-authored-by: Anghelo Carvajal <[email protected]>

---------

Co-authored-by: Anghelo Carvajal <[email protected]>
  • Loading branch information
mkst and AngheloAlf authored Dec 16, 2023
1 parent f5bc226 commit 46729c5
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 56 deletions.
13 changes: 4 additions & 9 deletions create_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ def create_psx_config(exe_path: Path, exe_bytes: bytes):
type: code
start: 0x800
vram: 0x{exe.destination_vram:X}
bss_size: 0x{exe.bss_size:X}
# bss_size: Please fill out this value when you figure out the bss size
subsegments:
"""
text_offset = exe.text_offset
Expand All @@ -290,18 +290,13 @@ def create_psx_config(exe_path: Path, exe_bytes: bytes):
- [0x800, rodata, 800]
"""
segments += f"""\
- [0x{text_offset:X}, asm, {text_offset:X}]
- [0x{text_offset:X}, asm, {text_offset:X}] # estimated
"""

if exe.data_vram != 0 and exe.data_size != 0:
if exe.data_offset != 0:
data_offset = exe.data_offset
segments += f"""\
- [0x{data_offset:X}, data, {data_offset:X}]
"""

if exe.bss_size != 0:
segments += f"""\
- {{ start: 0x{exe.size:X}, type: bss, name: {exe.bss_vram:X}, vram: 0x{exe.bss_vram:X} }}
- [0x{data_offset:X}, data, {data_offset:X}] # estimated
"""

segments += f"""\
Expand Down
216 changes: 169 additions & 47 deletions util/psx/psxexeinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,76 +11,209 @@

from pathlib import Path

import rabbitizer
import spimdisasm

# PSX EXE has the following layout
# header ; 0x80 bytes
# padding ; 0x780 bytes
# .rodata ; variable length
# .text ; variable length
# .data ; variable length
# .sdata ; variable length
# .bss ; variable length, all zeroes
# .sbss ; variable length, all zeroes

PAYLOAD_OFFSET = 0x800 # 0x80 byte header followed by 0x780 bytes of zeroes
WORD_SIZE_BYTES = 4

UNSUPPORTED_OPS = {
# MIPS II
"beql",
"bgtzl",
"blezl",
"bnel",
"ldc1",
"ldc2",
"ll",
"sc",
"sdc1",
"sdc2",
"sync",
"teq",
"tge",
"tgei",
"tgeiu",
"tgeu",
"tlt",
"tltu",
"tne",
"tnei",
# MIPS III
"dadd",
"daddi",
"daddiu",
"daddu",
"dsub",
"dsubu",
"ld",
"ldl",
"ldr",
"lld",
"lwu",
"scd",
"sd",
"sdl",
"sdr",
# MIPS IV
"movn",
"movz",
"pref",
"prefx",
}


def is_valid(insn) -> bool:
if not insn.isValid():
if insn.instrIdType.name in ("CPU_SPECIAL", "CPU_COP2"):
return True
else:
return False

opcode = insn.getOpcodeName()
if opcode in UNSUPPORTED_OPS:
return False

return True


def try_find_text(
rom_bytes, start_offset=PAYLOAD_OFFSET, valid_threshold=32
) -> tuple[int, int]:
start = end = 0
good_count = valid_count = 0

in_text = False
last_opcode = None

words = struct.iter_unpack("<I", rom_bytes[start_offset:])
for i, (word,) in enumerate(words):
insn = rabbitizer.Instruction(word)

if in_text:
if not is_valid(insn):
end = start_offset + i * WORD_SIZE_BYTES
break
else:
if is_valid(insn):
valid_count += 1

opcode = insn.getOpcodeName()
if last_opcode != opcode and opcode != "nop":
good_count += 1
else:
# reset
good_count = valid_count = 0

if good_count > valid_threshold:
in_text = True
start = start_offset + ((i + 1 - valid_count) * WORD_SIZE_BYTES)

last_opcode = insn.getOpcodeName()

return (start, end)


def try_get_gp(rom_bytes, start_offset, max_instructions=50) -> int:
# $gp is set like this:
# /* A7738 800B7138 0E801C3C */ lui $gp, (0x800E0000 >> 16)
# /* A773C 800B713C 90409C27 */ addiu $gp, $gp, 0x4090
gp = 0
words = struct.iter_unpack("<I", rom_bytes[start_offset:])
for i, (word,) in enumerate(words):
if i > max_instructions:
# give up
break
insn = rabbitizer.Instruction(word)
if insn.getOpcodeName() == "lui" and insn.rt.name == "gp":
gp = insn.getImmediate() << 16
elif insn.getOpcodeName() == "addiu" and insn.rt.name == "gp":
gp += insn.getImmediate()
break
return gp


def read_word(exe_bytes, offset) -> int:
return struct.unpack("<I", exe_bytes[offset : offset + 4])[0]


@dataclasses.dataclass
class PsxExe:
# Based on https://psx-spx.consoledev.net/cdromdrive/#filenameexe-general-purpose-executable
initial_pc: int # offset: 0x10
entrypoint: int # offset: 0x10
initial_gp: int # offset: 0x14
destination_vram: int # offset: 0x18
file_size: int # offset: 0x1C
data_vram: int # offset: 0x20
data_size: int # offset: 0x24
bss_vram: int # offset: 0x28
bss_size: int # offset: 0x2C
initial_sp_base: int # offset: 0x30
initial_sp_offset: int # offset: 0x34
payload_size: int # offset: 0x1C
# data_vram: int # offset: 0x20
# data_size: int # offset: 0x24
# bss_vram: int # offset: 0x28
# bss_size: int # offset: 0x2C
# initial_sp_base: int # offset: 0x30
# initial_sp_offset: int # offset: 0x34

text_start: int
data_start: int

size: int
sha1: str

@property
def text_offset(self) -> int:
return self.initial_pc - self.destination_vram + 0x800
return self.text_start

@property
def data_offset(self) -> int:
if self.data_vram == 0 or self.data_size == 0:
return 0
return self.data_vram - self.destination_vram + 0x800
return self.data_start

@staticmethod
def get_info(exe_path: Path, exe_bytes: bytes) -> PsxExe:
initial_pc = struct.unpack("<I", exe_bytes[0x10 : 0x10 + 4])[0]
initial_gp = struct.unpack("<I", exe_bytes[0x14 : 0x14 + 4])[0]
destination_vram = struct.unpack("<I", exe_bytes[0x18 : 0x18 + 4])[0]
file_size = struct.unpack("<I", exe_bytes[0x1C : 0x1C + 4])[0]
data_vram = struct.unpack("<I", exe_bytes[0x20 : 0x20 + 4])[0]
data_size = struct.unpack("<I", exe_bytes[0x24 : 0x24 + 4])[0]
bss_vram = struct.unpack("<I", exe_bytes[0x28 : 0x28 + 4])[0]
bss_size = struct.unpack("<I", exe_bytes[0x2C : 0x2C + 4])[0]
initial_sp_base = struct.unpack("<I", exe_bytes[0x30 : 0x30 + 4])[0]
initial_sp_offset = struct.unpack("<I", exe_bytes[0x34 : 0x34 + 4])[0]
entrypoint = read_word(exe_bytes, 0x10)
destination_vram = read_word(exe_bytes, 0x18)
payload_size = read_word(exe_bytes, 0x1C)

text_start, data_start = try_find_text(exe_bytes)

if text_start:
entrypoint_rom = entrypoint + PAYLOAD_OFFSET - destination_vram
initial_gp = try_get_gp(exe_bytes, entrypoint_rom)
else:
initial_gp = 0

sha1 = hashlib.sha1(exe_bytes).hexdigest()

return PsxExe(
initial_pc,
entrypoint,
initial_gp,
destination_vram,
file_size,
data_vram,
data_size,
bss_vram,
bss_size,
initial_sp_base,
initial_sp_offset,
payload_size,
text_start,
data_start,
len(exe_bytes),
sha1,
)


def main():
parser = argparse.ArgumentParser(description="Gives information on PSX EXEs")
parser.add_argument("exe", help="path to an PSX EXE")
parser.add_argument("exe", help="Path to an PSX EXE")

args = parser.parse_args()

exe_path = Path(args.exe)
exe_bytes = exe_path.read_bytes()
exe = PsxExe.get_info(exe_path, exe_bytes)

print(f"Initial PC: 0x{exe.initial_pc:08X}")
print(f"Entrypoint: 0x{exe.entrypoint:08X}")

print(f"Initial GP: ", end="")
if exe.initial_gp != 0:
Expand All @@ -90,23 +223,12 @@ def main():

print()
print(f"Destination VRAM: 0x{exe.destination_vram:08X}")
print(f"File size (without header): 0x{exe.file_size:X}")
print(f"Text binary offset: 0x{exe.text_offset:08X}")

if exe.data_vram != 0 and exe.data_size != 0:
print()
print(f"Data VRAM: 0x{exe.data_vram:08X}")
print(f"Data size: 0x{exe.data_size:08X}")
print(f"Data binary offset: 0x{exe.data_offset:08X}")

if exe.bss_vram != 0 and exe.bss_size != 0:
print()
print(f"bss VRAM: 0x{exe.bss_vram:08X}")
print(f"bss size: 0x{exe.bss_size:08X}")
print(f"Payload size (without header): 0x{exe.payload_size:X}")

print()
print(f"Initial SP base: 0x{exe.initial_sp_base:08X}")
print(f"Initial SP offset: 0x{exe.initial_sp_offset:08X}")
print(f"Text binary offset (estimate): 0x{exe.text_offset:X}")
if exe.data_offset != 0:
print(f"Data binary offset (estimate): 0x{exe.data_offset:X}")

print()
print(f"File size: 0x{exe.size:X}")
Expand Down

0 comments on commit 46729c5

Please sign in to comment.