绪论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 这样的图形界面程序。

posted @   7hu95b  阅读(5)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示