ELF 文件到底是什么:程序还没运行前,磁盘上那份可执行文件里装了什么
你在磁盘上看到一个 a.out、main、myapp,它还只是一个文件。可一旦 execve() 把它拉起来,它就变成了进程。
这中间靠的,就是 ELF。
ELF 的全名是 Executable and Linkable Format。名字已经把它说得很明白了:它既是“可执行”的,也是“可链接”的。Linux、BSD、Solaris 这类 Unix-like 系统之所以大量使用 ELF,不是因为它名字好听,而是因为它把“编译器怎么摆文件”“链接器怎么拼文件”“内核怎么把文件装进内存”这三件事拆得很清楚。
源码 -> 编译/汇编 -> .o 目标文件 -> 链接 -> ELF 可执行文件
-> 动态链接器 / 内核加载如果你只把 ELF 理解成“二进制文件格式”,还不够。它更像是一份搬运说明书:这部分要映射到内存、那部分只是给链接器看的、这部分得交给动态链接器、那部分要留给符号解析。
为什么 Linux 喜欢 ELF
先说结论:因为它分工清楚。
在 ELF 里,文件里不只是“程序代码”四个字。它把数据、代码、重定位信息、符号、动态库依赖、入口点、内存布局提示,拆成了不同层次。这样做的好处很直接:
- 编译器和链接器知道该往哪放东西
- 内核知道该把哪些内容映射到进程地址空间
- 动态链接器知道该去加载哪些共享库
- 调试器和分析工具知道该从哪读取符号和位置信息
Linux 内核里负责执行 ELF 的主路径在 fs/binfmt_elf.c。用户态执行一个 ELF 时,内核先读文件头和程序头表,再决定怎么映射各个 PT_LOAD 段;如果有解释器,也就是动态链接器,还会按 PT_INTERP 找到它,再把控制权交给它。
这就是为什么 ELF 不是单纯给机器看的,而是给一整条工具链看的。
先认识文件头
ELF 文件最开始是一小段固定格式的头部,叫 ELF header。
它最关键的作用只有一个:告诉你“这是个什么东西”。
你通常会在这里看到这些信息:
- 这是 32 位还是 64 位
- 字节序是小端还是大端
- 目标架构是什么
- 文件类型是可执行文件、共享库、目标文件,还是核心转储
- 程序入口点在哪里
- 程序头表和节头表分别从哪里开始
可以把它理解成书封面和目录页的结合体。你还没翻正文,就先知道这本书写给谁、从哪一页开始读、后面大概怎么排。
ELF 文件
├── ELF header
├── Program Header Table
├── file body ...
└── Section Header Table很多人第一次看 ELF 都会直接冲着 .text、.data、.bss 去,但实际上内核最先盯上的不是这些节,而是程序头表。
程序头表才是内核真正爱看的部分
程序头表是 ELF 里更偏“运行时”的那一层。
它告诉内核:这些内容要映射到什么样的虚拟地址、需要什么权限、文件里的哪一段对应内存里的哪一段。Linux 加载 ELF 时,最重要的就是 PT_LOAD 条目。每个 PT_LOAD 都描述了一段需要装入内存的区域。
这就是为什么很多文章会说“段(segment)是给运行时看的,节(section)是给链接器看的”。这句话大体没错,但别背得太死。更准确一点的说法是:
- 程序头表描述“怎么装进内存”
- 节头表描述“文件内部怎么组织”
程序头表里除了 PT_LOAD,你还常见到:
PT_INTERP:动态链接器路径PT_DYNAMIC:动态链接相关信息PT_GNU_STACK:栈是否可执行PT_NOTE:各种 note 信息
在 Linux 的用户态约定里,PT_INTERP 很重要。Documentation/userspace-api/ELF.rst 里明确说了,Linux 会拿第一个 PT_INTERP 去定位 ELF interpreter,也就是动态链接器本身,比如 /lib64/ld-linux-x86-64.so.2 之类的东西。
如果你把一个动态链接程序想成一场演出,主程序只是演员,PT_INTERP 指向的动态链接器才是后台总控。
节和段,不是一回事
这是 ELF 入门里最容易混的一对概念。
节(section)更像“给构建工具看的分类标签”。你在 readelf -S 里看到的 .text、.rodata、.data、.bss、.symtab、.strtab、.rela.dyn,都属于节。
段(segment)更像“给运行时装载看的打包结果”。你在 readelf -l 里看到的 LOAD、INTERP、DYNAMIC,都是程序头表里的项目。
一个简单对照表大概是这样:
| 维度 | section | segment | | --- | --- | --- | | 面向谁 | 链接器、调试器、构建工具 | 内核、动态链接器 | | 关心什么 | 文件内部分类 | 运行时映射和权限 | | 常见例子 | .text、.data、.symtab | PT_LOAD、PT_INTERP、PT_DYNAMIC |
为什么要分两套?
因为它们解决的问题不一样。节是“文件怎么写”;段是“内存怎么摆”。编译器和链接器需要更细颗粒度的信息,内核装载时却不想被这些细碎分类拖住,所以它只看对自己有用的那层。
符号表到底在干什么
如果你想知道一个 ELF 里到底有哪些函数、全局变量、未定义引用,符号表就是关键。
符号表里记录的是名字和位置之间的关系。它告诉工具链:
main在哪里printf是外部未定义符号,得去别的库里找- 某个全局变量的地址和大小是多少
- 哪些符号对外可见,哪些只是本文件内部使用
你常见的有两类:
.symtab:完整符号表,通常给调试和分析工具用.dynsym:动态符号表,给运行时动态链接器和共享库解析用
这两个不是重复劳动。.symtab 往往更完整,但很多发行版在 strip 之后会把它去掉;.dynsym 则要保留,因为运行时还得靠它找外部符号。
这也是为什么你用 nm、readelf -s、objdump -t 看一个程序时,看到的内容会比程序真正运行时更多。
动态链接那部分,最像“把戏台搭起来”
ELF 最有意思的地方,往往是动态链接。
一个动态链接程序并不是把所有库代码都拷进自己的文件里。它只在 ELF 里留下“我依赖谁”的线索,真正用到的时候再交给动态链接器去装载。
这几个名字很常见:
.interp:记录解释器路径,对应PT_INTERP.dynamic:动态链接信息,对应PT_DYNAMICDT_NEEDED:我需要哪些共享库DT_RPATH/DT_RUNPATH:库搜索路径线索.plt/.got:延迟绑定和地址跳转常用的配套结构
你可以把它理解成:
主程序 ELF
-> 告诉内核先把动态链接器装起来
-> 动态链接器读取 .dynamic
-> 找到 DT_NEEDED 对应的共享库
-> 把符号地址解析好
-> 跳到程序入口所以你看到一个程序“还没运行前”的文件,其实已经把未来运行时会用到的很多信息提前写好了。
用一个小图把它串起来
磁盘上的 ELF
├── ELF header
├── Program Header Table
│ ├── PT_LOAD -> 代码段、数据段映射
│ ├── PT_INTERP -> 动态链接器路径
│ └── PT_DYNAMIC -> 动态链接信息
├── sections
│ ├── .text
│ ├── .rodata
│ ├── .data
│ ├── .bss
│ ├── .symtab / .dynsym
│ └── .rela.* / .plt / .got
└── Section Header Table如果你用 readelf -hW、readelf -lW、readelf -SW 去看一个程序,这三层会很直观:
-h看头-l看程序头表-S看节头表
第一次看会有点乱,但一旦把“谁负责运行时,谁负责构建时”分开,很多字段就突然有意义了。
为什么你不该把 ELF 只当成“二进制文件”
ELF 的价值不只是“能执行”。
它把一整个软件交付流程串了起来:
- 编译器生成目标文件
- 链接器合并和重定位
- 内核负责装载
- 动态链接器负责补齐共享库
- 调试器和分析工具读取符号和段信息
这也是为什么 ELF 文件看起来像一堆冷冰冰的字段,实际上却是程序从磁盘走到内存、从静态文件变成进程的桥梁。
如果你以后再看到 ELF,别只把它当成一个缩写。它是“程序在运行前,已经把自己怎么装进去、怎么找库、怎么被定位”这整件事提前写在磁盘上的说明书。
而内核和链接器,只是照着说明书把它一步步变成可运行的进程。
京公网安备11011202100605号