MIPS 跳转寄存器 (JR) 指令经常出现在 C++ 代码的二进制文件中。那么,C++ 中的哪些功能使用了 JR 指令,为什么要使用这些指令?
最佳答案
分支指令只能用于目标地址在编译时已知并且在当前指令的小范围内的情况。您不能(轻松地)使用它来分支到一个静态未知的地址,并且必须在运行时计算/加载,或者跳转到太远的目标
下面是一些必须使用JR
或JALR
的例子(除了JALR
存储要返回的当前地址外,两者完全相同稍后):
跳转到任意地址:静态分支指令不能用于跳转到 32 位或 64 位地址,因为立即数只有 16 或 26 位长。您需要将完整地址加载到寄存器中并使用
跳转JR
/JALR
Function pointers : 调用函数只在运行时才知道,所以显然你需要一些方法来动态调用它
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); }
shared libraries 中的函数(*.dll, *.so...) 在加载之前对进程也是未知的,因此如果您手动加载这些库(使用
LoadLibrary()
,dlopen()
...),您还将获得地址到一个函数指针,并用JR
/JALR
调用它们。通常情况下,函数将使用JALR
调用,但如果它位于函数的末尾并且 tail-call optimization启用后将使用JR
Vtable在C++等很多OOP语言中也是函数指针的一个例子
struct A { virtual int getValue() = 0; }; int test_vtable(A *a) { return a->getValue() + 1; }
Jump table (就像在一个大开关 block 中)
typedef int (*func)(int); int doSomething(func f, int x, int y) { switch(x) { case 0: return f(x + y); case 1: return f(x + 2*y); case 2: return f(2*x + y); case 3: return f(x - y); case 4: return f(3*x + y); case 5: return f(x * y); case 6: return f(x); case 7: return f(y); default: return 3; } }
GCC 将上面的代码编译成
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
block 。它的地址存储在$2
中,需要使用jr
将指令指针移动到该地址。j $2
如上所示,但我认为这是一个反汇编程序错误,因为j
无法接收寄存器操作数。一旦你处于正确的情况下,jr
将再次用于调用f
函数指针
另见 Necessity of J vs. JAL (and JR vs. JALR) in MIPS assembly
关于c++ - C++ 的哪些功能使用了 JR 指令?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58477929/