1. 操作系统上的最小应用程序

要想理解 “操作系统”,就要理解什么是 “程序”

  • 一个Hello World示例
1
2
3
int main() {
printf("Hello, World\n");
}
  • 实际上,这个Hello World并不小

    • 当我们使用objdump工具查看这个Hello World后可以发现:

      • --verbose可以查看所有编译选项 (真不少)
        • printf 变成了 puts@plt
    • -Wl,--verbose可以查看所有链接选项 (真不少)

      • 原来链接了那么多东西
      • 还解释了 end 符号的由来
    • -static 会链接 libc (大量的代码)

  • Hello World的最小实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <sys/syscall.h>

.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:

下面我们在shell中编译运行:

1
2
3
gcc minimal.S -c && ld minimal.o
./a.out
# Hello, OS World

这就是一个minimal. S

  • 什么是程序:
1
2
3
4
5
struct CPUState {
uint32_t regs[32], csrs[CSR_COUNT];
uint8_t *mem;
uint32_t mem_offset, mem_size;
};
  • 处理器:无情的、执行指令的状态机
    • 从M[PC] 取出一条指令
    • 执行它
    • 循环往复

解决程序异常退出

  • 程序自己是不能 “停下来” 的

    • 指令集里没有一条关闭计算机的指令,那么操作系统是如何在关闭所有软件后,切断计算机的电源的?
  • 只能借助操作系统

1
2
3
movq $SYS_exit,  %rax   # exit(
movq $1, %rdi # status=1
syscall # );
  • 把 “系统调用” 的参数放到寄存器中
  • 执行 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 的二进制文件没有本质区别
    • 我们甚至可以像文本一样直接编辑可执行文件
  • 一切应用程序的实现:

    • 应用程序 = 计算 + 操作系统 API
      • 窗口管理器
        • 能直接管理屏幕设备 (read/write/mmap)
          • 能画一个点,理论上就能画任何东西
        • 能够和其他进程通信 (send, recv)
      • 任务管理器
        • 能访问操作系统提供的进程对象 (M1 - pstree)
      • 杀毒软件
        • 文件静态扫描 (read)、主动防御 (ptrace)
  • 操作系统的职责:提供令应用程序舒适的抽象 (对象 + 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 可以查看程序运行过程中的系统调用序列。