4. RISC-V存储器访问指令:Load与Store详解

存储器访问指令,说白了就是CPU和内存打交道的桥梁。我刚开始接触RISC-V时,觉得这玩意儿不就是读写内存嘛,有啥好讲的?后来在项目中踩过几次坑才发现——这里面的门道,比你想象的多得多。

4.1 Load指令:从内存读到寄存器

Load指令负责把数据从内存搬到寄存器。RISC-V里,Load指令的格式很规整:

lw rd, offset(rs1)    # 加载字(32位)
lh rd, offset(rs1)    # 加载半字(16位)
lb rd, offset(rs1)    # 加载字节(8位)
lhu rd, offset(rs1)   # 加载半字,无符号
lbu rd, offset(rs1)   # 加载字节,无符号

你想想看,为什么要有符号和无符号之分?

嗯,这里要注意:lblh会做符号扩展,把高位补成符号位。而lbulhu做零扩展,高位补0。我有个同事曾经用lb读一个0x80的字节,结果寄存器里变成了0xFFFFFF80,排查了半天才发现是符号扩展搞的鬼。

核心要点:Load指令的目标寄存器rd,必须是整数寄存器(x0-x31)。x0是硬连线为0的,往x0里写数据?没用的,它永远都是0。

4.2 Store指令:从寄存器写回内存

Store指令和Load正好相反,把寄存器的值写回内存。格式也类似:

sw rs2, offset(rs1)    # 存储字
sh rs2, offset(rs1)    # 存储半字
sb rs2, offset(rs1)    # 存储字节

注意看,Store指令的源操作数有两个:rs1是基地址寄存器,rs2是要存储的数据寄存器。这和Load指令不一样——Load只有一个源操作数(基地址),目标操作数是rd。

我在项目中遇到过一个问题:用sb指令存一个32位寄存器的值,结果只存了最低8位。当时我还纳闷,怎么数据不对?后来才意识到——Store指令只存你指定的宽度,高位数据?抱歉,不存。

避坑指南:我曾经在写DMA驱动时,用sw指令写一个未对齐的地址,结果触发了异常。RISC-V要求访存地址必须对齐——字访问要4字节对齐,半字要2字节对齐。不对齐?要么硬件帮你处理(慢),要么直接报错。

4.3 寻址模式:基址+偏移

RISC-V的Load/Store指令只支持一种寻址模式:基址+立即数偏移

说白了就是:有效地址 = rs1 + 立即数。这个立即数是个12位有符号数,范围是-2048到2047。

你可能会问:为什么只有这一种?

我个人习惯是这么理解的:RISC-V追求简洁,一种寻址模式够用了。你想实现其他寻址方式?比如寄存器间接寻址,那就先算好地址再访存呗。比如:

# 实现寄存器间接寻址(伪代码)
addi t0, a0, 0        # t0 = a0
lw   t1, 0(t0)        # 从t0指向的地址读数据

说白了,RISC-V把复杂留给了编译器,把简单留给了硬件。这也是它面积小、功耗低的原因之一。

4.4 字节序处理:大端还是小端?

字节序,这是个老生常谈的问题。RISC-V默认是小端(Little-Endian)模式。

什么叫小端?举个例子:

假设内存地址0x1000处存了一个32位数0x12345678,小端模式下:

地址 0x1000 0x1001 0x1002 0x1003
数据 0x78 0x56 0x34 0x12

看到了吗?最低有效字节(0x78)存到了最低地址。这就是小端。

大端则相反,最高有效字节存最低地址。RISC-V虽然默认小端,但规范里也允许实现为大端。不过说实话,我在实际项目中还没见过大端的RISC-V芯片。

经验之谈:如果你在RISC-V上做网络协议处理,一定要注意字节序转换。网络字节序是大端,而RISC-V是小端。我见过有人直接用lw读网络包头部,结果解析出来的端口号完全不对——因为没做字节序转换。

4.5 知识体系总览

下面这张图,是我梳理的存储器访问指令的核心逻辑:

RISC-V存储器访问指令知识体系 Load指令 lw / lh / lb lhu / lbu 符号扩展 vs 零扩展 Store指令 sw / sh / sb 仅存低位 rs2存数据,rs1存基址 寻址模式 基址 + 12位立即数偏移 范围:-2048 ~ 2047 字节序处理 默认小端 可配大端 图:RISC-V存储器访问指令四大核心模块

4.6 实战中的注意事项

最后,我总结几个实战中容易踩的坑:

  • 对齐问题:非对齐访问在RISC-V里要么触发异常,要么由硬件帮你拆成多次访问。后者性能很差,能避免就避免。
  • 符号扩展陷阱:lb读负数时,寄存器里会变成32位的负数。如果你只想取低8位,用lbu
  • Store指令不读内存:Store只写不读,别指望它把旧值带回来。想读-改-写?先lwsw
  • x0寄存器:往x0里写Store指令?可以,但数据会被丢弃。想用它清零?不行的,x0是硬连线0。

个人习惯:我写RISC-V汇编时,喜欢在Load/Store指令后面加注释,标明数据类型和用途。比如:lw t0, 0(sp) # 从栈顶加载返回地址。这样代码可读性高,调试时也省心。

好了,存储器访问指令就讲到这里。记住一句话:Load是从内存到寄存器,Store是从寄存器到内存。寻址只有一种,字节序默认小端。把这些基础打牢,后面写汇编代码就顺手多了。


公众号:蓝海资料掘金营,微信deep3321