寄存器再多也只有 32 个,真正的数据海洋在内存里。这一讲讲清楚数据怎么在内存和寄存器之间搬家——顺便解释一个新手必踩的坑:为什么从内存里读一个字节,有时读出正数有时读出负数?

load-store 架构:算术不碰内存

RISC-V 是典型的 load-store 架构:所有算术逻辑指令的操作数只能是寄存器。想给内存里的数加 1?三步走:读上来、加、写回去。

lw   t0, 0(a1)        # 从内存读一个字到 t0
addi t0, t0, 1        # 寄存器里 +1
sw   t0, 0(a1)        # 写回内存

对比 x86 允许 add [mem], 1 直接改内存——RISC-V 认为那是把指令搞复杂的罪魁祸首。规则简单,流水线才能跑得快。

lw / sw:一个字的搬运

lw   rd, offset(rs1)    # 从地址 rs1+offset 读 4 字节到 rd
sw   rs2, offset(rs1)   # 把 rs2 的 4 字节写到地址 rs1+offset

寻址方式只有一种:偏移 + 基地址寄存器。没有 x86 那种 [base + index*scale + disp] 的复杂花活,但足够表达数组、结构体、栈:

la   t0, array          # t0 = 数组首地址(la 是伪指令,第 07 讲讲)
lw   t1, 0(t0)          # array[0]
lw   t2, 4(t0)          # array[1] —— 每个字 4 字节,偏移 +4
lw   t3, 8(t0)          # array[2]

偏移量是 12 位有符号立即数(±2048),基地址灵活到可以拿 sp 当基址——栈上的局部变量全靠它。

字节序:小端是主场

一个 4 字节的字 0x11223344 存进内存,字节怎么排?RISC-V 规定 little-endian(小端):低位字节放低地址。

地址:  base    base+1  base+2  base+3
内容:  0x44    0x33    0x22    0x11

所以从内存按字节读回来时要「倒着拼」。这个知识点在分析协议、看 hexdump 时天天用。

lb 的坑:符号扩展

内存的最小寻址单位是字节,但寄存器是 32 位——从一个字节变成一个字,高位填什么?这就是 load 族的全部秘密:

指令 宽度 高位填充 用途
lw 4 字节 —— 读字
lh / lhu 2 字节 符号位 / 0 读半字
lb / lbu 1 字节 符号位 / 0 读字节

lb(load byte)把最高位复制到高 24 位:读到 0xFF,寄存器里是 0xFFFFFFFF(-1);lbu(load byte unsigned)高位全填 0:读到 0x000000FF(255)。

同一个字节,两种读法,两个答案。 存的是 ASCII 字符或颜色分量(本质无符号)就用 lbu;存的是有符号小整数就用 lb。写 C 的人其实早就在用它们——signed char 和 unsigned char 的差别就落在这一条指令上。

对齐:地址要合群

RV32I 要求访问的地址必须自然对齐:lw 的地址得是 4 的倍数,lh 是 2 的倍数。不对齐会触发异常(misaligned load)。这不是刁难——内存总线和 cache 都是按整块取数的,对齐让硬件路径最短。手写汇编时最常见的事故就是忘了偏移要按 4 步进。

动手验证

在模拟器沙盒里做一个小实验:

addi t0, zero, -1      # t0 = 0xFFFFFFFF
sw   t0, 0(sp)         # 存到栈顶(对齐的)
lb   a0, 0(sp)         # 读最低字节 → -1(符号扩展)
lbu  a1, 0(sp)         # 再读一次 → 255(补零)

单步跑,看 a0 和 a1 的差异——亲眼见过一次,比背十遍定义都牢。

免费下载 HI RISC-V App,模拟器离线可用。

本讲三句话

  1. load-store 架构:算术只碰寄存器,lw/sw 负责搬运,寻址 = 基地址 + ±2048 偏移
  2. 小端字节序:低位字节放低地址
  3. lb 与 lbu 差在符号扩展——同一个字节,两种读法两个答案

下一讲让程序学会「做决定」:分支与循环。