ArticlesReverse engineering
IntroductionHello guys!
My nickname is Artart78, and I'm uOFW's main developer and manager.
uOFW is a project of reverse engineering the entire PSP firmware. It's done to know the PSP better, and to be helpful for emulators (PCSP already used some of my code for audio and GE), SDK documentations, headers and libraries, and why not alternative firmwares for the PSP.
I made this tutorial to share the addictive activity of reverse engineering, to show people who "are not in that stuff" that it's not that hard, and maybe to recruit more people for my project.
To follow this tutorial, you need to have a quite full knowledge of the C language, but not its standard library: you need to know variables, functions, structures, arrays and pointers very well. If you don't, you may mix things up. Even if you only have a basic knowledge, you can still try though, but expect headaches! :)
If anyone needs help with the tutorial or reverse engineering in general, I can help them:
- by mail, at
This email address is being protected from spambots. You need JavaScript enabled to view it.
- on IRC, on FreeNode in example ( http://java.freenode.net ), open a chat with me using "/query artart78" and present your problem. Note I may not be here all the time.
- on Gobby 0.5/0.4.94 ( http://gobby.0x539.de/trac/wiki/Download ), server psnpt.com. I'm not there often, but it's a collaborative editor, so I can help people to reverse engineer there. You should first contact me on IRC or by mail though.
RequirementsYou will need the latest prxtool version.
Either compile it from https://github.com/pspdev/prxtool/ or get a precompiled binary <here>.
You will also need the latest 6.60 PSP firmware: http://du01.psp.update.playstation.org/update/psp/image/us/2011_0810_2ca64d59dcf48f45fb99b400a586b395/EBOOT.PBP
And a PSP to decrypt this firmware using PSARDumper: http://www.mediafire.com/?duli222vkej25v5
Install psardumper on your memory stick with the 6.60 update PBP as the "EBOOT.PBP" file on the root of your memory stick, then run psardumper decrypting all, and you will get a new directory at the root of your memory stick containing all the decrypted files.
How to use PRXTool?That's quite simple; however, you will have to use a command line:
- On Windows:
* Open cmd.exe in the fast execution menu or in C:\Windows\system32
* Type "dir" (without the quotes), and a space, and the full path to where you placed the prxtool.exe file.
* Type: "prxtool.exe -w <file.prx> -o <file.txt>", replacing <file.prx> and
* <file.txt> with the PRX file you want to reverse engineer
- On Linux: note I'm assuming you know command line a bit so, if you don't, ask me!
* In a console, type: git pull https://github.com/pspdev/prxtool (you need git)
* Enter the prxtool directory and do the usual ./configure, make, make install
* Use: prxtool -w module.prx to output the assembly to stdout, or prxtool -w module.prx -o module.txt to output it to a text file.
Reading the PRXTool outputNow you should have a prxtool output file, looking like assembly. Great! But what know?
Let's see. If you look carefully at your file, you'll see two or three "Sections": .text, containing the module text, which is the executed machine code; .rodata, containing read-only data; .data, containing data which may be modified during the program execution.
Let's first look at the .text section, the most important one. You'll first see headers like that:
; Subroutine sceMgr_driver_949CAC22 - Address 0x00000000 ; Exported in sceMgr_driver sceMgr_driver_949CAC22: It represents the beginning of an exported functions, which means this function can called from other modules. These are detected easily because they're stored in a list, in some part of the .rodata section of the module. First line tells the "name" of the function (actually, it's the module name followed by the function's NID — I'll probably explain that in a later section), second one tells the library in which the section was exported (a library is a set of functions and variables inside a module). Third line is just a label, which means the processor can jump here if it encounters a branching or jumping instruction (you'll see later).
; Subroutine sub_00000188 - Address 0x00000188 sub_00000188: ; Refs: 0x0000058C 0x000005D4 0x00000658 0x000006BC This one is quite similar; but it's not an exported function, which means it's only called from the module itself. The function start is determined by the jumps to it with the "jal" instructions, which means if the function is only used as a pointer, or if it's not used, the subroutine start won't be specified. If a part of the .text section seems like it can't be accessed from the previous defined "Subroutine", it probably is a function used only as a pointer.
The "Refs" are the module addresses from where the subroutine (function) is called.
Inside the functions, there are three different elements:
loc_00000784: ; Refs: 0x00000764 These are labels, like the sub_* ones, but accessed only from the functions which contains it, and from the addresses listed in "Refs". It's accessed with different instructions than "sub"s (with j or the branching instructions; I'll describe them later).
0x00000788: 0x00000000 '....' - nop This is an instruction. First part is its address (compared with the module start address), second part is the 32-bit value describing the instruction (useless, except for fast hexadecimal conversion when a specified number is written in decimal in the instruction description), third part is the string translation of the instruction value (useless), and last one (the - character is just a separator) is the instruction description. I'll describe it later. This one, 'nop', simply does nothing.
Then, there are the data sections. These simply contain lines with addresses describing where the data of the line is stored, the data itself, and the string (ASCII) translation of data, with dots for special characters. Then are listed the recognized strings (note it may be uncomplete or wrong: it may contain things which are not stored as strings).
MIPS descriptionThe processor is the central part of all the computer: it reads instructions from chips first, and then, it uses its hardware interface to start all peripherals and then, the OS and applications. MIPS describes a set of instructions and registers (some kind of "processor specifications" that all the processors of the same architecture follow).
Each instruction describes something to do; the instruction set is specific to MIPS.
Each instruction has a size of 32-bit and is read from the RAM. The PSP processor is able to read from 32 CPU registers, and also has special coprocessors: COP0, COP1 (FPU) and COP2 (VFPU) that I will maybe describe later; but they're used much less than the two other ones.
The processor flow is very basic: it reads an instruction, executes it, and goes to next line, except if a special instruction told him to go somewhere else, through a jump (go to address) or a branch (go to address depending on a condition).
So, you can see in your PRXTool output a set of addresses or instructions that will be executed from other modules, when they jump to specific parts: then can basically jump to the start of any exported function.
The list of "normal" CPU registers is:
$zr - its content is always 0
$at - assembler temporary, sometimes used to access hardware addresses or in parts written in assembly
$v0 - normal register, used by functions to return variables
$v1 - normal register, used by functions to return variables
$a0 - normal register, used to pass arguments to functions
$a1 - normal register, used to pass arguments to functions
$a2 - normal register, used to pass arguments to functions
$a3 - normal register, used to pass arguments to functions
$t0 - normal register, used to pass arguments to functions
$t1 - normal register, used to pass arguments to functions
$t2 - normal register, used to pass arguments to functions
$t3 - normal register, used to pass arguments to functions
$t4 - normal register
$t5 - normal register
$t6 - normal register
$t7 - normal register
$s0 - normal register, whose content is restored after a function returns
$s1 - normal register, whose content is restored after a function returns
$s2 - normal register, whose content is restored after a function returns
$s3 - normal register, whose content is restored after a function returns
$s4 - normal register, whose content is restored after a function returns
$s5 - normal register, whose content is restored after a function returns
$s6 - normal register, whose content is restored after a function returns
$s7 - normal register, whose content is restored after a function returns
$t8 - normal register
$t9 - normal register
$k0 - kernel register, used by code written in assembly
$k1 - kernel register, used by PSP to manage kernel functions access
$gp - global pointer, rarely used
$sp - pointer to the top of the stack, where any thread/function can store its data
$fp - normal register, whose content is restored after a function returns
$ra - return address, storing the address where a function has to return
There are also two special CPU registers: hi and lo, used for multiplications and divisions.
There is also pc, which contains the address of the current instruction. But it can't be accessed from the CPU; it's just used to describe jumps and branching.
All these registers are 32-bit long.
Basic instructions listNote that instead of the classic type names int, short etc., we will use the letter 's' or 'u' followed by a number: 's' is for 'signed' values, 'u' is for unsigned values, and then '8' is for chars, '16' is for shorts, '32' is for ints, and '64' is for long long ints. In example, 's32' is 'signed int' or 'int'.
Here is a list of the most simple instructions and their C equivalent ('$regX' is the content of any register, 'num' is a constant value):
nop <=> (nothing) * arithmetic instructions:
addu $reg1, $reg2, $reg3 <=> $reg1 = $reg2 + $reg3 addiu $reg1, $reg2, num <=> $reg1 = $reg2 + num subu $reg1, $reg2, $reg3 <=> $reg1 = $reg2 - $reg3 slt $reg1, $reg2, $reg3 <=> $reg1 = (s32)$reg2 < (s32)$reg3 slti $reg1, $reg2, num <=> $reg1 = (s32)$reg2 < (s32)num sltu $reg1, $reg2, $reg3 <=> $reg1 = (u32)$reg2 < (u32)$reg3 sltiu $reg1, $reg2, num <=> $reg1 = (u32)$reg2 < (u32)num lui $reg1, num <=> $reg1 = num << 16; * bitwise instructions (if you don't know what it is, check http://en.wikipedia.org/wiki/Bitwise_operations_in_C ):
and $reg1, $reg2, $reg3 <=> $reg1 = $reg2 & $reg3 andi $reg1, $reg2, num <=> $reg1 = $reg2 & num or $reg1, $reg2, $reg3 <=> $reg1 = $reg2 | $reg3 ori $reg1, $reg2, num <=> $reg1 = $reg2 | num xor $reg1, $reg2, $reg3 <=> $reg1 = $reg2 ^ $reg3 xori $reg1, $reg2, num <=> $reg1 = $reg2 ^ num nor $reg1, $reg2, $reg3 <=> $reg1 = ~($reg2 | $reg3) * multiplication / division:
mult $reg1, $reg2 <=> lo = (s32)$reg1 * (s32)$reg2; hi = ((s32)$reg1 * (s32)$reg2) >> 32 multu $reg1, $reg2 <=> lo = (u32)$reg1 * (u32)$reg2; hi = ((u32)$reg1 * (u32)$reg2) >> 32 div $reg1, $reg2 <=> lo = (s32)$reg1 / (s32)$reg2; hi = (s32)$reg1 % (s32)$reg2 divu $reg1, $reg2 <=> lo = (u32)$reg1 / (u32)$reg2; hi = (u32)$reg1 % (u32)$reg2 mfhi $reg1 <=> $reg1 = hi< mthi $reg1 <=> hi = $reg1 mflo $reg1 <=> $reg1 = lo mtlo $reg1 <=> lo = $reg1 * shifting:
sll $reg1, $reg2, num <=> $reg1 = $reg2 << num srl $reg1, $reg2, num <=> $reg1 = (u32)$reg2 >> num sra $reg1, $reg2, num <=> $reg1 = (s32)$reg2 >> num sllv $reg1, $reg2, $reg3 <=> $reg1 = $reg2 << $reg3 srlv $reg1, $reg2, $reg3 <=> $reg1 = (u32)$reg2 >> $reg3 srav $reg1, $reg2, $reg3 <=> $reg1 = (s32)$reg2 >> $reg3 * reading from or writing to RAM:
lb $reg1, num($reg2) <=> $reg1 = *(s8*)($reg2 + num) lbu $reg1, num($reg2) <=> $reg1 = *(u8*)($reg2 + num) lh $reg1, num($reg2) <=> $reg1 = *(s16*)($reg2 + num) lhu $reg1, num($reg2) <=> $reg1 = *(u16*)($reg2 + num) lw $reg1, num($reg2) <=> $reg1 = *(s32*)($reg2 + num) OR $reg1 = *(u32*)($reg2 + num) sb $reg1, num($reg2) <=> *(s8*)($reg2 + num) = $reg1 OR *(u8*)($reg2 + num) = $reg1 sh $reg1, num($reg2) <=> *(s16*)($reg2 + num) = $reg1 OR *(u16*)($reg2 + num) = $reg1 sw $reg1, num($reg2) <=> *(s32*)($reg2 + num) = $reg1 OR *(u32*)($reg2 + num) = $reg1 * macros (not actual MIPS instructions but PRXTool outputs them for clarity):
li $reg1, num (from: addiu $reg1, $zr, num) <=> $reg1 = num move $reg1, $reg2 (from: addiu $reg1, $reg2, 0 or addu $reg1, $reg2, $zr) <=> $reg1 = $reg2 JumpingFirst, I have to describe you the delay slot. That's it, after each jump or branch, there is a "delay" of 1 instruction: the instruction after the jump will be executed BEFORE jumping.
There four three jumping instructions: j, jal, jr and jalr.
j is used to jump to an address (in the PRXTool output, it'll be a label):
j loc_A [next instruction] is equivalent to:
[C equivalent of the "next instruction"] goto loc_A Yes, it's reversed, due to the delay slot!
jr is the same as the j instruction, the difference being that 'jr $reg1' jumps to the address stored in $reg1. It can be used to return from functions (jr $ra), or for C 'switch'es which were turned into a table of addresses (I'll explain that later).
jal is used to jump to a function: it will jump to an address (label like sub_... or exported function), like the j instruction, the difference being that the address where it has to return (the address of the instruction after the delay slot instruction) will be stored in the $ra register, so the called function can return to the caller function by using 'jr $ra'.
There is a last instruction, jalr, which acts like jal, but jumping to the address stored in a register like jr. It's used to call a function pointer.
Reverse engineering your first function: stack and calling/returning from functionsThe $sp register contains an address, which is the top of the stack; it's an address in the RAM which allows you to store values, when you need to store big values that can't fit in the registers (an array, structure, ..) or when you need to pass a pointer to a function ("register pointers" don't and can't exist!).
So, you have this $sp address. What know? That's it, it's a stack, so you have to place data above it, somewhere which isn't used yet. Since the stack is "reversed" (the bottom of the stack is at the highest address), we have to decrease the $sp value to access an unused memory space (and of course, to increase it with the same value at the end of the function, or otherwise the calling function won't read the correct values from its own stack part!). This is why you see, at the beginning and the end of most functions:
addiu $sp, $sp, -16 addiu $sp, $sp, 16 So that's it, this function, in example, allocates a stack of size 16.
The stack is mostly used to restore the registers whose value must be saved when a function is called (check register list): the functions which use these registers (or call other functions, which will indirectly modify $ra) will backup the used registers at the beginning of the function and restore them at its end. In example, you can have:
addiu $sp, $sp, -16 sw $ra, 0($sp) <function contents> lw $ra, 0($sp) jr $ra addiu $sp, $sp, 16 Note that these function may not be next to each other or at the very start/end of the functions all the time.
So, "what to do to RE this in a function", you may wonder. The answer is: do nothing, just ignore them. You just have to see the difference between when it just backups registers (which will be handled by the compiler, so we don't have to do it in C code) and when it stores real values in the stack! For this, check if the registers were modified before being stored into the stack.
Second part of functions is arguments and return values.
The arguments are simply stored in $a0, $a1, $a2, $a3, $t0, $t1, $t2 and $t3, in this order. In example, if I call:
myFunc(1, 2, 3, 4); It will become the assembly:
li $a0, 1 li $a1, 2 li $a2, 3 jal myFunc li $a3, 4 When more than 8 arguments are passed, they're put at the top of the stack. If a function does 'addiu $sp, $sp, -16' and then accesses 16($sp) or higher, it means it accesses the caller function's stack, so it will be the 9th argument.
If a 64-bit value is passed to a function, it will use $a(X) for the lowest 32-bits and $a(X+1) for the highest 32-bits, where X is an even number (that way, some register arguments aren't even used because the 64-bit value needs to be "aligned" to the next register). In example, if a function takes the arguments (u64 a, u32 b, u64 c), $a0 will contain the lowest 32-bits of 'a', $a1 will contain its highest 32-bits, $a2 will contain 'b', $a3 will be unused, $t0 will contain the lowest 32-bits of 'c', and $t1 will contain the highest 32-bits of 'c' (note this is very rare).
Return values are stored in $v0 and $v1. Most of the time, just $v0 is used, and $v1 is actually used to store the higher 32-bits of a 64-bit value which is returned. In example:
u64 myNumber(u32 num) { return (num << 32) | num; } Will become something like this in assembly:
myNumber: move $v0, $a0 jr $ra move $v1, $a0 And if something does this:
otherFunction(myNumber(412)); It will do like, in assembly:
jal myNumber li $a0, 412 move $a0, $v0 jal otherFunction move $a1, $v1 ExamplesNow, time for exercises!
First, one whose I will give the answer (try to guess what's the result before reading the answer):
sceSysconSetHRPowerCallback: 0x00001EB8: 0x27BDFFF0 '...'' - addiu $sp, $sp, -16 0x00001EBC: 0xAFBF0000 '....' - sw $ra, 0($sp) 0x00001EC0: 0x0C00089F '....' - jal sub_0000227C 0x00001EC4: 0x24060008 '...$' - li $a2, 8 0x00001EC8: 0x8FBF0000 '....' - lw $ra, 0($sp) 0x00001ECC: 0x03E00008 '....' - jr $ra 0x00001ED0: 0x27BD0010 '...'' - addiu $sp, $sp, 16 Let's see.. we will just ignore the 2 instructions at the beginning and the 3 instructions at the end, because it just backups and restores the $ra value. So, we now just have:
jal sub_0000227C li $a2, 8 Wait... It just uses $a2 to pass arguments to sub_0000227C?!? It's not possible for this register to be used later in the function, because anyway, all the $a*, $t* and $v* register values are considered as lost as long as a function is called. So, there must be some $a0 and $a1.. Oh but wait!
We're at the start of a function, right? So, maybe $a0 and $a1 were two arguments passed to sceSysconSetHRPowerCallback!
Also, $v0 isn't modified, so the value returned by sub_0000227C may be returned by sceSysconSetHRPowerCallback. If you don't know whether a function returns a value or not, make it return a value, so it can't hurt, whereas making it return nothing whereas it actually returned something can make the modules calling this function get incorrect results for the return value.
So, here is the solution:
s32 sceSysconSetHRPowerCallback(s32 arg0, s32 arg1) { return sub_0000227C(arg0, arg1, 8); } We don't know what's the type of arg0, arg1 and the return value, but that's not important anyway: it's smaller than an int, so an int is okay.
Here is another example, try to do it yourself!
sceSysconBatteryGetFullCap: 0x00003CB0: 0x27BDFFF0 '...'' - addiu $sp, $sp, -16 0x00003CB4: 0x00802821 '!(..' - move $a1, $a0 0x00003CB8: 0xAFBF0000 '....' - sw $ra, 0($sp) 0x00003CBC: 0x0C000F8E '....' - jal sub_00003E38 0x00003CC0: 0x24040067 'g..$' - li $a0, 103 0x00003CC4: 0x8FBF0000 '....' - lw $ra, 0($sp) 0x00003CC8: 0x03E00008 '....' - jr $ra 0x00003CCC: 0x27BD0010 '...'' - addiu $sp, $sp, 16 And a last example I this time invented, just to make you practice other instructions:
myFunc: andi $a0, $a0, 0xFF lui $v0, 0xFFFF or $v0, $v0, 0xFF nor $v0, $v0 and $a1, $a1, $v0 or $v0, $a0, $a1 sltiu $a2, $a2, 1 sll $a2, $a2, 16 jr $ra or $v0, $v0, $a2 Note: sltiu $a2, $a2, 1 <=> $a2 = ((unsigned int)$a2 < 1) <=> $a2 = ($a2 == 0)
Good luck!
If you don't know how to RE these functions, are blocked, or want me to check your result, contact me!
Building logical blocks (conditions)Now, let me present you new important instructions: branching instructions! They're a bit like the 'j' instruction, the different being that they only jump if a condition is true. They also have a delay slot.
Here is the list:
beq $reg1, $reg2, loc: jumps to loc if $reg1 is EQual to $reg2 ($reg1 == $reg2) bne $reg1, $reg2, loc: jumps to loc if $reg1 is Not Equal to $reg2 ($reg1 != $reg2) blez $reg1, loc: jumps to loc if $reg1 is Less than or Equal to Zero ($reg1 <= 0) bgez $reg1, loc: jumps to loc if $reg1 is Greater than or Equal to Zero ($reg1 >= 0) bgtz $reg1, loc: jumps to loc if $reg1 is Greater Than Zero ($reg1 > 0) bltz $reg1, loc: jumps to loc if $reg1 is Less Than Zero ($reg1 < 0) There are also PRXTool macros:
beqz $reg1, loc <=> beq $reg1, $zr, loc bnez $reg1, loc <=> bne $reg1, $zr, loc There are also the same instructions, but with a 'l' at the end: it means that the instruction in the delay slot will ONLY be executed if the condition is true, before jumping. In example:
beqzl $a0, loc_a li $a0, 1 loc_a: ... Is the same as the C code: if (a0 == 0) a0 = 1, whereas if it used 'beqz', $a0 would've been set to 1 all the time.
Note the conditions are evaluated BEFORE the delay slot: the tested $a0 is the one before running the delay slot instruction 'li $a0, 1'.
This sounds quite easy, but actually, the hard part is guessing C code without 'goto's!
So, let's take an example:
beqzl $a0, loc_A li $a1, 3 <some code> li $a1, 3 loc_A: Let's think about it. If $a0 is equal to 0, it sets $a1 to 3 and goes directly to loc_A. And if it isn't equal to 0, it will execute <some code> and then set $a1 to 3.
So, the closest C code we could make out of this without using GOTOs would be:
if (a0 == 0) a1 = 3; else { <some code> a1 = 3; } But, look! It sets a1 to 3 in both case, before leaving the 'if {} else {}' part. So instead of putting this code twice, we can just do:
if (a0 == 0) ; else { <some code> } a1 = 3; Ugh! That looks ugly! How to fix that? Well, let's see, if a0 is equal to 0, it doesn't do anything; otherwise, it executes <some code>.... Yes, that means that if a0 is NOT EQUAL to 0, it will execute <some code>! So here is the "final" code (not really final because register names shouldn't stay in the code):
if (a0 != 0) { <some code> } a1 = 3; Simpler than the original code, isn't it? You'll see, you'll use that sort of reasoning all the time.
Also, let me tell you some condition rules which will probably be very useful:
!(cond1 && cond2) <=> (!cond1 || !cond2) !(cond1 || cond2) <=> (!cond1 && !cond2) !(a > b) <=> a <= b !(a >= b) <=> a < b< !(a < b) <=> a >= b !(a <= b) <=> a > b a < b <=> b > a (try to keep the constant value at the right) Building logical blocks (loops)These are this sort of things:
li $a0, 0 loc_A: <some code> sltiu $v0, $a0, 16 bnez $v0, loc_A addiu $a0, $a0, 1 Let's think about it a bit. "sltiu $v0, $a0, 16" means "v0 = (a0 < 16)" and "bnez $v0, loc_A" check if "v0 != 0", which is: "a0 < 16".
So, if $a0 is less than 16 (before it is incremented), it returns back to loc_A. Yes, that's it, it's a loop! And it looks like this:
a0 = 0; do { <some code> int cond = a0 < 16; a0++; } while (cond != 0); Ugh! That temporary 'cond' variable is ugly. We just need to check the a0 value before it is incremented... Yes, we can use (a0++): it gives out the a0 value before incrementing it.
So we have:
a0 = 0; do { <some code> } while ((a0++) < 16); I don't really like "do {} while"s, because they're quite unclear to me, and I think I'm not the only one. Since a0 is clearly less than 16 when the loop is first ran, this is the same as:
a0 = 0; while ((a0++) < 16) { <some code> } Oh but, look! We have an initialization, a condition, and an incrementation... That's it, we can even use a 'for'!
for (a0 = 0; a0 < 16; a0++) { <some code> } Now, let's see an example where the 'while' really should be used:
jal myFunc li $a0, 1 beqz $v0, loc_B nop loc_A: <some code> jal myFunc li $a0, 1 bnez $v0, loc_A nop loc_B: Let's see, the loop part (loc_A) is:
do { <some code> } while (myFunc(1) != 0); We can certainly not replace this with a 'while', even for style: myFunc is executed only after the loop content has been executed.
When we add the beginning of the assembly, it gives out:
if (myFunc(1) != 0) { do { <some code> } while (myFunc(1) != 0); } Oh, but look! We test the same condition before entering the loop, and before repeating it. That's equivalent of a 'while' loop!
So, here is the final code:
while (myFunc(1) != 0) { <some code> } Data storage: global values, structuresGlobal values are quite simple: they're always specified as "Data ref" in PRXTool's output.
Let's just take an example:
; Data ref 0x0000BAFC ... 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000: 0x3C050001 '...<' - lui $a1, 0x1 ; Data ref 0x0000BAFC ... 0x00000000 0x00000000 0x00000000 0x00000000 0x00000004: 0x24A5BAFC '...$' - addiu $a1, $a1, -17668 PRXTool always says us that it accesses data, which is at 0xBAFC (0x10000 - 17668). So that's it, we now have a pointer to some memory area.
Now, what this address contains is unknown, and depends on the algorithm it uses with that address. Here, we can see that it only loads the address in $a1, so it loads a pointer to this area, whereas in example, if it used "lw"/"sw"/..., it means it accesses a normal global variable. So, in this case, it either uses a data buffer/array, a structure or a pointer (if it needs, in example, to pass it to a function).
Let's take an example with a 32-bit variable:
0x00000000: 0x3C020001 '...<' - lui $v0, 0x1 ; Data ref 0x00015480 ... 0x00000000 0x0001088C 0x00015C88 0x00000001 0x00000004: 0x8C425480 '.TB.' - lw $v0, 21632($v0) Is the code for: v0 = g_15480; where the value is defined as: s32 g_15480; (or u32) outside any function.
Here is an example for a string:
0x00000000: 0x3C070001 '...<' - lui $a3, 0x1 ; Data ref 0x00014D78 "hello" 0x00000004: 0x24E54D78 'xM.$' - addiu $a0, $a3, 19832 0x00000008: 0x0C00124C 'L...' - jal myFunc 0x0000000C: 0x00000000 '....' - nop Is the code for: myFunc(g_14D78); where g_14D78 is defined as: s8 g_14D78 = "hello";
Note that some global values are initialized, and some are not: you can check if some values are defined after the "Data ref" address: if there is something, it means the variable is initialized. Note that the number of displayed bytes may be wrong (it always displays four 32-bit values for number values or the end of the string if it detects the value as a string). Check the value size depending of the context.
You can also specify the constant values (they're in .rodata at the end of the PRXTool output) in the code itself. In example, in my previous example, it's better to use: myFunc("hello"), so we don't have to define the string.
Now, about structures... They're quite tricky. Let's take an example:
struct MyStruct { s32 a; s8 *b; s8 c[10]; s32 *d; }; First, what's the size of MyStruct? You should be able to answer that. The size of 'a' is 4, the size of 'b' is 4 (it's a pointer and the PSP is a 32-bit architecture, so memory is accessed over 32bits, which is 4 bytes), the size of 'c' is 10 (it's an array, so the characters are stored in the structure itself!), and the size of 'd' is 4 (pointer). So, the size of the structure is 4 + 4 + 10 + 4 = 22. Or probably 24, because the 'd' variable will be 4 bytes-aligned, which means it will act as if 'c' had a size of 12. That's not important anyway, if you make the array bigger than it really is, as soon as you use correct offsets.
Now, imagine this case:
struct MyStruct str; // str is at address 0x00001234 The address of 'a' is 0x1234, the address of 'b' is 0x1238, the address of 'c' is 0x123C, the address of 'd' is 0x1248 (0x123C + 12 because of the alignment).
So, if we do this:
; Data ref 0x00001234 lui $a3, 0x0 ; Data ref 0x00001234 addiu $a0, $a3, 4660 # 0x1234 lw $v0, 4($a0) $v0 will contain the value stored at the address $a0 + 4, so the value at 0x00001238, which is str.b. So the code would be: v0 = str.b.
Note that for optimization, it may use directly, if the structure is used only once in the function:
; Data ref 0x00001238 lui $a3, 0x0 ; Data ref 0x00001238 lw $v0, 0($a3) So you have to try guessing what contains a structure; most of the time, as soon as it loads a pointer and accesses variables at a constant offset (in our example, offset is 4), it means it uses a structure; you'll have to guess the structure size and also, guess when a variable is accessed at the middle of a global structure, but directly with it's offset (by not using a pointer, and doing like the second version of the example).
Advanced instructionsmovz $reg1, $reg2, $reg3 <=> if (reg3 == 0) reg1 = reg2; movn $reg1, $reg2, $reg3 <=> if (reg3 != 0) reg1 = reg2; ext $reg1, $reg2, pos, size This one is a bit tricky: in $reg1 are loaded the bits from the 'pos' to the 'pos + size - 1' offset (bit 0 being the lowest value one) of the value contained in $reg2.
To implement it, use: reg1 = (reg2 >> pos) & mask; where 'mask' is an hexadecimal value whose all and only the 'size' lowest bits are set; in example: if size is 4, mask will be 0xF (because 0xF is 1111 in binary); if size is 31, mask will be 0x1FFFFFFF; etc.
Examples:
ext $reg1, $reg2, 16, 16 <=> reg1 = (reg2 >> 16) & 0xFFFF; ext $reg1, $reg2, 0, 4 <=> reg1 = (reg2 >> 0) & 0xF <=> reg1 = reg2 & 0xF; ins $reg1, $reg2, pos, size This one is like the opposite operation of 'ext': instead of EXTracting bits, it INSerts them. So, it inserts the 'size' lowest bits of $reg2 at the position 'pos' of $reg1 (all the bits from 'pos' to 'pos + size - 1' of $reg1 are emptied before hand).
To implement it, use: reg1 = (reg1 & ~mask) | ((reg2 << pos) & mask) where 'mask' is an hexadecimal value whose all and only the bits from the 'pos' to the 'pos + size - 1' offsets are set.
Examples:
ins $reg1, $reg2, 16, 8 <=> reg1 = (reg1 & ~0x00FF0000) | ((reg2 << 16) & 0x00FF0000) <=> reg1 = (reg1 & 0xFF00FFFF) | ((reg2 << 16) & 0x00FF0000) ins $reg1, $zr, 0, 16 <=> reg1 = (reg1 & ~0x0000FFFF) | ((0 << 16) & 0x0000FFFF) <=> reg1 = (reg1 & 0xFFFF0000) <=> reg1 &= 0xFFFF0000 lwl/lwr $reg1, off($reg2) These instructions are used to read unaligned 32-bit values, when lwr reads from off($reg2) and lwl reads from (off + 3)($reg2); it means it reads an unaligned value at 'off'. Examples:
lwl $t0, 7($a2) lwr $t0, 4($a2) $t0 will contain the value stored at the unaligned $a2 + 4 offset (it can be in a __attribute__((packed)) structure or when using the inline version of memcpy/memset on an unaligned offset of a u8 buffer, in example).
Note it can use lwl and then lwr, or lwr and then lwl: the order doesn't matter.
swl/swr $reg1, off($reg2) Exactly the same as lwl/lwr, but for storing values. |