Like many RISC implementations, MIPS instruction set uses fixed-width 32-bit instructions, and instructions have only 16 bits for the offset field, meaning you can use only 16-bit constants, giving you 64KB of addressing. However, the actual address space of a MIPS CPU is 4GB (32-bit address size), so how can you access all that? Well, there is the option of using partial 16-bit moves to build a 32-bit address in a register and then use an indirect load/store to access it. It usually looks similar to:
lui $r1, 0x0123
addi $r1, $r1, 0xabcd
or
lui $r1, 0x0123
ori $r1, $r1, 0xabcd
Both of these load 0x0123abcd
into r1
aka at
(see here).
However, this requires knowledge of the address at compile- (or at least link-) time. In case our binary needs to be loaded at a different address (as often the case with shared libraries), it will have to be relocated (instructions would need to be patched). Patching takes time, prevents code page sharing and increases memory consumption, that's why position-independent code (PIC) is preferred to fixed-address.
So, how we can perform the address calculation independent from the load address? Well, we just need to take our execution address and add a fixed delta to it (usually binaries are loaded as one chunk to memory, so offsets from one part of it to another are fixed). The MIPS ABI states that for public functions, the value of $t9
at the function entry should be equal to its runtime address, and we can see the code using this fact:
109BC: li $gp, 0x830e4
109C4: addu $gp, $gp, $t9
If we assume that $t9
is equal to 0x109BC
, we get :
gp = 0x830e4 + 0x109BC = 0x93AA0
gp
stands for "global pointer" and is not supposed to change during the execution of the function (often it is assumed to have the same value in all functions of the program). This fact may be used by the compiler in all other calculations involving program addresses. For example, looking at the highlighted area:
lw $v1, -0x7fd4($gp)
0x93AA0-0x7fd4 = 0x8BACC
, and apparently the program stores 0x70000
at that address, which is used in the next instruction to load $a1
:
lw $a1, -0x1b4c($v1)
Calculating it: -0x1b4c+0x70000=0x6E4B4
(helpfully shown by the disassembler).
So, $v1
(and v0
later) here is just an intermediate variable used for the address calculation (usually $at
is reserved for this purpose but reusing it all the time may lead to slower code).
By the way, 0x70000
has all zeroes in the low 16 bits, and it's not an accident. It may happen to point to rsl_setNetCfgObj
but it's just a red herring.
If you go to the .got section of the binary, you will usually see something like this:
.got:00515B40 .word 0
.got:00515B44 .word 0x80000000
.got:00515B48 .word 0x510000
.got:00515B4C .word 0x4D0000
.got:00515B50 .word 0x420000
.got:00515B54 .word 0x4C0000
.got:00515B58 .word 0x520000
.got:00515B5C .word 0x430000
.got:00515B60 .word 0x440000
.got:00515B64 .word 0x450000
.got:00515B68 .word 0x460000
.got:00515B6C .word 0x470000
.got:00515B70 .word 0x480000
.got:00515B74 .word 0x490000
.got:00515B78 .word 0x4A0000
.got:00515B7C .word 0x530000
.got:00515B80 .word 0x4B0000
.got:00515B84 .word 0
.got:00515B88 .word 0
.got:00515B8C .word 0
These are so-called local GOT entries, and are used by the compiler purely for address calculations inside the binary and not as pointers to external symbols. The compiler allocates enough different addresses there so it can reach any required address in the local binary with just a 16-bit (signed) offset. The gp
itself is usually set to GOT+7FF0
which allows the compiler to load any GOT entry (either external symbol or a local address for further calculation) in one instruction (assuming that GOT does not exceed 64KB).
So, in summary: what you have here is not obfuscation or a disassembler bug but normal code demonstrating limitations of the MIPS instruction set and its calling conventions.
BTW, IDA knows about these things and by default represents most such references using final addresses. E.g., from a sample binary:
.text:0042C34C la $v0, dword_520000
.text:0042C350 lbu $v0, (byte_5193CD - 0x520000)($v0)
.text:0042C354 beqz $v0, loc_42C36C
.text:0042C358 li $v1, 8
.text:0042C35C li $v1, 9
.text:0042C360 la $v0, dword_520000
.text:0042C364 b loc_42C380
.text:0042C368 sb $v1, (sLastPayedFileInfo - 0x520000)($v0)
And with simplifications turned off:
.text:0042C34C lw $v0, -0x7FD8($gp)
.text:0042C350 lbu $v0, (byte_5193CD - 0x520000)($v0)
.text:0042C354 beq $v0, $zero, loc_42C36C
.text:0042C358 addiu $v1, $zero, 8
.text:0042C35C addiu $v1, $zero, 9
.text:0042C360 lw $v0, -0x7FD8($gp)
.text:0042C364 beq $zero, $zero, loc_42C380
.text:0042C368 sb $v1, (sLastPayedFileInfo - 0x520000)($v0)
While you can still somewhat see what's going on, I personally prefer the first one.
For more info on MIPS I would recommend the See MIPS Run book by Sweetman, as well as the MIPS ABI specifications (see here for a start). Or just go through the code instruction by instruction and try to figure out what they do.