Skip to content

第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 微码与微操作的详细分解

本章结束时,你将可以:

  1. 逐字描述指令周期(取指→译码→执行→访存→写回)
  2. 用 RISC-V 汇编写出一个简单的函数(含栈操作)
  3. 对比 RISC-V 和 x86 在设计哲学上的根本差异
  4. 画出单周期 CPU 的数据通路草图
  5. 解释"控制单元"如何把指令的二进制位变成控制信号

遭遇战 1:从 C 到汇编——指令的诞生

问题

编译器的黑箱里发生了什么?

c
int add(int a int b) {
 return a + b;
}

这 3 行 C 代码翻译成 RISC-V 汇编是什么?

解法:剥掉 C 的外衣

asm
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 之后只剩一行:

asm
add:
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 == ...) ...组合逻辑 + 状态机,输出控制信号
指令存储器你的代码 .textROM 或 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 汇编代码:

asm
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 指令一览

asm
# 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

asm
; x86: 从 esi 复制 ecx 字节到 edi
rep movsb

一条 x86 指令干了什么?

  1. 读 1 字节从 [esi]
  2. 写 1 字节到 [edi]
  3. esi++ edi++
  4. ecx--
  5. 如果 ecx != 0 回到步骤 1

相当于:

asm
# 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 为保留寄存器):

c
int max(int a int b) {
 return (a > b) ? a : b;
}

提示:blt 分支,mv 设置返回值。

挑战 2:画出 sw 指令在单周期 CPU 中的数据通路——标注每个部件的输入和输出。特别说明:ALU 在 sw 里到底算了什么。

挑战 3:RISC-V 为什么没有"push"和"pop"指令?(提示:栈操作是 $......$ 可以用现有指令组合实现。)

挑战 4:比较以下两端汇编在指令数量代码大小上的差异:

c
// 功能:*a = *a + *b
void add_ptr(int *a int *b) {
 *a = *a + *b;
}

分别写出 RISC-V 和 x86-64 的实现。


验收标准

学完本章后:

  1. [ ] 可以口述指令周期的 5 个阶段,并说出每一级的主要硬件部件
  2. [ ] 解释 RISC-V 指令格式中 opcode、funct3、funct7 的分工
  3. [ ] 说出 RISC 和 CISC 的核心设计哲学差异(至少 3 条)
  4. [ ] 解释为什么单周期 CPU 的时钟周期主要由 load 指令决定
  5. [ ] 在纸上画出 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 协议——这些决定了你的程序能跑多快。

Built with VitePress | Software Systems Atlas