10 - Executable files
10 - 可执行文件
有了系统调用和 libc,我们就真的可以实现 “任何程序” 了——例如,你可以想一想,如果要实现一个 MicroPython 解释器,我们需要实现什么,又需要借助哪些系统调用?当然,我们需要编译器帮我们编译 python.c 到可执行文件——我们一直以来都 “默认” 了编译器工具链可以帮助我们实现高级语言到可执行文件的翻译。今天是时候 “打开” 这部分内容了。
操作系统的核心我们已经理解得差不多了...
本讲内容:(静态链接) 可执行文件的概念和基本原理;我们如何自己动手构建一个可执行文件格式。
1. 可执行文件
1.1. 什么是可执行文件
学习操作系统前
那个 “双击可以弹出窗口的东西”
...
学习操作系统后
一个操作系统中的对象 (文件)
一个字节序列 (我们可以把它当字符串编辑)
一个描述了状态机初始状态的数据结构 (打扰了)
1.2. ELF: Executable and Linkable Format
ELF 是“Executable and Linkable Format(可执行和可链接格式)”的缩写。它是一种标准的文件格式,主要用于存储可执行文件、目标文件(如编译后的中间文件)和共享库等。
我们在《计算机系统基础》中学到的
binutils 中的工具可以让我们查看其中的重要信息
《计算机系统基础》常备工具
readelf & objdump
binutils 里原来还有不少宝藏!
我们在《计算机系统基础》里没学到的
如果我们想了解关于 ELF 文件的信息,有什么不那么 “原始” 的现代工具可用?
哦,有 elfcat,以及更多
1.3. 可执行文件:字节序列
无论如何,可执行文件都是字节序列
我们能不能得到一个最小的可执行文件 a.out
1.4. 可执行文件:进程初始状态的描述
回顾:System V ABI
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 竟然不允许我继续执行
只是一个瞬间快照
明明状态里该有的都有啊 (内存 + 寄存器)
往回走呢?2018 年你还可以发一篇 OSDI 了
想要继续执行?可以用 CRIU
今天多了一个应用:实现 Agent..
实现保存聊天的功能
智能 Agent 时代,这玩意可好用了
2.5. 曾经不是这样
UNIX a.out “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, ...
abcjs 代码 - 乐谱???
只要你有问题,就有答案...
3.4. 实现 ELF Binutils
实现的工具集
objdump/readfle/nm (显示)
cc/as (编译)
ld (链接)
大部分都复用自 GNU binutils
elf_to_fle
3.5. 生成可执行文件 - 预编译 - 编译
源代码 (.c) → 源代码 (.i)
Ctrl-C & Ctrl-V (#include)
字符串替换
今天:我们有过程宏
源代码 (.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)
操作系统内核解析 path、完成加载
可以在源码里找到手册 (System V ABI)中的概念
argc, argv, envp:
关键字搜索:PT_LOAD
有代码的地方就有 bug: CVE-2024-46826
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 文件是可以直接执行的)
Linux 的实现:binfmt_script.c
优先使用
#!作为解释器 (Magic Number)
操作系统的彩蛋...
Shebang机制,是一个隐藏在操作系统中的有趣特性。
它允许用户通过简单的文本标记( #! )来指定脚本的解释器,从而使脚本文件可以直接像可执行文件一样运行。
Shebang
如果一个文件的第一行是 #!A B C ,操作系统会将这个文件视为一个脚本文件,并尝试执行它。
操作系统会调用 execve 系统调用,将 A 作为解释器程序,B C 作为参数,any_file 作为脚本文件名传递给解释器。
Last updated