Skip to content

指令集架构的状态机模型

F4 计算机系统的状态机模型 - 指令集架构的状态机模型 | 一生一芯 v24.07 学习讲义

Finite-state machine | Wikipedia

Abstract

在处理器组成及工作原理中,我们从「指令怎么编码、程序怎么跑」两个角度理解处理器的工作原理;本篇把同一套东西抽象成一个数学模型:指令集架构(Instruction Set Architecture, ISA)就是一台只存在于纸面上的模型机,它的行为可以用状态机精确描述。

  • ISA 是规范,处理器是实现。 x86、ARM、RISC-V 和处理器组成及工作原理中提到的三指令模型,都是 ISA。

  • 状态机四要素:状态集合、激励事件、转移规则、初始状态。ISA 里它们分别对应「PC + GPR + 内存」「执行指令」「指令语义」「上电后的初值」。

  • 跑程序 = 按约定(手册、编译器、CPU 设计)一步步转移状态。 编译是把高级语言的状态机翻译成 ISA 的状态机;设计 CPU 则是用同步时序逻辑电路把这台模型机做出来。

指令集架构的本质

上一篇里的 GPR、PC、存储器、指令编码和存储程序循环,在计算机领域里统称指令集架构(Instruction Set Architecture,ISA,也常简称指令集)。

计算机术语中常见的x86、ARM、RISC-V都是 ISA。处理器组成及工作原理中提到的那个只有 ADD、LI、BNER0 三条指令的例子,同样是一种 ISA,下文把它叫做 sISA(simple ISA)。

ISA 的本质是一份规范:它定义一台模型机具备哪些功能、每条指令执行后系统该变成什么样。

模型机只存在于思维和手册里,我们先讨论「做什么」,不讨论「电路怎么连」。把原理和实现拆开,才容易看清处理器的本质;等用数字电路把这台模型机做出来,才得到真正能跑的计算机。

Tip

ISA 可以看作是一份“说明书”,处理器是按说明书做出来的芯片。同一份 ISA 可以有很多种实现(Intel 和 AMD 都实现 x86);开放的 ISA(如 RISC-V)也完全可以做出闭源商业芯片。

在计算机系统层次结构里,传统机器 \(M_1\)(机器指令系统)对应的就是 ISA 这一层:上面是汇编 / 操作系统 / 高级语言,下面才是具体电路。

商业 ISA 无非是 GPR 更多、指令更复杂。原理和 sISA 没有本质区别。要理解一个ISA,只需要去读相应的手册即可。

面向计算机系统的状态机模型

在把 ISA 说成状态机之前,首先学习一下有限状态机的数学模型。它并不神秘,交通灯就是一台状态机:

stateDiagram-v2
    [*] --> 红灯
    红灯 --> 绿灯: 定时到
    绿灯 --> 黄灯: 定时到
    黄灯 --> 红灯: 定时到

一份完整的状态机定义包含四部分:

要素 含义 交通灯
状态集合 \(S\) 系统所有可能停住的样子 \(\{红, 绿, 黄\}\)
激励事件集合 \(E\) 会推动系统变化的输入 \(\{定时到\}\)
状态转移规则 \(\delta\) 当前状态遇到某事件后变成哪一个次态:\(\delta: S \times E \to S\) \(\delta(绿, 定时到) = 黄\)
初始状态 \(s_0\) 尚未发生任何事件时的状态 上电后的红灯

这样的定义在数学上不必 100% 严谨,但对理解计算机已经够用。

数学中的状态机模型

在数学里,“状态机模型”通常就指一个状态转移系统,最常见的写法是一个四元组:

