引言

本次课内容:

  • 从状态机模型理解计算机系统
    • 程序
    • 指令集
    • CPU
  • 对 “程序如何在计算机上运行”建立基本认识

状态机模型

一个不是特别严谨的定义

  • 状态集合\(S = \{S_1, S_2, \dots\}\)
  • 激励事件\(E\)
  • 状态转移规则\(next: S \times E \to S\)
    • 描述每个状态在不同激励事件下的次态(next state)
  • 初始状态\(S_0 \in S\)

计算机系统都是状态机!

考虑一个简单的计算机系统: 程序直接在CPU上运行(无操作系统)

               +-------------------+
               |      Program      |
               +-------------------+
               |        ISA        |
               +-------------------+
               |        CPU        |
               +-------------------+

 

这三个抽象层次(程序, 指令集, CPU)都可以用状态机来理解!

程序是个状态机

大家眼中的C程序

C语言的组成

  • 变量 - 计算的对象
  • 语句 - 计算的操作流程
  • 输入输出函数 - 让变量与外界交互

 

一个例子

int main() {
  int x = 1;
  int y = 2;
  int z = x + y;
  printf("z = %d\n", z);
  return 0;
}

C程序的状态机模型

  • 状态集合\(S = \{<V, PC>\}\)
    • \(V = \{v_1, v_2, v_3, \dots\}\) = 程序中所有变量的取值
      • 包括全局变量和局部变量
    • \(PC\) = 程序计数器 = 当前执行的语句位置
  • 激励事件\(E = \{语句\}\)
    • 执行PC指向的语句
  • 状态转移规则\(next: S \times E \to S\)
    • 语句的语义(semantics)
  • 初始状态\(S_0 = <V_0, main函数的第一条语句>\)

例子

/* 1 */ int main() {
/* 2 */   int x = 1;
/* 3 */   int y = 2;
/* 4 */   int z = x + y;
/* 5 */   printf("z = %d\n", z);
/* 6 */   return 0;
/* 7 */ }

 

// S = <x, y, z, PC>
  S0 = <?, ?, ?, 2 > // '?'表示未初始化
  S1 = <1, ?, ?, 3 >
  S2 = <1, 2, ?, 4 >
  S3 = <1, 2, 3, 5 >
  S4 = <1, 2, 3, 6 > // 输出"z = 3"
  S5 = <1, 2, 3, 结束>

但好像还有很多细节没完全搞清楚

x = (-b+sqrt(b*b-4*a*c))/(2*a);
  • 如果执行的语句很复杂, 应该如何理解?
    • 可以将复杂语句拆分成若干简单语句, 从简单语句理解状态转移
    • 确实有工具做这件事: C Intermediate Language

 

  • 语句的语义是谁说了算?
    • 谭浩强的书 ❎
    • FM: C语言标准手册
      • 权威严谨, 但对新手来说不好读
      • 追根溯源的唯一选择

例: 真的是从main()第一条语句开始执行吗?

大家一定会觉得这是理所当然的, 至少很多C语言书籍都这么说

但怎么来动手验证一下呢?

 

要理解程序的动态行为, 当然是用trace工具!

$ cat a.c
int main() { return 0; }
$ gcc a.c
$ strace ./a.out

 

好像还有个exit_group(), 感觉从main()函数返回之后也有宝藏

 

调试器可以帮助我们更细致地理解这个问题

(gdb) starti

看看FM怎么说

5.1.2 Execution environments
Two execution environments are defined: freestanding and hosted. In both cases,
program startup occurs when a designated C function is called by the execution
environment...

有一个概念叫执行环境, 原来是它来调用一个专门的C函数

  • 执行环境有两种: 独立环境(freestanding)和宿主环境(hosted)

 

5.1.2.1 Freestanding environment
1. In a freestanding environment (in which C program execution may take place
without any benefit of an operating system), the name and type of the function
called at program startup are implementation-defined.

在独立环境下, 这个专门的C函数由具体实现来决定

5.1.2.2 Hosted environments
5.1.2.2.1 Program startup
1. The function called at program startup is named main...

在宿主环境下, 这个专门的C函数名称为main

