2.应用视角的操作系统
1. 操作系统上的最小应用程序
要想理解 “操作系统”,就要理解什么是 “程序”
- 一个Hello World示例
1 | int main() { |
-
实际上,这个Hello World并不小
-
当我们使用objdump工具查看这个Hello World后可以发现:
--verbose
可以查看所有编译选项 (真不少)- printf 变成了 puts@plt
-
-Wl,--verbose
可以查看所有链接选项 (真不少)- 原来链接了那么多东西
- 还解释了 end 符号的由来
-
-static
会链接 libc (大量的代码)
-
-
Hello World的最小实现
1 | #include <sys/syscall.h> |
下面我们在shell中编译运行:
1 | gcc minimal.S -c && ld minimal.o |
这就是一个minimal. S
- 什么是程序:
1 | struct CPUState { |
- 处理器:无情的、执行指令的状态机
- 从M[PC] 取出一条指令
- 执行它
- 循环往复
解决程序异常退出
-
程序自己是不能 “停下来” 的
- 指令集里没有一条关闭计算机的指令,那么操作系统是如何在关闭所有软件后,切断计算机的电源的?
-
只能借助操作系统
1 | movq $SYS_exit, %rax # exit( |
- 把 “系统调用” 的参数放到寄存器中
- 执行 syscall,操作系统接管程序
操作系统可以任意改变程序状态 (甚至终止程序)
所有二进制程序 = 状态机
- 状态
- gdb 内可见的内存和寄存器
- 初始状态
- 由 ABI 规定 (例如有一个合法的 %rsp)
- 状态迁移
- 执行一条指令
- 我们花了一整个《计算机系统基础》解释这件事
- gdb 可以单步观察状态机的执行
- syscall 指令: 将状态机 “完全交给” 操作系统
- 执行一条指令
2. 操作系统上的应用程序
-
应用程序和minimal. S 一样,都是状态机
- 任何程序 = minimal. S = 状态机
- 总是从被操作系统加载开始
- 通过另一个进程执行 execve 设置为初始状态
- 经历状态机执行 (计算 + syscalls)
- 进程管理:fork, execve, exit, …
- 文件/设备管理:open, close, read, write, …
- 存储管理:mmap, brk, …
- 最终调用 _exit (exit_group) 退出
- 总是从被操作系统加载开始
- 任何程序 = minimal. S = 状态机
-
可执行文件是操作系统中的对象
- 与 minimal 的二进制文件没有本质区别
- 我们甚至可以像文本一样直接编辑可执行文件
-
一切应用程序的实现:
- 应用程序 = 计算 + 操作系统 API
- 窗口管理器
- 能直接管理屏幕设备 (read/write/mmap)
- 能画一个点,理论上就能画任何东西
- 能够和其他进程通信 (send, recv)
- 能直接管理屏幕设备 (read/write/mmap)
- 任务管理器
- 能访问操作系统提供的进程对象 (M1 - pstree)
- 杀毒软件
- 文件静态扫描 (read)、主动防御 (ptrace)
- 窗口管理器
- 应用程序 = 计算 + 操作系统 API
-
操作系统的职责:提供令应用程序舒适的抽象 (对象 + API)
3. 编译器与编译优化
3.1 什么是编译器
- 编译器的输入
- 高级语言 ( C ) 代码 = 状态机
- 编译器的输出
- 汇编代码 (指令序列) = 状态机
- 编译器 = 状态机之间的翻译器
3.2 为什么c被称为高级汇编语言
- 存在 C 代码到指令集的直接对应关系
- 状态机和迁移都可以 “直译”
- 于是计算机系统里多了一个抽象层 (“一生二、二生三、三生万物”)
- 更 “高级” 的语言就很难了
- C++ virtual void foo();
- Python [1, 2, 3, *rest]
- Javascript await fetch(…)
Everything (高级语言代码、机器代码) 都是状态机;而编译器实现了两种状态机之间的翻译。无论何种状态机,在没有操作系统时,它们只能做纯粹的计算,甚至都不能把结果传递到程序之外——而程序与操作系统沟通的唯一桥梁是系统调用 (例如 x86-64 的 syscall 指令)。如此重要的桥梁,操作系统中自然也有工具:strace 可以查看程序运行过程中的系统调用序列。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Asuka's Blog!