当我了解MIPS处理器时,我突然意识到对$0寄存器的读取总是返回0,对$0的写入总是被丢弃。来自MIPS程序员手册:
2.13.4.1 CPU通用寄存器[…]r0硬连接到值零,并且可以用作任何指令的目标寄存器结果将被丢弃。当值是必需的。
由此可知,指令or $0,$r31,$0
是no-op。
想象一下,当我在ELF MIPS二进制文件的启动代码中摸索时,当我看到以下指令序列时,我会感到惊讶:
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
地址0x610处的指令将$ra的值复制到$r0中,根据上面的段落,这相当于丢弃它。然后,地址0x628处的指令从$0读取值,但由于$0硬连接到0,因此将$ra设置为0。
这一切似乎毫无意义:既然只执行0x628就足够了,为什么还要执行语句0x610。能说会道的人在写这段代码时显然有一些意图。看来$0毕竟是可读写的!
那么,在什么情况下,程序可以像其他通用寄存器一样读取/写入$0寄存器呢?
编辑:查看glibc源代码是没有帮助的。__start
的代码使用宏:
https://github.com/bminor/glibc/blob/master/sysdeps/mips/start.S#L80
ENTRY_POINT:
# ifdef __PIC__
SETUP_GPX($0)
...
注意这里是如何故意指定$0的。SETUP_GPX宏定义如下:
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
"保存旧ra"清楚地表明了保存寄存器的意图,但为什么是0美元?
它使用$0
,因为在入口点没有理由保存$ra
,所以它被丢弃了。由于它是来自宏的手工编写的asm代码,因此没有像通常情况下那样进行优化。
请注意,glibc仅将其用于PIC。(请参阅Linux上的所有MIPS代码都应该是PIC吗?)
MIPSjal
(与其他j
指令一样)不是PIC;它用CCD_ 7替换PC的低28位。在那1/16的地址空间内,这是一个绝对的调用。
但b
指令编码确实使用了相对位移,因此它仍然有效。bal
是用于无条件PIC函数调用的伪指令:它设置PC += imm16<<2
(参见同一链接)。它是条件分支和链接的伪指令,用于测试$0
是否为>= 0
,因此它总是被采用。正如您的反汇编所示,真正的指令是"BGEZAL——大于或等于零的分支和链接"。它只能在-2^17/+(2^17-4)字节内工作。
"和链接"部分就是这个代码想要的:它通过使用分支和链接将PC进入$ra
,因为在PIC中,您在组装或链接时不知道自己的地址。
无论如何,这解释了bgezal $0
读取$0
的原因。通过对该宏的特殊使用,他们可以省去将旧值写入$0
的无用操作,从而为每个可执行文件至少节省4个字节。但他们没有:/不过,代码只运行一次。