看看FM怎么说(续)

2. ...
— If the value of argc is greater than zero, the string pointed to by argv[0]
  represents the program name; ...
  If the value of argc is greater than one, the strings pointed to by argv[1]
  through argv[argc-1] represent the program parameters.

上次课提到的命令行参数=main()函数的参数, 原来是有手册依据的

 

5.1.2.3 Program execution
2. Accessing a volatile object, modifying an object, modifying a file, or calling
a function that does any of those operations are all side effects, which are
changes in the state of the execution environment...

一些语句的操作会引起副作用, 从而导致执行环境状态的变化

  • C程序确实是一个状态机
    • 我们对C程序的行为建模, 从大方向来说符合手册

 

虽然main()函数不是真正的程序入口, 但足够用来理解状态机模型

RTFM

C语言标准手册精确定义了C语言的每一处细节

  • 想了解一切细节发生的依据, RTFM是唯一正确的选择

大部分C语言的材料都没有覆盖到所有细节, 因此不要迷信书籍和博客

  • 书籍和博客的作者对C语言的认识, 比不上C语言标准的制定者

 

正确的学习方法:

  1. 通过书籍入门
  2. 把书籍丢掉
  3. 通过手册成为专业人士

 

开放讨论: 公司不一定完全按标准来实现

  • 从一个领域来看, 遵循标准有利于领域的稳定和繁荣
    • TCP/IP(网络), SQL(数据库), OpenGL(图形), 3GPP组织(通信), …

CPU是个状态机

CPU = 数字逻辑电路 = 状态机

CPU如何设计是微结构层次的话题

但无论怎么设计, 总归是一个数字逻辑电路

  • 数字逻辑电路 = 组合逻辑电路 + 时序逻辑电路

 

  • 状态集合\(S = \{<时序逻辑元件的值>\}\)
    • 具体包括寄存器, 存储器, 触发器等
  • 激励事件\(E = \{组合逻辑\}\)
  • 状态转移规则\(next: S \times E \to S\)
    • 由设计中的组合逻辑电路决定
    • 依据: 架构师的设计文档
  • 初始状态\(S_0 = <复位时时序逻辑元件的值>\)

例: Johnson计数器

// S = <A, B, C, D>
  S0 = <0, 0, 0, 0>
  S1 = <1, 0, 0, 0>
  S2 = <1, 1, 0, 0>
  S3 = <1, 1, 1, 0>
  S4 = <1, 1, 1, 1>
  S5 = <0, 1, 1, 1>
  S6 = <0, 0, 1, 1>
  S7 = <0, 0, 0, 1>
  S8 = <0, 0, 0, 0> = S0

从状态机的视角来说, CPU和这个计数器并没有本质上的区别

指令集也是个状态机

指令集是什么?

课本上通常会给一个抽象的定义, 例如

指令集是软件和硬件之间的接口

这句话本身是对的, 但对初学者来说没什么实际意义

  • 你很可能还分不清软件和硬件的边界, 更别说它们之间的接口了 😂

 

我们来进行这样的比喻: 指令集是一本手册规范, 使得

指令集手册定义了CPU执行指令的行为

就好比

C语言标准手册定义了C程序执行语句的行为

指令集和处理器是不同层次的概念

  • 指令集和处理器都各自有3种知识产权模式
    • 开放免费(Open & Free) - 不花钱就能用
    • 可授权(Licensable) - 交钱就能用
    • 封闭(Closed) - 有钱也不能用

 

下图取自《关于RISC-V和开源处理器的一些解读》

正确理解开放指令集和开源处理器

  • 封闭指令集不可能做出开源处理器(矩阵的左下三角)
  • 开放指令集并不意味着做出的处理器必须开源(矩阵的右上三角)
  • 过去只有可授权和封闭模式, 非专业人士大多只看到矩阵的对角线
    • 不易区分指令集和处理器

包云岗老师对文章《点评RISC-V芯片出货量突破100亿》的补充评论:

RISC-V好比处理器领域的马克思主义,RISC-V手册好比《共产党宣言》,全世界各国都可以获取(即使是被
极限制裁的俄罗斯),关键是谁能实践好,也就是参照RISC-V手册做出高水平的处理器芯片,进而影响和主导
RISC-V的后续演进。

