计算机体系结构 03:ISA 是机器能执行的契约
ISA 是机器能执行的契约
本篇的核心问题是:给定一串 32 位比特,机器怎样知道这是加法、访存还是分支?ISA(Instruction Set Architecture,指令集架构)回答的不是“电路怎样做得快”,而是“软件交给硬件的这串比特,在架构状态上必须产生什么效果”。架构状态至少包括 PC、通用寄存器、内存中可见的字节,以及遇到不支持指令或非法访问时的处理边界。
本系列第一条实验线选择 RV32I 作为共同语言。RISC-V 官方非特权规范把 RV32I 定义为 32 位整数寄存器宽度的基础整数 ISA;基础整数 ISA 提供算术、load/store 和控制流指令,扩展如乘除法、压缩指令、原子操作和特权机制另行定义。本文只使用一个更小的教学子集:add/sub/and/or、addi/andi/ori、lw/sw、beq/bne/blt/bge。这个子集足够执行一个求和循环,但不能代表完整 RV32I,更不能代表一个操作系统可用的处理器。
证据等级:手算、功能执行。功能执行证据来自 examples/computer-architecture/base/ 中的 C11 参考执行器。当前机器缺少可用的 RISC-V 后端工具链,Apple clang 21 对 --target=riscv32-unknown-elf 报告没有兼容 target,Apple llvm-objdump 也没有列出 RISC-V target。因此本文不声称完成了本机真实 RISC-V 编译和反汇编,只使用手写机器码、官方编码规则和执行器轨迹作证。
ISA 把程序约束在少数状态变化上
对一条指令来说,最重要的问题只有三个:从哪里取操作数,怎样计算,下一个 PC 是多少。微架构可以单周期执行、五级流水、乱序推测,最后提交给软件看的架构状态必须符合 ISA 的规则。比如 add x3,x3,x4 的规则是读取 x3 和 x4,把低 XLEN 位的和写入 x3。RV32I 的 XLEN 是 32,整数加法溢出不会产生算术异常;硬件是否有进位链、旁路网络或乱序执行窗口,不属于这条指令的语义。
RV32I 有 32 个整数寄存器,编号 x0 到 x31。其中 x0 是硬零寄存器:读出总是 0,写入会被丢弃。这个规则让很多伪操作可以由普通指令表达,例如 mv x5,x6 可以写成 addi x5,x6,0。教学执行器每一步都会把 x0 强制恢复为 0,并用 programs/x0.hex 检查这个不变量。
1 | |
如果第一条写入真的生效,第二条会得到 124;实际轨迹最后仍显示 x0:0,x1 来自硬零寄存器加 1。这是 ISA 级规则,不是编译器优化。
从 32 位机器码拆出字段
RV32I 基础指令固定为 32 位。不同格式复用若干字段:最低 7 位是 opcode,rd 是目的寄存器,rs1/rs2 是源寄存器,funct3/funct7 进一步区分具体操作。以求和程序中的 add x3,x3,x4 为例,机器码是:
1 | |
按 R-type 格式拆开:
| 字段 | 位段 | 值 | 含义 |
|---|---|---|---|
| opcode | 6:0 | 0x33 |
整数寄存器-寄存器运算 |
| rd | 11:7 | 3 |
结果写入 x3 |
| funct3 | 14:12 | 0 |
加减类 |
| rs1 | 19:15 | 3 |
读取 x3 |
| rs2 | 24:20 | 4 |
读取 x4 |
| funct7 | 31:25 | 0 |
add,不是 sub |
同一个 opcode 不能单独决定操作。add 和 sub 都使用 0x33,区别在 funct7。这就是“编码”与“汇编助记符”的关系:助记符方便人读,机器真正执行的是字段组合。
load/store 架构把内存访问限制在专门指令上
RV32I 是 load-store 架构。整数算术指令只读写寄存器;访问内存必须使用 lw/sw 等 load/store 指令。求和程序的核心循环先从地址 x1 + 0 读一个 32 位字到 x4,再把它加到累加器 x3:
1 | |
本系列教学环境把内存定义为 4096 字节、小端、字访问必须自然对齐。RISC-V 规范把自然对齐访问的行为说清楚,非对齐访问是否被环境透明处理取决于 EEI(Execution Environment Interface)。教学执行器选择更严格的边界:lw 地址不是 4 的倍数就显式失败。这个选择不是在声称所有 RISC-V 机器都必须失败,而是在为后续模型固定一个可检查的环境。
失败用例:
1 | |
真实输出为:
1 | |
错误边界同样属于教学模型的契约。支持什么、不支持什么,必须比“能跑某个例子”更明确。
分支的立即数字段不是顺着写的
循环离不开分支。求和程序用 bne x2,x0,-16 回到循环体开头:
1 | |
B-type 立即数看起来最容易出错,因为偏移的各个位被分散在机器码的不同位置。官方规范解释了 B/J 格式为什么采用这种“旋转”布置:它让若干立即数字段位置和其他格式重叠,减少硬件里动态移位和多路选择的成本。对读者来说,重要的是不要把机器码里的位段直接按书写顺序拼成一个普通 12 位数。
手算这条分支时,先从字段恢复有符号偏移 -16。执行到 PC=28 时,如果 x2 != x0,下一个 PC 就是 28 + (-16) = 12;否则顺序执行到 PC=32。执行器输出的分支事件包含 taken 和 target,因此可以逐条核对。
求和循环的机器状态轨迹
examples/computer-architecture/base/programs/sum.hex 先初始化指针、计数器和累加器,再循环读取 5 个字:
1 | |
数据区是地址 128、132、136、140、144 上的 3,4,5,6,7。运行命令:
1 | |
关键结果:
1 | |
可下载附件:sum.hex、sum.jsonl、environment.txt。
这个结果只证明在声明的子集和环境中,机器码按预期修改了寄存器与内存。它不证明完整 RV32I 合规,不证明流水线周期数,也不证明真实 CPU 性能。
验收
本篇验收标准是能完成三件事。第一,给定一条简单 RV32I 机器码,能拆出 opcode、寄存器字段和立即数。第二,能沿求和循环说清 PC、寄存器、内存如何变化。第三,能指出证据边界:手写机器码加功能执行轨迹不是完整工具链编译,也不是微架构时序。
练习一:拆 addi
题目:0x00408093 是哪条指令?它修改什么状态?
解答:最低 7 位是 0x13,属于 I-type 整数立即数运算。rd=1,rs1=1,funct3=0,立即数为 4,所以是 addi x1,x1,4。它把 x1 加 4 后写回 x1,PC 顺序加 4。
练习二:预测循环次数
题目:求和程序中 x2 初值为 5,每轮执行 addi x2,x2,-1,随后 bne x2,x0,-16。循环体中的 lw 执行几次?
解答:lw 在计数器递减之前执行。五轮中 x2 进入循环时分别为 5、4、3、2、1;第五轮递减后变为 0,分支不再跳转。所以 lw 执行 5 次,累加 5 个字,最终 mem148=25。






