引言

本次课内容:

计算机系统的状态机模型

  • 程序
  • 指令集
  • CPU

对 “程序如何在计算机上运行”建立基本认识

状态机模型

一个不是特别严谨的定义

  • 状态集合S = {S1, S2, ...}
  • 激励事件
  • 状态转移规则
    • 描述每个状态在不同激励事件下的次态(next state)
  • 初始状态S0 ∈ 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 = {v1, v2, v3, ...} = 程序中所有变量的取值
      • 包括全局变量和局部变量
    • PC = 程序计数器 = 当前执行的语句位置
  • 激励事件
    • 执行PC指向的语句
  • 状态转移规则
    • 语句的语义(semantics)
  • 初始状态S0 = <V0, 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程序确实是一个状态机

 

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

RTFM

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

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

 

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

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

 

正确的学习方法:

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

CPU是个状态机

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

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

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

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

 

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

例: 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程序执行语句的行为

指令集和CPU是不同层次的概念

随着开源芯片相关概念的普及, 总会有缺乏专业素质的自媒体哗众取宠

 

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

  • 俄罗斯专家脑抽掉陷阱了? 🙃

开放指令集和开源CPU也是不同层次的概念

下图取自《关于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在中国的一条轨迹》

RTFM

虚假的RISC-V手册 ❎

真正的RISC-V手册

《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(续)

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

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

正确做法:

  • 通过《RISC-V读本》学习RISC-V
  • 通过《RISC-V Instruction Set Manual》了解所有最新的细节
    • 例如原子指令语义的定义
    • 不要再问RISC-V手册有没有中文版: 没有!

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

  • 状态集合S = {<R, M>}
    • R = {PC, x0, x1, x2, ...}
      • RISC-V手册 -> 2.1 Programmers’ Model for Base Integer ISA
      • PC = 程序计数器 = 当前执行的指令位置
    • M = 内存
      • RISC-V手册 -> 1.4 Memory
  • 激励事件: 执行PC指向的指令
  • 状态转移规则: 指令的语义(semantics)
  • 初始状态S0 = <R0, M0>

 

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

A: RTFM (《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

  • fstate: {PC_c, v1, v2, v3, ...} -> {R, M} (R中包含PC_isa)
  • fcompile: {语句} -> {指令序列}

使得

  fstate(next(S_c, 语句))
= next(fstate(S_c), fcompile(语句))
= next(S_isa, 指令序列)

 

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

汇编

  • 汇编 = 指令的符号化表示
  • 汇编程序 = 指令集的状态机
  • 汇编课 = RTFM(指令集手册)

 

汇编课就这样上完了 😂

微结构设计

微结构设计的工作: 将指令集的状态机S_isa用电路来实现成CPU的状态机S_cpu

  • fstate: {R, M} -> {时序逻辑电路}
  • fuarch: {指令} -> {组合逻辑电路}

使得

  fstate(next(S_isa, 指令))
= next(fstate(S_isa), fuarch(指令))
= next(S_cpu, 组合逻辑电路)

 

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

程序如何在计算机上运行

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

 

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

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

  • 有些概念和问题从状态机模型思考可以给我们带来新的启发
    • 后面我们会遇到
  • 不过最重要的是给大家传达一种观念

机器永远是对的

 

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

  • 每一次状态转移都有手册依据
  • 如果你不理解计算机系统的行为, 很大概率是因为你不了解相关手册中的某些关键细节
    • “永远”的理解: 商业产品也会有bug(Intel奔腾的fdiv bug), 但你恰好遇上的概率很小

总结

计算机系统都是状态机

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

 

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

 

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