☰
RISC-V入门必读:base ISA与ABI寄存器约定详解
2026/10/4 13:50:02 网站建设 项目流程

1. 从零上手 RISC-V:为什么 base ISA 和 ABI 寄存器约定是绕不开的第一道坎

接触 RISC-V 的人,十有八九是从一块开发板或者一个模拟器开始的。你可能已经跑通了第一个裸机程序,或者用工具链编译了一个简单的 C 程序烧进去,看着串口打印出 “Hello RISC-V” 心里挺美。但接下来想深入一点,比如自己写个启动文件、对接一下中断、或者把 FreeRTOS 往上搬的时候,大概率会卡在同一个地方:寄存器到底该怎么用,谁负责保存,谁可以随便改。这个问题不搞清楚,写出来的代码要么跑飞,要么在某个不经意的函数调用后数据全乱套。

这就是 base ISA 和 ABI 寄存器约定要解决的事。base ISA 定义了处理器能看懂哪些指令、有哪些寄存器、内存怎么访问,它是硬件和软件之间的第一层契约。而 ABI 寄存器约定则是在此之上,规定了函数调用时参数放哪、返回值放哪、哪些寄存器由调用者保存、哪些由被调用者保存。两者一个管“能不能做”,一个管“怎么做才不乱”。对于做 RISC-V CPU 设计的人来说,base ISA 是你要实现的指令集子集;对于写底层软件的人来说,ABI 约定是你写汇编和 C 混合代码时必须刻在脑子里的规则。

这篇文章适合两类人看。一类是刚开始学 RISC-V、准备自己写启动代码或者移植操作系统的嵌入式开发者,另一类是对 RISC-V 处理器设计感兴趣、想搞清楚指令集和调用规范之间关系的学生或工程师。我会从 base ISA 的寄存器结构讲起,把 RV32I 和 RV64I 的差异、x0 到 x31 的用途、ABI 名称的由来、调用者保存与被调用者保存的划分逻辑,以及实际写代码时怎么用这些规则,全部拆开揉碎讲一遍。中间会穿插一些我在实际项目里踩过的坑,比如栈对齐没做导致浮点运算崩溃、临时寄存器用错导致中断返回后数据错乱之类的,希望能帮你少走点弯路。

2. base ISA 到底定义了什么:寄存器、指令格式与地址空间

2.1 通用寄存器的物理布局与设计哲学

RISC-V 的 base ISA 定义了一个非常规整的通用寄存器堆,总共 32 个整数寄存器,在 RV32I 里每个宽度是 32 位,在 RV64I 里是 64 位。编号从 x0 到 x31,这个编号是硬件层面的,你在写汇编的时候可以直接用 x0、x1 这样的名字,也可以用 ABI 名称如 zero、ra、sp 等。硬件只认编号,ABI 名称是给人和编译器看的。

x0 这个寄存器很特殊,它被硬连线为常量 0。什么意思呢?就是你读它永远得到 0,写它会被直接丢弃,不会报错也不会有任何效果。这个设计看起来有点浪费,但实际上非常巧妙。它让很多指令可以简化,比如addi x0, x0, 0就成了一条空操作指令 nop,不需要单独设计 nop 指令。再比如mv x1, x2实际上就是addi x1, x2, 0的伪指令形式。有了 x0,指令编码可以更规整,译码逻辑也更简单。

剩下的 31 个寄存器没有硬件强制规定的用途,理论上你可以在汇编里随便用。但一旦涉及到函数调用、中断处理、操作系统,就必须遵守 ABI 约定,否则你写的代码没法跟编译器生成的代码对接,也没法跟库函数配合。这就是为什么理解 ABI 寄存器约定比单纯记住寄存器编号更重要。

2.2 RV32I 与 RV64I 的关键差异

base ISA 目前最常用的是 RV32I 和 RV64I 两个版本,I 表示整数指令集,是必须实现的基础。RV32I 有 32 个 32 位寄存器,地址空间是 32 位,也就是 4GB。RV64I 把寄存器宽度和地址空间都扩展到 64 位,但寄存器个数还是 32 个。

这里有个容易混淆的点:RV64I 的寄存器是 64 位宽,但指令长度还是 32 位(不考虑压缩指令集)。这意味着指令里能直接编码的立即数范围变小了,所以 RV64I 增加了一些新的指令来加载 64 位立即数,比如lui和addiw的组合用法就跟 RV32I 不太一样。另外 RV64I 里所有涉及字操作的指令,比如addw、subw、sllw,都是对低 32 位做运算,然后符号扩展到 64 位。这个细节在写汇编的时候特别容易出错,我见过有人在 RV64 上做 32 位加法忘了用addw,结果高位垃圾数据导致条件判断完全错误。

