(拓展)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 a
和 readelf -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 变化情况。