mirror of
https://github.com/Virtual-World-RE/NeoGF.git
synced 2024-12-27 07:31:34 +01:00
b8f217d7b6
Added section split when overflowing, better formating for stats, new controls on errors that could occur.
307 lines
15 KiB
Python
307 lines
15 KiB
Python
from pathlib import Path
|
|
import logging
|
|
import re
|
|
|
|
|
|
__version__ = "0.0.7"
|
|
__author__ = "rigodron, algoflash, GGLinnk"
|
|
__license__ = "MIT"
|
|
__status__ = "developpement"
|
|
|
|
|
|
# raised when the action replay ini file contains a bad formated entry
|
|
class InvalidIniFileEntryError(Exception): pass
|
|
# raised when trying to resolve an invalid dol file offset
|
|
class InvalidImgOffsetError(Exception): pass
|
|
# raised when trying to resolve an out of section Virtual address
|
|
class InvalidVirtualAddressError(Exception): pass
|
|
# raised when Virtual address + length Overflow out of sections
|
|
class SectionsOverflowError(Exception): pass
|
|
|
|
|
|
# Get non-overlapping intervals from interval by removing intervals_to_remove
|
|
# intervals_to_remove has to be sorted by left val
|
|
# return [[a,b], ...] or None
|
|
def remove_intervals_from_interval(interval:list, intervals_to_remove:list):
|
|
interval = interval[:]
|
|
result_intervals = []
|
|
for interval_to_remove in intervals_to_remove:
|
|
if interval_to_remove[2] < interval[1]: continue # end before
|
|
if interval_to_remove[1] > interval[2]: break # begin after
|
|
|
|
if interval_to_remove[1] <= interval[1]: # begin before
|
|
if interval_to_remove[2] >= interval[2]: # total overlap
|
|
return None
|
|
interval[1] = interval_to_remove[2] # begin truncate
|
|
elif interval_to_remove[2] >= interval[2]: # end truncate
|
|
interval[2] = interval_to_remove[1]
|
|
break
|
|
else: # middle truncate
|
|
result_intervals.append( ["empty", interval[1], interval_to_remove[1]] )
|
|
interval[1] = interval_to_remove[2]
|
|
|
|
return result_intervals + [interval]
|
|
|
|
|
|
# Parse an ini file and return a list of [ [virtual_address:int, value:bytes], ... ]
|
|
# All ARCodes present in the ini will be enabled without taking care of [ActionReplay_Enabled] section
|
|
# raise an Exception if lines are in invalid format:
|
|
# * empty lines are removed
|
|
# * lines beginning with $ are concidered as comments and are removed
|
|
# * lines beginning with [ are concidered as comments and are removed
|
|
# * others lines have to be in format: "0AXXXXXX XXXXXXXX" with A in [2,3,4,5] and X in [0-9a-fA-F]
|
|
def parse_action_replay_ini(path:Path):
|
|
action_replay_lines = path.read_text().splitlines()
|
|
|
|
# address = (first 4 bytes & 0x01FFFFFF) | 0x80000000
|
|
# opcode = first byte & 0xFE
|
|
pattern = re.compile("^(0[2345][0-9a-zA-Z]{6}) ([0-9a-zA-Z]{8})$")
|
|
result_list = []
|
|
|
|
for action_replay_line in action_replay_lines:
|
|
if len(action_replay_line) == 0:
|
|
continue
|
|
if action_replay_line[0] in ["$", "["]:
|
|
continue
|
|
res = pattern.fullmatch(action_replay_line)
|
|
|
|
if res is None:
|
|
raise InvalidIniFileEntryError(f"Error - Arcode has to be in format: '0AXXXXXX XXXXXXXX' with A in [2,3,4,5] and X in [0-9a-fA-F] line \"{action_replay_line}\".")
|
|
|
|
virtual_address = (int(res[1], base=16) & 0x01FFFFFF) | 0x80000000
|
|
opcode = int(res[1][:2], base=16) & 0xFE
|
|
bytes_value = None
|
|
|
|
if opcode == 0x04:
|
|
bytes_value = int(res[2], 16).to_bytes(4, "big")
|
|
elif opcode == 0x02:
|
|
bytes_value = (int(res[2][:4], 16) + 1) * int(res[2][4:], 16).to_bytes(2, "big")
|
|
else:
|
|
raise InvalidIniFileEntryError("Error - Arcode has to be in format: '0AXXXXXX XXXXXXXX' with A in [2,3,4,5] and X in [0-9a-fA-F] line \"{action_replay_line}\".")
|
|
|
|
result_list.append( (virtual_address, bytes_value) )
|
|
return result_list
|
|
|
|
|
|
class Dol:
|
|
HEADER_LEN = 0x100
|
|
__path = None
|
|
__header = None
|
|
__data = None
|
|
# List of 18 tuples [(offset, address, length, is_used), ] that describe all sections of the dol
|
|
__sections_info = None
|
|
# (address, length)
|
|
__bss_info = None
|
|
__entry_point = None
|
|
def __init__(self, path:Path):
|
|
self.__path = path
|
|
data = path.read_bytes()
|
|
self.__header = data[:Dol.HEADER_LEN]
|
|
self.__data = data[Dol.HEADER_LEN:]
|
|
|
|
self.__bss_info = ( int.from_bytes(data[0xd8:0xdc], "big"), int.from_bytes(data[0xdc:0xe0], "big") )
|
|
self.__entry_point = int.from_bytes(data[0xe0:0xe4], "big")
|
|
|
|
self.__sections_info = []
|
|
for i in range(18):
|
|
offset = int.from_bytes(data[i*4:i*4+4], "big")
|
|
address = int.from_bytes(data[0x48+i*4:0x48+i*4+4], "big")
|
|
length = int.from_bytes(data[0x90+i*4:0x90+i*4+4], "big")
|
|
is_used = (offset != 0) and (address != 0) and (length != 0)
|
|
self.__sections_info.append( (offset, address, length, is_used) )
|
|
# print a table with each sections
|
|
def __str__(self):
|
|
res = f"Entry point: {self.__entry_point:08x}\n\n|"
|
|
res += "-"*50 + "|\n| Section | Offset | Address | Length | Used |\n|" + "-"*9 + ("|"+"-"*10)*3 + "|" + "-"*7 + "|\n"
|
|
i = 0
|
|
for section in self.__sections_info:
|
|
res+= "| text"+str(i) if i < 7 else "| data"+str(i)
|
|
if i < 10: res += " "
|
|
res += f" | {section[0]:08x} | {section[1]:08x} | {section[2]:08x} | {str(section[3]).ljust(5)} |\n"
|
|
i += 1
|
|
res += "|"+"-"*50+f"|\n\nbss: address:{self.__bss_info[0]:08x} length:{self.__bss_info[1]:08x}"
|
|
return res
|
|
"""
|
|
# search_raw: bytecode
|
|
# we could also identify text sections to improve search
|
|
def search_raw(self, bytecode:bytes):
|
|
if len(bytecode) == 0:
|
|
raise Exception("Error - No bytecode.")
|
|
offsets = []
|
|
for i in range(len(self.__data) - len(bytecode) + 1):
|
|
if self.__data[i:i+len(bytecode)] == bytecode:
|
|
offsets.append(self.resolve_img2virtual(i + Dol.HEADER_LEN))
|
|
return offsets if len(offsets) > 0 else None
|
|
"""
|
|
# When patching a section we could overflow on the next section
|
|
# Return list of [ [virtual_address:int, value:bytes], ... ]
|
|
# raise SectionsOverflowError if part of the bytecode is out of the existing sections
|
|
# raise InvalidVirtualAddressError if the base virtual address is out of the existing sections
|
|
def __get_section_mapped_values(self, virtual_address:int, bytes_value:bytes):
|
|
for section_info in self.__sections_info:
|
|
if not section_info[3]: continue
|
|
# first byte out of section:
|
|
section_end_address = section_info[1] + section_info[2]
|
|
if virtual_address >= section_info[1] and virtual_address < section_end_address:
|
|
if virtual_address + len(bytes_value) <= section_end_address:
|
|
return [ [section_info[0] + virtual_address - section_info[1], bytes_value] ] # dol offset and value
|
|
# Here we have to split the value to find where in the dol is the second part
|
|
splited_len = section_end_address - virtual_address
|
|
splited_result = [[section_info[0] + virtual_address - section_info[1], bytes_value[:splited_len]]]
|
|
try:
|
|
splited_result += self.__get_section_mapped_values(virtual_address + splited_len, bytes_value[splited_len:])
|
|
return splited_result
|
|
except InvalidVirtualAddressError:
|
|
raise SectionsOverflowError(f"Error - Value Overflow in an inexistant dol initial section: {virtual_address:08x}:{bytes_value.hex()}")
|
|
raise InvalidVirtualAddressError(f"Error - Not found in dol initial sections: {virtual_address:08x}")
|
|
# Resolve a dol absolute offset to a virtual memory address
|
|
def resolve_img2virtual(self, offset:int):
|
|
memory_address = None
|
|
for section_info in self.__sections_info:
|
|
if not section_info[3]: continue
|
|
if offset >= section_info[0] and offset < section_info[0] + section_info[2]:
|
|
return section_info[1] + offset - section_info[0]
|
|
raise InvalidImgOffsetError(f"Error - Invalid dol image offset: {offset:08x}")
|
|
# Resolve a virtual memory address to a dol absolute offset
|
|
def resolve_virtual2img(self, address:int):
|
|
for section_info in self.__sections_info:
|
|
if not section_info[3]: continue
|
|
if address >= section_info[1] and address < section_info[1] + section_info[2]:
|
|
return section_info[0] + address - section_info[1]
|
|
raise InvalidVirtualAddressError(f"Error - Not found in dol initial sections: {address:08x}")
|
|
def stats(self):
|
|
print(self)
|
|
|
|
# https://www.gc-forever.com/yagcd/chap4.html#sec4
|
|
# system: 0x80000000 -> 0x80003100
|
|
# available: 0x80003100 -> 0x81200000
|
|
# apploader: 0x81200000 -> 0x81300000
|
|
# Bootrom/IPL: 0x81300000 -> 0x81800000
|
|
|
|
# Now we have to generate a memory map with splited bss and empty spaces
|
|
# [ [section_name, beg_addr, end_addr, length], ... ]
|
|
result_intervals = [
|
|
["system", 0x80000000, 0x80003100, 0x3100],
|
|
["apploader", 0x81200000, 0x81300000, 0x100000],
|
|
["Bootrom/IPL", 0x81300000, 0x81800000, 0x500000]]
|
|
|
|
i = 0
|
|
for section in self.__sections_info[:7]:
|
|
if section[3]:
|
|
result_intervals.append( [f".text{i}", section[1], section[1] + section[2], section[2]] )
|
|
i += 1
|
|
|
|
i = 0
|
|
for section in self.__sections_info[7:18]:
|
|
if section[3]:
|
|
result_intervals.append( [f".data{i}", section[1], section[1] + section[2], section[2]] )
|
|
i += 1
|
|
|
|
result_intervals.sort(key=lambda x: x[1])
|
|
i = 0
|
|
for bss_interval in remove_intervals_from_interval([None, self.__bss_info[0], self.__bss_info[0] + self.__bss_info[1]], result_intervals):
|
|
result_intervals.append( [f".bss{i}", bss_interval[1], bss_interval[2], bss_interval[2] - bss_interval[1]] )
|
|
i += 1
|
|
|
|
# We search now available program space
|
|
result_intervals.sort(key=lambda x: x[1])
|
|
empty_intervals = remove_intervals_from_interval(["empty", 0x80003100, 0x81200000], result_intervals)
|
|
for empty_interval in empty_intervals:
|
|
result_intervals += [[empty_interval[0], empty_interval[1], empty_interval[2], empty_interval[2] - empty_interval[1]]]
|
|
|
|
result_intervals.sort(key=lambda x: x[1])
|
|
str_buffer = "\n|"+"-"*46+"|\n| Section | beg_addr | end_addr | length |\n|" + "-"*13 + ("|"+"-"*10)*3 + "|\n"
|
|
for interval in result_intervals:
|
|
str_buffer += f"| {interval[0].ljust(11)} | {interval[1]:08x} | {interval[2]:08x} | {interval[3]:08x} |\n"
|
|
|
|
print(str_buffer + "|"+"-"*46+"|")
|
|
def extract(self, filename:str, section_index:int):
|
|
if section_index > 17:
|
|
raise Exception("Error - Section index has to be in 0 - 17")
|
|
|
|
begin_offset = self.__sections_info[section_index][0] - self.HEADER_LEN
|
|
end_offset = begin_offset + self.__sections_info[section_index][2]
|
|
|
|
section_type = "text" if section_index < 7 else "data"
|
|
Path(f"{filename}_{section_type}{section_index}").write_bytes(self.__data[begin_offset:end_offset])
|
|
# [ [virtual_address:int, value:bytes], ... ]
|
|
def patch_action_replay(self, virtualaddress_bytes_list:list):
|
|
self.__data = bytearray(self.__data)
|
|
for virtualaddress_bytes in virtualaddress_bytes_list:
|
|
offset = self.resolve_virtual2img(virtualaddress_bytes[0])
|
|
for mapped_list in self.__get_section_mapped_values(virtualaddress_bytes[0], virtualaddress_bytes[1]):
|
|
print(f"Patching {virtualaddress_bytes[0]:08x} at dol offset {offset:08x} with value {virtualaddress_bytes[1].hex()}")
|
|
self.__data[offset: offset + len(virtualaddress_bytes[1])] = virtualaddress_bytes[1]
|
|
|
|
|
|
def get_argparser():
|
|
import argparse
|
|
parser = argparse.ArgumentParser(description='dol file format utilities - [GameCube] v' + __version__)
|
|
parser.add_argument('--version', action='version', version='%(prog)s ' + __version__)
|
|
parser.add_argument('-v', '--verbose', action='store_true', help='verbose mode')
|
|
parser.add_argument('input_path', metavar='INPUT', help='')
|
|
parser.add_argument('arg2', metavar='arg2', help='', nargs='?', default=None)
|
|
|
|
group = parser.add_mutually_exclusive_group(required=True)
|
|
group.add_argument('-v2i', '--virtual2image', action='store_true', help="-v2i source.dol virtual_address: Translate a virtual address into a dol offset if this was originaly mapped from data or text. virtual_address has to be in hexadecimal: 80003100.")
|
|
group.add_argument('-i2v', '--image2virtual', action='store_true', help="-i2b source.dol dol_offset: Translate a dol offset to a virtual address mapped from data or text. dol_offset has to be in hexadecimal: 2000.")
|
|
group.add_argument('-s', '--stats', action='store_true', help="-s source.dol: Get stats about entry point, sections, bss and unused virtual address space.")
|
|
group.add_argument('-e', '--extract', action='store_true', help="-e source.dol section_index: Extract a section. index must be between 0 and 17")
|
|
group.add_argument('-par', '--patch-action-replay', action='store_true', help="-p source.dol action_replay.ini: Patch initialised data inside the dol with an ini file containing a list of [write] directives. Handle only ARCodes beginning with 04.")
|
|
return parser
|
|
|
|
|
|
if __name__ == '__main__':
|
|
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO)
|
|
args = get_argparser().parse_args()
|
|
|
|
p_input = Path(args.input_path)
|
|
|
|
if args.verbose:
|
|
logging.getLogger().setLevel(logging.DEBUG)
|
|
|
|
if not p_input.is_file():
|
|
raise Exception("Error - Invalid dol file path.")
|
|
|
|
dol = Dol(p_input)
|
|
|
|
if args.virtual2image:
|
|
if args.arg2 is None:
|
|
raise Exception("Error - Virtual address has to be specified in hexadecimal: 80003000.")
|
|
virtual_address = int(args.arg2, 16)
|
|
try:
|
|
offset = dol.resolve_virtual2img(virtual_address)
|
|
print(f"Virtual address {virtual_address:08x} is at dol offset {offset:08x}")
|
|
except InvalidVirtualAddressError:
|
|
print("This virtual address is not in the dol.")
|
|
elif args.image2virtual:
|
|
if args.arg2 is None:
|
|
raise Exception("Error - dol offset has to be specified in hexadecimal: 1234.")
|
|
offset = int(args.arg2, 16)
|
|
try:
|
|
virtual_address = dol.resolve_img2virtual(offset)
|
|
print(f"Dol offset {offset:08x} is at virtual address {virtual_address:08x}")
|
|
except InvalidImgOffsetError:
|
|
print("This dol offset is invalid.")
|
|
elif args.stats:
|
|
dol.stats()
|
|
elif args.extract:
|
|
logging.info("### Extract section")
|
|
if args.arg2 is None:
|
|
raise Exception("Error - Section index has to be specified.")
|
|
index = int(args.arg2)
|
|
|
|
section_type = "text" if index < 7 else "data"
|
|
logging.info(f"Extracting section {index} in file {p_input.name}_{section_type}{index}...")
|
|
dol.extract(p_input.name, index)
|
|
elif args.patch_action_replay:
|
|
logging.info("### Patch dol using Action Replay ini file")
|
|
if args.arg2 is None:
|
|
raise Exception("Error - Action Replay ini file has to be specified.")
|
|
action_replay_ini_path = Path(args.arg2)
|
|
if not action_replay_ini_path.is_file():
|
|
raise Exception("Error - Invalid action replay ini file path.")
|
|
|
|
logging.info(f"Patching dol {p_input} using .ini file {action_replay_ini_path}...")
|
|
dol.patch_action_replay(parse_action_replay_ini(action_replay_ini_path))
|