还有一个实际项目里经常遇到的差异:RV32I 的函数参数和返回值用 a0-a7 这 8 个寄存器传递,RV64I 也是一样,但因为寄存器变宽了,传递 64 位指针或者 long long 类型时就不用像 RV32 那样拆成两个寄存器了。这个差异在移植代码的时候要特别注意,尤其是涉及指针运算和结构体传参的地方。

2.3 指令格式与寄存器编码的对应关系

RISC-V 的指令格式有 R 型、I 型、S 型、B 型、U 型、J 型六种,每种格式里寄存器的位置和位数都是固定的。比如 R 型指令里,rs1、rs2、rd 各占 5 位,正好能覆盖 32 个寄存器。这个 5 位的设计不是随便定的,它直接决定了寄存器堆的大小就是 32 个,不能再多了。

为什么是 32 个而不是 16 个或者 64 个?16 个寄存器对于编译器优化来说太少了,很多临时变量放不下,会导致频繁的栈读写。64 个寄存器又会让指令编码变长,因为每个寄存器字段要 6 位,三条寄存器操作数的指令就会多出 3 位,整个指令长度可能要从 32 位变成 40 位甚至更多,这对代码密度和译码复杂度都不利。32 个寄存器是一个经过权衡的甜点值,既能满足大部分编译需求,又能保持指令长度固定为 32 位。

在实际写汇编或者看反汇编的时候,你会发现指令里的寄存器编号就是 x0 到 x31 的二进制编码。比如add x1, x2, x3对应的机器码里,rd 字段是 1,rs1 字段是 2,rs2 字段是 3。理解这个对应关系对调试很有帮助,尤其是当你只有机器码没有反汇编器的时候,可以手动译码看看寄存器用得对不对。

3. ABI 寄存器约定:函数调用背后的隐形规则

3.1 调用者保存与被调用者保存的划分逻辑

ABI 寄存器约定最核心的内容就是把 32 个寄存器分成两类:调用者保存(caller-saved)和被调用者保存(callee-saved)。这个划分的逻辑其实很直观:如果一个寄存器被调用者要用,但调用者之后还要用它的值,那被调用者就得先保存再恢复,这就是被调用者保存。反过来,如果调用者知道某个寄存器在函数调用后可能被改掉,那调用者自己在调用前就得保存,这就是调用者保存。

具体到 RISC-V 的 ABI,被调用者保存的寄存器是 s0-s11(也就是 x8、x9、x18-x27),还有 sp(x2)。调用者保存的寄存器包括 t0-t6(x5-x7、x28-x31)、a0-a7(x10-x17),以及 ra(x1)。ra 比较特殊,它虽然是调用者保存,但通常由被调用者在入口处保存到栈上,因为被调用者自己还要调用别的函数,需要把返回地址存起来。

这个划分不是拍脑袋定的,它直接影响了编译器的寄存器分配策略。编译器会尽量把生命周期长的变量放在 s 寄存器里,因为它们在函数调用后还能保持;把临时变量放在 t 寄存器里,因为不需要额外保存。你在写汇编的时候如果乱用寄存器,比如在一个循环里用了 t0 然后又调用了函数,那 t0 的值很可能就被破坏了,这种 bug 特别难查,因为编译器生成的代码和你的汇编混在一起,出问题的地方往往离现场很远。

3.2 参数传递与返回值:a0-a7 和栈的配合

RISC-V 的 ABI 规定,函数的前 8 个整型参数用 a0-a7 传递,超出的部分通过栈传递。返回值放在 a0 和 a1 里,a0 是主返回值,a1 是辅助返回值,比如返回一个结构体的时候会用到。浮点参数用 fa0-fa7 传递,浮点返回值用 fa0。

这里有个细节值得注意:参数在栈上的布局是从右往左还是从左往右?RISC-V 的约定是,超出寄存器范围的参数从栈顶开始依次排列,而且栈指针 sp 在函数入口处必须保持 16 字节对齐。这个 16 字节对齐的要求经常被忽略,尤其是在手写汇编的时候。我见过一个案例,有人在中断处理程序里手动调整了 sp 但没有保持对齐,结果调用了浮点运算库函数,库函数里用了对齐的向量加载指令,直接触发异常。查了半天才发现是栈对齐的问题。

