背景

程序 = 状态机

  • 程序执行 (进程) = 状态机上的路径
  • 状态机管理
    • fork - 复制
    • execve - 重置
    • exit - 终止

状态机 = 可执行文件

  • 一直以来 “最神秘” 的一种文件
    • 双击即可打开
    • 为什么??

本次课内容与目标

理解静态链接的可执行文件

  • 可执行文件的加载
  • xv6 加载器

可执行文件的加载

小知识:可执行文件 $\ne$ ELF

#!/bin/bash -x
echo Hello

#!/usr/bin/env python3
print('Hello')

#!./a.out
  • 为什么是 /usr/bin/env
    • 因为 #! 需要绝对路径 (背后是 execve)
  • Shebang 究竟发生了什么?
    • 不妨 strace 一下!

虚假的进程 (ELF) 初始化

execve(path, argv, envp)

  • “重置一个状态机”,为它传入参数 argv 和 envp
    • 概念上好理解……但什么叫传入

真正的进程 (ELF) 初始化

RTFM

  • System V ABI (x86-64)
    • Section 3.4: Process Initialization
      • 之前用 gdb 调试过 “初始状态”
        • 看到了寄存器的初始值
      • 手册完整规定了 execve 后的进程状态
        • libc 会使用它
        • 根据 ABI,你可以开发自己的 libc!
  • Gitlab repo

挑战:不使用 execve 加载 ELF

理论上把可执行文件 (ELF) 指定的数据搬运到内存,就可以实现二进制文件的加载。

试一试?


execve 本质上是 “多余” 的

  • 可以用 mmap/munmap + 一小段 trampoline code 实现
  • 这个系统调用可以被库函数 (和其他系统调用) “模拟”

模拟系统调用 (1): 互相模拟、互相伤害

既然完全可以 “自己加载” 可执行文件。我们也可以在一个操作系统里实现另一个操作系统的 API 啊

指令面前系统平等 (Linux, Windows, ...)


WSL (Windows Subsystem for Linux)

  • Windows 执行 ELF64; 在 Windows 中实现 Linux 系统调用
    • 南大系统实验全家桶完全没问题

Wine (POSIX Subsystem for Windows 😂)

  • 支持 Windows PE (Portable Executable) 格式
  • 实现 Windows API (Overview)

模拟系统调用 (2): 把系统调用挪到用户空间

“微内核” (Microkernel)

  • 操作系统只提供非常有限的 API 和权限管理
    • 进程/线程创建和通信
    • 内存映射
    • 设备寄存器
  • 其他都实现在用户程序
    • 例如内核里没有文件系统
    • 想打开文件?发消息给服务器吧

“外核” (Exokernel)

  • 操作系统 = 库函数
  • 一个硬件上可以跑多个 libOS

灌鸡汤:《操作系统》这门课究竟学什么?

相比知识点本身,获得知识的方法对系统的掌控力更重要。


状态机视角

  • 程序 = 状态机
  • 操作系统 = 状态机的虚拟化

机器永远是对的

  • 只要按照 spec 实现,就绝对能 work
    • 地址空间里的每一个地址都有解释 (vdso, vvar, ...)
    • CPU: 指令实现对了,仙剑就能跑、操作系统就能跑
    • System-V ABI: 实现对了,不用 execve 也可以加载可执行文件

xv6 进程的加载和执行

再次强调

听课以为自己懂了,不自己调试是不行的。


肉眼 “看懂” 代码和自己写出代码之间有巨大的鸿沟

  • 做实验是不会骗人的……

initcode 的加载和执行

回顾上次代码课的调试 (和阅读的源代码)

  • 用户进程映射在 0 开始的内存位置
    • memlayout.h
    • 但断点只能打在 0x4,否则 gdb 会 crash

  • 系统调用的入口在 TRAMPOLINE
    • #define TRAMPOLINE (MAXVA - PGSIZE)
    • 0x3ffffff000
      • 马甲符号: uservec, trampline

exec: 开始执行

神奇的 “trampoline” (跳板)

  • 从一处跳到另一处的代码
  • 很多有趣的用途 (C 的 nested function; 插桩; ...)

xv6 trap trampoline

  • 保存寄存器
    • swap(a0, sscratch)
    • 然后执行保存 (类似上学期实现的 setjmp)
  • 恢复寄存器
    • 完整的逆过程

exec: 系统调用的执行

来到 sys_exec

  • 复制系统调用参数
    • 涉及到用户/内核之间的数据复制
  • 创建新的地址空间并加载
    • 同 mmap
  • 创建堆栈上的参数
    • 回顾 ABI 中的要求
  • 重置 p->trapframe 中的寄存器
  • 释放内存

总结

总结

本次课内容与目标

  • 理解静态链接的可执行文件
    • 可执行文件的加载
    • xv6 加载器

Take-away messages

  • RTFM; RTFSC
  • 纸面上的理解都是片面的