RISCV ISA设计初衷是简单和模块化。为了达到这个目标,RISCV简化了复杂寻址方式,因为复杂寻址方式是其中一个最大的开销。寻址模式在小的design中decode花销较大,在大的design中隐式dependencies花销很大。所以RISCV只设计了三种寻址方式。
- PC-relative, via the
auipc
,jal
andbr*
instructions. - Register-offset, via the
jalr
,addi
and all memory instructions. - Absolute, via the
lui
instruction(though arguably this is justx0
-offset)
这些寻址模式是经过精心选择的,以便以最低的硬件复杂性实现高效的代码。我们通过依赖现代化工具链来优化软件中的寻址来实现这种简单性,这与传统的ISA形成鲜明的对比,后者在硬件中实现了过多的寻址模式。研究表明,RISCV方法是可靠的,我们能够在benchmark中实现类似的代码大小,同时具有更简单的解码规则和大量的自由编码空间。
所有的硬件复杂度降低都导致软件复杂度上升。这篇文章介绍了另一个复杂的软件:code model概念。就像relocation和relaxation一样,code model不是RISCV独有的概念。实际上,RISCV工具链比大多数流行ISA有更少的code model,这主要是因为我们依赖于软件优化,而不是古怪的寻址模式,这使得我们的寻址模式更加灵活。
what’s a code model
大多数程序不会用symbols填充整个地址空间(大多数程序根本不会填充它,但是可能会用heap填充这些空间)。ISA倾向于利用这种局限性,在硬件上实现更短的寻址模式,并依赖于软件来提供更大的地址寻址模式。code model决定了哪种软件寻址模式,因此,也就决定了被链接的程序施加什么样的约束。软件寻址模式决定了程序怎么看待地址,而硬件寻址模式决定指令中的地址是如何处理的。
code model是必须的,因为它分割了编译器和链接器。当产生一个未链接的object,编译器不知道任何symbol的绝对地址,但是它又必须要知道该使用哪种寻址模式,因为某些寻址模式可能需要操作scratch register。因为编译器不能产生实际的寻址代码,它只能产生template,然后链接器一旦知道了symbol实际地址,就可以fix up。code model决定了哪种template,也就决定了哪种relocation被发射。
下面是个很好的例子。c code如下。
1 | long global_symbol[2]; |
尽管GCC可以通过一条命令产生binary,但实际是几个命令的集合:preprocessor, compiler, assembler, linker。用--save-temps
参数可以让GCC保留中间文件,这是用于观察工具链中间过程很有用。
1 | $ riscv64-unknown-linux-gnu-gcc cmodel.c -o cmodel -O3 --save-temps |
每一步会产生如下文件。
cmodel.i
:预处理后的文件,展开了宏,比如#include, #ifdefcmodel.s
:编译后的文件,产生了汇编代码,RISCV汇编语法的text文件cmodel.o
:汇编后的文件,未链接的目标文件,ELF文件,但是不可执行cmodel
:linker的输出,链接后的目标文件,可执行的ELF
为了理解为什么会有code model,我们必须先检查工具链流程的细节。因为这个简单的源文件没有预处理宏,所以预处理比较简单,发出一些指令,以便稍后生成调试信息时使用。
1 | $ cat cmodel.i |
预处理后的输出到编译器,产生个assembly文件。
1 | $ cat cmodel.s |
生成的汇编包含一条指令对,lui
和ld
。这意味着global_symbol必须在32bit寻址范围之内(不是相对某个寄存器或PC,而是实际32bit地址)。注意这个限制和指针的size没有关系,指针仍然可能时64bit的,但是所有全局符号必须由一个32bit的绝对地址来寻址。
编译器产生汇编代码之后,GCC调用assembler来产生目标文件。这个文件是个ELF binary,Binutils可以读取这个文件。我们可以使用objdump来显示符号表,反汇编,这可以显示relocations信息。
1 | $ riscv64-unknown-linux-gnu-objdump -d -t -r cmodel.o |
现在我们有了目标文件,但是我们还是不知道global symbols的实际地址。assembler的工作只是转换文本指令为bits,但是有些bits依赖于global symbol的地址,所以assembler还是不知道这些到底是什么。为了使得linker填充这些bits到最终的可执行目标文件,assembler产生完整的relocation table,让linker填充。relocation define a bit range that the linker is meant to fill out when linking the code together. 这些特殊定义是ISA相关的,RISCV定义可以在这里找到, ELF psABI document.
在assembling之后,GCC调用linker产生可执行程序。这是可执行的ELF。因为它包含了c库程序,这里只列出了相关的段。
1 | $ riscv64-unknown-linux-gnu-objdump -d -t -r cmodel |
这里有些有趣的事情值得注意。
- symbol表包含实际的绝对地址,这是linker的作用
- text段包含正确的global symbol引用,而不是一堆0
- 对global symbol的relocation已经被移除,因为它们不再需要了。有些relocation也可能存在,以支持动态链接,但在这个简单例子中没有
到目前为止,这个例子已经使用了RISCV默认的code model medlow 。为了更具体地展示什么是code model,最好将它与我们的另一个代码模型medany进行对比。差异可以总结为一个简单例子输出。
1 | 0000000000000000 main: |
具体来说,medany 产生auipc/ld
对来引用global symbol,这允许代码在任何地址进行链接;而medlow生成lui/ld
对来引用global symbol,这限制了代码被链接到地址0周围。它们都生成32bit有符号偏移量来引用global symbol,所以它们都将生成的代码限制在2GiB范围内。
what does -mcmodel=medlow mean
这选择了medium-low code model。意味着程序和它静态定义的symbol必须在2G地址范围内,并且必须位于绝对地址-2G和+2G之间。寻址global symbol使用lui/addi
指令对,它将发射R_RISCV_HI20/R_RISCV_LO12_I
序列。下面是例子。
1 | int main() { |
what does -mcmodel=medany mean
这选择了medium-any code model。这意味着程序和它静态定义的symbol必须在2GB范围内。寻址global symbol使用auipc/addi
指令对。它发射R_RISCV_PCREL_HI20
/ R_RISCV_PCREL_LO12_I
序列。下面是例子(为了更清楚匹配medany例子,使用了 -mexplicit-relocs
)。
1 | $ cat cmodel.c |
值得注意的是,-mcmodel=medany
默认是-mno-explicit-relocs
,它拥有更好的性能。这种性能效果会有细微的差别,我们将在以后讨论它。
The difference between a Code Model and an ABI
一个经常被误解的是code model和ABI之间的区别。ABI决定了函数之间的接口,而code model决定函数里的代码是怎样产生的。具体来说,这两种RISCV code model都将代码寻址symbol限制为32bit偏移,但是在RV64I-based系统仍然将指针编码为64bit。
具体来说,用medany编译的函数可以被medlow编译的函数调用,反之亦然。为了允许可执行文件被链接,需要满足这两个函数对符号寻址的限制,但是这个限制通常是正确的。由于code model不影响内存中结构的布局,也不影响函数之间参数的传递方式,所以它对程序来说基本上是透明的。
对比两个不同的ABI生成的链接代码,就不是这样了。想象一个包含double参数的函数。一个函数用lp64d编译,它期望参数在a register。而使用lp64编译的函数,把参数放在x register。这样程序就不能正常工作了。
code models and linker relaxation
到目前为止,我们还没有讨论code model和linker relaxation如何相互影响,那是因为答案现在很简单,假设你使用了各种RISCV分支的工具链组件,那么一切都可以正常工作,因为一些补丁尚未在上游找到。
linker relaxation实际上是一个非常重要的优化,它对RISCV ISA产生了重大影响。linker relaxation允许RISCV放弃寻址模式,否则需要在许多代码上获得合理性能。在RISCV目标上,可以采用以下寻址模式。
- Symbols within a 7-bit offset from 0 (or from
__global_pointer$
): 2 bytes. - Symbols within a 12-bit offset from 0 (or from
__global_pointer$
): 4 bytes. - Symbols within a 17-bit offset from 0: 6 bytes.
- Symbols within a 32-bit offset from 0: 8 bytes. On RV32I this is the entire address space.
这都可以通过一个单一的代码模型来实现,同时通过8中指令格式(U, I, S, CI, CR, CL and CS),而不用任何模式位。你可以将其视为一种可变长度的地址编码,在硬件中可选支持这种编码,更多信息,请参阅“compressed macro-op fusion”一文。由于这些压缩都是由链接器透明的实现的,所以我们只需要一个code model。将这种行为与ARMv8对比,后者需要为它可以发出的每个地址生成序列选择不同的code model。
实现可变长寻址序列通常是为CISC处理器预留的功能,CISC处理器通过在硬件中实现过多的寻址模式来实现这一功能,并在可能的情况下在assembly时减少寻址序列。RISCV使用fusible multi-instruction addressing sequences和Linker relaxation,从而使实现更简单,并能产生相似代码大小。
翻译自:
https://www.sifive.com/blog/all-aboard-part-4-risc-v-code-models