学习目标

  • 理解 RISC-V 寄存器组织
  • 实现寄存器读写
  • 处理 x0 特殊性

4.1 思考:为什么需要寄存器?

你有没有想过,为什么 CPU 需要寄存器?

想象一下,如果每次计算都要从内存读取数据:

int a = memory[0x1000];
int b = memory[0x1004];
int c = a + b;  // 需要两次内存访问!

而使用寄存器:

int a = reg[10];  // 寄存器访问,极快!
int b = reg[11];
int c = a + b;

寄存器就是 CPU 内部的高速存储,比内存快 100 倍以上!

4.2 RISC-V 有多少个寄存器?

RISC-V 有 32 个通用寄存器,编号 x0-x31。

问题:为什么是 32 个?为什么不是 16 个或 64 个?

思考一下:

  • 太少:频繁访问内存,性能差
  • 太多:指令编码困难,硬件复杂

32 个是一个平衡点:5 位就能编码(2^5 = 32)。

4.3 x0 的秘密

x0 是一个特殊的寄存器,永远是 0

问题:为什么要浪费一个寄存器存储 0?

想想这些场景:

addi x1, x0, 10   # x1 = 0 + 10,相当于 li x1, 10
add  x2, x3, x0   # x2 = x3 + 0,相当于 mv x2, x3

x0 让很多操作变得简单!这就是 RISC 的哲学:用简单的指令组合出复杂的功能

4.4 实现寄存器

typedef struct {
    uint32_t gpr[32];  // 通用寄存器
    uint32_t pc;       // 程序计数器
} CPU_state;

CPU_state cpu;

问题:这样实现有什么问题?

提示:x0 应该永远是 0,但这里可以被修改!

4.5 正确的实现

uint32_t reg_read(int idx) {
    if (idx == 0) return 0;  // x0 永远返回 0
    return cpu.gpr[idx];
}

void reg_write(int idx, uint32_t val) {
    if (idx == 0) return;    // 写 x0 无效
    cpu.gpr[idx] = val;
}

思考:为什么要用函数而不是直接访问数组?

这就是封装的好处:

  1. 保证 x0 的特殊性
  2. 方便添加调试信息
  3. 可以统计寄存器访问次数

4.6 实践练习

实现一个函数,打印所有寄存器的值:

void dump_registers() {
    const char *abi_name[] = {
        "zero", "ra", "sp", "gp", "tp",
        "t0", "t1", "t2",
        "s0", "s1",
        "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7",
        "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", "s10", "s11",
        "t3", "t4", "t5", "t6"
    };

    for (int i = 0; i < 32; i++) {
        printf("x%d(%s) = 0x%08x\n", i, abi_name[i], reg_read(i));
    }
    printf("pc = 0x%08x\n", cpu.pc);
}

问题:你能说出 a0-a7 寄存器的用途吗?

下一步

第 5 章:指令译码

更新时间: