跳转至

ELF

ELF不仅仅是一个文件格式,它是一个分层化的容器标准,旨在高效地承载程序从编译、链接到加载、执行的整个生命周期所需的所有信息。

核心三视角

这是理解ELF的精髓。同一个ELF文件,从不同角色的视角看,结构完全不同:

  • 链接视角: 将文件视为 “节”的集合

    • 目标文件(.o) 主要使用此视图。
    • 关键节: .text(代码),.data(已初始化全局变量),.bss(未初始化全局变量),.rodata(只读数据),以及你我在链接脚本中熟知的那些自定义节。
    • 目的: 让链接器能够收集、合并来自不同.o文件的同类节,并解决符号引用。
  • 执行视角: 将文件视为 “段”的集合

    • 可执行文件 主要使用此视图。
    • 关键段: LOAD 类型的段。
    • 目的: 告诉操作系统如何将文件映射到进程的虚拟内存。一个段由一个或多个属性相同的“节”组成,例如:将 .text.rodata 合并到一个只读、可执行的段;将 .data 放入一个可读写的段
  • “节”与“段”的关系:

    • 节是链接器的原料,段是加载器的成品。
    • 链接脚本的核心工作之一,就是定义如何将 分组并编排到最终的 中。

两个“路由表”

ELF文件内部有两个至关重要的“表”,分别服务于链接和执行阶段:

  • 节头表:

    • 服务对象: 链接器、调试器。
    • 内容: 描述了文件中每一个 的信息(名称、类型、大小、在文件中的偏移等)。
    • 关键点: 这个表在运行时不是必需的,可以被剥离以减小文件体积。
  • 程序头表:

    • 服务对象: 操作系统加载器。
    • 内容: 描述了文件中每一个 的信息(类型、在文件和内存中的偏移、大小、权限等)。
    • 关键点: 对于可执行文件或共享库,此表是必需的。加载器就是根据它来设置进程的内存镜像的。

符号表

  • 作用: 记录了程序中的函数和全局变量名(符号)与其地址的绑定关系。
  • 链接时: 用于解决跨文件的符号引用(undefined reference 错误就是在这里检查的)。
  • 运行时:
    • 对于动态链接,用于在加载时解析对共享库中符号的引用。
    • 也是调试器和栈回溯工具定位函数和变量的依据。

链接脚本

链接脚本是连接“链接视角”和“执行视角”的桥梁

  1. 链接脚本指挥链接器:

    • 将输入的 .o 文件中的(如 .text.*)收集、排序、合并到输出文件的特定中。
    • 然后,通过 SECTIONS 命令中的 :AT(ALIGN()) 等指令,以及隐式或显式地定义内存区域,最终决定了这些输出节如何被编排到程序头所描述的里。
  2. ENTRY(_start) 等符号,最终成为了ELF头中的入口点地址,指导加载器从哪里开始执行。

总结:

ELF是一个设计精良的契约。编译器产出遵循此契约的 .o 文件;链接器(在你编写的链接脚本指导下)消费这些 .o 文件,并生成一个同样遵循此契约的、包含完整程序视图的可执行文件;最后,操作系统加载器读取并执行此契约,将一个静态的文件瞬间转化为一个活的进程。