When I learned about the MIPS processor, it was pounded into my head that reads of the $0 register always return 0, and writes to $0 are always discarded. From the MIPS Programmer's manual:
2.13.4.1 CPU General-Purpose Registers [...] r0 is hard-wired to a value of zero, and can be used as the target register for any instruction whose result is to be discarded. r0 can also be used as a source when a zero value is needed.
From this follows that the instructions or $0,$r31,$0
is a no-op.
Imagine my surprise when I was poking around in the startup code of an ELF MIPS binary, when I saw the following instruction sequence:
00000610 03 E0 00 25 or $0,$ra,$0
00000614 04 11 00 01 bgezal $0,0000061C
00000618 00 00 00 00 nop
0000061C 3C 1C 00 02 lui $28,+0002
00000620 27 9C 84 64 addiu $28,$28,-00007B9C
00000624 03 9F E0 21 addu $28,$28,$ra
00000628 00 00 F8 25 or $ra,$0,$0
The instruction at address 0x610 is copying the value of $ra into $r0, which according to the paragraph above is tantamount to discarding it. Then, the instruction at address 0x628 is reading the value back from $0, but since $0 is hardwired to 0, it results in setting $ra to 0.
This all seems rather pointless: why execute statement 0x610 when it would be enough to just execute 0x628. The glibc folks clearly had some intent in mind when they wrote this code. It seems as if $0 is writeable and readable after all!
So under what circumstances can a program read / write to the $0 register as if it were any one of the other general purpose registers?
EDIT:
Looking at the glibc source code isn't helpful. The code for __start
uses a macro:
https://github.com/bminor/glibc/blob/master/sysdeps/mips/start.S#L80
ENTRY_POINT:
# ifdef __PIC__
SETUP_GPX($0)
...
Notice how $0 is deliberately being specified here. The SETUP_GPX macro is defined here:
https://github.com/bminor/glibc/blob/master/sysdeps/mips/sys/asm.h#L75
# define SETUP_GPX(r) \
.set noreorder; \
move r, $31; /* Save old ra. */ \
bal 10f; /* Find addr of cpload. */ \
nop; \
10: \
.cpload $31; \
move $31, r; \
.set reorder
"Save old ra" clearly signals the intent of saving the register, but why $0?
It's using $0
because at the entry point there is no reason to save $ra
, so it's just discarded. Since it's hand written asm code coming from a macro, it's not optimized away as would normally be the case.
Note that glibc only uses this for PIC. (See Is all MIPS code on Linux supposed to be PIC?)
MIPS jal
(like other j
instructions) is not PIC; it replaces the low 28 bits of PC with imm26 << 2
. It's an absolute call within that 1/16th of address space.
But the b
instruction encoding does use a relative displacement, so it still works. bal
is a pseudo-instruction for an unconditional PIC function call: It sets PC += imm16<<2
(see that same link). It's a pseudo-instruction for a conditional branch-and-link that tests $0
for >= 0
, so it's always taken. As your disassembly shows, the real instruction is "BGEZAL -- Branch on greater than or equal to zero and link". It only works within -2^17 / +(2^17 - 4) bytes.
The "and link" part is what this code wants: It's getting PC into $ra
by using a branch-and-link, because in PIC you don't know your own address at assemble or link time.
Anyway, this explains why bgezal $0
is reading $0
. By special-casing this use of that macro, they could have saved at least 4 bytes per executable by leaving out the useless write of the old value into $0
. But they didn't :/ The code only runs once, though.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With