另外,结构体传参的规则也比较绕。小于等于两个 XLEN 的结构体会用寄存器传递,大于两个 XLEN 的通过栈传递,而且传递的是结构体的副本。这个规则在写 C 和汇编混合代码的时候要特别小心,因为汇编里你得手动按照 ABI 的布局去取参数,搞错了就会读到垃圾数据。

3.3 栈帧布局与帧指针的取舍

标准的 RISC-V 栈帧布局是这样的:调用者在调用前把超出寄存器的参数压栈,然后执行 call 指令,call 会把返回地址放到 ra 里。被调用者在入口处先调整 sp 开辟自己的栈帧,然后把需要保存的寄存器和 ra 压栈。如果用了帧指针 fp(也就是 s0),还要把旧的 fp 保存起来,然后把当前 sp 赋给 fp。

帧指针用不用,在 RISC-V 社区里一直有争论。用了 fp 的好处是栈回溯方便,调试器可以顺着 fp 链找到调用栈。不用 fp 的好处是省了一个寄存器,而且少了两条指令(保存旧 fp 和设置新 fp)。在资源紧张的嵌入式场景里,很多人选择不用 fp,把 s0 当普通寄存器用。但这样一来,如果程序崩溃了,你只能靠 sp 和 ra 去猜调用栈,调试难度会大不少。

我的建议是,在开发阶段打开 fp,方便调试;在发布版本里根据实际情况决定是否关掉。GCC 和 Clang 都有-fomit-frame-pointer选项来控制这个行为。如果你在写启动代码或者中断处理程序,那 fp 基本用不上,因为那些代码通常不遵循标准的函数调用约定。

4. 实操:手写汇编验证 ABI 寄存器约定的行为

4.1 搭建一个最小的裸机测试环境

要验证 ABI 寄存器约定,最直接的办法就是写一段汇编,调用一个 C 函数,然后观察寄存器的变化。我用的是 QEMU 的 virt 机器,配合 riscv64-unknown-elf-gcc 工具链。如果你手头有真实的开发板,比如基于 GD32V 或者 ESP32-C3 的板子,也可以,但 QEMU 更方便观察寄存器状态。

先准备一个链接脚本,把代码段定位到 0x80000000,这是 QEMU virt 机器的内存起始地址。然后写一个启动汇编,设置好 sp,然后调用 main。启动汇编里不需要遵守完整的 ABI,因为这是最底层的入口,但 sp 必须设置,而且最好 16 字节对齐。

.section .text.start .globl _start _start: la sp, _stack_top andi sp, sp, -16 call main j .

这段代码里,la是加载地址的伪指令,andi sp, sp, -16确保 sp 低 4 位清零,也就是 16 字节对齐。call main会跳转到 main 并把返回地址放到 ra 里。如果 main 正常返回,就会执行j .进入死循环。

4.2 编写测试函数观察寄存器保存行为

接下来写一个 C 函数,里面嵌入汇编来观察 t 寄存器和 s 寄存器的变化。思路是这样的:在调用一个函数之前,把 t0 和 s0 设置成特定的值,调用之后再看它们有没有变。按照 ABI,t0 应该被破坏,s0 应该保持不变。

