Ianus Inferus/地狱门神

Exoptatus infera advenisti.

View on GitHub Go Back

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 2TSOEnabler。如果想要将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 增加整数除法的说明

Go Back