10 - Executable files

10 - 可执行文件

有了系统调用和 libc,我们就真的可以实现 “任何程序” 了——例如,你可以想一想,如果要实现一个 MicroPython 解释器,我们需要实现什么,又需要借助哪些系统调用?当然,我们需要编译器帮我们编译 python.c 到可执行文件——我们一直以来都 “默认” 了编译器工具链可以帮助我们实现高级语言到可执行文件的翻译。今天是时候 “打开” 这部分内容了。

操作系统的核心我们已经理解得差不多了...

本讲内容:(静态链接) 可执行文件的概念和基本原理;我们如何自己动手构建一个可执行文件格式。

1. 可执行文件

1.1. 什么是可执行文件

学习操作系统前

  • 那个 “双击可以弹出窗口的东西”

...

学习操作系统后

  • 一个操作系统中的对象 (文件)

  • 一个字节序列 (我们可以把它当字符串编辑)

  • 一个描述了状态机初始状态的数据结构 (打扰了)

1.2. ELF: Executable and Linkable Format

ELF 是“Executable and Linkable Format(可执行和可链接格式)”的缩写。它是一种标准的文件格式,主要用于存储可执行文件、目标文件(如编译后的中间文件)和共享库等。

我们在《计算机系统基础》中学到的

  • binutilsarrow-up-right 中的工具可以让我们查看其中的重要信息

    • 《计算机系统基础》常备工具

      • readelf & objdump

    • binutils 里原来还有不少宝藏!

我们在《计算机系统基础》里没学到的

  • 如果我们想了解关于 ELF 文件的信息,有什么不那么 “原始” 的现代工具可用?

    • 哦,有 elfcat,以及更多

1.3. 可执行文件:字节序列

无论如何,可执行文件都是字节序列

我们能不能得到一个最小的可执行文件 a.out

1.4. 可执行文件:进程初始状态的描述

回顾:System V ABIarrow-up-right

  • Section 3.4: “Process Initialization”

    • 只规定了部分寄存器和栈

    • 其他状态 (主要是内存) 由可执行文件指定

这份文档是关于AMD64架构的System V应用二进制接口(ABI)的详细规范。简单来说,它定义了在AMD64架构(也就是64位x86架构)上,程序如何运行、如何与操作系统交互,以及如何在不同程序之间共享数据。

“可执行文件” 需要包含什么?

  • (今天先假设不依赖 “动态链接库”)

  • 基本信息 (版本、体系结构……)

  • 内存布局 (哪些部分是什么数据)

  • 其他 (调试信息、符号表……)

1.5. 可执行文件其实不需要那么复杂

只需要

  • 一个头 (类似 7f 45 4c 46; 这是一个 jg 71)

  • 一段 Trampoline Code (PIC)

    • 操作系统会给一个文件描述符 (指向文件本身)

    • 这段代码直接执行 hardcoded mmap

  • (就能实现 “加载” 的功能)

剩下的信息都可以用更友好的方式表示

  • 反正是数据结构,一个 json

    • 符号表,调试信息,……

  • 最小的 ELF 实验...

  • 确实可以execve 但是 下一步就是 exit...

2. 和 ELF 的搏斗的每一年

2.1. ELF 不仅仅是可执行文件

一个经典、没有任何问题的设计

  • E = Executable

  • L = Linkable

    • 一个标准,管好所有相关工具链

    • 甚至还有 Core Dump

但是

  • ……

2.2. 搏斗!

你期末复习的时候吐血了吗?

  • 同学 A: 吐了

  • 同学 B: 吐了

    • “连 CSAPP 这一章都讲得不怎么样”

不只是你们,我们也在吐血

  • 任课教师 A: 第一次打开 ppt 真的有点吐血的感觉

  • 任课教师 B: 根本讲不完,我估计听懂的人也不多

  • 任课教师 C (我自己):太好了,再也不用教了

我们面对的挑战:到底要不要读手册呢?

2.3. 为什么

反思

  • ELF 不是一个人类友好的 “状态机数据结构描述”

为什么没有用 JSON?

ELF 到处都是 offset...这是一个极其反人类的东西...

    • 为了性能,彻底违背了可读 (“信息局部性”) 原则

几乎让你直接去读一个内存里的二叉树

  • “Core dump”

    • Segmentation fault (core dumped)

    • (为什么叫 “core dump”?)

  • 地狱笑话:今天的 core dump 是个 ELF 文件

    • 内存状态...

    • “状态快照”——Everything is a state machine

      • 只是一个状态!而且不能向前后退

2.4. Core Dump

Core Dump(核心转储)是一种在程序运行过程中,由于某些错误或异常导致程序崩溃时,操作系统生成的文件。这个文件包含了程序崩溃时的内存映像、寄存器状态、堆栈信息等详细信息,用于帮助开发者调试程序。

在不那么方便 gdb 的时候

  • ulimit -c unlimited (或者一个你认为合理的值)

  • Crash (SIGSEGV, SIGABRT, SIGILL, ...) 时会做 core dump

    • 适合于 production systems