\[ (S,\;E,\;\delta,\;s_0) \]
  • \(S\):状态集合。系统所有可能处于的“样子”。

  • \(E\):激励事件集合。你把它理解成“推动系统变化的输入”(也可以理解成“发生了一件事”)。

  • \(\delta\):转移规则。描述“在当前状态 \(s \in S\) 遇到事件 \(e \in E\) 后,系统进入哪个次态”:\(s' = \delta(s,e)\)。

  • \(s_0\):初始状态。系统还没开始运行前的样子。

一段运行(execution / trace)怎么写很简单:给定事件序列 \(e_1,e_2,\\dots\),系统依次按

\[ s_1=\delta(s_0,e_1),\quad s_2=\delta(s_1,e_2),\quad \dots \]

一路走下去。

和 FSM 的关系

FSM(Finite-State Machine, 有限状态机)通常就是在上面这个框架下,再强调“状态集合是有限的”;而“状态机”这个词在计算机体系结构里经常用得更宽泛,本质上就是在讲同一套“状态 + 事件 + 转移规则”的思路。

回忆时序逻辑:状态就是那些可以稳定存储的信息。 交叉配对反相器、锁存器、D 触发器能存储 \(1 bit\) 的信息;若干触发器拼成寄存器,就能记住一组数。计数器、数列求和电路的工作过程,其实就是「当前寄存器取值 + 组合逻辑算出下一取值 + 时钟边沿写回」,也就是状态机在电路里的样子。

同步时序电路的设计正是从状态图 / 状态表出发:先规定有哪些状态、怎么转移,再编码、再映射成门和触发器。ISA 走的是同一条路,只是状态不再是几个触发器,而是整台模型机的程序员可见存储。

状态机视角下的ISA

把状态机四要素映射到 ISA 上:

要素 在 ISA 中是什么
状态集合 \(S\) 一组具体的 PC、GPR、内存。全体状态就是这三者所有取值的组合:\(S = PC \times GPR \times Mem\)
激励事件 \(E\) 执行一条指令(指令会改写状态)
转移规则 \(\delta\) 指令的语义:手册约定「在某状态下执行某指令后,次态是什么」
初始状态 \(s_0\) 尚未执行任何指令时的取值(上电 / 复位后的 PC、寄存器、内存)

「执行哪条指令」并不是随便挑的:由当前状态决定——取出 \(M[PC]\),再按它的语义转移。程序一旦放进内存、PC 指向开头,状态机就会自己一步步走下去。这正是存储程序的含义。

sISA 的状态实例化

sISA 的转移规则就是这三条指令的语义(编码约定见指令的编码):

 7  6 5    4 3    2 1    0
+----+------+------+------+
| 00 | dest | src1 | src2 |  R[dest] = R[src1] + R[src2]   ADD
+----+------+------+------+
| 10 | dest |     imm     |  R[dest] = imm                 LI
+----+------+------+------+
| 11 |     addr    | src2 |  if (R[0] != R[src2]) PC=addr  BNER0
+----+-------------+------+

普通指令执行完后,PC 还会按通常方式指向相邻下一条;BNER0 在条件成立时改写 PC,否则同样顺序往下。

一个状态可写成 \((PC,\, r_0,\, r_1,\, r_2,\, r_3,\, M)\)。sISA 的指令不改内存(没有 load/store),\(M\) 在整个程序运行期间不变,表示状态时可以省略它:

\[ s = (PC,\, r_0,\, r_1,\, r_2,\, r_3) \]

一次具体的转移

数列求和程序里,地址 \(5\) 存放的是机器码 00101001,即 ADD r2, r2, r1,语义为 \(R[2] \leftarrow R[2] + R[1]\)。

执行前状态为 \((5,\, 10,\, 1,\, 0,\, 1)\):PC 指向地址 \(5\),\(r_1=1\)(当前项),\(r_2=0\)(部分和)。执行后 \(r_2\) 变成 \(0+1=1\),PC 顺序变为 \(6\):

\[ \delta\bigl((5,\,10,\,1,\,0,\,1),\; \texttt{00101001}\bigr) = (6,\,10,\,1,\,1,\,1) \]

这就是上一篇推演表里的其中一步。处理器并没有「理解」求和,它只是按照事先约定好的ISA把寄存器改成次态。

程序运行的状态机模型

把上一篇 \(1+2+\cdots+10\) 的指令序列放进内存,初始状态约定为 \((0,\,0,\,0,\,0,\,0)\)。前几步转移如下(中间两次 LI 省略,内存始终不变故不画出):

stateDiagram-v2
    [*] --> s0
    s0: (0, 0, 0, 0, 0)
    s1: (1, 10, 0, 0, 0)
    s4: (4, 10, 0, 0, 1)
    s5: (5, 10, 1, 0, 1)
    s6: (6, 10, 1, 1, 1)
    s4b: (4, 10, 1, 1, 1)
    s0 --> s1: LI r0, 10
    s1 --> s4: 其余初始化
    s4 --> s5: ADD r1, r1, r3
    s5 --> s6: ADD r2, r2, r1
    s6 --> s4b: BNER0 跳回 4
    s4b --> s5: 下一圈 i 再加 1

循环会一直转到 \(r_1 = r_0 = 10\),部分和 \(r_2 = 55\),最后停在地址 \(7\) 的自跳转。程序的一次完整运行,就是从 \(s_0\) 出发、按 \(\delta\) 走出来的一条状态路径。

Example

状态 \((6,\,10,\,1,\,1,\,1)\) 即将执行地址 \(6\) 的 BNER0 r1, 4。此时 \(r_1=1\)、\(r_0=10\),次态是什么?

条件 \(r_1 \neq r_0\) 成立,PC 被改写成 \(4\),GPR 不变,次态为 \((4,\,10,\,1,\,1,\,1)\)——也就是图里 s6 → s4b 那一步。若某次比较时 \(r_1=r_0\),则不跳转,PC 顺序变为 \(7\),循环结束。

计算机系统的层次抽象

同一套状态机模型,还可以套到更高层的程序、更底层的电路上。三者对上之后,编译和 CPU 设计各自在干什么就清楚了。

C 程序 ISA 数字电路
状态 变量 + 「执行到哪一句」的 PC PC + GPR + 内存 时序元件里存的值
激励 执行一条语句 执行一条指令 组合逻辑算出的下一状态
转移规则 语句的语义 指令的语义 组合逻辑的具体逻辑
初态 main 第一条语句,变量未赋值 复位后的 PC / 寄存器 / 内存 电路复位时触发器的值

电路那一层,就是时序逻辑里反复出现的闭环:

    +------------------+
+-->| Sequential Logic |----+
|   +------------------+    |
| next state                | current state
|                           |
|  +---------------------+  |
+--| Combinational Logic |<-+
   +---------------------+

寄存器存当前状态,组合逻辑算次态,时钟边沿写回——和计数器、数列求和电路同一套路。CPU 能做出来,是因为它也是「时序 + 组合」,因而也一定能从状态机来理解。

编译的本质

简单来说,编译的本质就是把 C 程序的状态机译成 ISA 的状态机。

计算机只能执行指令,读不懂 sum = sum + i;。所谓编译(compile),就是构造一个指令序列,使得C程序执行一条语句后的状态,与 ISA 执行对应指令序列后的状态,语义上等价。

落到具体工作上只有两件事:

  1. 状态怎么对应: C 的 PC 对到 ISA 的 PC;C 的变量对到 GPR 或内存(上一篇求和里,i、s 分别住在 r1、r2)。

  2. 转移怎么对应: 把语句译成指令序列(sum = sum + i 对到 ADD r2, r2, r1)。

高级语言的变量名更直观,循环的条件和循环体也分得更开;汇编里这些都混在指令里,要靠上下文推断。这就是编程的本质里「用少量现有功能组合出复杂过程」在两层语言上的差别。

同一段求和,两种状态写法

上一篇推演里出现过 \((6,\,10,\,1,\,1,\,1)\):部分和 \(r_2=1\),当前项 \(r_1=1\)。写成 C 就是 \(sum=1\)、\(i=1\)。名字不同、存放位置不同,指的是同一份计算进度——编译要保证的,正是这种对齐。

CPU 设计的本质

设计 CPU,就是构造数字电路,使得 ISA 执行一条指令后的次态,与这块电路在组合逻辑控制下的次态,语义上等价。

所以:

在 CPU 上运行程序 = 用编译得到的指令序列,驱动 CPU 电路做状态转移。

微结构设计画的是「按 ISA 手册,CPU 内部该有哪些模块」;逻辑设计再把模块映射成门和触发器。软件编程则发生在更上面:程序员编写 C 程序,编译器译成 ISA 指令,电路按指令转移。三层状态机在运行时叠在一起,看起来像一台机器在「执行程序」。

因此,站在宏观角度来看,ISA是连接软硬件的桥梁,是计算机系统的核心抽象:

ISA 手册里还有什么

指令语义只是 ISA 的一部分。完整手册通常还约定:

  • 输入 / 输出: 怎样和外界交换数据

  • 系统状态: 特权级、控制寄存器等

  • 中断 / 异常: 执行流被打断时状态如何保存和恢复

  • 虚存管理: 程序看到的地址如何映射到物理内存

  • 内存模型: 多核、乱序访问时,怎样才算「看见了同一次写入」

凡是程序员能观察到的、执行指令会改变的东西,都属于 ISA 状态;手册是这份状态如何转移的唯一依据。