南京大学 操作系统 (JYY) 学习笔记:可执行文件、链接器与 Shebang 的彩蛋
写在前面这是本系列的第十篇。我们已经知道进程从execve后的初始状态开始可以通过mmap改变自己的地址空间通过fork创建新的进程。有了系统调用和libc我们真的可以实现“任何程序”了。但在此之前我们一直默认了编译器工具链gcc/ld可以奇迹般地帮我们实现从高级语言到可执行文件的翻译。今天是时候“打开”这个黑盒了什么是可执行文件静态链接到底做了什么以及大名鼎鼎的#!(Shebang) 究竟藏着怎样的内核魔法课前小插曲Testkit 的修复与反思关于之前的 Testkit如果工程实现里有多个.c文件执行顺序和快照机制往往让人摸不着头脑。没听懂没关系以后有机会让豆包/Kimi 详细教一下怎么用。核心回顾操作系统的对象进程状态机。管理 API:fork,execve,exit。连续的内存段可以被看作一个能在进程间共享、或映射到文件的对象。管理 API:mmap,munmap,mprotect。什么是可执行文件学习操作系统前那个鼠标“双击可以弹出窗口的东西”。学习操作系统后它是一个操作系统中的对象文件。它是一个字节序列可以用十六进制编辑器直接编辑。它是一个描述了状态机初始状态的数据结构听起来开始让人头秃了。ELF (Executable and Linkable Format)在《计算机系统基础》中我们已经接触过 ELF可执行和可链接格式。这是一种标准的文件格式用于存储可执行文件、目标文件和共享库。我们常用binutils中的工具如readelf和objdump来查看其中的信息。如果想用更现代、更友好的工具可以尝试elfcat。无论工具怎么变可执行文件在底层永远只是一段字节序列。可执行文件进程初始状态的描述根据 AMD64 架构的圣经《System V ABI》规范操作系统只规定了部分寄存器和栈的初始状态而其他的状态主要是内存布局全权由可执行文件来指定。一个合格的可执行文件需要包含什么假设没有动态链接基本信息版本、体系结构。内存布局哪些段放代码.text哪些段放数据.data,.bss。其他信息调试信息、符号表等。极简主义可执行文件其实不需要那么复杂只要满足最基本的要求我们甚至可以手捏一个最小的可执行文件一个合法的头Magic Number7f 45 4c 46也就是\x7fELF。一段蹦床代码Trampoline Code。# 最小的 ELF 实验用 Python 打印字节流即可生成 a.outhex_data 457f 464c 0102 0001 0000 0000 0000 0000 0002 003e 0001 0000 0078 0040 0000 0000 0040 0000 0000 0000 0000 0000 0000 0000 0000 0000 0040 0038 0001 0000 0000 0000 0001 0000 0007 0000 0078 0000 0000 0000 0078 0040 0000 0000 0000 0000 0000 0000 0007 0000 0000 0000 0007 0000 0000 0000 1000 0000 0000 0000 3c6a 3158 0fff 0005 用strace运行它你会发现它确实能被execve成功加载然后下一秒直接exit(0)rootLAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec10/elf-play# strace ./a.outexecve(./a.out,[./a.out], 0x7fff138033d0 /*36vars */)0exit(0)? exited with0和 ELF 搏斗的每一年ELF 是一个经典且极其稳固的设计。E ExecutableL Linkable。它统一了工具链甚至连 Core Dump核心转储用的也是 ELF 格式。但是它也是折磨历届 CS 学生的元凶同学 A期末复习看吐了。老师 A第一次打开 PPT 真的有点吐血的感觉。老师 C (JYY 自己)太好了以后再也不用教了为什么 ELF 这么难懂反思一下ELF 根本不是一个“人类友好”的数据结构描述。为什么不用 JSON因为为了极致的加载性能和空间利用率ELF 彻底违背了可读性信息局部性原则。它到处都是 offset偏移量几乎是让你直接去大脑里人肉解析一棵内存里的树当你听到“程序头表”、“节头表”看到隐晦的R_X86_64_32标记和无尽的指针时人类的大脑是抗拒的。顺带一提Core Dump 的本质Segmentation fault (core dumped)—— 为什么叫“核心转储”地狱笑话今天的 core dump 就是一个 ELF 文件。它的本质是操作系统在程序崩溃的瞬间拍下的一张“状态快照” (内存 寄存器)。Everything is a state machine如果配合ulimit -c unlimited生成 Core Dump再用 GDB 挂载你可以精确查看到程序死前的遗言。想要不仅能看还能让状态机“后退”可以使用CRIU工具。在智能 Agent 时代这类工具可以完美实现复杂应用状态的冻结与恢复。重新设计Funny Little Executable (FLE)既然 ELF 不当人那我们就自己设计一个对人类直接可读的可执行文件格式FLE。核心设计思路越“平坦”越容易理解。所有需要的信息都立即可见不要用指针跳来跳去。回归链接和加载的核心概念代码、符号、重定位。FLE 的三要素代码 (emoji1)符号 (emoji2)重定位 (emoji3)凑齐这三要素我们就可以完成链接到加载的全流程这就是 DSL (Domain Specific Language领域特定语言) 的魅力。做了减法去掉了不必要的复杂特性反而能让人更专注底层的本质。rootLAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec10/fle/demo# make# 编译阶段将 .c 编译成自定义的 .fle (目标文件)./cc-Wall-g-Osfoo.c-ofoo.o...# 链接阶段自定义的 ld 将多个 .fle 合并成最终的 hello 可执行文件./ld foo.fle libc.fle main.fle-ohello rootLAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec10/fle/demo# ./helloMessage: Hello World!... rootLAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec10/fle/demo# echo $?42从源码到加载全流程拆解1. 预编译 - 编译 (.c-.s)预处理:处理#include(其实就是无情的 Ctrl-C Ctrl-V) 和宏替换。编译:将“高级状态机” (C语言) 翻译成“低级状态机” (汇编)最终生成带标注的指令序列。2. 汇编 (.s-.o)将汇编指令打包成 sections (.text,.data,.bss等)。为每个 section 记录核心三要素代码字节序列、符号标记当前位置、重定位暂时不能确定的地址留给链接器填空。3. 静态链接 (.o-a.out)合并所有目标文件的 sections。把代码“平铺”成一整块字节序列。确定所有全局符号的绝对物理地址。解析全部重定位填空最终得到一个可以直接运行的可执行文件。4. 加载 (Loader)把生成的“字节序列”用mmap搬到内存。设置好寄存器和 PC开始运行# 极简版加载器伪代码memmmap.mmap(fileno-1,lengthlen(bs),protmmap.PROT_READ|mmap.PROT_WRITE|mmap.PROT_EXEC,flagsmmap.MAP_PRIVATE|mmap.MAP_ANONYMOUS,)mem.write(bs)mem.flush()call_pointer(mem,fle[symbols][_start])操作系统内核与 Shebang (#!) 的浪漫彩蛋加载器其实是操作系统内核实现的一部分。当你调用execve(path, argv, envp)时内核Linux 中的binfmt_elf.c会亲自解析可执行文件扫描PT_LOAD段并完成内存映射。但是等等运行程序的两种方法/bin/ls(这是一个标准的 ELF 二进制文件)./a.py(这明明是个纯文本啊操作系统怎么懂 Python)Shebang 机制的内核魔术这是 UNIX 给程序员留下的最浪漫的彩蛋对#注释的“妙用”。如果一个文本文件的第一行是#!A B C操作系统在execve尝试加载它时会发现开头的 Magic Number 不是 ELF 的\x7fELF而是#!(十六进制为0x23 0x21)。此时内核binfmt_script.c会立刻心领神会把执行命令偷偷狸猫换太子转换为execve(A, [A, B C, any_file], envp)/* Linux 内核源码片段检查是否以 #! 开头 */if((bprm-buf[0]!#)||(bprm-buf[1]!!))return-ENOEXEC;...fileopen_exec(i_name);这就是为什么你在 Python 脚本的第一行写上#!/usr/bin/env python3后这个普通的文本文件就拥有了像二进制程序一样直接运行的魔法。rootLAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec10/shebang# ./goodargv[0]A argv[1]B C argv[2]./good rootLAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec10/shebang# ./bad-bash: ./bad:..: bad interpreter: Permission denied计算机系统里没有魔法所有的神奇现象背后都是一行行清晰可查的 C 语言代码

相关新闻