自测题

出自《从技术的角度来看,RISC-V 能对芯片发展、科技自主起到哪些作用?》

  • X86是一种指令集规范?
  • 苹果M1牛是因为采用了ARM指令集?
  • 国产处理器的实现和国外还有差距?
  • 几个月就可以定义一个新指令集?
  • 根据指令集规范实现一个处理器不容易?
  • 给定一个指令集只有一种处理器实现?
  • 可以给一个处理器实现换一个指令集?

 

答案: Yes, No, Yes, Yes, Yes, No, Yes

更多参考资料: 《4年21份资料10万字:记录RISC-V在中国的一条轨迹》

总会有缺乏专业素质的自媒体哗众取宠

 

《俄数字发展部部长:将大力扶持国产 RISC-V 处理器发展》

  • 俄罗斯部长脑抽掉陷阱了? 🙃

 

推荐参加“一生一芯”补充基础知识, 零基础亦可学习

RTFM学习指令集

虚假的RISC-V手册 ❎

真正的RISC-V手册

《The RISC-V Reader》是一本科普读物, 并不是官方手册

  • 书的前言
We intend this slim book to work as both an introduction and a reference to RISC-V
for students and embedded systems programmers interested in writing RISC-V code.

我们打算将这本小书作为RISC-V的介绍和参考资料,供有兴趣编写RISC-V代码的学生和嵌入式系统程序员使用

翻译团队把书名翻译成《RISC-V手册》, 太误导新人了 😂

RTFM学习指令集(续)

《The RISC-V Reader》的作者(David Patterson & Andrew Waterman)也来自RISC-V团队, 书的质量并不低

  • 书中介绍了很多软硬件协同工作的例子和指令集设计的考量
  • 据翻译团队透露, 书籍的中文版已经重新排版, 正在出版中
    • 书名修改成《RISC-V读本》 ✅
  • 但这仍然不能替代真正的官方手册

 

正确做法:

  • 通过《RISC-V读本》学习RISC-V
  • 通过《The RISC-V Instruction Set Manual》了解所有最新的细节
    • 例如原子指令语义的定义

指令集手册也定义了一个状态机!

  • 状态集合\(S = \{<R, M>\}\)
    • \(R = \{PC, x_0, x_1, x_2, \dots\}\)
      • RISC-V手册 -> 2.1 Programmers’ Model for Base Integer ISA
      • \(PC\) = 程序计数器 = 当前执行的指令位置
    • \(M\) = 内存
      • RISC-V手册 -> 1.4 Memory
  • 激励事件\(E = \{指令\}\)
    • 执行PC指向的指令
  • 状态转移规则\(next: S \times E \to S\)
    • 指令的语义(semantics)
  • 初始状态\(S_0 = <R_0, M_0>\)

Q: RISC-V处理器的复位PC值是多少?

A: RTFM (《The RISC-V Reader》中找不到答案)

用C程序理解指令

计算1+2+...+100的指令序列

// PC: instruction      | label: statement
    0: li   x1, 0       |   pc0: x1 = 0;
    1: li   x2, 0       |   pc1: x2 = 0;
    2: li   x3, 100     |   pc2: x3 = 100;
    3: addi x2, x2, 1   |   pc3: x2 = x2 + 1;
    4: add  x1, x1, x2  |   pc4: x1 = x1 + x2;
    5: blt  x2, x3, 3   |   pc5: if (x2 < x3) goto pc3;   // branch if less than
    6: j    6           |   pc6: goto pc6;

指令并没有想象中的那么神秘

  • 就是用来改变状态机状态的激励

 

指令的两种表示:

  • 符号化表示 - 面向程序员
  • 编码表示 - 面向电路设计

指令集手册不仅仅包含指令

全称: 指令集体系结构(Instruction Set Architecture, ISA), 还有

  • 输入输出
  • 系统状态
  • 中断异常
  • 虚存管理
  • 内存模型

这些都可以用状态机模型来描述行为

 

