绪论2:应用视角的操作系统
本文重点内容:
- 强行构造最小的 Hello World 程序
- 状态机模型
- 操作系统的程序剖析
一、强行构造最小的 Hello World 程序?
(1)精简 hello.c
程序:去掉头文件、主函数为空,但是发现编译时报错。
- 掌握常用的 GDB 调试指令、发现并追踪
return
错误的整个过程。比如starti
代表从第一条指令开始执行。 - 从 C 语言入手精简的困难较高,于是转向汇编程序。
(2)编写 minimal.S
汇编程序。
- 在程序中添加三行系统调用,能让程序主动停止,避开 return 报错。
- 掌握从
*.S → *.s → *.o → *.out
的编译过程。 - 对比预编译得到的
*.s
相比*.S
新增了什么内容。以#
行代表了编译指令。
高级程序设计语言可以视为“看得见的语句”,而系统调用往往由编译器添加,所以可以视为“看不见的语句”,最后的指令便由这两部分组成。
操作系统的所有程序都是建立在这些有限的系统调用之上的,它们也被称为操作系统的 API。
二、程序设计的状态机和编译器的功能
(一)状态机模型
操作系统中的任何程序都是调用 syscall 的状态机。
(1)汇编代码的状态机模型:
- 状态 = 内存 M + 寄存器 R
- 初始状态 = ABI 规定 (例如有一个合法的 %rsp)
- 状态迁移 = 执行一条指令
(2)简单 C 程序的状态机模型(语义):
- 状态 = 堆 + 栈。
- 初始状态 = main 的第一条语句。
- 状态迁移 = 执行一条语句中的一小步。
对应到代码实现:
- 状态:Stack frame 的列表 + 全局变量。
- 初始状态:仅有一个 frame: main(argc, argv) ;全局变量为初始值
- 状态迁移:
- 执行 frames.top.PC 处的简单语句
- 函数调用 = push frame (frame.PC = 入口)
- 函数返回 = pop frame
(二)编译器
- 编译器的主要功能就是“翻译”:
.s = compile(.c)
- 编译器可以对代码进行优化,只要能保证最终结果的准确性(外部观测结果和编译运行结果一致)。
- 使用 volatile 禁用对该变量的任何优化,每次都从寄存器取值。
- 使用“编译屏障” compiler barrier,其作用类似内存屏障。
三、操作系统的程序
(一)查看可执行文件
- 使用 vim+xxd 查看
- 使用
objdump -d a.out
查看二进制执行文件的组成 - 使用 vscode 的 binary editor 插件。
(二)系统级的应用程序(库)举例
建议多看一下这些系统级的程序库的实现代码,提高代码水平。为了初学者快速入门,可以先看最初版的实现,它们往往都比较简短(最新版往往都很长)。
- GNU Coreutils, GNU Binaryutils(objdump 的实现就在其中)。
- busybox, toybox 等。
(三)使用 strace 等工具对可执行文件进行跟踪和调试
程序运行的三个阶段:
- 加载:执行 execve 设置初始状态。
- 状态机执行:进程管理(fork)、文件管理(open, close...)、内存管理(mmap, brk...)...
- 退出:调用
_exit
退出
使用 strace gcc hello.c |& vim -
查看 gcc 的编译过程。使用 |&
是因为 strace 的输出会导向“错误输出设备”,所以需要使用 &
将管道合并,统一给 vim 显示。除此之外,strace 还可以调试像 x11 这样的图形界面程序。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步