2022-04-15 23:33:34 +02:00
from pathlib import Path
import logging
import re
2022-04-16 17:25:34 +02:00
__version__ = " 0.0.7 "
2022-04-16 00:24:42 +02:00
__author__ = " rigodron, algoflash, GGLinnk "
2022-04-15 23:33:34 +02:00
__license__ = " MIT "
__status__ = " developpement "
2022-04-16 17:25:34 +02:00
# raised when the action replay ini file contains a bad formated entry
2022-04-15 23:33:34 +02:00
class InvalidIniFileEntryError ( Exception ) : pass
2022-04-16 17:25:34 +02:00
# raised when trying to resolve an invalid dol file offset
2022-04-15 23:33:34 +02:00
class InvalidImgOffsetError ( Exception ) : pass
2022-04-16 17:25:34 +02:00
# raised when trying to resolve an out of section Virtual address
2022-04-15 23:33:34 +02:00
class InvalidVirtualAddressError ( Exception ) : pass
2022-04-16 17:25:34 +02:00
# raised when Virtual address + length Overflow out of sections
class SectionsOverflowError ( Exception ) : pass
2022-04-15 23:33:34 +02:00
# 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 ]
2022-04-16 00:23:01 +02:00
# 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
2022-04-16 14:08:51 +02:00
# * others lines have to be in format: "0AXXXXXX XXXXXXXX" with A in [2,3,4,5] and X in [0-9a-fA-F]
2022-04-16 00:23:01 +02:00
def parse_action_replay_ini ( path : Path ) :
action_replay_lines = path . read_text ( ) . splitlines ( )
2022-04-16 14:08:51 +02:00
2022-04-16 17:25:34 +02:00
# address = (first 4 bytes & 0x01FFFFFF) | 0x80000000
# opcode = first byte & 0xFE
2022-04-16 14:08:51 +02:00
pattern = re . compile ( " ^(0[2345][0-9a-zA-Z] {6} ) ([0-9a-zA-Z] {8} )$ " )
2022-04-16 00:23:01 +02:00
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 :
2022-04-16 14:08:51 +02:00
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 } \" . " )
2022-04-16 00:23:01 +02:00
2022-04-16 14:08:51 +02:00
virtual_address = ( int ( res [ 1 ] , base = 16 ) & 0x01FFFFFF ) | 0x80000000
opcode = int ( res [ 1 ] [ : 2 ] , base = 16 ) & 0xFE
2022-04-16 00:23:01 +02:00
bytes_value = None
2022-04-16 17:25:34 +02:00
2022-04-16 14:08:51 +02:00
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 " )
2022-04-16 00:23:01 +02:00
else :
2022-04-16 14:08:51 +02:00
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} \" . " )
2022-04-16 17:25:34 +02:00
2022-04-16 00:23:01 +02:00
result_list . append ( ( virtual_address , bytes_value ) )
return result_list
2022-04-15 23:33:34 +02:00
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 ) :
2022-04-16 17:25:34 +02:00
res = f " Entry point: { self . __entry_point : 08x } \n \n | "
res + = " - " * 50 + " | \n | Section | Offset | Address | Length | Used | \n | " + " - " * 9 + ( " | " + " - " * 10 ) * 3 + " | " + " - " * 7 + " | \n "
2022-04-15 23:33:34 +02:00
i = 0
for section in self . __sections_info :
2022-04-16 17:25:34 +02:00
res + = " | text " + str ( i ) if i < 7 else " | data " + str ( i )
2022-04-15 23:33:34 +02:00
if i < 10 : res + = " "
2022-04-16 17:25:34 +02:00
res + = f " | { section [ 0 ] : 08x } | { section [ 1 ] : 08x } | { section [ 2 ] : 08x } | { str ( section [ 3 ] ) . ljust ( 5 ) } | \n "
2022-04-15 23:33:34 +02:00
i + = 1
2022-04-16 17:25:34 +02:00
res + = " | " + " - " * 50 + f " | \n \n bss: address: { self . __bss_info [ 0 ] : 08x } length: { self . __bss_info [ 1 ] : 08x } "
2022-04-15 23:33:34 +02:00
return res
"""
# search_raw: bytecode
2022-04-16 17:25:34 +02:00
# we could also identify text sections to improve search
2022-04-15 23:33:34 +02:00
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
"""
2022-04-16 17:25:34 +02:00
# 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 } " )
2022-04-15 23:33:34 +02:00
# 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 :
2022-04-16 00:52:52 +02:00
if not section_info [ 3 ] : continue
2022-04-15 23:33:34 +02:00
if offset > = section_info [ 0 ] and offset < section_info [ 0 ] + section_info [ 2 ] :
return section_info [ 1 ] + offset - section_info [ 0 ]
2022-04-16 17:25:34 +02:00
raise InvalidImgOffsetError ( f " Error - Invalid dol image offset: { offset : 08x } " )
2022-04-15 23:33:34 +02:00
# Resolve a virtual memory address to a dol absolute offset
def resolve_virtual2img ( self , address : int ) :
for section_info in self . __sections_info :
2022-04-16 00:52:52 +02:00
if not section_info [ 3 ] : continue
2022-04-15 23:33:34 +02:00
if address > = section_info [ 1 ] and address < section_info [ 1 ] + section_info [ 2 ] :
return section_info [ 0 ] + address - section_info [ 1 ]
2022-04-16 17:25:34 +02:00
raise InvalidVirtualAddressError ( f " Error - Not found in dol initial sections: { address : 08x } " )
2022-04-15 23:33:34 +02:00
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 ] )
2022-04-16 17:25:34 +02:00
str_buffer = " \n | " + " - " * 46 + " | \n | Section | beg_addr | end_addr | length | \n | " + " - " * 13 + ( " | " + " - " * 10 ) * 3 + " | \n "
2022-04-15 23:33:34 +02:00
for interval in result_intervals :
2022-04-16 17:25:34 +02:00
str_buffer + = f " | { interval [ 0 ] . ljust ( 11 ) } | { interval [ 1 ] : 08x } | { interval [ 2 ] : 08x } | { interval [ 3 ] : 08x } | \n "
2022-04-15 23:33:34 +02:00
2022-04-16 17:25:34 +02:00
print ( str_buffer + " | " + " - " * 46 + " | " )
2022-04-15 23:33:34 +02:00
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 ] )
2022-04-16 17:25:34 +02:00
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 ]
2022-04-15 23:33:34 +02:00
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 = ' ' )
2022-04-16 00:23:01 +02:00
parser . add_argument ( ' arg2 ' , metavar = ' arg2 ' , help = ' ' , nargs = ' ? ' , default = None )
2022-04-15 23:33:34 +02:00
group = parser . add_mutually_exclusive_group ( required = True )
2022-04-16 00:23:01 +02:00
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. " )
2022-04-15 23:33:34 +02:00
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 )
2022-04-16 00:23:01 +02:00
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 :
2022-04-15 23:33:34 +02:00
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 ) )