但 gdb 竟然不允许我继续执行

只是一个瞬间快照

  • 明明状态里该有的都有啊 (内存 + 寄存器)

  • 想要继续执行?可以用 CRIUarrow-up-right

    • 今天多了一个应用:实现 Agent..

    • 实现保存聊天的功能

    • 智能 Agent 时代,这玩意可好用了

2.5. 曾经不是这样

UNIX a.outarrow-up-right “assembler output”

  • 一个相对平坦的数据结构

  • 功能太少 (不支持动态链接、调试信息、内存对齐、thread-local……),自然被淘汰

2.6. 换句话来说

支持的特性越多,越不人类友好

  • 听到 “程序头表”、“节头表”,大脑需要额外转译

    • “程序头表”(Program Header Table)

    • “节头表”(Section Header Table)

  • 含义隐晦的 R_X86_64_32, R_X86_64_PLT32

  • 大量的 “指针” (人类无法阅读的偏移量)

    • (我竟然已经被训练成基本可以正常阅读了 )

    • 卷王们在考试周经历了同样的魔鬼训练

      • 这就是大学需要改变的地方

人类友好的方式

  • 越 “平坦”,越容易理解

  • 所有需要的信息都立即可见

2.7. 重新设计的机会

那就设计一个 FLE 吧

  • Funny (Fluffy) Linkable Executable

  • AIGC

    • Friendly Learning Executable (GPT-4o)

    • Fast Learner's Executable (DeepSeek-r1)

核心设计思路

  • 一切都对人类直接可读 (所有信息都在局部)

    • 所有需要的东西不要以指针形式...

    • 所有东西都在手边...

  • 回归链接和加载中的核心概念:代码、符号、重定位

    • 你们会怎么设计?

3. Funny Little Executable

3.1. 代码 - 符号

代码 (emoji1)、符号 (emoji2)、重定位 (emoji3)

  • 凑齐这三要素,我们就可以完成链接到加载的全流程了!

3.2. Demo

编译 - 连接

成功执行...

3.3. DSL: Domain Specific Language

我们为什么需要 DSL?

  • 只要领域内的表达能力

    • 做了减法,就可以更专注

    • 更简洁、更 natural

DSL 的例子

  • 正则表达式

    • 省略了关于字符串...扫描还要找子串的复杂度

  • Markdown, TeX, ...

  • abcjsarrow-up-right 代码 - 乐谱???

只要你有问题,就有答案...

3.4. 实现 ELF Binutils

实现的工具集

  • objdump/readfle/nm (显示)

  • cc/as (编译)

  • ld (链接)

大部分都复用自 GNU binutils

  • elf_to_fle

3.5. 生成可执行文件 - 预编译 - 编译

源代码 (.c) → 源代码 (.i)

源代码 (.i) → 汇编代码 (.s)

  • “高级状态机” 到 “低级状态机” 的翻译

  • 最终生成带标注的指令序列

3.6. 生成可执行文件 - 汇编

汇编代码 (.s) → 目标文件 (.o)

  • 文件 = sections (.text, .data, .rodata.str.1, ...)

    • 对于 ELF,每个 section 有它的权限、内存对齐等信息

  • section 中的三要素

    • 代码 (字节序列)

    • 符号:标记 “当前” 的位置

    • 重定位:暂时不能确定的数值 (链接时确定)

      • Quick Quiz: ELF 中全局和局部符号有什么区别?还有其他类型的符号吗?

3.7. 生成可执行文件 - 静态 链接

多个目标文件 (.o) → 可执行文件 (a.out)

  • 合并所有的 sections

    • 分别合并 .text, .data, .bss 中的代码

    • 把 sections “平铺” 成字节序列

    • 确定所有符号的位置

    • 解析全部重定位

  • 得到一个可执行文件

    • (程序初始内存状态的描述)

3.8. 最后一步:加载

把 “字节序列” 搬到内存

  • 没错,就只做这一件事

  • 然后设置正确的 PC,开始运行

4. 操作系统和加载器

4.1. 加载器是内核实现的一部分

execve(path, argv, envp)

4.2. 等等

运行程序的两种方法

  • /bin/ls (这是一个 ELF) v.s. ./a.py (这可是文本啊!)

5. #! - Shebang

UNIX 对 # 注释的 “妙用”

  • any_file: #!A B C

    • 操作系统会执行 execve(A, ["A", "B C", "any_file"], envp)

    • (我们的 FLE 文件是可以直接执行的)

    • 优先使用 #! 作为解释器 (Magic Number)

操作系统的彩蛋...

Shebang机制,是一个隐藏在操作系统中的有趣特性。

它允许用户通过简单的文本标记( #! )来指定脚本的解释器,从而使脚本文件可以直接像可执行文件一样运行。

Shebang

  • 如果一个文件的第一行是 #!A B C ,操作系统会将这个文件视为一个脚本文件,并尝试执行它。

  • 操作系统会调用 execve 系统调用,将 A 作为解释器程序,B C 作为参数,any_file 作为脚本文件名传递给解释器。

Last updated