RISCV - 跳转指令如何与 PC 相关?

RISCV - How are jump instructions PC-relative?

在 RISC-V Unpriviliged 规范 V20191213 中,说明如下,(第 21 页)

The unconditional jump instructions all use PC-relative addressing to help support position-independent code.

查看JALR指令的定义,

The indirect jump instruction JALR (jump and link register) uses the I-type encoding. The target address is obtained by adding the sign-extended 12-bit I-immediate to the register rs1, then setting the least-significant bit of the result to zero.

这个地址计算显然不是 PC 相关的。那么,为什么规范声称所有跳转指令都使用 PC 相对寻址?

另外,PC相对寻址如何支持位置独立代码?不应该正好相反吗?

This address calculation is clearly not PC-relative. So, why does the spec claim that all jump instructions use PC-relative addressing?

是的,你是对的。你不得不欣赏JALR的一些不同用途:

  1. 从子程序到 return,
  2. 进行间接函数调用,
  3. 进行远程 (+/-2GB) 函数调用

前两个根本不需要任何偏移量,因为所需地址已在寄存器中完全指定,因此不存在需要相对于 pc 的问题。

最后一个使用 2 指令序列,其中第一个是 AUIPC,这使得序列相对于 pc。

Also, how can having PC-relative addressing support position-independent code? Shouldn't it be the exact opposite?

PC relative 意味着代码可以加载到内存中的任何地方并且仍然可以运行——也就是说 代码可以找到代码的其他部分 而不管改变加载代码块的初始地址(或者,如果在 ROM 中,它出现在地址 space 中)。

具有引用代码的指针的数据需要 relocated (adjusted),具体取决于代码加载的最终位置。

(当然,位置独立性并不意味着代码一旦开始执行就可以移动!一旦开始执行,数据结构就会捕获代码地址,因此代码必须保持固定在所选位置在执行期间加载它。位置独立意味着加载时位置独立。)

因此,可以在子程序调用时捕获 return 地址作为 direct/absolute 指针值,在用于 return 来自子程序,例如使用 JALR.

在这种情况下,即使return地址是一个绝对指针值,它是在运行时间[=76=时从调用者的位置动态捕获的],因此不会违反加载时位置独立性。

(你不希望通过 JALR 的 return 操作是 pc 相关的,因为 JALR 的 pc 地址在 returning 被调用者,与调用者中的 return 链接位置无关。重要的是捕获相对于调用子例程的 JAL 的 return 地址。)


在一个假设的机制中,如果我们想要某种形式的 运行 时间-位置独立性,我们可能会让 software/hardware 计算两个差异,即:

  • 调用者的调用指令地址和被调用者的第一条指令地址,作为return "offset"传递给被叫方以代替 return 地址,并且,

  • 被调用者的第一条指令的地址和被调用者return发送给调用者的指令的地址。

这两个差异可以在 return 时应用于与 pc 相关的 return 指令(例如,来自 returning 指令的 pc)。这将提供一些 运行 时间-位置独立性的度量(例如 return 链接),但是 (a) 会过于复杂,并且 (b) 也不足以使所有指针(或指针的用法)运行时间位置无关。


load/run位置无关代码的步骤如下:

  1. 加载代码
  2. 重新定位数据
  3. 运行啦!

如果数据已经知道代码将加载到哪里,则可以省略重定位步骤 2。否则,数据重定位需要在它引用代码的各个地方(可能无处),将加载代码的基地址与引用代码的数据位置相加。

一旦我们通过第 2 步,数据将使用绝对地址绑定到代码,并且在执行终止之前无法移动代码。

位置无关代码的一个优点是这样的代码不需要重定位,因此可以是只读的,但如果需要,仍然可以在不同的进程中加载​​到不同的地址。