课程链接:jyywiki.cn/OS/2022/
操作系统上的程序
1. 数字电路与状态机
- 状态 = 寄存器保存的值 (flip-flop)
- 初始状态 = RESET (implementation dependent)
- 迁移 = 组合逻辑电路计算寄存器下一周期的值
例:
X′=¬X∧Y
Y′=¬X∧¬Y
shell代码如下:
#include <stdio.h>
#include <unistd.h>
#define REGS_FOREACH(_) _(X) _(Y) //寄存器
#define RUN_LOGIC X1 = !X && Y; \
Y1 = !X && !Y;
#define DEFINE(X) static int X, X##1; //定义寄存器
#define UPDATE(X) X = X##1; //更新寄存器
#define PRINT(X) printf(#X " = %d; ", X); //打印寄存器
int main() {
REGS_FOREACH(DEFINE);
while (1) { // clock
RUN_LOGIC;
REGS_FOREACH(PRINT);
REGS_FOREACH(UPDATE);
putchar('\n'); sleep(1);
}
}
利用命令gcc -E 文件名 展开红,可以得到直接用c实现数字电路模拟器的代码
更完整的实现数字模拟器:
代码如下:
#include <stdio.h>
#include <unistd.h>
#define REGS_FOREACH(_) _(X) _(Y)
#define OUTS_FOREACH(_) _(A) _(B) _(C) _(D) _(E) _(F) _(G)
#define RUN_LOGIC X1 = !X && Y; \
Y1 = !X && !Y; \
A = (!X && !Y) || (X && !Y); \
B = 1; \
C = (!X && !Y) || (!X && Y); \
D = (!X && !Y) || (X && !Y); \
E = (!X && !Y) || (X && !Y); \
F = (!X && !Y); \
G = (X && !Y);
#define DEFINE(X) static int X, X##1;
#define UPDATE(X) X = X##1;
#define PRINT(X) printf(#X " = %d; ", X);
int main() {
REGS_FOREACH(DEFINE);
OUTS_FOREACH(DEFINE);
while (1) {
RUN_LOGIC;
OUTS_FOREACH(PRINT);
REGS_FOREACH(UPDATE);
putchar('\n');
fflush(stdout);
sleep(1);
}
}
利用管道连接后端实现:
后端.py代码如下:
import fileinput
TEMPLATE = '''
\033[2J\033[1;1f
AAAAAAAAA
FF BB
FF BB
FF BB
FF BB
GGGGGGGGGG
EE CC
EE CC
EE CC
EE CC
DDDDDDDDD
'''
BLOCK = {
0: '\033[37m░\033[0m', # STFW: ANSI Escape Code
1: '\033[31m█\033[0m',
}
VARS = 'ABCDEFG'
for v in VARS:
globals()[v] = 0
stdin = fileinput.input()
while True:
exec(stdin.readline())
pic = TEMPLATE
for v in VARS:
pic = pic.replace(v, BLOCK[globals()[v]]) # 'A' -> BLOCK[A], ...
print(pic)
管道连接命令为:.c文件名 | python3 .py文件名
运行结果如下:
2.what is the 程序 ?
2.1 源代码角度
C 程序的状态机模型 (语义,semantics)
-
状态 = 堆 + 栈
-
初始状态 =
main的第一条语句 -
迁移 = 执行一条简单语句
- 任何 C 程序都可以改写成 “非复合语句” 的 C 代码
c程序是一个状态机,那么如何理解函数调用和函数返回?
-
状态 = stack frame 的列表 (每个 frame 有 PC) + 全局变量
-
初始状态 = main(argc, argv), 全局变量初始化
-
迁移 = 执行 top stack frame PC 的语句; PC++
- 函数调用 = push frame (frame.PC = 入口)
- 函数返回 = pop frame
应用:将任何递归程序就地转为非递归
-
汉诺塔难不倒你 hanoi-nr.c
-
A → B, B → A 的也难不倒你
-
还是一样的
call(),但放入不同的Frame
-
代码如下:
#include <stdio.h>
#include <assert.h>
#define call(...) ({ *(++top) = (Frame) { .pc = 0, VA_ARGS }; })
#define ret() ({ top--; })
#define goto(loc) ({ f->pc = (loc) - 1; })
typedef struct {
int pc, n;
char from, to, via;
} Frame;
void hanoi(int n, char from, char to, char via) {
Frame stk[64], *top = stk - 1;
call(n, from, to, via);
Frame *f;
for (f; (f = top) >= stk; f->pc++) {
switch (f->pc) {
case 0: if (f->n == 1) { printf("%c -> %c\n", f->from, f->to); goto(4); } break;
case 1: call(f->n - 1, f->from, f->via, f->to); break;
case 2: call( 1, f->from, f->to, f->via); break;
case 3: call(f->n - 1, f->via, f->to, f->from); break;
case 4: ret(); break;
default: assert(0);
}
}
}
int main(){
hanoi(3,'A','B','C');
return 0;
}
2.2 二进制代码角度
也是状态机
- 状态 = 内存 M + 寄存器 R
- 初始状态 = (稍后回答)
- 迁移 = 执行一条指令
操作系统上的程序,所有的指令都只能计算
- deterministic: mov, add, sub, call, ...
- non-deterministic: rdrand, ...
- 但这些指令甚至都无法使程序停下来 (NEMU: 加条
trap指令)
如下图,
为了让程序停下来,我们可以使用某些指令,例如neum_trap
2.2.1 调用操作系统 syscall
-
把 (M,R) 完全交给操作系统,任其修改
-
一个有趣的问题:如果程序不打算完全信任操作系统?
-
实现与操作系统中的其他对象交互
- 读写文件/操作系统状态 (例如把文件内容写入 M)
- 改变进程 (运行中状态机) 的状态,例如创建进程/销毁自己
- 程序 = 计算 + syscall
- 问题:怎么构造一个最小的 Hello, World?
2.2.2 构造最小 Hello, World
如果是下面代码的hello world的话,这个代码并不是最小的,甚至可以说很大
int main() {
printf("Hello, World\n");
}
解决办法:hello world 的汇编实现
.globl. start
_start:
movq $SYS_ _write, %rax # write(
movq $1, %rdi # fd=1,
movq $st, %rsi # buf=st,
movq $(ed - st), %rdx # count=ed-st
syscall # );
movq $SYS_ exit, %rax # exit(
movq $1, %rdi # status=1
syscall # );
st:
.ascii "\033[01;31mHello, OS World\033[0m\n"
ed:
ps:printf的背后sys_write函数 即系统调用syscall
状态机视角的程序
- 程序 = 计算 → syscall → 计算 → ...
2.2.3 特殊编码的字符实现终端控制
-
vi.c from busybox
-
telnet ``towel.blinkenlights.nl(电影;Ctrl-] and q 退出) -
dialog --msgbox 'Hello, OS World!' 8 32 -
ssh sshtron.zachlatta.com(网络游戏)-
所以编程可以从一开始就不那么枯燥
-
看似复杂,实际简单明了
-
3.what is the 编译器 ?
-
编译器:源代码 S (状态机) → 二进制代码 C (状态机)
-
C=compile(S)
-
编译 (优化) 的正确性 (Soundness):
-
S 与 C 的可观测行为严格一致
- system calls; volatile variable loads/stores; termination
-
rivially 正确 (但低效) 的实现
- 解释执行/直接翻译 S 的语义
-
-
现代 (与未来的) 编译优化
-
在保证观测一致性 (sound) 的前提下改写代码 (rewriting)
-
Inline assembly 也可以参与优化
- 其他优化可能会跨过不带 barrier 的
asm volatile
- 其他优化可能会跨过不带 barrier 的
-
Eventual memory consistency
-
Call to external CU = write back visible memory
-
talk is cheap, show me the code!
-
-
-
4.操作系统中的一般程序
-
操作系统收编了所有的硬件/软件资源
-
只能用操作系统允许的方式访问操作系统中的对象
- 从而实现操作系统的 “霸主” 地位
- 例子:
-
#include <stdio.h> #include <fcntl.h> #include <unistd.h> void try_open(const char *fname) { int fd = open(fname, O_RDWR); printf("open("%s") returns %d\n", fname, fd); if (fd < 0) { perror(" FAIL"); } else { printf(" SUCCESS!\n"); close(fd); } } int main() { try_open("/something/not/exist"); try_open("/dev/sda"); // hard drive }
-
这是为 “管理多个状态机” 所必须的
- 不能打架,谁有权限就给他
-
-
杀人的面试题 (1):一个普通的、人畜无害的 Hello World C 程序执行的第一条指令在哪里?
-
等价问法
-
“二进制程序状态机的初始状态是什么?”
main的第一条指令 ❌libc的_start❌
-
-
问 gdb 吧
info proc {mappings,...}- 打印进程内存
-
-
main()之前发生了什么?-
ld-linux-x86-64.so加载了 libc-
之后 libc 完成了自己的初始化
- RTFM: libc startup on Hurd
main()的开始/结束并不是整个程序的开始/结束- 例子:
-
#include <stdio.h> __attribute__((constructor)) void hello() { printf("Hello, World\n"); } // See also: atexit(3) __attribute__((destructor)) void goodbye() { printf("Goodbye, Cruel OS World!\n"); } int main() { }
-
-
-
main 执行之前、执行中、执行后,发生了哪些操作系统 API 调用?
-
这门课中很重要的工具:strace
-
system call trace
-
理解程序运行时使用的系统调用
- demo:
strace ./hello-goodbye - 在这门课中,你能理解 strace 的输出并在你自己的操作系统里实现相当一部分系统调用 (mmap, execve, ...)
- demo:
-
-
5.总结
-
程序 = 状态机
- 源代码 S: 状态迁移 = 执行语句
- 二进制代码 C: 状态迁移 = 执行指令
- 编译器 C=compile(S)
-
应用视角的操作系统
- 就是一条 syscall 指令
-
计算机系统不存在玄学;一切都建立在确定的机制上
-
理解操作系统的重要工具:gcc, binutils, gdb, strace
-
参考文章: