MIPS 跳转寄存器 (JR) 指令经常出现在 C++ 代码的二进制中。那么,C++ 中的哪些功能使用了 JR 指令以及为什么使用这些指令?
分支指令只能用于目标地址在“编译时”已知并且位于当前指令“小范围内”的情况。您不能(轻松)使用它来分支到未知的地址静态并且必须在运行时计算/加载,或者跳转到太远的目标
因此,这里有一些必须使用 JR
或
JALR
的示例(两者完全相同,只是
JALR
存储当前地址以便稍后返回):
跳转到任意地址JR
/JALR
跳转
:要调用的函数只有在运行时才知道,所以显然你需要某种方法来动态调用它
int Add(int a, int b);
int Sub(int a, int b);
int Mul(int a, int b);
int Div(int a, int b);
int (*p[4]) (int x, int y) = { Add, Sub, Mul, Div };
int test_function_pointer(int i, int x, int y) {
return p[i](x, y);
}
共享库
(*.dll、*.so...)中的函数在加载之前对于进程来说也是未知的,因此如果您手动加载这些库(使用LoadLibrary()
,
dlopen()
...)您还可以获取函数指针的地址并使用 JR
/
JALR
调用它们。通常,将使用 JALR
调用函数,但如果它位于函数末尾并且启用了 尾部调用优化,则将使用
JR
来代替
Vtable 在许多 OOP 语言(如 C++)中也是函数指针的一个示例:
struct A {
virtual int getValue() = 0;
};
int test_vtable(A *a) {
return a->getValue() + 1;
}
doSomething(int (*)(int), int, int):
sltu $2,$5,8
beq $2,$0,$L2 # x >= 8: default case
move $25,$4
lui $2,%hi($L4)
addiu $2,$2,%lo($L4) # load address of $L4 to $2
sll $5,$5,2 # effective address = $L4 + x*4
addu $5,$2,$5
lw $2,0($5)
nop
j $2
nop
$L4:
.word $L11
.word $L5
.word $L6
.word $L7
.word $L8
.word $L9
.word $L10
.word $L11
$L11:
jr $25
move $4,$6
$L9:
sll $4,$6,2
jr $25
addu $4,$4,$6
# ... many more cases below
您可以在
Compiler Explorer 上看到完整的输出$L4
是一个跳转表,其中包含您要分支到的位置的地址,即此代码段中的
case
块。它的地址存储在
$2
中,需要使用 jr
将指令指针移动到该地址。 j $2
如上所示,但我认为这是一个反汇编程序错误,因为j
无法接收寄存器操作数。一旦您处于正确的情况,则再次使用 jr
来调用 f
函数指针
另请参阅 MIPS 汇编中 J 与 JAL(以及 JR 与 JALR)的必要性