riscv-code-models

RISCV ISA设计初衷是简单和模块化。为了达到这个目标,RISCV简化了复杂寻址方式,因为复杂寻址方式是其中一个最大的开销。寻址模式在小的design中decode花销较大,在大的design中隐式dependencies花销很大。所以RISCV只设计了三种寻址方式。

  • PC-relative, via the auipc, jal and br* instructions.
  • Register-offset, via the jalr, addi and all memory instructions.
  • Absolute, via the lui instruction(though arguably this is just x0-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
2
3
4
5
long global_symbol[2];

int main() {
return global_symbol[0] != 0;
}

尽管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, #ifdef
  • cmodel.s :编译后的文件,产生了汇编代码,RISCV汇编语法的text文件
  • cmodel.o :汇编后的文件,未链接的目标文件,ELF文件,但是不可执行
  • cmodel :linker的输出,链接后的目标文件,可执行的ELF

为了理解为什么会有code model,我们必须先检查工具链流程的细节。因为这个简单的源文件没有预处理宏,所以预处理比较简单,发出一些指令,以便稍后生成调试信息时使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ cat cmodel.i
# 1 "cmodel.c"
# 1 "built-in"
# 1 "command-line"
# 31 "command-line"
# 1 "/scratch/palmer/work/upstream/riscv-gnu-toolchain/build/install/sysroot/usr/include/stdc-predef.h" 1 3 4
# 32 "command-line" 2
# 1 "cmodel.c"
long global_symbol;

int main() {
return global_symbol != 0;
}

预处理后的输出到编译器,产生个assembly文件。

1
2
3
4
5
6
$ cat cmodel.s
main:
lui a5,%hi(global_symbol)
ld a0,%lo(global_symbol)(a5)
snez a0,a0
ret

生成的汇编包含一条指令对,luild。这意味着global_symbol必须在32bit寻址范围之内(不是相对某个寄存器或PC,而是实际32bit地址)。注意这个限制和指针的size没有关系,指针仍然可能时64bit的,但是所有全局符号必须由一个32bit的绝对地址来寻址。

编译器产生汇编代码之后,GCC调用assembler来产生目标文件。这个文件是个ELF binary,Binutils可以读取这个文件。我们可以使用objdump来显示符号表,反汇编,这可以显示relocations信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ riscv64-unknown-linux-gnu-objdump -d -t -r cmodel.o

cmodel.o: file format elf64-littleriscv

SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 cmodel.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .text.startup 0000000000000000 .text.startup
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 g F .text.startup 000000000000000e main
0000000000000010 O *COM* 0000000000000008 global_symbol

Disassembly of section .text.startup:

0000000000000000 main:
0: 000007b7 lui a5,0x0
0: R_RISCV_HI20 global_symbol
0: R_RISCV_RELAX *ABS*
4: 0007b503 ld a0,0(a5) # 0 main
4: R_RISCV_LO12_I global_symbol
4: R_RISCV_RELAX *ABS*
8: 00a03533 snez a0,a0
c: 8082 ret

现在我们有了目标文件,但是我们还是不知道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
2
3
4
5
6
7
8
9
10
11
12
13
14
$ riscv64-unknown-linux-gnu-objdump -d -t -r cmodel
cmodel: file format elf64-littleriscv

SYMBOL TABLE:
0000000000012038 g O .bss 0000000000000010 global_symbol
...

Disassembly of section .text:

0000000000010330 main:
10330: 67c9 lui a5,0x12
10332: 0387b503 ld a0,56(a5) # 12038 global_symbol
10336: 00a03533 snez a0,a0
1033a: 8082 ret

这里有些有趣的事情值得注意。

  • symbol表包含实际的绝对地址,这是linker的作用
  • text段包含正确的global symbol引用,而不是一堆0
  • 对global symbol的relocation已经被移除,因为它们不再需要了。有些relocation也可能存在,以支持动态链接,但在这个简单例子中没有

到目前为止,这个例子已经使用了RISCV默认的code model medlow 。为了更具体地展示什么是code model,最好将它与我们的另一个代码模型medany进行对比。差异可以总结为一个简单例子输出。

1
2
3
4
5
6
7
8
9
0000000000000000 main:
0: 00000797 auipc a5,0x0
0: R_RISCV_PCREL_HI20 global_symbol
0: R_RISCV_RELAX *ABS*
4: 0007b503 ld a0,0(a5) # 0 main
4: R_RISCV_PCREL_LO12_I .LA0
4: R_RISCV_RELAX *ABS*
8: 00a03533 snez a0,a0
c: 8082 ret

具体来说,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int main() {
return global_symbol[0] != 0;
}

$ riscv64-unknown-linux-gnu-gcc cmodel.c -o cmodel -O3 --save-temps -mcmodel=medlow

$ cat cmodel.s
main:
lui a5,%hi(global_symbol)
ld a0,%lo(global_symbol)(a5)
snez a0,a0
ret

$ riscv64-unknown-linux-gnu-objdump -d -r cmodel.o
cmodel.o: file format elf64-littleriscv

Disassembly of section .text.startup:

0000000000000000 main:
0: 000007b7 lui a5,0x0
0: R_RISCV_HI20 global_symbol
0: R_RISCV_RELAX *ABS*
4: 0007b503 ld a0,0(a5) # 0 main
4: R_RISCV_LO12_I global_symbol
4: R_RISCV_RELAX *ABS*
8: 00a03533 snez a0,a0
c: 8082 ret

$ riscv64-unknown-linux-gnu-objdump -d -r cmodel
Disassembly of section .text:

0000000000010330 main:
10330: 67c9 lui a5,0x12
10332: 0387b503 ld a0,56(a5) # 12038 global_symbol
10336: 00a03533 snez a0,a0
1033a: 8082 ret

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
$ cat cmodel.c
long global_symbol[2];

int main() {
return global_symbol[0] != 0;
}

$ riscv64-unknown-linux-gnu-gcc cmodel.c -o cmodel -O3 --save-temps -mcmodel=medany -mexplicit-relocs

$ cat cmodel.s
main:
.LA0: auipc a5,%pcrel_hi(global_symbol)
ld a0,%pcrel_lo(.LA0)(a5)
snez a0,a0
ret

$ riscv64-unknown-linux-gnu-objdump -d -r cmodel.o
cmodel.o: file format elf64-littleriscv

SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 cmodel.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .text.startup 0000000000000000 .text.startup
0000000000000000 l .text.startup 0000000000000000 .LA0
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 g F .text.startup 000000000000000e main
0000000000000010 O *COM* 0000000000000008 global_symbol

Disassembly of section .text.startup:

0000000000000000 main:
0: 00000797 auipc a5,0x0
0: R_RISCV_PCREL_HI20 global_symbol
0: R_RISCV_RELAX *ABS*
4: 0007b503 ld a0,0(a5) # 0 main
4: R_RISCV_PCREL_LO12_I .LA0
4: R_RISCV_RELAX *ABS*
8: 00a03533 snez a0,a0
c: 8082 ret

$ riscv64-unknown-linux-gnu-objdump -d -r cmodel.o
Disassembly of section .text:

0000000000010330 main:
10330: 00002797 auipc a5,0x2
10334: d087b503 ld a0,-760(a5) # 12038 global_symbol
10338: 00a03533 snez a0,a0
1033c: 8082 ret
...

值得注意的是,-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