指令集手册通过定义状态机进行状态转移的规则, 来从概念上描述一台抽象计算机所具备的, 程序可以使用的功能

使用计算机底层功能的用户(系统程序员)无需了解具体的电路设计

  • 只需了解计算机的行为即可 -> 状态机模型

程序如何在计算机上运行

编译

编译器的工作: 将C程序的状态机\(S_c\)翻译成指令集的状态机\(S_{isa}\)

  • \(s_{compile}: \{PC_c, v_1, v_2, v_3, \dots\} \to \{R, M\}\) (\(R\)中包含\(PC_{isa}\))
  • \(e_{compile}: \{语句\} \to \{指令序列\}\)
  • \(next: S \times E \to S\)

使得

\(s_{compile}(next(S_c, 语句))\)

\(= next(s_{compile}(S_c), e_{compile}(语句))\)

\(= next(S_{isa}, 指令序列)\)

 

说人话: C程序执行一条语句后的状态, 与抽象计算机执行编译后的指令序列后的状态,语义上是等价的

汇编

  • 汇编指令 = 指令的符号化表示
  • 汇编程序 = 驱动指令集状态机的输入
    • 执行汇编程序 = 指令集状态机发生状态转移
  • 汇编课 = RTFM(指令集手册)

 

汇编课就这样上完了 😂

CPU微结构设计

CPU微结构设计的工作: 根据指令集的状态机\(S_{isa}\)用电路实现CPU的状态机\(S_{cpu}\)

  • \(s_{arch}: \{R, M\} \to \{时序逻辑电路\}\)
  • \(e_{arch}: \{指令\} \to \{组合逻辑电路\}\)
  • \(next: S \times E \to S\)

使得

\(s_{arch}(next(S_{isa}, 指令))\)

\(= next(s_{arch}(S_{isa}), e_{arch}(指令))\)

\(= next(S_{cpu}, 组合逻辑电路)\)

 

说人话: 抽象计算机执行一条指令后的状态, 与CPU在根据指令语义设计出的组合逻辑电路控制下的次态,语义上是等价的

程序如何在计算机上运行

程序和指令集都没有实体, 计算机的实体是电路, 如何联系它们?

 

  • 根据指令集手册的功能描述, 画一张CPU的电路图 -> 微结构设计
  • 用RTL代码描述CPU电路图 -> RTL设计
  • 根据RTL代码生成版图文件 -> 后端物理设计
  • 根据版图文件生产芯片 -> 制造生产
  • 编写程序 -> 软件编程
  • 将程序翻译成指令集手册中描述的指令序列 -> 编译
  • 程序在CPU上执行 = 指令序列控制CPU芯片电路进行状态转移
    • 三个状态机产生联系: \(S_c\) ~ \(S_{isa}\) ~ \(S_{cpu}\)

一句话就能说清楚, 为什么搞那么复杂?

  • 状态机模型是理解复杂系统的一种有效方法

一句话就能说清楚, 为什么搞那么复杂?(2)

  • 不过最重要的是给大家传达一种观念

机器永远是对的

计算机系统的行为是按照官方手册的描述精确发生的

  • 每一次状态转移都有手册依据
  • 如果你不理解计算机系统的行为, 很大概率是因为你不了解相关手册中的某些关键细节
    • 这对初学者会有一定压力(哪都不太了解)
      • 但只有树立正确的观念, 才能解决问题
  • “永远”的理解: 商业产品也会有bug(Intel奔腾的fdiv bug), 但你恰好遇上的概率很小

总结

计算机系统都是状态机

程序 抽象计算机 CPU
状态 \(\{<V, PC>\}\) \(\{<R, M>\}\) \(\{时序逻辑电路\}\)
状态转移规则 C语言语句的语义 指令的语义 组合逻辑电路
FM C语言标准手册 指令集手册 架构设计文档

 

  • 程序编译 = 将语句翻译成语义等价的指令序列
  • 微结构设计 = 按照指令语义设计行为等价的电路
  • 程序运行 = 指令序列驱动电路进行状态转移

 

  • 机器永远是对的
  • 通过RTFM了解细节成为专业人士, 不要迷信非官方材料