跳转至

(拓展)ELF 文件

约 1049 个字 59 行代码 预计阅读时间 4 分钟

ELF (Executable and Linkable Format)文件,也就是在 Linux 中的目标文件,主要有以下三种类型1

  • 可重定位文件(Relocatable File),包含由编译器生成的代码以及数据。链接器会将它与其它目标文件链接起来从而创建可执行文件或者共享目标文件。在 Linux 系统中,这种文件的后缀一般为 .o
  • 可执行文件(Executable File),就是我们通常在 Linux 中执行的程序。
  • 共享目标文件(Shared Object File),包含代码和数据,这种文件是我们所称的库文件,一般以 .so 结尾。一般它有两种使用情景:
    • 链接器(Link eDitor, ld)可能会处理它和其它可重定位文件以及共享目标文件,生成另外一个目标文件。
    • 动态链接器(Dynamic Linker)将它与可执行文件以及其它共享目标组合在一起生成进程镜像。

静态链接库

Linux 下的静态链接库是 ar File Format (archive) 而并非 ELF 格式。它通常是多个 .o 文件的打包。

文件格式的具体规范可以参考 ELF 文件 - CTF Wiki,在此不过多介绍。

节 (Section) 与段 (Segment)

ELF 里,节(Section)是程序或文件中的一种逻辑划分单元,通常用于组织代码、数据或元数据;而段是一个或多个属性类似的 section 的“打包”,用于被内核分段加载。不论是 section 还是 segment 都对应着一段地址(以及对应 alignment 要求),在程序被内核加载时,对应 section/segment 会自动映射到对应地址空间。

对于 Section 我们是不陌生的。只要我们要写汇编代码,就需要指定代码或数据的 Section。

在 ELF 文件中,有一些常见的 section:

  • 代码相关节区
    • .text
      存放程序的可执行指令(机器代码),是只读的。例如函数的主体代码。
    • .init
      包含程序的初始化代码(如 _start 入口前的逻辑)。
    • .fini
      包含程序的终止代码(如程序退出时的清理逻辑)。
    • .plt(Procedure Linkage Table)
      动态链接时用于跳转到外部函数的存根代码。
    • .got(Global Offset Table)
      动态链接时用于存储全局变量和函数地址的表。
  • 数据相关节区
    • .data 存放已初始化的全局变量和静态变量(读写权限)。
    • .rodata(或 .rodata1
      存放只读数据(如字符串常量、全局常量等)。
    • .bss(Block Started by Symbol)
      存放未初始化的全局变量和静态变量(实际不占磁盘空间,运行时初始化为零)。
    • .tdata(Thread-Local Data)
      存放已初始化的线程局部变量(TLS, Thread-Local Storage)。
    • .tbss(Thread-Local BSS)
      存放未初始化的线程局部变量(TLS,运行时初始化为零)。
  • 调试与符号信息
    • .symtab
      存储程序的符号表(函数、变量名等)。
    • .strtab
      存储符号表中的字符串(如符号名称)。
    • .debug_*
      调试信息(如 .debug_info.debug_line)。
    • .comment
      存放编译器版本信息等注释。
  • 动态链接相关
    • .dynamic
      存储动态链接所需的元数据(如依赖的共享库)。
    • .dynsym
      动态链接符号表。
    • .dynstr
      动态链接字符串表。
    • .interp
      指定动态链接器的路径(如 /lib/ld-linux.so.2)。
  • 其他
    • .rel.* / .rela.*
      重定位信息(如 .rel.text.rel.data),用于链接时修正地址。
    • .eh_frame
      异常处理框架信息(用于 C++ 异常等)。
    • .ctors / .dtors
      构造函数(Constructor)和析构函数(Destructor)列表(C++ 全局对象初始化)。
    • .shstrtab
      存储节区名称的字符串表。

下面是一段示例汇编代码,使用了三个 section。

.section .text
.global _start
_start:
    movl $4, %eax       # system call number (write)
    movl $1, %ebx       # file descriptor (stdout)
    movl $msg, %ecx     # string address
    movl $len, %edx     # string length
    int $0x80           # call kernel
    movl $1, %eax       # exit system call
    int $0x80

.section .rodata
msg:
    .ascii "Hello, World!\n"  # string + newline
    len = . - msg             # calculate string length

.section .bss
.lcomm buffer, 256       # uninitialized buffer

使用 gcc a.s -o a -nostdlib 编译为 ELF 可执行文件。使用 readelf -S areadelf -l a 分别查看其 Section 和 Segment 信息:

There are 8 section headers, starting at offset 0x330:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .note.gnu.build-i NOTE             00000000004000e8  000000e8
       0000000000000024  0000000000000000   A       0     0     4
  [ 2] .text             PROGBITS         000000000040010c  0000010c
       000000000000001d  0000000000000000  AX       0     0     1
  [ 3] .rodata           PROGBITS         0000000000400129  00000129
       000000000000000e  0000000000000000   A       0     0     1
  [ 4] .bss              NOBITS           0000000000600138  00000138
       0000000000000100  0000000000000000  WA       0     0     8
  [ 5] .symtab           SYMTAB           0000000000000000  00000138
       0000000000000168  0000000000000018           6    11     8
  [ 6] .strtab           STRTAB           0000000000000000  000002a0
       000000000000004e  0000000000000000           0     0     1
  [ 7] .shstrtab         STRTAB           0000000000000000  000002ee
       0000000000000041  0000000000000000           0     0     1

Elf file type is EXEC (Executable file)
Entry point 0x40010c
There are 3 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x0000000000000137 0x0000000000000137  R E    0x200000
  LOAD           0x0000000000000138 0x0000000000600138 0x0000000000600138
                 0x0000000000000000 0x0000000000000100  RW     0x200000
  NOTE           0x00000000000000e8 0x00000000004000e8 0x00000000004000e8
                 0x0000000000000024 0x0000000000000024  R      0x4

 Section to Segment mapping:
  Segment Sections...
   00     .note.gnu.build-id .text .rodata
   01     .bss
   02     .note.gnu.build-id

可以看到,Flag 类似的 section 被放到了一个 segment 中。内核对不同 program/segment type 有不同处理。所有内核支持的 program type 可见 linux/include/uapi/linux/elf.h

如果对内核如何加载 ELF 感兴趣可以看 源代码 或者看 这篇文章

练习:Thread Local Storage

尝试把上面的示例代码 section .rodata 改为 section .tdata,查看 section 和 segment 变化情况。