Skip to content
Published at:

04. 第一个 RISC-V 程序

在开始学习具体指令之前,先亲手运行一个完整的 RISC-V 程序。本章使用在线模拟器,零安装即可运行和调试汇编代码。

使用 RISC-V ALE 在线环境

本教程前 12 章使用 RISC-V ALE(Assembly Language Environment),浏览器直接运行,无需安装任何工具链。

界面布局

  • 左侧:代码编辑器(支持语法高亮,Ctrl+Enter 或点击 Run 运行)
  • 右侧上半:寄存器面板(显示 x0-x31 的当前值,十六进制)
  • 右侧下半:内存面板(以地址-字节的表格形式显示)

基本操作

  1. 打开 https://riscv-programming.org/ale/
  2. 将本章代码粘贴到编辑器
  3. 点击 Run 执行
  4. 观察寄存器变化和输出

ALE 环境说明

  • ALE 模拟的是 Linux RV64 用户态环境(riscv64-linux-gnu ABI)
  • 支持 ecall 系统调用(write、exit、brk 等常用 syscall)
  • 默认启用 RV64IMAFDC(基础 + 乘除 + 原子 + 浮点 + 压缩指令)
  • 内存默认从 0x10000 开始分配

Hello World

将以下代码粘贴到 ALE 编辑器并点击 Run,右侧 Console 区域会输出 "Hello, RISC-V!":

asm
.section .text          # 代码段开始
.globl _start           # 声明 _start 为全局入口
_start:
    # --- write(1, msg, 14) ---
    li a7, 64           # a7 = 64: syscall号 write
    li a0, 1            # a0 = 1:  fd = stdout(标准输出)
    la a1, msg          # a1 = msg地址: 要输出的字符串首地址
    li a2, 15           # a2 = 15: 输出字节数(含换行符)
    ecall               # 触发系统调用,内核执行 write

    # --- exit(0) ---
    li a7, 93           # a7 = 93: syscall号 exit
    li a0, 0            # a0 = 0:  退出码 0(成功)
    ecall               # 触发系统调用,进程退出

.section .data          # 数据段开始
msg:
    .ascii "Hello, RISC-V!\n"

逐行注解

