Articles

Reverse engineering

Introduction

Hello 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.
 

Requirements

You 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 output

Now 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 description

The 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 list

Note 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

Jumping

First, 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 functions

The $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

Examples

Now, 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, structures

Global 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 instructions

movz $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.