register long t0_val asm("t0") = 0x1111; register long s0_val asm("s0") = 0x2222; void callee(void) { asm volatile("li t0, 0x3333"); asm volatile("li s0, 0x4444"); } int main(void) { asm volatile("li t0, 0x1111"); asm volatile("li s0, 0x2222"); callee(); asm volatile("mv %0, t0" : "=r"(t0_val)); asm volatile("mv %0, s0" : "=r"(s0_val)); // 此时 t0_val 应该是 0x3333,s0_val 应该是 0x2222 return 0; }

编译运行后,如果你在调试器里看寄存器,会发现 t0 变成了 0x3333,而 s0 还是 0x2222。但这里有个陷阱:callee 函数里直接改了 s0 却没有保存和恢复,这违反了 ABI。编译器在优化的时候可能会假设 s0 在函数调用后不变,从而生成错误的代码。正确的做法是,如果 callee 要用 s0,必须在入口处压栈保存,在返回前恢复。

这个实验说明了一个重要问题:ABI 约定是编译器和你之间的契约,你违反了它,编译器不会报错,但生成的代码行为就是未定义的。在实际项目里,手写汇编函数一定要严格遵守 ABI,否则出了问题很难定位。

4.3 用反汇编验证编译器的寄存器分配

写完 C 代码后,用riscv64-unknown-elf-objdump -d反汇编看看编译器生成的汇编代码。你会发现编译器在函数入口处会有一组压栈指令,把 ra 和用到的 s 寄存器保存起来。比如:

main: addi sp, sp, -32 sd ra, 24(sp) sd s0, 16(sp) ... call callee ... ld ra, 24(sp) ld s0, 16(sp) addi sp, sp, 32 ret

这段代码里,sp 减了 32,保持了 16 字节对齐。ra 和 s0 被保存到栈上,返回前恢复。如果你在写汇编的时候也遵循这个模式,就能和编译器生成的代码无缝对接。反过来,如果你自己写的汇编函数没有保存 s 寄存器,而 C 代码里又假设 s 寄存器不变,那就会出现数据错乱。

我建议你在学习阶段多看看反汇编,尤其是编译器优化级别从 -O0 到 -O2 的变化。你会发现 -O0 的时候编译器很老实,每个变量都往栈上放;-O2 的时候寄存器分配很激进,t 寄存器被反复复用,s 寄存器只在真正需要跨调用的时候才保存。理解这些行为,对你写高效的汇编和调试优化后的代码都很有帮助。

5. 常见问题与排查技巧实录

5.1 栈对齐导致的异常排查

栈对齐问题是我遇到最多的坑之一。RISC-V 的 ABI 要求 sp 在函数入口处 16 字节对齐,但很多手写汇编或者不规范的代码会破坏这个规则。症状通常是调用某些库函数时触发异常,比如浮点运算、向量操作、或者用了对齐加载指令的 memcpy。

排查方法很简单:在异常处理程序里打印出 mcause 和 mtval,看看是不是 load/store 地址不对齐。如果是,再检查 sp 的值是不是 16 的倍数。我通常会在启动代码里加一句andi sp, sp, -16,确保初始 sp 是对齐的。然后在每个手写汇编函数的入口处检查 sp 有没有被意外修改。

还有一个隐蔽的情况:中断处理程序里如果用了浮点寄存器,但没有保存和恢复,也会导致浮点状态错乱。RISC-V 的浮点寄存器是独立的,ABI 里把 fa0-fa7 和 ft0-ft11 都归为调用者保存,但中断处理程序如果不保存这些寄存器,被中断的浮点运算就会出错。这个问题的排查方法是,在中断入口处把所有浮点寄存器都压栈,返回前恢复,看看问题是否消失。

5.2 临时寄存器误用导致的数据错乱

t 寄存器是调用者保存的,意味着函数调用后它们的值可能被改掉。但有些人在写汇编的时候会忘记这一点,在一个循环里用了 t0 保存循环计数,然后循环体里调用了函数,结果 t0 被改,循环次数就乱了。

这种 bug 的特点是:单步调试的时候看起来正常,因为单步不会触发函数调用;全速运行就出错,而且出错的位置和实际原因往往隔得很远。排查方法是,在怀疑的代码段前后打印 t 寄存器的值,看看调用前后有没有变化。如果变了,那就说明有函数调用破坏了 t 寄存器,你需要把循环计数放到 s 寄存器里,或者在调用前手动保存 t 寄存器。

我个人的习惯是,在写汇编函数的时候,先列出这个函数用到了哪些寄存器,然后对照 ABI 表格,看看哪些需要保存。t 寄存器默认不保存,但如果函数内部有调用,那调用前要把还需要用的 t 寄存器压栈。s 寄存器如果用了,必须在入口处保存,返回前恢复。这个习惯能避免大部分寄存器相关的 bug。

5.3 参数传递错误的快速定位方法

参数传递错误通常表现为函数收到的参数不对,或者返回值不对。排查的时候,首先确认参数个数有没有超过 8 个,超过的话要看栈上的参数布局对不对。RISC-V 的栈上参数是从低地址到高地址依次排列的,第一个栈参数在 sp+0 的位置,第二个在 sp+8(RV64)或 sp+4(RV32)。

如果参数个数没超过 8 个,那就检查 a 寄存器的值对不对。可以在调用前用调试器看 a0-a7 的值,然后在被调用函数入口处再看一遍,看看有没有变化。如果调用前是对的,入口处变了,那可能是 call 指令或者跳转指令出了问题。如果入口处是对的,函数内部用错了,那就是函数自己的问题。

返回值错误的话,检查 a0 和 a1 有没有被正确设置。有些函数会返回结构体,这时候 a0 和 a1 一起用,a0 是低地址部分,a1 是高地址部分。如果只设置了 a0 没设置 a1,返回值就会不对。这个细节在写返回结构体的汇编函数时特别容易忽略。

5.4 常见问题速查表

问题现象可能原因排查方法解决措施
调用库函数时触发异常sp 未 16 字节对齐检查 sp 低 4 位是否为零入口处andi sp, sp, -16
循环次数错乱t 寄存器被函数调用破坏调用前后打印 t 寄存器值改用 s 寄存器或调用前压栈
浮点运算结果异常浮点寄存器未保存检查中断处理程序是否保存 fa/ft 寄存器中断入口压栈所有浮点寄存器
函数参数错误栈上参数布局不对检查 sp+0、sp+8 处的值按 ABI 布局重新排列参数
返回值错误a1 未设置检查 a0 和 a1 的值返回结构体时同时设置 a0 和 a1
中断返回后数据错乱中断处理程序破坏了 s 寄存器检查中断入口是否保存 s0-s11中断入口保存所有用到的 s 寄存器

这张表里的问题我都实际遇到过,尤其是栈对齐和 t 寄存器误用这两个,几乎每个刚接触 RISC-V 汇编的人都会踩一遍。我的建议是,在写任何汇编代码之前,先把 ABI 寄存器约定打印出来贴在显示器旁边,写几行就对照一下,养成习惯之后就不容易出错了。

6. 从 base ISA 到 ABI:CPU 设计者需要关注什么

6.1 硬件实现寄存器堆时的取舍

如果你是在做 RISC-V CPU 设计,那 base ISA 里的寄存器堆是你必须实现的第一个模块。32 个寄存器,x0 硬连线为 0,其余 31 个可读写。听起来简单,但实际实现的时候有几个点要注意。

首先是读端口和写端口的数量。RISC-V 的指令格式决定了大部分指令需要同时读两个寄存器、写一个寄存器,所以寄存器堆至少要有两个读端口和一个写端口。如果你想做双发射或者乱序执行,那端口数量还要增加。端口多了,面积和功耗都会上去,所以要在性能和成本之间做权衡。

其次是 x0 的处理。有些实现会把 x0 也做成一个真实的寄存器,只是在写的时候忽略写入,读的时候返回 0。但更常见的做法是直接在读端口上做多路选择,如果读的是 x0 就返回 0,不实际访问寄存器堆。这样能省一点面积,也能避免 x0 被意外写入。

还有一个细节是寄存器的复位值。RISC-V 没有规定寄存器在上电后的初始值,所以你不能假设它们是 0。启动代码必须显式地初始化 sp、gp 等寄存器,否则程序行为不可预测。我在实际项目里见过因为忘了初始化 gp 导致全局变量访问出错的情况,查了好久才发现是 gp 没设置。

6.2 指令译码与寄存器索引的对应

指令译码的时候,rs1、rs2、rd 这三个字段的提取是固定的。R 型指令里,rs1 在 bit 15-19,rs2 在 bit 20-24,rd 在 bit 7-11。I 型指令里,rs1 在 bit 15-19,rd 在 bit 7-11,没有 rs2。S 型指令里,rs1 在 bit 15-19,rs2 在 bit 20-24,没有 rd。B 型指令和 S 型类似,但立即数的编码方式不同。

这些字段的位置是硬件译码器必须硬编码的,不能改。你在写 Verilog 或者 Chisel 的时候,直接按位提取就行。但要注意,提取出来的 5 位索引要直接送到寄存器堆的读地址端口,中间不要加额外的逻辑,否则会增加关键路径延迟。

如果你在实现压缩指令集,那译码会更复杂一些,因为压缩指令的寄存器字段位置和 32 位指令不一样。但 base ISA 的译码相对简单,先把这部分做稳,再考虑扩展。

6.3 异常与中断对寄存器约定的影响

异常和中断会打断正常的指令流,硬件需要保存一些状态,软件需要保存寄存器。RISC-V 的硬件在进入异常时会自动把当前 pc 保存到 mepc,把异常原因保存到 mcause,但不会自动保存通用寄存器。这意味着异常处理程序必须自己保存和恢复用到的寄存器。

这里有个设计上的选择:是保存所有寄存器,还是只保存用到的?保存所有寄存器简单粗暴,但性能差;只保存用到的需要编译器或者程序员分析,容易出错。常见的做法是,在异常入口处保存 ra、sp、gp、tp、t0-t6、a0-a7 这些调用者保存的寄存器,以及用到的 s 寄存器。浮点寄存器如果用了也要保存。

中断返回的时候,用 mret 指令,它会从 mepc 恢复 pc,从 mstatus 恢复中断使能状态。但通用寄存器需要软件自己恢复。如果保存和恢复的顺序不对,或者漏了某个寄存器,就会出现数据错乱。我的经验是,写一个宏来统一保存和恢复,避免手写出错。

7. 实际项目中的经验与建议

7.1 工具链选择与编译选项的影响

RISC-V 的工具链主要有 GCC 和 Clang 两个选择。GCC 的支持更成熟,尤其是对 RV32I 和 RV64I 的基础支持很稳定。Clang 的代码生成质量在某些场景下更好,但工具链的完整性稍差一些。我一般用 GCC 做嵌入式开发,用 Clang 做性能分析。

编译选项里,-mabi和-march是两个必须设置的。-march=rv32i表示目标架构是 RV32I,-mabi=ilp32表示 ABI 是 32 位整数、长整数和指针都是 32 位。如果是 RV64I,那就是-march=rv64i和-mabi=lp64。这两个选项必须匹配,否则链接的时候会报错。

还有一个选项是-mcmodel,控制代码和数据的寻址范围。默认是medlow,代码和数据都在 2GB 范围内。如果程序比较大,可能需要改成medany,让编译器用更灵活的寻址方式。这个选项在写大型项目的时候要注意,选错了会导致链接错误或者运行时地址计算错误。

7.2 调试技巧:如何快速定位寄存器相关问题

调试寄存器相关的问题,最有效的工具是调试器和反汇编。我通常会在出问题的函数入口和出口处设置断点,然后观察寄存器的变化。如果发现某个寄存器的值不符合预期,就往前追溯,看看是哪条指令改的。

还有一个技巧是在代码里插入汇编断点,比如ebreak指令。执行到 ebreak 的时候,调试器会停下来,你可以检查所有寄存器的状态。这个方法比打印调试更直接,因为打印本身会用到寄存器和栈,可能会掩盖问题。

如果问题只在优化后的代码里出现,那大概率是编译器假设了某些 ABI 规则,而你的代码违反了。这时候可以试试降低优化级别,看看问题是否消失。如果消失了,那就说明是 ABI 违规导致的。然后对照 ABI 表格,检查你的汇编代码有没有正确保存和恢复寄存器。

7.3 给初学者的学习路径建议

如果你是刚开始学 RISC-V,我的建议是先不要碰复杂的 SoC 或者操作系统,从最小的裸机程序开始。写一个启动汇编,设置 sp,调用一个 C 函数,点亮一个 LED 或者打印一个字符。这个过程能让你理解 base ISA 的寄存器结构和最基本的 ABI 规则。

然后写几个汇编函数,练习参数传递和返回值。比如写一个加法函数,参数用 a0 和 a1,返回值用 a0。再写一个函数,用 s 寄存器保存中间结果,练习压栈和出栈。这些练习看起来简单,但能帮你把 ABI 约定刻在脑子里。

接下来可以尝试写一个简单的异常处理程序,理解 mepc、mcause、mstatus 这些 CSR 的用法。然后试着在异常处理程序里保存和恢复寄存器,确保被中断的程序能正确恢复执行。这个过程中你会遇到栈对齐、寄存器保存顺序等问题,解决这些问题能让你对 ABI 的理解更上一层楼。

最后再去看操作系统的移植或者 CPU 的 RTL 实现,那时候你会发现,base ISA 和 ABI 寄存器约定就像地基一样,虽然不显眼,但上面的所有东西都靠它撑着。地基没打好,上面盖什么都会塌。

我在实际项目里最大的体会是,RISC-V 的文档虽然齐全,但很多细节散落在不同的规范里。base ISA 在指令集手册里,ABI 在调用规范文档里,CSR 在特权架构手册里。你需要把这些文档对照着看,才能拼出完整的图景。刚开始可能会觉得乱,但看多了就会发现,这些规范的设计其实很一致,理解了 base ISA 的寄存器结构,ABI 的约定就顺理成章了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询