SiFive 博客
来自 RISC-V 专家的最新洞察与深度技术解析

RISC-V 代码模型(2026 版)
本文是在 2017 年《RISC-V 代码模型》一文基础上的更新版本。本版介绍了最新的工具链实现,并引入了新的 large 代码模型。
RISC-V 指令集架构 (ISA) 在设计上兼顾简洁与模块化。为了实现上述设计目标,RISC-V 有意识地减少了寻址方式的种类,从而降低了实现复杂 ISA 时的一项重大成本。寻址方式成本高昂:在小型设计中,会增加解码开销;在大型设计中,则会引入隐式依赖成本。RISC-V 仅有三种寻址方式:
- 基于程序计数器 (PC) 相对寻址,通过
auipc、jal和br*指令实现。 - 寄存器偏移寻址,通过
jalr、addi及所有内存指令实现。 - 绝对寻址,通过
lui指令实现(本质上也可以说只是x0偏移寻址)。
这些寻址方式经过精心挑选,旨在以最低的硬件复杂度实现高效的代码生成。这种简化得益于现代工具链在软件层面对寻址的优化,而传统 ISA 的做法则截然不同:它们往往在硬件层面实现大量繁复的寻址方式。研究表明,RISC-V 的这种设计思路是有效的:在基准测试中,其生成的代码体积与传统架构相当,同时指令解码规则大幅简化,并且保留了大量可用的编码空间。
降低硬件复杂度的代价是软件复杂度增加。本文将介绍 RISC-V 软件体系中的另一项复杂机制:代码模型。与重定位和指令松弛类似,代码模型并不是 RISC-V 独有的概念。实际上,RISC-V 工具链支持的代码模型数量少于大多数主流 ISA,这在很大程度上是因为我们依赖软件优化而非复杂多样的硬件寻址方式,从而使得现有的硬件寻址方式具备更高的灵活性。
什么是代码模型?
大多数程序并不会使用全部可用地址空间来存放符号。事实上,绝大多数程序甚至不会填满地址空间;而填满的程序通常会用堆占满。各类 ISA 往往会利用这种局部性:在硬件中实现较短范围的寻址方式,同时依赖软件来提供更大范围的地址访问能力。代码模型用于确定程序在软件层面采用哪一种寻址方式,从而对最终链接生成的程序施加相应的约束。软件寻址方式决定程序员在编程时如何看待地址;硬件寻址方式则决定指令中地址位的处理方式。
由于编译器与链接器之间存在职责分离,代码模型十分必要:在生成尚未链接的目标文件时,编译器并不知道任何符号的绝对地址,但仍然必须决定使用哪一种寻址方式,因为某些寻址方式在操作时可能需要占用临时寄存器。由于编译器在此阶段无法生成实际的寻址代码,因此会生成寻址模板(称为重定位),待链接器在获知各个符号的实际地址后再对其进行修正。代码模型决定这些寻址模板的具体形式,进而决定生成哪些重定位。
这一点最好通过一个示例来说明。假设有如下 C 代码:
long global_symbol[2];
int main() {
return global_symbol[0] != 0;
}
虽然在这个简单示例中,单次 GCC 调用即可生成二进制文件,但在实际执行过程中,GCC 驱动程序会依次运行预处理器、编译器、汇编器,最后再运行链接器。GCC 的 --save-temps 参数允许用户查看所有这些中间文件,对于了解工具链的内部运作非常有用。
$ riscv64-unknown-linux-gnu-gcc cmodel.c -o cmodel -O3 --save-temps
GCC 封装脚本的每一步运行都会生成一个文件:
cmodel.i:预处理后的源文件。该文件会展开所有预处理指令(例如#include或#ifdef)。cmodel.s:编译器输出的汇编文件(RISC-V 汇编格式的文本文件)。cmodel.o:汇编器输出的未链接目标文件(ELF 格式,但不是可执行 ELF)。cmodel:链接器生成的输出文件,即已链接的可执行文件(可执行 ELF 文件)。
要理解代码模型存在的原因,首先需要更详细地分析这一工具链流程。由于源文件非常简单且未使用任何预处理宏,预处理阶段几乎没有工作:仅输出一些指令,以便在后续生成调试信息时使用:
$ 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;
}
随后,将预处理后的输出送入编译器,由其生成汇编文件。该文件是纯文本形式,包含 RISC-V 汇编代码,因此易于阅读:
$ cat cmodel.s
main:
lui a5,%hi(global_symbol)
ld a0,%lo(global_symbol)(a5)
snez a0,a0
ret
生成的汇编代码中包含一对用于寻址全局符号 global_symbol 的指令:lui 和 ld。这种实现方式对 global_symbol 的地址施加了约束:该符号必须能够通过 32 位有符号绝对常量进行寻址。需要注意的是,这里指的是真正的 32 位绝对地址,而不是相对于某个寄存器或 PC 的 32 位偏移。需要注意,对符号地址的这种限制与该架构中的指针大小无关:指针仍然可能是 64 位,但所有全局符号都必须能够通过 32 位绝对地址进行寻址。
当编译器生成汇编代码后,GCC 封装脚本会调用汇编器生成目标文件。该文件为 ELF 二进制文件,可以通过 GNU Binutils 提供的多种工具进行读取。在本例中,我们将使用 objdump 来显示符号表、对文本段进行反汇编,并查看汇编器生成的重定位信息:
$ 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
至此,我们已有一个目标文件,但仍不知道任何全局符号的实际地址。这里正体现了工具链各组件职责之间的一种交叉关系:汇编器的职责是将文本形式的指令转换为机器码,但在这些二进制位依赖于全局符号地址的情况下(例如上述代码中的 lui 指令),汇编器无法确定这些位的实际取值。为了使链接器能够在生成最终可执行文件时回填这些位,汇编器会为每一段需要由链接器填入的位范围生成重定位条目。重定位用于定义一个位范围,指明链接器在链接代码时需要填入具体值的位置。文本段中出现的各种重定位类型的具体定义取决于具体的 ISA;在 RISC-V 中,这些定义可以在其 ELF psABI 文档中找到。
程序汇编完成后,GCC 封装脚本调用链接器生成可执行文件。这同样是一个 ELF 文件,但这一次是完整的可执行文件。由于其中包含大量 C 标准库代码,这里只展示与示例相关的部分:
$ 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
这里有几点值得注意:
- 符号表中的符号已经具有实际的绝对值。这正是链接器的核心作用。
- 文本段中已经包含能够正确访问全局符号的机器码位值,而不再只是一些占位的 0。
- 针对全局符号的重定位条目已经被移除,因为它们已不再需要。某些可执行文件中可能仍存在重定位以支持动态链接等功能,但在这个简单示例中不存在。
至此,本示例一直使用 RISC-V 默认的代码模型 medlow。为了更具体地说明代码模型的含义,可以将其与另一种代码模型 medany 进行对比。两者之间的差异可以通过一个简单示例输出加以概括:
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 指令对来引用全局符号,因此代码可以在任意地址进行链接;而 medlow 则生成 lui/ld 指令对来引用全局符号,这会将代码限制在接近地址 0 的区域进行链接。两种模型在引用符号时都生成 32 位有符号偏移量,因此它们都要求生成的代码必须链接在 2 GiB 的地址窗口之内。
-mcmodel=medlow 代表什么含义?
该选项用于选择 medium-low 代码模型。在该模型下,程序及其静态定义的符号必须位于同一个 2 GiB 地址范围内,并且整体地址必须落在 −2 GiB 到 +2 GiB 的绝对地址区间之间。对于全局符号的寻址,编译器会使用 lui 指令构造地址的高位部分,然后再通过加载/存储指令(例如用于数据访问的 lui+ld )或 addi (用于地址生成)完成访问。这一实现方式会生成 R_RISCV_HI20/R_RISCV_LO12_I 的重定位对。下面给出一个使用 medlow 代码模型生成代码的示例:
$ 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=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
-mcmodel=medany 代表什么含义?
该选项用于选择 medium-any 代码模型。在该模型下,程序及其静态定义的符号必须位于任意一个 2 GiB 地址范围内。对于全局符号的寻址,编译器会使用 auipc 指令生成一个 PC 相对基址,然后再通过加载/存储指令(例如用于数据访问的 auipc+ld )或 addi (用于地址生成)完成访问。这一实现方式会生成 R_RISCV_PCREL_HI20/R_RISCV_PCREL_LO12_I 的重定位对。下面给出一个使用 medany 代码模型生成代码的示例:
$ 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
$ 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
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=large 代表什么含义?
该选项用于选择large 代码模型。该模型允许程序及其静态定义的符号分布在整个 64 位地址空间中。该代码模型仅在 RISC-V RV64 架构上可用。在 RV32 架构中,medlow 与 medany 代码模型已经能够覆盖整个 4 GiB 地址空间,因此无需提供large 代码模型。请注意,large 代码模型不能与位置无关代码 (-fPIC) 或 GCC 的 -mexplicit-relocs 选项同时使用。
large 代码模型在 GCC 14 和 LLVM 20 中引入。其规范定义见 psABI 第 5.4 节。
与 medlow 和 medany 直接在指令立即数中编码符号地址不同,large 代码模型采用常量池(也称字面池)机制:每个符号的完整 64 位地址会存储在代码附近嵌入的常量池条目中,而代码通过 PC 相对的 auipc/ld 指令对从该常量池加载地址。这意味着:常量池条目必须位于访问该条目的指令 ±2 GiB 范围内,但目标符号本身可以位于整个 64 位地址空间中的任意位置。
为了支持非常大的文本段,每个函数的常量池会保存在与该函数相邻的文本段中。因此,large 代码模型要求文本段同时具备可读和可执行属性。GCC 已通过在文本段中直接生成常量池条目来实现这一机制。相比之下,LLVM 由于内部架构的原因,目前会将常量池条目放在单独的段中,这在文本段非常大的情况下可能会限制可扩展性。
large 代码模型不需要新增任何重定位类型,而是复用了现有重定位机制:
R_RISCV_PCREL_HI20 + R_RISCV_PCREL_LO12_I:用于auipc/ld指令对,从常量池加载地址。R_RISCV_64:用于常量池条目本身,用于保存目标符号的完整 64 位地址。
对于函数调用,large 代码模型同样采用常量池机制。详细说明见下文“函数调用与代码模型 ”一节。
下面给出一个使用large 代码模型生成代码的示例(请注意,必须添加 -fno-PIC 选项):
$ 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=large -fno-PIC
$ cat cmodel.s
main:
ld a5,.LC0
ld a0,0(a5)
snez a0,a0
ret
.align 3
.LC0:
.dword .LANCHOR0
需要注意的是,编译器会生成一条 ld a5,.LC0 的伪指令,用于从常量池条目 (.LC0) 中加载 global_symbol 的地址。该常量池条目保存了完整 64 位地址。随后,汇编器会将该伪指令展开为一对 auipc/ld 指令,以通过 PC 相对方式从常量池中加载该地址:
$ 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.startup 0000000000000000 .text.startup
000000000000000e g F .text.startup 0000000000000010 main
0000000000000000 g O .bss 0000000000000010 global_symbol
Disassembly of section .text.startup:
0000000000000006 <.LC0>:
...
6: R_RISCV_64 .LANCHOR0
000000000000000e <main>:
e: 00000797 auipc a5,0x0
e: R_RISCV_PCREL_HI20 .LC0
e: R_RISCV_RELAX *ABS*
12: ff87b783 ld a5,-8(a5) # 6 <.LC0>
12: R_RISCV_PCREL_LO12_I .L0
12: R_RISCV_RELAX *ABS*
16: 6388 ld a0,0(a5)
18: 00a03533 snez a0,a0
1c: 8082 ret
与 medlow 和 medany 代码模型相比,这里存在几个关键差异:
- 常量池条目
.LC0被放置在文本段中、靠近代码的位置,其中包含global_symbol的完整 64 位地址,并带有R_RISCV_64重定位。 - 位于偏移
0xe和0x12的auipc/ld指令对用于从常量池加载地址,而不是直接加载符号的值。 - 随后的第二条
ld指令(偏移0x16)再使用已加载的地址去访问实际数据。
在完成链接之后,常量池条目会被解析为 global_symbol 的实际地址:
$ riscv64-unknown-linux-gnu-objdump -d cmodel
Disassembly of section .text:
00000000000104a0 <main>:
104a0: 00000797 auipc a5,0x0
104a4: ff87b783 ld a5,-8(a5) # 10498
104a8: 6388 ld a0,0(a5)
104aa: 00a03533 snez a0,a0
104ae: 8082 ret
此时,位于 0x10498 的常量池条目已经包含 global_symbol (0x858d8) 的解析地址,
而 auipc/ld 指令对将在运行时加载该地址。
代码模型总结
| 代码模型 | 寻址方式 | 地址范围 | 指令序列 | 重定位类型 | 适用架构 |
|---|---|---|---|---|---|
| medlow (default) | Absolute (lui) |
-2 GiB ~ +2 GiB | lui + ld/sd |
R_RISCV_HI20 + R_RISCV_LO12_I/S |
RV32, RV64 |
| medany | PC-relative (auipc) |
Any 2 GiB window | auipc + ld/sd |
R_RISCV_PCREL_HI20 + R_RISCV_PCREL_LO12_I/S |
RV32, RV64 |
| large | Constant pool | Full 64-bit space | auipc + ld (pool) + ld/sd |
R_RISCV_PCREL_HI20 + R_RISCV_PCREL_LO12_I + R_RISCV_64 |
RV64 only |
函数调用与代码模型
上表总结了不同代码模型对数据符号的寻址方式,而函数调用的处理方式有所不同。在 medlow 和 medany 代码模型下,函数调用均使用相同的 PC 相对 auipc+jalr 指令序列(带 R_RISCV_CALL_PLT 重定位),与所选代码模型无关。编译器还可能将其优化为 auipc+jr (即 jalr x0)用于尾调用。这意味着,即使在 medlow 模型下,函数调用也不受 −2 GiB 到 +2 GiB 的绝对地址限制,而是可以访问 PC ±2 GiB 范围内的任意目标函数。
以下是使用 medlow/medany 编译的函数调用示例:
$ cat cmodel_call.c
extern void bar(void);
void foo() {
bar();
}
$ riscv64-unknown-linux-gnu-objdump -d -r cmodel_call.o
Disassembly of section .text:
0000000000000000 <foo>:
0: 00000317 auipc t1,0x0
0: R_RISCV_CALL_PLT bar
0: R_RISCV_RELAX *ABS*
4: 00030067 jr t1 # 0 <foo>
这种设计基于一个合理的假设:程序中的函数通常会放置在同一个文本段中,因此任意调用者与被调用函数之间的距离不会超过 2 GiB。在绝大多数实际程序中,这一假设都是成立的。
在large 代码模型下,由于被调用函数可能位于 64 位地址空间的任意位置,函数调用通过常量池实现:
$ riscv64-unknown-linux-gnu-objdump -d -r cmodel_call_large.o
Disassembly of section .text:
0000000000000006 <.LC0>:
...
6: R_RISCV_64 bar
000000000000000e <foo>:
e: 00000317 auipc t1,0x0
e: R_RISCV_PCREL_HI20 .LC0
e: R_RISCV_RELAX *ABS*
12: ff833303 ld t1,-8(t1) # 6 <.LC0>
12: R_RISCV_PCREL_LO12_I .L0
12: R_RISCV_RELAX *ABS*
16: 1141 addi sp,sp,-16
18: e406 sd ra,8(sp)
1a: 9302 jalr t1
1c: 60a2 ld ra,8(sp)
1e: 0141 addi sp,sp,16
20: 8082 ret
编译器会将被调用函数的完整 64 位地址存储在常量池条目 .LC0 中,通过 auipc/ld 指令对将其加载到寄存器 t1 中,然后再使用 jalr t1 进行间接调用。与 medlow/medany 使用的两条指令序列 auipc+jalr 相比,这种方式需要三条指令,并且还需要一个 8 字节的常量池条目。在函数调用频繁的代码中,这种额外开销会迅速累积。因此,large 代码模型应仅在函数确实需要跨越 2 GiB 以上地址范围时才使用。
代码模型与 ABI 的区别
代码模型与应用二进制接口 (ABI) 之间的区别常被误解。ABI 用于规定函数之间的接口规范,而代码模型则决定函数内部代码的生成方式。例如,medlow 和 medany 代码模型将对符号的寻址限制在 32 位偏移范围内,但在基于 RV64I 的系统上,指针仍然以 64 位编码。
由于代码模型不会影响内存中结构体的布局,也不会改变函数之间参数的传递方式,因此对程序来说通常是透明的。也正因为如此,使用不同代码模型编译的函数通常可以互相调用。具体而言,只要各代码模型对符号寻址所施加的限制都能够满足,使用 medlow、medany 和 large 代码模型生成的目标文件通常可以一起链接。这一限制条件通常会自动满足,并且链接器会对其进行安全验证。
相比之下,为两种不同 ABI 生成的代码进行链接则是无效的。设想一下,如果某个函数包含一个 double 类型参数:对于 lp64d 编译的函数,该参数会被期望放在浮点寄存器中。如果该函数被一个按照 lp64 编译的函数调用,而调用方将该参数放在 X 寄存器(整数寄存器)中,那么程序就无法正确运行。
代码模型与链接器松弛
到目前为止,我们尚未讨论代码模型与链接器松弛之间的关系,主要是因为答案很简单:它们可以直接协同工作。所有必要的补丁已经合入各类工具链组件,包括 GNU ld (binutils) 和 LLVM 的 lld(从 LLVM 15 开始支持 RISC-V 的链接器松弛)。
事实上,链接器松弛是一项重要的优化,它对 RISC-V ISA 的设计产生了显著影响:借助链接器松弛,RISC-V 可以省去原本为了在许多代码库中获得合理性能而必需的寻址方式。在 RISC-V 目标平台上,可以使用以下寻址范围:
- 符号位于 0(或
global_pointer$)起的 7 位偏移范围内:占 2 字节。 - 符号位于 0(或
global_pointer$)起的 12 位偏移范围内:占 4 字节。 - 符号位于 0 起的 17 位偏移范围内:占 6 字节。
- 符号位于 0 起的 32 位偏移范围内:占 8 字节。在 RV32I 上即为整个地址空间。
- 超出 32 位偏移范围的符号(large 代码模型):通过常量池实现地址计算,需要 16 字节(由 8 字节
auipc+ld指令对 +8 字节常量池条目组成;最终的ld/sd/jalr访问指令不计入该大小,与上述统计方式保持一致)。
在 2 GiB 的范围内,链接器松弛可以透明地为每一次符号访问选择最短的寻址序列。通过这种方式,仅使用一种代码模型和一种硬件寻址方式(寄存器 + 偏移),并结合 8 种指令格式(U、I、S、CI、CR、CIW、CL、CS),即可实现变长地址编码,而无需任何模式位。对于需要访问完整 64 位地址空间的程序,large 代码模型提供了另一种方案:通过常量池进行地址解析,并且这一机制独立于链接器松弛。这一设计与 ARMv8 的 GCC 实现形成对比——在 ARMv8 上,编译器需要为每一种可能生成的地址生成序列选择不同的代码模型。
不过,目前 large 代码模型的链接器松弛支持仍然比较有限。即使目标符号实际上距离很近、可以使用更短的指令序列访问,链接器也无法将常量池加载序列(auipc + ld + ld/jalr)松弛优化为 medlow/medany 使用的两条指令序列(例如 lui + ld 或 auipc + ld)。这意味着,在 large 代码模型下:每一次全局符号访问或函数调用至少会多出一条额外指令的开销;每个常量池条目还会额外占用 8 字节数据空间。因此,如果程序不需要访问超过 2 GiB 的地址范围,通常应优先选择 medlow 或 medany,以获得更好的代码尺寸和运行性能。
实现变长寻址序列通常是 CISC 处理器的典型特性,这类处理器通过在硬件中实现大量寻址方式,并在汇编阶段根据条件机会性地缩短地址生成序列。RISC-V 采用可融合的多指令寻址序列和链接器松弛的方法,既能保持简单的实现,又能获得相似的代码大小。
作者介绍
Palmer Dabbelt 是原作者之一,曾就职于 SiFive 公司。目前他在 Meta 担任工程师,此前曾在 Rivos 任工程师,并在 Google 担任软件工程师。在更早之前,他曾在 SiFive 担任软件工程总监及工程师。
Kito Cheng 现任 SiFive 首席工程师,同时也是 RISC-V psABI 工作组主席。他负责维护并参与开发 GCC 和 LLVM 中的 RISC-V 后端,推动 ABI 的演进以及编译器对新 ISA 扩展(例如向量扩展和硬件控制流完整性)的支持。
Read more of the All Aboard blog series:
- All Aboard, Part 0: Introduction
- All Aboard, Part 1: The -march, -mabi, and -mtune arguments to RISC-V Compilers
- All Aboard, Part 2: Relocations in ELF Toolchains
- All Aboard, Part 3: Linker Relaxation in the RISC-V Toolchain
- All Aboard, Part 4: The RISC-V Code Models (2026 Edition) (current page)
- All Aboard, Part 5: Per-march and per-mabi Library Paths on RISC-V Systems
- All Aboard, Part 6: Booting a RISC-V Linux Kernel
- All Aboard, Part 7: Entering and Exiting the Linux Kernel on RISC-V
- All Aboard, Part 8: The RISC-V Linux Port is Upstream!
- All Aboard, Part 9: Paging and the MMU in the RISC-V Linux Kernel
- All Aboard, Part 10: How to Contribute to the RISC-V Software Ecosystem
- All Aboard, Part 11: RISC-V Hackathon, Presented by SiFive











