背景

Operating Systems: Yet Another Three Easy Pieces

  • 程序
  • 操作系统 API
  • 计算机硬件

都是很抽象的大概念

  • 必须手撕代码才能理解

本次课内容与目标

用代码回答 “什么是操作系统上的程序”

  • 会用 “状态机” 理解程序的运行
  • 知道指令分为 syscall 和其他
  • 降低对 “操作系统” 的恐惧

程序和状态机

一个干净的理论模型 (Simple Sequential Execution)

状态 = $M$ (内存) + $R$ (寄存器)

  • x86-64 有哪些寄存器?

初始状态

  • 程序启动时操作系统给安排的状态

状态转移 = 时钟驱动的指令执行

  • 假设当前状态是 $(M, R)$
    • 从 $R[PC]$ 取一条指令
    • 解析指令;取出必要的数据
    • 计算结果 (可能有非确定性,例如 rdrand)
    • 更新得到 $(M', R')$

程序 = 状态机

这不是废话吗?有什么吗?

用处很多,贯穿整个 “操作系统” 课程

  • 帮你理解概念和 debug
  • 理解现代乱序多发射处理器
  • 理解现代多处理器系统 (SSE 来自 armv8)
  • 证明顺序/并发程序的正确性
  • 深入理解操作系统中的很多机制
  • ……

但今天主要讲代码

  • 带大家初步感受一下这个理解的威力

构造一个最小的 Hello, World

(程序 = 状态机)

失败的尝试

int main() {
  printf("Hello, World\n");
}

gcc 编译出来的文件不满足 “最小”

  • printf 变成了 puts@plt

  • 直接用 ld 链接失败
    • 不知道怎么链接库函数……

  • 空的 main 函数倒是可以
    • 链接时得到奇怪的警告 (可以定义成 _start)
    • segmentation fault

调试 Segmentation Fault

我们需要一个工具帮助我们观察程序 (状态机) 的执行

  • 什么工具能做到呢?

gdb!

  • 初学者必须克服的恐惧:STFW/RTFM (M 非常有用)
    • starti 可以帮助我们从第一条指令开始执行程序
      • start 会在 main 停下
    • layout asm 可以更方便地查看汇编
    • info registers 可以查看寄存器
    • 牢记状态机模型在调试时有奇效
      • 状态 = 寄存器 (info registers) + 内存 (pmap)

不想异常退出?

有办法让状态机 “停下来” 吗?

  • 纯 “计算” 的状态机:不行
  • 要么死循环,要么 undefined behavior

解决办法:交给操作系统

#include <sys/syscall.h>

int main() {
  syscall(SYS_exit, 42);
}
  • 调试代码:syscall 的实现在哪里?
    • 坏消息:在 libc 里,不方便直接链接
    • 好消息:代码很短,而且似乎看懂了

Hello, World 的汇编实现

minimal.S

movq $SYS_exit,  %rax   # exit(
movq $1,         %rdi   #   status=1
syscall                 # );

我是从哪里获得这些黑科技代码的???

  • syscall (2), syscalls (2)
    • The Friendly Manual 才是最靠谱的信息来源

思考题:syscall 指令让状态机模型发生了什么变化?

  • 我们之后会回来;今天继续讲代码

彩蛋:ANSI Escape Code

为什么 Hello World 有颜色??

特殊编码的字符实现终端控制

  • vi.c from busybox
  • telnet towel.blinkenlights.nl (Ctrl-] and q 退出)
  • dialog --msgbox 'Hello OS World!' 8 32
    • 所以编程可以从一开始就不那么枯燥

操作系统中的程序

操作系统中的程序

操作系统中同时运行多个程序,并维护 “操作系统定义” 的对象 (比如文件,包括可执行文件)。

程序 = 状态机 $(M, R)$


指令大致分为两种

  • 内部指令:add, call, ret, ...
    • 确定的:完全根据当前 $(M, R)$ 计算出一个新的 $(M, R)$
    • 非确定的:rdrand
  • 外部指令 (只有一条)
    • syscall: 交给操作系统,让操作系统决定下一个状态是什么
      • 除了可以观察 syscall 返回的结果,程序完全不知道 syscall “发生了什么”

为什么要这么做?

操作系统实现了资源的统一管理。


程序只能用操作系统允许的方式访问操作系统中的对象

  • 从而实现操作系统的 “霸主” 地位
  • 这是为 “管理多个状态机” 所必须的
    • 不能打架,谁有权限就给他

