第4章:CPU 与 ISA——地心引擎
叙事密度:中高 | 主语言:C + RISC-V 汇编 | 问题驱动 | 卷三·地心篇
元数据卡
| 属性 | 内容 |
|---|---|
| 卷号 | Vol 3 — 深入计算机系统 |
| 章节 | 第4章:CPU 与 ISA |
| 前置 | 第3章(数字逻辑);C 基础 |
| 后置 | 第5章:缓存与内存层次 |
| 理论深度 | (5/5) |
| C 相关度 | ≈5% 代码量;C→汇编对照为全文暗线 |
| 核心模型 | 指令周期、RISC-V ISA、数据通路 |
你的进度
"从逻辑门往上走一层,你到达了地心的引擎室——CPU。它不停地从内存读取指令、解码、执行。你在变量村写的每一行 Java 代码,最终都是被这台引擎翻译成机器指令来驱动的。"
上一章你趴在地心,手摸到了硅片上最原始的几何:逻辑门。你用 NAND 搭出了加法器,用 D 触发器搭建了寄存器。时钟信号从你的电路里流过,像一个精准的心脏在跳动。
"我有一堆门,一堆触发器,一个时钟……但怎么让它跑起来?怎么让它执行 x = a + b; 这种 C 语句?"
答案不在硬件里——在 指令 里。
CPU 不是一台机器,它是一套指令协议的物理实现。指令集架构(ISA)是这个协议的契约,CPU 是执行这个契约的引擎。
这一章,我们给熔岩焊一个气缸。
你的任务
本章分层
- 必读:指令周期五阶段(取指/译码/执行/访存/写回);从 C 到汇编的对应关系;RISC 与 CISC 的设计哲学差异
- 选读:RISC-V 的 6 种指令格式(R/I/S/B/U/J)的二进制编码;单周期 CPU 数据通路的图形理解
- 进阶:控制信号表的完整推导;单周期 CPU 的关键路径分析 本章不会要求你掌握
- RISC-V 指令的完整二进制编码规则(能看懂即可)
- 单周期 CPU 的每一条控制信号线的设计
- x86 微码与微操作的详细分解
本章结束时,你将可以:
- 逐字描述指令周期(取指→译码→执行→访存→写回)
- 用 RISC-V 汇编写出一个简单的函数(含栈操作)
- 对比 RISC-V 和 x86 在设计哲学上的根本差异
- 画出单周期 CPU 的数据通路草图
- 解释"控制单元"如何把指令的二进制位变成控制信号
遭遇战 1:从 C 到汇编——指令的诞生
问题
编译器的黑箱里发生了什么?
int add(int a int b) {
return a + b;
}这 3 行 C 代码翻译成 RISC-V 汇编是什么?
解法:剥掉 C 的外衣
add:
addi sp sp -8 # 栈指针减 8(分配栈帧)
sw s0 4(sp) # 保存 s0 到栈上
addi s0 sp 8 # s0 = sp + 8(建立帧指针)
sw a0 -8(s0) # 参数 a 入栈(冗余优化关掉后可见)
sw a1 -4(s0) # 参数 b 入栈
lw a5 -8(s0) # 读 a
lw a4 -4(s0) # 读 b
add a5 a5 a4 # a5 = a + b
mv a0 a5 # 返回值放入 a0
lw s0 4(sp) # 恢复 s0
addi sp sp 8 # 栈指针加 8
ret # 返回你可能觉得太多了。对,
-O0未优化的编译结果就是这么多。-O2之后只剩一行:asmadd: add a0 a0 a1 ret但底层逻辑完全相同——只是省略了栈帧的进出。
每一个汇编指令都是 CPU 能直接理解的最小操作。RISC-V 的每条指令正好 32 位——不多不少,一个 word。
Java 差异:JVM 的字节码是另一种"指令集"——但它跑在虚拟机上,由 JIT 编译成真·机器码。你在 JVM 里看到的
iadd指令,最终会被翻译成add a0 a0 a1。Python 差异:Python 根本没有"指令"这个概念。
def add(a b): return a + b运行时走的是 CPython 的字节码解释器循环——一个用 C 写的巨大switch语句。
遭遇战 2:CPU 的器官
问题
指令有了。谁来执行它?
解法:参见 CPU 解剖图
一个 CPU 由四个主要部件组成:
┌──────────────────────────┐
│ 控制单元 CU │
│ (译码器 + 状态机) │
└────┬────┬────┬────┬───────┘
│ │ │ │
┌────┘ │ │ └───────┐
▼ ▼ ▼ ▼
┌─────────┐ ┌─────┐ ┌────┐ ┌────────┐
│ PC │ │ 寄存器 │ │ ALU │ │ 立即数 │
│(指令地址)│ │ 文件 │ │ │ │ 生成器 │
└────┬────┘ └─────┘ └──┬─┘ └────────┘
│ │
▼ ▼
┌────────┐ ┌───────────┐
│指令存储器│ │ 数据存储器 │
└────────┘ └───────────┘逐个器官
| 部件 | C 的视角 | 硬件本质 |
|---|---|---|
| 程序计数器 PC | 你的进度儿执行 | 一个寄存器,存下一条指令的地址 |
| 寄存器文件 | int a int b... | 32 个 64 位 D 触发器阵列 |
| ALU | + - * / & | ^ << >> | 全加器 + 移位器 + 比较器 |
| 控制单元 | if (opcode == ...) ... | 组合逻辑 + 状态机,输出控制信号 |
| 指令存储器 | 你的代码 .text | ROM 或 L1 I-cache |
| 数据存储器 | 堆栈和堆 | SRAM 或 L1 D-cache |
获得技能:指令周期五步舞
CPU 的"心跳"就是指令周期。每一拍,CPU 做一个动作。
┌───────────────────┐
│ IF 取指令 │ PC → 指令存储器 → 取出 32 位指令
└────────┬──────────┘
▼
┌───────────────────┐
│ ID 译码 │ 拆指令 → 读寄存器 → 生成立即数
└────────┬──────────┘
▼
┌───────────────────┐
│ EX 执行 │ ALU 运算 / 地址计算
└────────┬──────────┘
▼
┌───────────────────┐
│ MEM 访存 │ 读/写数据存储器(仅 load/store 指令)
└────────┬──────────┘
▼
┌───────────────────┐
│ WB 写回 │ 结果写回寄存器文件
└───────────────────┘周期时间 = 最慢的那一级的延迟。在单周期 CPU 里,这个最慢级通常是 **MEM(访存)**或 EX(大 ALU)。
为什么现代 CPU 的主频卡在 3-5 GHz 之间上不去?
因为一级的门延迟就是 10-50 ps,但一个指令周期要走完五级+所有连线——3 GHz 意味着周期约 333 ps。超过这个频率,信号来不及从取指单元传到写回单元。
遭遇战 3:指令的二进制血肉
问题
一段 RISC-V 汇编代码:
add x1 x2 x3 # x1 = x2 + x3它在 CPU 眼里长什么样子?
进阶:下面的 RISC-V 指令格式细节是专业读者才会深入的内容。普通读者只需要知道「每条指令是 32 位的固定长度数字」即可。
解法:RISC-V 指令格式
RISC-V 所有指令都是 32 位。它以 固定长度 换来了译码器的简单——这是 RISC 的信条之一。
add x1 x2 x3 的二进制:
funct7 rs2 rs1 funct3 rd opcode
0000000 00011 00010 000 00001 0110011逐段拆解:
| 字段 | 位范围 | 值 | 含义 |
|---|---|---|---|
| opcode | [6:0] | 0110011 | 运算类指令(OP) |
| rd | [11:7] | 00001 | 目标寄存器 x1 |
| funct3 | [14:12] | 000 | 算术运算子类型 |
| rs1 | [19:15] | 00010 | 源寄存器 x2 |
| rs2 | [24:20] | 00011 | 源寄存器 x3 |
| funct7 | [31:25] | 0000000 | 区分 ADD(0) 和 SUB(32) |
RISC-V 只有 6 种基本指令格式:R、I、S、B、U、J。不需要那么多格式,因为译码器简单意味着更小的芯片面积、更低的功耗、更高的时钟频率。
参考附录:以下指令表供查阅,不需要背诵。你在实际写汇编时可以随时回查。
常见 RISC-V 指令一览
# R-type(寄存器-寄存器)
add rd rs1 rs2 # rd = rs1 + rs2
sub rd rs1 rs2 # rd = rs1 - rs2
sll rd rs1 rs2 # rd = rs1 << rs2
xor rd rs1 rs2 # rd = rs1 ^ rs2
or rd rs1 rs2 # rd = rs1 | rs2
and rd rs1 rs2 # rd = rs1 & rs2
# I-type(立即数)
addi rd rs1 imm12 # rd = rs1 + 符号扩展(imm12)
lw rd imm12(rs1) # rd = MEM[rs1 + imm12]
jalr rd rs1 imm12 # rd = PC+4; PC = rs1 + imm12
# S-type(存储)
sw rs2 imm12(rs1) # MEM[rs1 + imm12] = rs2
# B-type(分支)
beq rs1 rs2 label # if (rs1 == rs2) PC += offset
blt rs1 rs2 label # if (rs1 < rs2) PC += offset
# U-type(上立即数)
lui rd imm20 # rd = imm20 << 12
auipc rd imm20 # rd = PC + (imm20 << 12)
# J-type(跳转)
jal rd label # rd = PC+4; PC += offset所有指令都遵守"最多两个源操作数 + 一个目标寄存器"。这就是 RISC-V 的"三操作数一致"原则——译码逻辑均匀、规则。
深水章·独立内容:单周期 CPU 设计是计算机体系结构课程的核心,但本卷主线不需要掌握全部细节。以下内容作为独立的深水章提供。
常见陷阱:单周期 CPU 的数据通路
把第 3 章的数字器件全焊在一起。
进阶:单周期数据通路的细节已超出主线要求。以下内容为专业读者准备。
R-type 指令的数据流
以 `add x1 位指令 → 译码器拆分字段 → 寄存器文件读 x2 x3 → ALU 做加法 → 结果写回 x1 → PC = PC + 4(下一条)
**关键路径**:PC → 指令存储器 → 寄存器文件读 → ALU → 写回 → (准备下一周期)
这条路径的总延迟决定了 CPU 可以跑多快。
### Load 指令的数据流
`lw x1 4(x2)`——从内存地址 `x2+4` 读入 x1:PC → 指令存储器 → 译码 → 读 x2,生成立即数 4 → ALU 计算 x2 + 4(地址) → 数据存储器读该地址 → 结果写回 x1
多了一级:**数据存储器访问**。Load 是单周期 CPU 里**最慢的指令**,因为 MEM 级是整个数据通路里最大的延迟块。
### 控制信号表
控制单元根据 opcode 输出这些信号:
| 指令 | RegWrite | ALUSrc | MemRead | MemWrite | MemtoReg | Branch | ALUOp |
|---|---|---|---|---|---|---|---|
| R-type | 1 | 0 | 0 | 0 | 0 | 0 | 10 |
| lw | 1 | 1 | 1 | 0 | 1 | 0 | 00 |
| sw | 0 | 1 | 0 | 1 | X | 0 | 00 |
| beq | 0 | 0 | 0 | 0 | X | 1 | 01 |
> 这张表格总共需要 **不到 100 个逻辑门** 来实现。RISC-V 的控制单元极其简单——这是它被全世界的大学选为教学架构的根本原因。
```c
// 控制单元逻辑(组合逻辑,用 C 描述)
void control_unit(uint32_t opcode int *reg_write int *alu_src
int *mem_read int *mem_write int *mem_to_reg
int *branch int *alu_op) {
switch (opcode) {
case OPCODE_R_TYPE: // 0110011
*reg_write = 1; *alu_src = 0; *mem_read = 0;
*mem_write = 0; *mem_to_reg = 0; *branch = 0; *alu_op = 2;
break;
case OPCODE_LOAD: // 0000011
*reg_write = 1; *alu_src = 1; *mem_read = 1;
*mem_write = 0; *mem_to_reg = 1; *branch = 0; *alu_op = 0;
break;
case OPCODE_STORE: // 0100011
*reg_write = 0; *alu_src = 1; *mem_read = 0;
*mem_write = 1; *branch = 0; *alu_op = 0;
break;
case OPCODE_BRANCH: // 1100011
*reg_write = 0; *alu_src = 0; *mem_read = 0;
*mem_write = 0; *branch = 1; *alu_op = 1;
break;
}
}遭遇战 4:RISC vs CISC——两种哲学
问题
x86 的每条指令长度从 1 字节到 15 字节不等。RISC-V 全部 4 字节。为什么?
解法:两条路的分叉
1970年代:内存昂贵。工程师拼命让一条指令做更多事,以节省代码体积——这叫 CISC(Complex Instruction Set Computer)。
1980年代:David Patterson 和 John Hennessy 发现,CISC 指令集里 80% 的指令极少被使用,但译码器为了支持它们却占了大量芯片面积。于是提出 RISC:用简单指令的组合代替复杂指令。
| 对比维度 | RISC-V(RISC) | x86(CISC) |
|---|---|---|
| 指令长度 | 固定 32 位(基础集) | 变长 1~15 字节 |
| 寄存器数量 | 32 个通用寄存器 | 16 个(x64) |
| 寻址模式 | 仅基址+偏移 | 多种:立即、直接、间接、变址... |
| 内存操作 | 仅 load/store 指令访存 | ALU 指令可直接操作内存 |
| 译码器复杂度 | 低(统一格式) | 极高(加前缀、RIP-relative...) |
| 典型芯片面积 | 译码器 < 5% | 译码器 ≈ 20-30%(含微码 ROM) |
x86 的"一朵奇葩"——rep movsb
; x86: 从 esi 复制 ecx 字节到 edi
rep movsb这一条 x86 指令干了什么?
- 读 1 字节从
[esi] - 写 1 字节到
[edi] - esi++ edi++
- ecx--
- 如果 ecx != 0 回到步骤 1
相当于:
# RISC-V 版本(loop)
loop:
lb t0 0(a0) # 读取源
sb t0 0(a1) # 写入目标
addi a0 a0 1 # 源指针++
addi a1 a1 1 # 目标指针++
addi a2 a2 -1 # 计数--
bne a2 x0 loop # 继续循环RISC 版 6 条指令,CISC 版 1 条。听起来 CISC 赢了?
不是。rep movsb 在现代 x86 CPU 里不是用"硬件直接执行"的——它是用微码拆成了好几个微操作(μops),然后在内部的 RISC 核心上执行的。换句话说,现代 x86 CPU 的内部是一个 RISC 核心,CISC 指令是外面的一个"翻译层"。
真相:x86 赢的不是指令集设计,是生态和向后兼容。1978 年 8086 的指令在今天的新 CPU 上仍然能跑——40 多年的代码遗产。
Python 差异:如果你觉得 CISC 的指令很抽象,想想 Python 的
bytes.copy()——一行 Python 最后变成了rep movsb或者几十条 RISC-V 指令。每增加一层抽象,你就多叠了一级指令翻译。
通关挑战
挑战 1:把以下 C 代码手动编译为 RISC-V 汇编(用 a0~a7 作为参数/返回值寄存器,s0~s11 为保留寄存器):
int max(int a int b) {
return (a > b) ? a : b;
}提示:blt 分支,mv 设置返回值。
挑战 2:画出 sw 指令在单周期 CPU 中的数据通路——标注每个部件的输入和输出。特别说明:ALU 在 sw 里到底算了什么。
挑战 3:RISC-V 为什么没有"push"和"pop"指令?(提示:栈操作是 $......$ 可以用现有指令组合实现。)
挑战 4:比较以下两端汇编在指令数量和代码大小上的差异:
// 功能:*a = *a + *b
void add_ptr(int *a int *b) {
*a = *a + *b;
}分别写出 RISC-V 和 x86-64 的实现。
验收标准
学完本章后:
- [ ] 可以口述指令周期的 5 个阶段,并说出每一级的主要硬件部件
- [ ] 解释 RISC-V 指令格式中 opcode、funct3、funct7 的分工
- [ ] 说出 RISC 和 CISC 的核心设计哲学差异(至少 3 条)
- [ ] 解释为什么单周期 CPU 的时钟周期主要由 load 指令决定
- [ ] 在纸上画出
add指令的数据通路
常见卡点
| 卡点 | 解释 |
|---|---|
| "PC 为什么加 4 而不是加 1?" | RISC-V 是字节寻址的。每条指令 4 字节,所以下一条的地址 = 当前 PC + 4。ARM 早期版本用 4 字节寻址,PC 加 1。细节不同,本质一样。 |
| "寄存器文件一次读两个数,怎么写一个数?" | 寄存器文件有 2 个读端口 + 1 个写端口——物理上就是三组不同的地址线和数据线。多端口寄存器文件用多组译码器实现。 |
| "条件分支怎么处理?" | beq 在 EX 级通过 ALU 计算 rs1 - rs2 == 0,如果相等则控制单元选通 PC 加上偏移量,否则 PC+4。关键:分支在单周期 CPU 里不额外开销周期。 |
| "单周期 CPU 为什么慢?" | 因为时钟周期必须适应最慢的指令(load)。大部分指令(如 add)远不需要那么长时间。流水线就是用来解决这个问题的——第 5 章见。 |
| "RISC-V 的立即数为什么要 sign-extend?" | 因为负数补码表示的立即数在加法中自然工作。如果零扩展,addi x1 x0 -1 会变成 x0 + 65535,而不是 x0 - 1。 |
| "JAL 里的 rd 是干什么的?" | JAL 把返回地址(PC+4)写入 rd——通常是 x1(ra)。函数调用时,这就是"记得怎么回来"的机制。没有它,递归无法实现。 |
现在不需要理解
- 异常与中断:CPU 如何处理除零、页错误、外部中断。这需要完整的特权级模型,推迟到操作系统卷。
- 分支预测:现代 CPU 在取指级猜分支方向。猜对了继续,猜错了要把流水线清空。这是高性能 CPU 的核心竞争力之一。
- 推测执行:分支预测"猜"到了就开始执行。Spectre 漏洞就是利用了推测执行对缓存的副作用。RISC-V 在架构层面不承诺推测执行的安全性。
- 动态调度:乱序执行(Out-of-Order)——CPU 不按程序顺序执行指令,只看哪些指令的数据准备好了。Tomasulo 算法在 1967 年就发明了。
- 向量扩展:RISC-V 的 V 扩展(向量指令)用于 SIMD 计算——一条指令操作多个数据。AVX-512 是 x86 的对称方案。
旅人笔记
我们从第 3 章的逻辑门走到了第 4 章的 CPU。
你现在知道了:
- 计算机执行的不是"代码",是"指令的二进制表示"。C 代码是写在人类这张纸上的简谱,汇编是指挥谱,二进制编码是 CD 上的凹坑。
- CPU 是一个极其规整的状态机。它在 5 个阶段间循环,每个时钟沿推进一次——像一台 50 亿 RPM 的蒸汽机,用硅而不是钢铁锻造。
- ISA 是一个合同。硬件保证按合同执行指令序列的语义。软件保证生成合法的指令序列。两者以 ISA 为边界隔离——你不需要知道 CPU 内部晶体管怎么翻跟头,只要知道指令的行为。
- RISC-V 是一面镜子。它简单到你可以画出它的全部数据通路。ARM/x86 的核心也是 RISC——但它们被几十年的兼容性包袱裹了一圈又一圈。
下一章,你把单周期 CPU 拆开,在它的身体里插入流水线级间寄存器——让它每个时钟周期完成一条指令,而不是四五个时钟周期才完成一条。
发动机将从"单缸冲压"变成"四缸流水"。
→ 下一站预告
第5章:缓存体系——速度的缓冲带
指令执行越来越快,但内存跟不上 CPU 的脚步。为了弥合这个速度鸿沟,工程师在 CPU 和内存之间插入了一层精巧的缓存。缓存行、局部性、MESI 协议——这些决定了你的程序能跑多快。