指令含义
.section .text伪指令告诉汇编器以下内容放入代码段
.globl _start伪指令_start 符号导出为全局可见,链接器/加载器需要它作为入口
_start:标签程序入口点。注意 Linux 程序的真正入口是 _start(不是 main
li a7, 64伪指令 (Load Immediate)将立即数 64 加载到寄存器 a7。64 是 Linux RISC-V 的 write 系统调用号
li a0, 1伪指令a0 = 1,write 的第一个参数:文件描述符,1 = stdout
la a1, msg伪指令 (Load Address)a1 = msg 的内存地址,write 的第二个参数:缓冲区指针
li a2, 14伪指令a2 = 14,write 的第三个参数:写入字节数(字符串 "Hello, RISC-V!\n" 为 14 字节)
ecall指令触发环境调用(Environment Call),从用户态陷入内核态,执行 a7 指定的系统调用
li a7, 93伪指令a7 = 93,exit 系统调用号
li a0, 0伪指令a0 = 0,exit 的参数:退出码(0 = 正常退出)
.section .data伪指令以下内容放入数据段
msg:标签字符串起始地址的符号名
.ascii "Hello, RISC-V!\n"伪指令在内存中逐字节存放 ASCII 字符,\n 是换行符(0x0A)

关键理解

  • lila伪指令(pseudo-instruction),汇编器会将其展开为 1-2 条真实指令。例如 li a0, 1 会被展开为 addi a0, x0, 1。第 9 章会详细讲解伪指令体系
  • ecall 是 RISC-V 唯一的用户态到内核态入口,所有系统调用都通过它完成。Linux on RISC-V 约定:a7 存 syscall 号,a0-a5 传参,a0 存返回值
  • 程序最后必须调用 exit 系统调用,否则 CPU 会继续执行 msg 后面的不管什么数据,把它当成指令来译码——结果通常是崩溃(segfault)

运行结果:执行后在 ALE 的 Console 区域看到:

Hello, RISC-V!

寄存器面板中,a0 最终为 0(exit code),a7 为 93(最后执行的 syscall 号)。

RISC-V 六种指令格式

指令格式(instruction format)定义了 32 位机器码中各位域的含义。所有 RV64I 基础指令的数字编码共享六种格式——这种规整性是 RISC-V 译码硬件简单(因而功耗低、面积小)的根本原因。

格式一览(bit 31 到 bit 0):

R-type ┌─────────┬───────┬───────┬─────┬───────┬─────────┐
       │ funct7  │ rs2   │ rs1   │funct3│  rd   │ opcode  │
       └────7────┴───5───┴───5───┴──3───┴───5───┴────7────┘

I-type ┌────────────────────┬───────┬─────┬───────┬─────────┐
       │     imm[11:0]      │ rs1   │funct3│  rd   │ opcode  │
       └─────────12─────────┴───5───┴──3───┴───5───┴────7────┘

S-type ┌───────┬───────┬───────┬─────┬───────┬─────────┐
       │imm[11:5]│ rs2   │ rs1   │funct3│imm[4:0]│ opcode  │
       └───7────┴───5───┴───5───┴──3───┴───5────┴────7────┘

B-type ┌───────────┬───────┬───────┬─────┬──────────┬─────────┐
       │imm[12|10:5]│ rs2   │ rs1   │funct3│imm[4:1|11]│ opcode  │
       └─────7──────┴───5───┴───5───┴──3───┴────5─────┴────7────┘

U-type ┌──────────────────────────┬───────┬─────────┐
       │       imm[31:12]         │  rd   │ opcode  │
       └───────────20─────────────┴───5───┴────7────┘

J-type ┌──────────────────────────┬───────┬─────────┐
       │  imm[20|10:1|11|19:12]  │  rd   │ opcode  │
       └───────────20─────────────┴───5───┴────7────┘

各格式的用途

格式用途实例
R-type三寄存器运算add x5, x6, x7
I-type立即数运算 + Load + JALRaddi x5, x6, 10, ld x5, 8(x6), jalr x1, 0(x5)
S-typeStore 指令sd x5, 8(x6)
B-type条件分支beq x5, x6, label
U-typeLUI, AUIPClui x5, 0x12345
J-typeJAL(无条件跳转并链接)jal x1, label

关键设计:字段位置的一致性。注意所有六种格式中:

  • opcode 始终在位 [6:0]——译码器第一眼就知道指令类型
  • rd 始终在位 [11:7](S-type 和 B-type 没有 rd,但 imm 在这些位置同样规整)
  • rs1 始终在位 [19:15]
  • rs2 始终在位 [24:20](前提是该格式需要 rs2)
  • funct3 始终在位 [14:12]

这不是巧合——而是 RISC-V 精心设计的结果。统一的字段位置让译码器在不知道指令具体是什么之前就可以提前读出 rd/rs1/rs2 的寄存器号,开始寄存器文件的读取。对比 x86 的变长编码(译码器必须先看 1-4 字节前缀才能确定指令边界,再看 ModR/M 字节才能确定操作数),RISC-V 的译码几乎可以用组合逻辑完成。

汇编与反汇编

汇编(assemble)和反汇编(disassemble)是互逆的过程,它们之间的桥梁就是指令格式中定义的位域映射。

汇编:助记符 $\to$ 机器码

add x5, x6, x7 为例:

add x5, x6, x7            # 汇编助记符

字段分解:
  opcode  = 0110011        # R-type 标识
  rd      = x5  = 00101    # 目标寄存器编号
  funct3  = 000            # ADD 操作码
  rs1     = x6  = 00110    # 源寄存器1编号
  rs2     = x7  = 00111    # 源寄存器2编号
  funct7  = 0000000        # ADD 变体标识

拼接: 0000000_00111_00110_000_00101_0110011
Hex:  0x00_73_02_B3        # 每字节一组的十六进制表示

汇编器的工作就是查表:根据助记符填 opcode + funct3 + funct7,根据操作数填 rd/rs1/rs2 或立即数位域。

反汇编:机器码 $\to$ 助记符

给定 32 位机器码 0x00830283

机器码: 0x00830283 = 0000_0000_1000_0011_0010_0010_1000_0011

字段分解:
  opcode  = 0000011         # bits [6:0]  → 这是 Load 指令
  查看 funct3 = 010          # bits [14:12] → LW (load word)
  因此格式为 I-type:
    rd    = 00101 = x5      # bits [11:7]
    rs1   = 00110 = x6      # bits [19:15]
    imm   = 000000001000     # bits [31:20] → 8

结果: lw x5, 8(x6)          # "从 x6+8 地址处读 4 字节到 x5"

opcode 表速查(RISC-V 基础指令的操作码分类):

opcode[6:0]类型意义
0110011R-type寄存器-寄存器整数运算
0010011I-type立即数整数运算
0000011I-type整数 Load
0100011S-type整数 Store
1100011B-type条件分支
1100111I-typeJALR(间接跳转)
1101111J-typeJAL(直接跳转)
0110111U-typeLUI(加载立即数到高位)
0010111U-typeAUIPC(PC 相对地址高位)
1110011I-type系统指令(ecall/ebreak/CSR 操作)

objdump 工具。在安装了 RISC-V 工具链的环境(第 13 章会介绍安装)中,objdump -d 可以反汇编任意 RISC-V 二进制文件:

bash
riscv64-unknown-elf-objdump -d program.elf | less

输出每行显示:地址、hex 机器码(小端显示)、反汇编助记符。这是调试、逆向、性能分析的核心工具。

为什么指令格式重要

你可能觉得指令格式是"硬件设计师才需要关心"的细节,但对汇编程序员来说,理解格式有两个实际价值:

  1. 读懂 objdump 输出。反汇编通常给出十六进制机器码 + 助记符。能手工验证机器码是否匹配助记符,是排查汇编器 bug 或编译器 bug 的基本功

  2. 理解指令集的局限性。I-type 的立即数字段只有 12 位(-2048 ~ 2047)。当你需要在 addi 中加载一个大立即数时,汇编器会报错——因为 addi 的硬件格式装不下。这时你需要理解为什么必须用 lui + addi 组合(或直接用 li 伪指令让汇编器自动展开)。立即数的位宽限制贯穿整个指令集,理解格式就等于理解什么能做、什么不能做

本章要点

  • RISC-V ALE 在线环境零安装即可运行和调试汇编代码,本教程前 12 章均基于此
  • Linux RISC-V 系统调用:a7 存 syscall 号(write=64, exit=93),a0-a5 传参,ecall 触发
  • 六种指令格式(R/I/S/B/U/J)覆盖所有 RV64I 指令,字段位置的一致性使译码硬件和人脑解析都简单
  • opcode[6:0] 决定指令类别(R-type/I-type/...),funct3+funct7 在同一类别内区分具体操作
  • 汇编和反汇编可逆:objdump -d 是核心调试工具,理解指令格式让你能手工验证机器码