系统中常见的应用程序

Core Utilities (coreutils)

  • standard programs for text and file manipulation
  • 系统中安装的是 GNU Coreutils

系统/工具程序

  • bash, apt, ip, ssh, vim, tmux, jdk, python, ...
    • 这些工具的原理都不复杂 (例如 apt 其实只是 dpkg 的壳)
    • Ubuntu Packages (和 apt-file 工具) 支持文件名检索
      • 例子:搜索 SDL2/SDL.h

其他各种应用程序

  • 浏览器、音乐播放器……

程序作为操作系统中的对象

可执行文件 = 字节序列

  • 与大家平时创建的文本文件 (例如程序) 没有本质区别
  • 操作系统提供 API 打开、读取、改写 (都需要相应的权限)
  • 因此我们可以用 vim, cat, xxd 等命令查看可执行文件
    • vim 中打开,二进制的部分显示异常,但可以看到字符串常量 (例子:vim a.out)
    • 使用 xxd 可以看到文件以 "\x7f" "ELF" 开头

解析可执行文件

  • binutils 里的工具创建和解析
  • 如果需要用代码解析,/usr/include/elf.h 提供了必要的定义

来点真的例子

面试题

一个普通的 C 程序执行的第一条指令在哪里?

  • main 的第一条指令 ❌
  • libc_start
(gdb) starti
Starting program: /tmp/a.out 

Program stopped.
0x00007ffff7dd6090 in _start () from /lib64/ld-linux-x86-64.so.2
(gdb) bt
#0  0x00007ffff7dd6090 in _start () from /lib64/ld-linux-x86-64.so.2
#1  0x0000000000000001 in ?? ()

main() 之前发生了什么? (cont'd)

readelf 告诉你答案。

进程初始时到 main() 执行时,进程的内存中多了 libc-2.27.so

  • ld-linux-x86-64.so 加载了 libc
  • 之后 libc 完成了自己的初始化
    • main() 的开始/结束并不是整个程序的开始/结束

面试题++

main 执行之前、执行中、执行后,发生了哪些操作系统 API 调用


呃……

再来点真的例子 (前方高能)

Trace (踪迹)

In general, trace refers to the process of following anything from the beginning to the end. For example, the traceroute command follows each of the network hops as your computer connects to another computer.

这门课中很重要的工具:strace

  • system call trace
  • 理解程序运行时使用的系统调用
    • demo: strace ./hello-goodbye
    • 在这门课中,你能理解 strace 的输出并在你自己的操作系统里实现相当一部分系统调用 (mmap, execve, ...)

本质上,所有的程序和 Hello World 类似

  • 被操作系统加载
    • 通过另一个进程执行 execve
  • 状态机执行 (内部 + 唯一的外部指令 syscall)
    • 进程管理:fork, execve, exit, ...
    • 文件/设备管理:open, close, read, write, ...
    • 存储管理:mmap, brk, ...
  • 直到 _exit (exit_group) 退出

(对部分初学者来说,会对这一点感到非常惊讶)

  • 这么炫酷的操作系统、这么炫酷的程序,就这些 API?

更多的 Demos

编译器 (gcc),代表其他工具程序

  • strace -f gcc a.c (gcc 会启动其他进程)
    • 主要的系统调用:execve, read, write
      • (上学期给大家讲过)

图形界面程序 (xedit),代表其他图形界面程序 (例如 vscode)

  • strace xedit
    • 主要的系统调用:poll, recvmsg, writev
    • 图形界面程序和 X-Window 服务器按照 X11 协议通信
    • 虚拟机中的 xedit 将 X11 命令通过 ssh (X11 forwarding) 转发到 Host

各式各样的应用程序

都在操作系统 API (当然还有操作系统中的对象) 上构建

  • 窗口管理器
    • 管理设备和屏幕 (read/write/mmap)
    • 进程间通信 (send, recv)

  • 任务管理器
    • 访问操作系统提供的进程对象 (readdir/read)

  • 杀毒软件
    • 文件静态扫描 (read)
    • 动态防御 (ptrace)
    • 其他更复杂的安全机制……

总结

总结

本次课内容与目标

  • 用代码回答 “什么是操作系统上的程序”
    • 会用 “状态机” 理解程序的运行
    • 知道指令分为 syscall 和其他
    • 降低对 “操作系统” 的恐惧

Take-away messages

  • 程序 = 状态机
  • 操作系统 = 对象 + API
  • RTFM, RTFSC

End.