# Copyright 2019 RoadrunnerWMC
#
# This file is part of ndspy.
#
# ndspy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ndspy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ndspy. If not, see <https://www.gnu.org/licenses/>.
"""
Support for executable code files.
"""
import struct
from . import _common
from . import codeCompression
# Code settings full struct (from nds-bootstrap):
# typedef struct {
# u32 auto_load_list_offset;
# u32 auto_load_list_end;
# u32 auto_load_start;
# u32 static_bss_start;
# u32 static_bss_end;
# u32 compressed_static_end;
# u32 sdk_version;
# u32 nitro_code_be;
# u32 nitro_code_le;
# } module_params_t;
[docs]class MainCodeFile:
"""
Either the main ARM7 code file or the main ARM9 code file.
"""
[docs] class Section:
"""
A single section within an ARM7 or ARM9 code file. Code not
technically contained within a section defined in the sections
table in the code settings block is represented as an "implicit"
section.
"""
# There's an implicit first section in the file, which is not
# defined in the sections table with the others. This attribute
# will be True if this is that section.
implicit = False
def __init__(self, data, ramAddress, bssSize, *, implicit=False):
self.data = bytearray(data)
self.ramAddress = ramAddress
self.bssSize = bssSize
self.implicit = implicit
def __str__(self):
data = _common.shortBytesRepr(self.data)
imp = ' implicit' if self.implicit else ''
return f'<code-section at 0x{self.ramAddress:08X}: {data}{imp}>'
def __repr__(self):
data = _common.shortBytesRepr(self.data)
return (f'{type(self).__name__}({data}'
f', 0x{self.ramAddress:08X}'
f', 0x{self.bssSize:X}'
f'{", implicit=True" if self.implicit else ""})')
sections = None
ramAddress = 0x00000000
codeSettingsOffs = 0x00000000
def __init__(self, data, ramAddress, codeSettingsPointerAddress=None):
self.sections = []
self.ramAddress = ramAddress
data = codeCompression.decompress(data)
self.codeSettingsOffs = None
if codeSettingsPointerAddress:
# (codeSettingsPointerAddress might be None if it's not
# available, or 0 if the ROM has it set to 0)
try:
codeSettingsAddr, = struct.unpack_from(
'<I', data, codeSettingsPointerAddress - ramAddress - 4)
self.codeSettingsOffs = codeSettingsAddr - ramAddress
assert 0 <= self.codeSettingsOffs < len(data) - 4
except Exception:
# Something was probably out of range. Fall back to the
# manual search
self.codeSettingsOffs = None
if self.codeSettingsOffs is None:
# Manual search algorithm used as a fallback
self.codeSettingsOffs = self._searchForCodeSettingsOffs(data)
if self.codeSettingsOffs is not None:
copyTableBegin, copyTableEnd, dataBegin = \
struct.unpack_from('<3I', data, self.codeSettingsOffs)
copyTableBegin -= ramAddress
copyTableEnd -= ramAddress
dataBegin -= ramAddress
else:
# No code settings, so the entire file is one implied section
copyTableBegin = copyTableEnd = 0
dataBegin = len(data)
def makeSection(ramAddr, ramLen, fileOffs, bssSize, implicit=False):
sdata = data[fileOffs : fileOffs + ramLen]
self.sections.append(self.Section(sdata,
ramAddr,
bssSize,
implicit=implicit))
# Implicit first section
makeSection(ramAddress, dataBegin, 0, 0, implicit=True)
copyTablePos = copyTableBegin
while copyTablePos < copyTableEnd:
secRamAddr, secSize, bssSize = \
struct.unpack_from('<3I', data, copyTablePos)
copyTablePos += 12
makeSection(secRamAddr, secSize, dataBegin, bssSize)
dataBegin += secSize
[docs] @classmethod
def fromCompressed(cls, data, *args):
"""
Create a main code file from compressed code data.
This function is a bit outdated, since the default constructor
can now detect code compression. There is no real reason to use
this function anymore, and it may be removed at some point.
Parameters are the same as those of the default constructor.
"""
data = codeCompression.decompress(data)
return cls(data, *args)
[docs] @classmethod
def fromSections(cls, sections, ramAddress):
"""
Create a main code file from a list of sections.
"""
self = cls(b'', ramAddress)
self.sections = sections
return self
[docs] @classmethod
def fromFile(cls, filePath, ramAddress):
"""
Load a main code file from a filesystem file.
"""
with open(filePath, 'rb') as f:
return cls(f.read(), ramAddress)
[docs] def save(self, *, compress=False):
"""
Generate a bytes object representing this code file.
"""
data = bytearray()
for s in self.sections:
data.extend(s.data)
# Align to 0x04
while len(data) % 4:
data.append(0)
# These loops are NOT identical!
# The first one only operates on sections with length != 0,
# and the second operates on sections with length == 0!
sectionTable = bytearray()
for s in self.sections:
if s.implicit: continue
if len(s.data) == 0: continue
sectionTable.extend(
struct.pack('<3I', s.ramAddress, len(s.data), s.bssSize))
for s in self.sections:
if s.implicit: continue
if len(s.data) != 0: continue
sectionTable.extend(
struct.pack('<3I', s.ramAddress, len(s.data), s.bssSize))
sectionTableOffset = len(data)
data.extend(sectionTable)
def setInt(addr, val):
struct.pack_into('<I', data, addr, val)
sectionTableAddr = self.ramAddress + sectionTableOffset
sectionTableEnd = sectionTableAddr + len(sectionTable)
cso = self.codeSettingsOffs
if cso is not None:
setInt(cso + 0x00, sectionTableAddr)
setInt(cso + 0x04, sectionTableEnd)
setInt(cso + 0x08, self.ramAddress + len(self.sections[0].data))
else:
# Welp, hopefully we only have one section :P
pass
if compress:
data = bytearray(codeCompression.compress(data, True))
setInt(cso + 0x14, self.ramAddress + len(data))
else:
setInt(cso + 0x14, 0)
return data
[docs] def saveToFile(self, filePath, *, compress=False):
"""
Generate file data representing this main code file, and save it
to a filesystem file.
"""
d = self.save(compress=compress)
with open(filePath, 'wb') as f:
f.write(d)
def _searchForCodeSettingsOffs(self, data):
"""
Find the offset of the code settings area in the data given.
Return None if it can't be found.
"""
# Simple heuristic that works in most arm9.bin's:
for i in range(0, 0x8000, 4):
if data[i:i+8] == b'\x21\x06\xC0\xDE\xDE\xC0\x06\x21':
return i - 0x1C
# But to support arm7, which lacks that magic, we can fall back
# to a different heuristic based on the assumption that the code
# section table will be the very last thing in the code file.
# Which... isn't a great assumption, but it's the best I've got
# right now.
expectedTableEnd = self.ramAddress + len(data)
expectedTableEndBytes = struct.pack('<I', expectedTableEnd)
if expectedTableEndBytes in data:
try:
match = data.index(expectedTableEndBytes, 0)
except ValueError:
match = None
while match is not None:
possibleTableStart, = struct.unpack_from('<I', data, match - 4)
if (possibleTableStart % 4 == 0
and (expectedTableEnd - possibleTableStart) % 12 == 0
and expectedTableEnd - possibleTableStart < 0x100):
# Probably a match
return match - 4
try:
match = data.index(expectedTableEndBytes, match + 1)
except ValueError:
match = None
def __str__(self):
linesList = [f'<main-code at 0x{self.ramAddress:08X}']
for s in self.sections:
linesList.append(f' {s}')
linesList.append('>')
return '\n'.join(linesList)
def __repr__(self):
return (f'{type(self).__name__}.fromSections({self.sections!r}'
f', 0x{self.ramAddress:08X})')
[docs]class Overlay:
"""
An ARM7 or ARM9 code overlay.
"""
data = None
ramAddress = 0x00000000
ramSize = 0
bssSize = 0
staticInitStart = 0x00000000
staticInitEnd = 0x00000000
fileID = 0
compressedSize = 0
flags = 0
unkAddress = 0x00000000
def __init__(self, data, ramAddress, ramSize, bssSize, staticInitStart,
staticInitEnd, fileID, compressedSize, flags):
self.ramAddress = ramAddress
self.ramSize = ramSize
self.bssSize = bssSize
self.staticInitStart = staticInitStart
self.staticInitEnd = staticInitEnd
self.fileID = fileID
self.compressedSize = compressedSize
self.flags = flags
if self.compressed:
self.data = bytearray(codeCompression.decompress(data))
else:
self.data = bytearray(data)
@property
def compressed(self):
return bool(self.flags & 1)
@compressed.setter
def compressed(self, value):
if value:
self.flags |= 1
else:
self.flags &= ~1
@property
def verifyHash(self):
return bool(self.flags & 2)
@verifyHash.setter
def verifyHash(self, value):
if value:
self.flags |= 2
else:
self.flags &= ~2
[docs] def save(self, *, compress=False):
"""
Generate a bytes object representing this overlay.
"""
self.ramSize = len(self.data)
if compress:
data = codeCompression.compress(self.data, False)
else:
data = self.data
self.compressedSize = len(data)
self.compressed = compress
return data
def __str__(self):
fields = []
fields.append('at 0x%08X' % self.ramAddress)
fields.append(f'file={self.fileID}')
if self.compressed:
fields.append('compressed')
if self.verifyHash:
fields.append('verify-hash')
return f'<overlay {" ".join(fields)}>'
def __repr__(self):
fields = []
fields.append(_common.shortBytesRepr(self.data))
fields.append('0x%08X' % self.ramAddress)
fields.append('0x%X' % self.ramSize)
fields.append('0x%X' % self.bssSize)
fields.append('0x%08X' % self.staticInitStart)
fields.append('0x%08X' % self.staticInitEnd)
fields.append(repr(self.fileID))
fields.append('0x%X' % self.compressedSize)
fields.append('0x%X' % self.flags)
return f'{type(self).__name__}({", ".join(fields)})'
[docs]def loadOverlayTable(tableData, fileCallback, idsToLoad=None):
"""
Parse ARM7 or ARM9 overlay table data to create a dictionary of
Overlays. This is the inverse of saveOverlayTable().
"""
ovs = {}
for i in range(0, len(tableData), 32):
(ovID, ramAddr, ramSize, bssSize, staticInitStart, staticInitEnd,
fileID, compressedSize_Flags) = struct.unpack_from('<8I', tableData, i)
if idsToLoad is not None and ovID not in idsToLoad:
continue
fileData = fileCallback(ovID, fileID)
ovs[ovID] = Overlay(fileData, ramAddr, ramSize, bssSize,
staticInitStart, staticInitEnd, fileID,
compressedSize_Flags & 0xFFFFFF, compressedSize_Flags >> 24)
return ovs
[docs]def saveOverlayTable(table):
"""
Generate a bytes object representing this dictionary of Overlays, in
proper ARM7 or ARM9 overlay table format. This is the inverse of
loadOverlayTable().
"""
if not table: return b''
ovt = bytearray()
# Ensure that we loop over overlay IDs in order
for ovId in sorted(table):
ov = table[ovId]
values = []
values.append(ovId) # 0x00
values.append(ov.ramAddress) # 0x04
values.append(ov.ramSize) # 0x08
values.append(ov.bssSize) # 0x0C
values.append(ov.staticInitStart) # 0x10
values.append(ov.staticInitEnd) # 0x14
values.append(ov.fileID) # 0x18
values.append(ov.compressedSize | ov.flags << 24) # 0x1C
ovt.extend(struct.pack('<8I', *values))
return ovt