ELF
ELF不仅仅是一个文件格式,它是一个分层化的容器标准,旨在高效地承载程序从编译、链接到加载、执行的整个生命周期所需的所有信息。
核心三视角
这是理解ELF的精髓。同一个ELF文件,从不同角色的视角看,结构完全不同:
-
链接视角: 将文件视为 “节”的集合。
- 目标文件(.o) 主要使用此视图。
- 关键节:
.text(代码),.data(已初始化全局变量),.bss(未初始化全局变量),.rodata(只读数据),以及你我在链接脚本中熟知的那些自定义节。 - 目的: 让链接器能够收集、合并来自不同.o文件的同类节,并解决符号引用。
-
执行视角: 将文件视为 “段”的集合。
- 可执行文件 主要使用此视图。
- 关键段:
LOAD类型的段。 - 目的: 告诉操作系统如何将文件映射到进程的虚拟内存。一个段由一个或多个属性相同的“节”组成,例如:将
.text和.rodata合并到一个只读、可执行的段;将.data放入一个可读写的段。
-
“节”与“段”的关系:
- 节是链接器的原料,段是加载器的成品。
- 链接脚本的核心工作之一,就是定义如何将节 分组并编排到最终的段 中。
两个“路由表”
ELF文件内部有两个至关重要的“表”,分别服务于链接和执行阶段:
-
节头表:
- 服务对象: 链接器、调试器。
- 内容: 描述了文件中每一个节 的信息(名称、类型、大小、在文件中的偏移等)。
- 关键点: 这个表在运行时不是必需的,可以被剥离以减小文件体积。
-
程序头表:
- 服务对象: 操作系统加载器。
- 内容: 描述了文件中每一个段 的信息(类型、在文件和内存中的偏移、大小、权限等)。
- 关键点: 对于可执行文件或共享库,此表是必需的。加载器就是根据它来设置进程的内存镜像的。
符号表
- 作用: 记录了程序中的函数和全局变量名(符号)与其地址的绑定关系。
- 链接时: 用于解决跨文件的符号引用(
undefined reference错误就是在这里检查的)。 - 运行时:
- 对于动态链接,用于在加载时解析对共享库中符号的引用。
- 也是调试器和栈回溯工具定位函数和变量的依据。
链接脚本
链接脚本是连接“链接视角”和“执行视角”的桥梁。
-
链接脚本指挥链接器:
- 将输入的
.o文件中的节(如.text.*)收集、排序、合并到输出文件的特定节中。 - 然后,通过
SECTIONS命令中的:AT(ALIGN())等指令,以及隐式或显式地定义内存区域,最终决定了这些输出节如何被编排到程序头所描述的段里。
- 将输入的
-
ENTRY(_start)等符号,最终成为了ELF头中的入口点地址,指导加载器从哪里开始执行。
总结:
ELF是一个设计精良的契约。编译器产出遵循此契约的 .o 文件;链接器(在你编写的链接脚本指导下)消费这些 .o 文件,并生成一个同样遵循此契约的、包含完整程序视图的可执行文件;最后,操作系统加载器读取并执行此契约,将一个静态的文件瞬间转化为一个活的进程。