程序 = 状态机
- gdb 单步执行 = 状态迁移
-
- 状态里有什么?gdb 可以打印
- 有一些特殊的状态迁移
硬件 = 状态机
- 指令执行 = 状态迁移
-
- 从 CPU Reset 开始执行 Firmware
- Firmware 加载操作系统 (程序)
操作系统 = 状态机 (毫无疑问)
程序是一种真正意义上的 "数学严格" 的对象
- Everything is a state machine
-
- 程序 = 初始状态 + 迁移函数
- 在这个视角下,程序和数学对象已经无限接近了:
Quick Quiz
- 怎样给出 f的定义? (《离散数学》课程内容)
- 用数学严格的语言描述计算机世界中的对象
-
- 《计算机科学的数学基础》
-
-
- Proof Assistant (妄想)
-
为什么会有程序?
- 是因为程序必须在无情执行指令的机器 上执行
- 只有程序才配得上它
程序天生是 "人类" 的,也是 "反人类" 的
- 人类的一面:程序连接了人类世界需求
-
- 大语言模型有非常惊艳的编程能力
- 反人类的一面:程序会在机器上执行
-
- 大语言 模型还是经常有幻觉
-
-
- (对程序的行为的 100% 掌控就是非常困难的)
-
- "Maintainability": 代码字面意义 和实际行为 直接关联
- 软件:物理世界实际的事 在 计算机世界的需求投影
所以我们要 尽量写出写 绝对正确的代码
-
我们能证明程序的正确性吗?
int popcount(int x) {
int count = 0;
while (x) {
x -= (x & -x);
count++;
}
return count;
} -
我们还需要 "什么叫对"
-
Specification: 不 crash; 没有 UB; assert satisfy, ...
int popcount(int x) {
int count = 0;
while (x) {
x -= (x & -x);
count++;
}
return count;
}void popcount_spec() {
for (uint64_t x = 0; x <= 0xffffffff; x++) {
assert( ((x >> 0) & 1) +
((x >> 1) & 1) + ...
((x >> 31) & 1) == popcount(x) );
}
For all executions of ff, the specification is satisfied.
暴力枚举
- 写一个 driver code,枚举所有的 xx ,运行 f(x)f (x)
-
- Testing: 选一些有代表性的
写出证明
- 直接证明 "For all executions of ff, the specification is satisfied."
-
- 就和在《离散数学》课上的证明一样
"Proof Assistant" (Coq, Lean, ...)
- 它会拒绝一切伪证

计算机辅助证明
这并不是一个新概念:我们不再用自然语言,而是用严格的、机器可检查的逻辑语言书写证明,核心技术是 揭示了数学证明与类型系统之间的深刻联系的 Curry-Howard Correspondence:
- 命题即类型:逻辑中的命题 (如 A→B) 对应函数类型 A→B。
- 证明即程序:命题的构造性证明过程对应该类型的一个具体程序(例如,一个实现 A→B的函数即为 "A 蕴含 B" 的证明)。
逻辑中的蕴含消除规则对应函数调用:若有一个类型为 A→B的函数 (证明),且输入类型 AA的值 (前提),则输出类型 B 的值 (结论)。而对于一阶谓词,例如 ∃x.P(x),就必须提供一个 x 的实例,这构成了基于构造的 "直觉逻辑" 的基础。
Proof assistant 是人工智能时代堪称完美的辅助工具:如果我们要信任 AI 产生的结果,可以让它们给出一个 proof assistant 认可的证明!延伸阅读:科普文章: "Formal verification doesn't result in perfect code; it simply narrows the possibility for errors and vulnerabilities to creep in," Parno says. "What makes the technique so attractive is that you push the uncertainty or scope of problems down to smaller and smaller windows"
<<正式的软件验证措施>>

4.2 数学视角的操作系统
程序 = 状态机
- 状态
-
- 栈帧中的变量和 PC ; 局部变量
- 初始状态
-
- main(argc, argv)
- 状态迁移
-
- 执行 "当前栈帧 PC" 的语句
- 有一种特殊的语句:syscall
- (我们不妨就认可状态机是可以 "数学定义" 的)

操作系统 = 状态机的管理者
- 可以同时容纳多个 "程序状态机"
-
- 程序内计算:选一个程序执行一步
- 系统调用:创建新状态机、退出状态机、打印字符......
有了一个有趣的想法......
- 能不能我们自己定义 "状态机"
-
- 用我们喜欢的语言、喜欢的方式
- 不要受限于 C、汇编......
- 自己模拟状态机的执行
-
- 不就有了一个 "玩具操作系统" 吗?
Life is short, you need Python!
def StateMachine():
b = sys_read()
if b == 0:
sys_write('I got a zero.')
else:
sys_write('I got a one.')
def main():
sys_spawn(StateMachine)
操作系统中的对象
- 状态机 (进程)
-
- Python 代码
- 初始时,仅有一个状态机 (main)
- 允许执行计算或 read, write, spawn 系统调用
- 一个进程间共享的 buffer ("设备")
系统调用
- read(): 返回随机的 0 或 1
- write(s): 向 buffer 输出字符串
s
- spawn(f): 创建一个可运行的状态机
f

难点是多状态机的管理
- 如何在状态机之前来回切换
- 实现我们单 CPU 上运行多个程序的效果?
一些途径
- 用一个模拟器去解释执行语句 (2022 年的实现)
-
- 创建多个模拟器对象单步执行 (J2ME KVM 就是如此)
- "虚拟机" (QEMU TCG)
- 是否有语言机制能 "暂存" 函数的运行状态,并且之后回复?
-
- 有:Generators/Coroutines
30 行代码讲完 UNIX 系统的基本模型
- 进程
- 系统调用
- 上下文切换
- 调度
你会在 Linux Kernel 中看到 "类似" 的代码
- "procs" → cpu->runqueue
- "current" → current = (current_thread_info()->task)
借助 py 来看看

操作系统是最早的实用并发程序
- 每个进程 (程序) 都是顺序状态机
-
- 但发生中断/系统调用以后,操作系统代码直接执行
- 如果有多个处理器 (或者允许此时切换到另一个程序执行),就有了并发
4.3 状态机模型与模型检查器
Mosaic Model and Checker
状态
- 多个 "应用程序" 状态机
-
- 当然,可以是模型
初始状态
- 仅有一个 "main" 状态机
-
- 这个状态机处于初始状态
迁移
- 选择一个状态机执行一步
-
- 就像我们在操作系统模型上看到的那样
计算机系统中的不确定性 (non-determinism)
程序中的不确定性都是操作系统给的
调度:状态机的选择不确定
- 多处理器的 "选择" 是无法控制的
- 操作系统可以主动随机选择状态机执行一步
I/O:系统外的输入不确定
- read 返回的结果也有两种可能性
这样程序的执行就不是 "一条线" 了
- 从初始状态出发,可能有多个可能的 "下一个状态"
这就是为什么 并发 程序难以预测了

Mosaic Model and Checker
看到并发的这个模型图,《离散数学》忽然更有用了
程序定义了状态机 G(V,E)
- 加上一个起点 v0
- 再加上 F⊆V是 "坏" (faulty) 的状态
-
- 程序正确 ≡ 不存在从 v0到 v∈F的路径

- 也可以通过这个来理解 网络安全的 验证思路
还记得你们怎么在 G里找路径吗
- DFS/BFS 啊!
- 恭喜你!你离图灵奖进了一步 (迈出了重要的第一步 Bush)
-
-
要有一个疯狂 的想法:软件是可以自动证明的
-
Turing Award Lecture: Model checking: algorithmic verification and debugging
-
插曲:如果你想得图灵奖?
需要找一个 tractable 的问题
- F⊆V+ 暴力枚举 显然太 trivial 了
-
- 我们有更好的方式表达规约 (temporal logic)
- G(A→FB)
-
-
- "如果 A 发生,则最终 B 会发生"
- 哦!你需要构建一个好的逻辑系统
-
- 提出一些有趣的检查算法
在软件系统里又是 useful 的
- 要走很多弯路,而且今天 temporal logic(时序逻辑) 也没落了
- 但 Verification(验证) 没有死,而且在 AI 时代肯定会走得更远
Putting Them Together
模型
- 理论上,我们可以建模任何系统调用
- 当然,我们选择建模最重要的那些
-
- Three Easy Pieces!
检查器
- 最简单的BFS就行 (只要能获得状态机的状态)
可视化
- 我们就是绘制一个顶点是状态的图 G(V,E)
于是,我们有了一个更复杂的"玩具"
|-----|----------------|-----------------------|
| 模块 | 系统调用 | 行为 |
| 基础 | choose(xs) | 返回一个 xs 中的随机的选择 |
| 基础 | write(s) | 向调试终端输出字符串 s |
| 基础 | sched() | 切换到随机的线程/进程执行 |
| 虚拟化 | fork() | 创建当前状态机的完整复制 |
| 并发 | spawn(fn) | 创建从 fn 开始执行的线程 |
| 持久化 | bread(k) | 读取虚拟设磁盘块 kk 的数据 |
| 持久化 | bwrite(k, v) | 向虚拟磁盘块 kk 写入数据 vv |
| 持久化 | sync() | 将所有向虚拟磁盘的数据写入落盘 |
| 持久化 | crash() | 模拟系统崩溃 |
操作系统模型和检查器:
我们可以把 "状态机的管理者" 这个思想在 Python 世界中构造出来:
- 我们用 Python 的函数来声明状态机,并且实现状态空间的遍历。
- 所有的实现都是 "最简" 的------但它真的能用来澄清 Three Easy Pieces 里的概念:Concurrency, Virtualization, 和 Persistence。
下面是我们绘制一个 "Hello World" 状态空间的例子。Hello 会调用一个有趣的系统调用 fork,它的行为是复制状态机的当前状态:

- 将会在这个专栏里,同时分析模型和真实操作系统 (Linux) 系统调用的行为,在概念 (抽象) 和具体 (实现) 层面理解操作系统。
4.4 总结
Take-away Messages : 程序就是状态机;状态机可以用程序表示。因此:我们可以用更 "简单" 的方式 (例如 Python) 描述状态机、建模操作系统 上的应用,并且实现操作系统的可执行模型。而一旦把操作系统、应用程序当做 "数学对象" 处理,那么我们图论、数理逻辑中的工具就能被应用于处理程序,甚至可以用图遍历的方法证明程序的正确性。
