每一个程序,最终都会变成一系列由 CPU 执行的指令。
例如,一行简单的 C 语言代码:
c = a + b;
对程序员来说非常直观。
但是,处理器并不能直接理解 C 语言。
程序经过编译之后,会变成机器指令,例如:
add $t0, $t1, $t2
或者其他处理器架构中的等效指令。
接下来,CPU 必须按照一套非常有组织的流程来执行这些指令。
从高层来看,这个过程通常被概括为:
取指(Fetch)→ 译码(Decode)→ 执行(Execute)
但现代 CPU 实际执行的工作,要比这三个词复杂得多。
CPU 需要从内存中取得指令,判断这条指令是什么意思,读取所需要的操作数,执行运算,在必要的时候访问内存,然后保存计算结果。
理解这个过程,是理解“软件如何最终变成真实硬件活动”的最好方式之一。
1. 程序是一系列指令
当一个程序运行时,它的机器代码会被存放在内存中。
CPU 所看到的程序,本质上就是一系列二进制机器指令。
从概念上看,内存中可能存放着:
Instruction 1
Instruction 2
Instruction 3
Instruction 4
每一条指令都会告诉处理器执行某一种操作。
常见的操作包括:
- 两个数相加
- 数字相减
- 从内存加载数据
- 把数据写入内存
- 比较两个数值
- 跳转到另一条指令
- 调用一个函数
处理器不断读取这些指令,然后逐条执行。
这个不断重复的过程,被称为 指令周期(Instruction Cycle)。
2. 程序计数器告诉 CPU 去哪里找指令
CPU 必须知道下一条应该执行的指令在哪里。
为此,处理器内部有一个特殊的寄存器,通常称为 程序计数器(Program Counter),简称 PC。
程序计数器中保存的是下一条指令的地址。
例如:
PC = 0x1000
表示下一条指令位于内存地址:
0x1000
处理器随后会取得这个地址中的指令。
执行之后,程序计数器通常会继续移动到下一条指令的位置。
从概念上看:
PC → 指令内存 → 指令
如果没有程序计数器,CPU 就不知道自己当前运行到了程序中的什么位置,也不知道接下来应该执行哪条指令。
3. 第一阶段:取指 Fetch
第一个主要步骤是 取指阶段(Fetch Stage)。
CPU 将程序计数器中保存的地址发送给内存系统。
内存系统随后把这个地址对应的指令返回给处理器。
在一个简化的系统中,可以表示为:
程序计数器 → 内存 → 指令
例如,假设程序计数器中保存的是:
0x1000
而内存地址 0x1000 中保存了一条 ADD 指令。
CPU 就会把这条指令读取出来。
在现代处理器中,这条指令通常并不是直接从主内存 RAM 中取得,而更可能来自 指令缓存(Instruction Cache)。
但是基本原理仍然相同:
CPU 根据一个地址取得下一条需要执行的指令。
4. 指令进入 CPU
指令被取得之后,CPU 必须暂时保存这条指令,以便进一步判断它究竟表示什么。
在一些简单的处理器结构图中,这条指令可能被放入一个叫做 指令寄存器(Instruction Register) 的部件中。
现代 CPU 的内部结构通常更加复杂,但基本原理是一样的。
此时,处理器内部已经拿到了这条二进制指令。
例如,这个二进制模式可能代表:
ADD R1, R2, R3
它的含义是:
读取寄存器 R2 中的值。
读取寄存器 R3 中的值。
把两个值相加。
然后把结果保存到寄存器 R1 中。
但是,此时 CPU 仍然需要某些电路来识别这些二进制位究竟代表什么意思。
这就是下一步的任务:
译码。
5. 第二阶段:译码 Decode
第二个主要阶段是 译码(Decode)。
一条机器指令内部包含多个字段,用来告诉处理器应该执行什么操作,以及应该使用哪些操作数。
根据不同的指令集,这些字段可能包括:
- 操作类型
- 源寄存器
- 目标寄存器
- 立即数
- 内存寻址信息
其中,用来表示操作类型的字段通常称为:
操作码(Opcode)
对于一条 ADD 指令来说,操作码告诉 CPU:
需要执行加法。
其他操作码还可能表示:
- SUB
- AND
- OR
- LOAD
- STORE
- BRANCH
- JUMP
译码器会检查指令中的这些二进制位,然后判断接下来需要启动哪些硬件。
6. 控制单元协调整个操作
CPU 并不是简单地把整条指令直接送进 ALU。
实际上,指令经过译码之后,会产生一系列 控制信号(Control Signals)。
这些控制信号负责协调处理器内部不同部件的工作。
控制逻辑可能需要告诉 CPU:
- 应该读取哪些寄存器
- ALU 应该执行什么运算
- 是否需要读取内存
- 是否需要写入内存
- 是否应该把结果写回寄存器
- 下一条指令应该从哪里取得
例如,一条 ADD 指令可能产生类似下面这样的控制行为:
读取寄存器 R2
读取寄存器 R3
告诉 ALU 执行加法
把结果写入 R1
因此,控制单元实际上就像整个 数据通路(Datapath) 的协调者。
7. CPU 读取源寄存器
很多指令所使用的数据,已经存放在 CPU 的寄存器中。
假设处理器执行:
ADD R1, R2, R3
CPU 必须取得 R2 和 R3 中保存的数值。
假设寄存器文件中:
R2 = 10
R3 = 20
寄存器文件会把这两个数值发送到执行单元。
从概念上看:
R2 → ALU 输入 A
R3 → ALU 输入 B
机器指令中的寄存器名称,实际上是在告诉 CPU 应该读取寄存器文件中的哪些位置。
真正进入 ALU 参与运算的,并不是“R2”或者“R3”这些名字,而是这些寄存器中保存的实际数值。
8. 第三阶段:执行 Execute
现在,CPU 进入 执行阶段(Execute Stage)。
对于算术和逻辑指令来说,执行阶段通常会使用 ALU。
ALU 的全称是:
Arithmetic Logic Unit
中文通常称为:
算术逻辑单元
ALU 可以执行很多操作,例如:
- 加法
- 减法
- AND
- OR
- 比较
- 移位
继续使用前面的例子:
R2 = 10
R3 = 20
ALU 接收到这两个数值。
控制单元告诉 ALU:
执行加法。
于是 ALU 计算:
10 + 20 = 30
此时,结果已经在 CPU 内部产生了。
但是,这条指令通常还没有真正结束。
因为这个结果还需要保存到某个地方。
9. 并不是每条指令都以相同方式使用 ALU
所谓的“执行阶段”,并不意味着 CPU 每次都只是进行普通的算术运算。
不同类型的机器指令,会以不同方式使用数据通路。
例如,一条算术指令可能让 ALU 计算:
A + B
一条比较指令可能让 ALU 判断:
A < B
而一条内存访问指令,则可能利用 ALU 来计算一个内存地址。
例如,一条 LOAD 指令可能表示:
从地址 R2 + 8 的位置加载数据。
此时 ALU 可能首先计算:
address = R2 + 8
然后 CPU 再使用这个地址去访问内存。
因此,即使某些指令表面上看起来并不是“算术指令”,它们仍然可能需要使用 ALU。
10. LOAD 和 STORE 指令的内存访问
算术指令通常主要处理寄存器中的数据。
但是,程序中的大量数据实际上存放在内存中。
这时,LOAD 和 STORE 指令就非常重要。
LOAD 指令负责把数据从内存复制到寄存器:
Memory → Register
也就是:
内存 → 寄存器
而 STORE 指令负责把数据从寄存器写入内存:
Register → Memory
也就是:
寄存器 → 内存
例如:
LOAD R1, 8(R2)
可能表示:
首先计算 R2 中的地址再加上 8。
然后读取这个内存地址中的数据。
最后把读取到的数据存入 R1。
因此,CPU 内部可能经历这样的数据流:
寄存器 → ALU → 地址 → 内存 → 寄存器
这也是为什么执行一条机器指令,绝不仅仅只是使用 ALU 做一次计算。
11. 写回阶段保存结果
操作完成之后,计算结果通常还需要返回到寄存器文件。
这个阶段通常称为:
写回(Write Back)
继续使用前面的 ADD 例子:
R2 = 10
R3 = 20
ALU 得到:
30
然后 CPU 执行:
R1 = 30
因此,整个过程大约可以表示为:
指令
→ 译码
→ 读取 R2 和 R3
→ ALU
→ 结果
→ 写入 R1
到了这里,这条 ADD 指令才算真正执行完成。
新的数值 30 已经保存在 R1 中,可以继续被后面的指令使用。
12. 程序计数器移动到下一条指令
在执行当前指令的同时,处理器还必须确定:
下一条指令在哪里?
在正常情况下,程序计数器会按照指令顺序向前移动。
如果当前指令位于某个地址,那么下一条指令通常位于紧接着的下一个指令地址。
但是,当 CPU 遇到 控制流指令(Control Flow Instruction) 时,情况就会发生变化。
常见的控制流指令包括:
- 分支
- 跳转
- 函数调用
- 函数返回
例如,一条分支指令可能告诉处理器:
如果两个数相等,就从另一个地址继续执行。
这时,程序计数器的值就会被修改。
程序中的循环、if 语句、函数调用以及其他控制结构,最终在机器层面都依赖类似的机制来实现。
13. Fetch、Decode、Execute 实际上是一个更大的周期
我们经常使用:
Fetch → Decode → Execute
来描述 CPU 的指令执行过程。
这种表达非常有用,因为它抓住了指令执行最核心的概念。
但是,一个更加完整的简化指令周期通常应该表示为:
Fetch
→ Decode
→ Read Registers
→ Execute
→ Memory Access
→ Write Back
也就是:
取指
→ 译码
→ 读取寄存器
→ 执行
→ 访问内存
→ 写回
不同类型的指令,并不一定会使用其中的所有阶段。
例如,ADD 指令可能根本不需要访问数据内存。
STORE 指令通常不会把计算结果重新写入一个通用寄存器。
而分支指令可能主要修改程序计数器。
CPU 中的数据通路是一套共享硬件。
不同的机器指令决定:
这一次究竟需要使用其中哪些部分。
14. 一条 MIPS ADD 指令如何穿过整个 CPU
现在来看一条典型的 MIPS 风格指令:
add $t0, $t1, $t2
它的含义是:
$t0 = $t1 + $t2
现在我们已经可以完整跟踪这条指令在 CPU 内部的执行过程了。
首先,程序计数器确定这条指令所在的地址。
CPU 取得这条机器指令。
译码器识别出:
这是一条 ADD 指令。
指令中还指定:
$t1
和:
$t2
是两个源寄存器。
寄存器文件同时读取这两个寄存器中的值。
控制单元告诉 ALU:
执行加法。
ALU 将两个数值相加。
计算结果通过写回数据通路返回。
最后,结果被保存到:
$t0
因此,在汇编语言中看起来只有短短一行:
add $t0, $t1, $t2
实际上却会启动 CPU 内部多个硬件部件:
Program Counter
→ Instruction Memory
→ Decoder
→ Control Logic
→ Register File
→ ALU
→ Write-Back Path
→ Register File
也就是:
程序计数器
→ 指令内存
→ 译码器
→ 控制逻辑
→ 寄存器文件
→ ALU
→ 写回通路
→ 寄存器文件
这才是一条简单汇编指令背后真正的硬件含义。
15. 现代 CPU 执行指令的速度更快
基本的指令周期模型,对于理解处理器非常重要。
但是,真正的现代 CPU 要复杂得多。
现代 CPU 通常不会等待一条指令完全执行结束之后,才开始处理下一条指令。
现代处理器可能使用:
- 流水线(Pipelining)
- 多个执行单元
- 分支预测
- 高速缓存
- 超标量执行(Superscalar Execution)
- 乱序执行(Out-of-Order Execution)
- 寄存器重命名(Register Renaming)
以流水线为例,多条指令可以同时处于不同的执行阶段。
例如:
一条指令正在执行。
另一条指令正在译码。
还有一条指令可能已经开始取指。
因此,简单的:
Fetch → Decode → Execute
并不能完整描述现代高性能 CPU 的内部工作方式。
但是,它仍然是理解现代 CPU 的基础。
因为流水线、超标量、分支预测、乱序执行等更复杂的技术,本质上都在尝试让这些基本的指令处理步骤执行得更快、更高效。
结语
CPU 并不是把整个程序当成一个巨大的操作一次性执行。
CPU 执行的是:
一条又一条机器指令。
每一条指令都会经历一系列有组织的硬件活动。
程序计数器确定指令的位置。
CPU 取得指令。
译码器判断指令是什么意思。
控制单元启动所需要的数据通路。
寄存器提供操作数。
ALU 执行算术或逻辑操作。
必要的时候,CPU 会访问内存。
最后,计算结果可能被重新写入寄存器。
然后 CPU 继续执行下一条指令。
因此,我们熟悉的:
Fetch → Decode → Execute
也就是:
取指 → 译码 → 执行
代表了计算机内部最基本、也最重要的工作过程之一。
一条简单的汇编指令:
add $t0, $t1, $t2
从程序员的角度看,似乎只是一次简单的加法。
但是在处理器内部,它实际上会形成一条协调的数据流,依次经过:
程序计数器、指令内存、译码器、控制逻辑、寄存器文件、ALU 和写回电路。
理解指令周期,可以帮助我们建立一座非常重要的桥梁:
从软件,一直理解到硬件。
接下来,一个很自然的问题就是:
指令和数据究竟来自哪里?CPU 又是如何与内存通信的?
这就直接引出了下一个主题:
