RISC-V学习笔记
Ianus Inferus
2022-01-05
学习资料
RISC-V Specifications,ISA Specification中,应用程序只需要使用Unprivileged Spec中规定的指令,操作系统、hypervisor还需要使用Privileged Spec中规定的指令。
The RISC-V Reader中文翻译版,初看比较容易,但是缺少一些细节,有些地方会出现每个字都认识但是看不懂的情况,这时候可以阅读Specification中对应的地方。
RISC-V ELF psABI Document,ABI文档,需要从releases中下载。
RISC-V Assembly Programmer’s Manual,汇编手册。
一些注意到的点
模块化
RISC-V是模块化设计的,存在几个基础架构(RV32I、RV32E、RV64I、RV128I)和一些扩展(M、A、F、D等)。由于I(整数)、M(乘除)、A(原子操作)、F(单精度浮点数)、D(双精度浮点数)使用较广,又用G表示这五个模块,比如RV64G表示RV64IMAFD。
基础的RV32I、RV64I比较重要,扩展可以根据需要阅读。RV64I是在RV32I基础上略加修改而成的,需要先阅读RV32I。个人的预测是64位会在带操作系统(Linux、Android)的设备中广泛使用,而32位主要是在bare metal的设备上使用。
寄存器
存在一个常量寄存器x0 = 0,简化了很多需要立即数0的设计。
默认提供32个寄存器,不像x86-32中那样寄存器紧张。专用寄存器较少。每条指令最多可以使用3个寄存器。
内存一致性
RISC-V的内存一致性模型默认为weak memory order,和ARM相似,但支持实现为total store order(保存操作是全序的,同一个核的两个保存操作不会交换顺序),也就是x86中使用的那个。total store order比weak memory order更严格地限制了乱序执行的种类。各CPU架构支持的内存一致性模型可以参考Memory ordering。苹果在M1上转译x86代码是通过在硬件上实现了一种机制可以在两种memory order之间切换,参考How x86 to arm64 Translation Works in Rosetta 2和TSOEnabler。如果想要将x86代码转译到RISC-V上运行,应该需要类似地在硬件上支持total store order。在从x86向RISC-V移植多线程程序时,也需要注意正确使用memory barrier。
内存对齐
RISC-V提供了多种选择,硬件和操作系统可以不支持非对齐内存访问,也可以通过软件或者硬件支持非对齐内存访问。
移位
RV32I中的移位指令SLL、SRL、SRA,和x86、ARM一样,只会使用第二个操作数的最低5位,因此会出现一些不符合直觉的情况,例如
int x = 1;
int y = 32;
int z = x << y; // z == 1
RV64I中也有类似情况
long long x = 1;
long long y = 64;
long long z = x << y; // z == 1
在C中,这是一个不显然的undefined behavior,在第二个操作数不是字面量时需要特别注意,参考Why doesn’t left bit-shift, “«”, for 32-bit integers work as expected when used more than 32 times?。
整数除法
M扩展中定义了整数除法指令DIV/DIVU,与其他常见架构的区别是,除以0时没有硬件异常,而是使用1填充所有位。如果要检查整数除以0时的问题,需要在调用此指令之前检查除数是否为0。
跳转
无条件跳转JAL是相对于指令位置(PC)的,JALR也可以先调用AUIPC再执行来进行相对于PC的跳转。这使得生成position-independent code很容易,而position-independent code无需relocation table entry,方便将代码加载到动态位置。
条件跳转的比较和跳转实现在一条指令中,避免了x86的EFLAGS/RFLAGS状态寄存器。
编译和汇编
编译一个程序
由于RISC-V目前主要的芯片都是嵌入式平台的,所以我们通常进行cross-compile。gcc对于每个target,需要在编译gcc本身时设置对应的参数,非常麻烦,一般使用别人编译好的工具链,这里为了简便我们使用天然支持cross-compile的clang配合musl工具链(下载其中的riscv64-linux-musl-native.tgz)来进行编译。clang版本为11.0.1。
下面假设riscv64-linux-musl-native.tgz中的内容被解压到了/root/riscv64-linux-musl-native
。
cd /root
tar xvzf /path/to/riscv64-linux-musl-native.tgz
写一个最简单的C程序,如下
#include <stdio.h>
int add(int lhs, int rhs)
{
return lhs + rhs;
}
int main(int argc, char** argv)
{
printf("Hello, RISC-V!\n");
printf("Number %d+%d=%d\n", 1, 2, add(1, 2));
return 0;
}
保存为program.c
。
执行编译和链接命令,如下,注意其中的版本号有可能会发生变化
clang -target riscv64-linux-musl --sysroot=/root/riscv64-linux-musl-native --prefix=/root/riscv64-linux-musl-native/usr/lib/gcc/riscv64-linux-musl/11.2.1 -L/root/riscv64-linux-musl-native/usr/lib/gcc/riscv64-linux-musl/11.2.1 -march=rv64g -mabi=lp64d -c program.c -o program.o
clang -target riscv64-linux-musl --sysroot=/root/riscv64-linux-musl-native --prefix=/root/riscv64-linux-musl-native/usr/lib/gcc/riscv64-linux-musl/11.2.1 -L/root/riscv64-linux-musl-native/usr/lib/gcc/riscv64-linux-musl/11.2.1 -march=rv64g -mabi=lp64d -static -o program program.o
这里我们使用了-march=rv64g -mabi=lp64d,即RV64G架构和通过双精度浮点数寄存器传递浮点参数的lp64d应用程序二进制接口规范,其他可选项可以参考这篇文章。
反汇编
执行
llvm-objdump -d program.o
结果如下
program.o: file format elf64-littleriscv
Disassembly of section .text:
0000000000000000 <add>:
0: 13 01 01 fe addi sp, sp, -32
4: 23 3c 11 00 sd ra, 24(sp)
8: 23 38 81 00 sd s0, 16(sp)
c: 13 04 01 02 addi s0, sp, 32
10: 13 86 05 00 mv a2, a1
14: 93 06 05 00 mv a3, a0
18: 23 26 a4 fe sw a0, -20(s0)
1c: 23 24 b4 fe sw a1, -24(s0)
20: 03 25 c4 fe lw a0, -20(s0)
24: 83 25 84 fe lw a1, -24(s0)
28: 3b 05 b5 00 addw a0, a0, a1
2c: 03 34 01 01 ld s0, 16(sp)
30: 83 30 81 01 ld ra, 24(sp)
34: 13 01 01 02 addi sp, sp, 32
38: 67 80 00 00 ret
000000000000003c <main>:
3c: 13 01 01 fa addi sp, sp, -96
40: 23 3c 11 04 sd ra, 88(sp)
44: 23 38 81 04 sd s0, 80(sp)
48: 13 04 01 06 addi s0, sp, 96
4c: 13 06 05 00 mv a2, a0
50: 93 06 00 00 mv a3, zero
54: 23 26 d4 fe sw a3, -20(s0)
58: 23 24 a4 fe sw a0, -24(s0)
5c: 23 30 b4 fe sd a1, -32(s0)
0000000000000060 <.LBB1_1>:
60: 17 05 00 00 auipc a0, 0
64: 13 05 05 00 mv a0, a0
68: 23 3c c4 fc sd a2, -40(s0)
6c: 23 38 d4 fc sd a3, -48(s0)
70: 97 00 00 00 auipc ra, 0
74: e7 80 00 00 jalr ra
78: 93 05 10 00 addi a1, zero, 1
7c: 13 06 20 00 addi a2, zero, 2
80: 23 34 a4 fc sd a0, -56(s0)
84: 13 85 05 00 mv a0, a1
88: 23 30 b4 fc sd a1, -64(s0)
8c: 93 05 06 00 mv a1, a2
90: 23 3c c4 fa sd a2, -72(s0)
94: 97 00 00 00 auipc ra, 0
98: e7 80 00 00 jalr ra
000000000000009c <.LBB1_2>:
9c: 97 05 00 00 auipc a1, 0
a0: 93 85 05 00 mv a1, a1
a4: 23 38 a4 fa sd a0, -80(s0)
a8: 13 85 05 00 mv a0, a1
ac: 83 35 04 fc ld a1, -64(s0)
b0: 03 36 84 fb ld a2, -72(s0)
b4: 83 36 04 fb ld a3, -80(s0)
b8: 97 00 00 00 auipc ra, 0
bc: e7 80 00 00 jalr ra
c0: 83 35 04 fd ld a1, -48(s0)
c4: 23 34 a4 fa sd a0, -88(s0)
c8: 13 85 05 00 mv a0, a1
cc: 03 34 01 05 ld s0, 80(sp)
d0: 83 30 81 05 ld ra, 88(sp)
d4: 13 01 01 06 addi sp, sp, 96
d8: 67 80 00 00 ret
添加汇编代码
将program.c
中的add
函数去掉
#include <stdio.h>
int add(int lhs, int rhs);
int main(int argc, char** argv)
{
printf("Hello, RISC-V!\n");
printf("Number %d+%d=%d\n", 1, 2, add(1, 2));
return 0;
}
增加汇编代码add.S
.global add
add:
addi sp, sp, -32
sd ra, 24(sp)
sd s0, 16(sp)
addi s0, sp, 32
mv a2, a1
mv a3, a0
sw a0, -20(s0)
sw a1, -24(s0)
lw a0, -20(s0)
lw a1, -24(s0)
addw a0, a0, a1
ld s0, 16(sp)
ld ra, 24(sp)
addi sp, sp, 32
ret
执行编译和链接
clang -target riscv64-linux-musl --sysroot=/root/riscv64-linux-musl-native --prefix=/root/riscv64-linux-musl-native/usr/lib/gcc/riscv64-linux-musl/11.2.1 -L/root/riscv64-linux-musl-native/usr/lib/gcc/riscv64-linux-musl/11.2.1 -march=rv64g -mabi=lp64d -c program.c -o program.o
clang -target riscv64-linux-musl --sysroot=/root/riscv64-linux-musl-native --prefix=/root/riscv64-linux-musl-native/usr/lib/gcc/riscv64-linux-musl/11.2.1 -L/root/riscv64-linux-musl-native/usr/lib/gcc/riscv64-linux-musl/11.2.1 -march=rv64g -mabi=lp64d -c add.S -o add.o
clang -target riscv64-linux-musl --sysroot=/root/riscv64-linux-musl-native --prefix=/root/riscv64-linux-musl-native/usr/lib/gcc/riscv64-linux-musl/11.2.1 -L/root/riscv64-linux-musl-native/usr/lib/gcc/riscv64-linux-musl/11.2.1 -march=rv64g -mabi=lp64d -static -o program program.o add.o
尝试将addw
指令改为subw
指令,即将加法改为减法,编译。
运行
模拟器
一般使用QEMU来进行模拟,安装还挺麻烦的。
真机
可以买个开发板,比如说我买的是全志的哪吒开发板。
哪吒开发板的情况可以参考网友的视频,使用USB转接串口来进行命令行输入输出,使用Android的adb命令上传下载文件。哪吒开发板的CPU构架为RV64GCV,注意其中的V扩展(向量扩展)为0.7.1-Workshop草稿版本,如果需要使用建议使用全志的官方SDK。
其他
C++
C++和C的主要区别在于需要C++标准库。如果使用clang-libc++-musl,需要自行编译一下libc++。另外可能需要-mno-relax
选项。
我开发的TypeMake已经支持了RISC-V。
修改记录
2022-01-25 增加整数除法的说明