Linux ELF(Executable and Linkable Format)文件是Linux系统中最常用的可执行文件格式,其执行过程涉及操作系统内核、动态链接器以及程序自身的协同工作,理解ELF文件的执行机制,需要从其文件结构、加载流程、链接方式以及运行时环境等多个维度展开。
ELF文件的基本结构
ELF文件采用分段(Segment)和分节(Section)相结合的组织方式,不同部分在执行时承担不同角色,其核心结构包括ELF头、程序头表、节区头表以及各个节区/段。
ELF头
ELF文件开头的ELF头是文件的“身份证”,长度固定(64位系统下64字节),包含文件的基本属性和关键指针,其中最重要的字段包括:
e_ident
:文件标识魔数(如0x7FELF
),用于确认文件类型为ELF;e_type
:文件类型,如ET_EXEC
(可执行文件)、ET_DYN
(共享库)、ET_REL
(可重定位文件);e_machine
:目标机器架构(如EM_X86_64
、EM_ARM
);e_entry
:程序入口地址,即CPU开始执行的第一条指令在内存中的位置;e_phoff
:程序头表偏移量,指向加载时需要的段信息;e_shoff
:节区头表偏移量,指向链接时需要的节区信息;e_flags
:处理器特定标志;e_ehsize
、e_phentsize
、e_phnum
:ELF头大小、程序头表条目大小及数量;e_shentsize
、e_shnum
、e_shstrndx:节区头表条目大小、数量及节区名称字符串表索引。
程序头表与段
程序头表(Program Header Table)是一个结构数组,每个条目描述一个“段”(Segment),段是加载到内存中的单位,用于定义文件的“镜像”,常见的段包括:
PT_LOAD
:可加载段,如代码段(.text
)和数据段(.data
),操作系统会将其映射到进程的虚拟内存空间;PT_DYNAMIC
:动态段,包含动态链接所需的信息(如依赖库列表、重定位表等);PT_INTERP
:解释器段,指定动态链接器的路径(如/lib64/ld-linux-x86-64.so.2
),仅对动态链接的可执行文件存在;PT_NOTE
:注释段,包含辅助信息(如版本、构建信息等)。
节区头表与节区
节区头表(Section Header Table)描述文件的“节区”(Section),节区是链接时的基本单位,包含代码、数据、符号表等信息,关键节区包括:
.text
:代码节,存储程序的机器指令;.data
:已初始化数据节,存储程序运行时需要初始化的全局变量和静态变量;.bss
:未初始化数据节,存储未初始化的全局变量和静态变量,加载时由内核清零;.symtab
:符号表,包含定义和引用的函数、变量符号信息;.dynsym
:动态符号表,仅包含动态链接相关的符号;.rela.dyn
/.rela.plt
:重定位表,用于动态链接时调整地址引用;.strtab
:字符串表,存储符号名称等字符串数据。
ELF文件的执行过程
ELF文件的执行可分为加载、链接、运行三个阶段,涉及操作系统内核、动态链接器(如ld-linux.so.2)和程序本身的协作。
加载阶段:将文件映射到内存
当用户在终端执行一个ELF文件(如./a.out
)时,shell会调用execve
系统调用,触发内核的加载流程,加载过程的核心任务是将ELF文件中的可加载段(PT_LOAD
类型的段)映射到进程的虚拟地址空间,具体步骤如下:
- 解析ELF头:内核首先读取文件开头的ELF头,验证魔数和文件类型(确保是
ET_EXEC
或ET_DYN
),并通过e_entry
获取程序入口地址。 - 处理解释器:如果ELF文件是动态链接的(
PT_INTERP
段存在),内核会根据该段指定的路径(如/lib64/ld-linux-x86-64.so.2
)加载动态链接器到内存中;如果是静态链接的,则跳过此步骤。 - 映射可加载段:内核遍历程序头表,对每个
PT_LOAD
段,根据其p_vaddr
(虚拟地址)、p_filesz
(文件中段大小)和p_memsz
(内存中段大小)参数,在进程的虚拟内存中创建对应的映射区域。- 代码段(
.text
)通常映射为“可读、可执行”(r-x
); - 数据段(
.data
)映射为“可读、可写”(rw-
); .bss
段不占用文件空间,内核仅分配内存并清零(p_filesz=0
,p_memsz>0
)。
- 代码段(
- 设置入口点:加载完成后,内核将CPU的指令指针(RIP/EIP)设置为ELF头的
e_entry
(动态链接时,实际跳转到动态链接器的入口点,由链接器进一步处理)。
链接阶段:解析符号与重定位
加载完成后,如果ELF文件是动态链接的,控制权会移交给动态链接器(ld-linux.so.2);静态链接文件则直接跳转到程序入口点执行,动态链接是ELF文件执行的关键环节,主要解决符号解析和地址重定位问题。
- 符号解析:程序运行时可能依赖外部共享库(如
libc.so.6
)中的函数(如printf
)或变量(如errno
),动态链接器通过ELF文件的.dynstr
(动态字符串表)和.dynsym
(动态符号表)获取依赖库列表,然后在内存中查找已加载的共享库(或按/etc/ld.so.cache
配置的路径查找并加载),将程序中的符号引用(如printf
)与共享库中的符号定义(如libc
中的printf
地址)绑定。 - 重定位:程序中的代码和数据可能包含地址引用(如函数调用、全局变量访问),这些引用在链接时是相对地址(如
R_X86_64_PC32
),需要调整为内存中的绝对地址,动态链接器通过.rela.dyn
(数据重定位)和.rela.plt
(函数重定位)表,遍历所有重定位条目,修改内存中的指令或数据,例如将call printf
指令中的地址替换为printf
的实际内存地址。 - 控制权移交:完成符号解析和重定位后,动态链接器会初始化程序的运行时环境(如设置栈、调用
.init
节区的初始化函数),最后跳转到程序的入口点(通常是_start
,由C语言运行时库(CRT)提供)。
运行阶段:执行程序逻辑
入口点_start
是程序执行的起点,它由CRT(如libc
)提供,主要完成以下工作:
- 初始化运行时环境:设置栈指针(RSP/RBP)、初始化全局和静态变量(
.data
复制,.bss
清零)、调用atexit
注册的退出函数等。 - 调用
main
函数:_start
最终调用程序员编写的main
函数,并将命令行参数(argc
、argv
)和环境变量(envp
)传递给它。 - 程序执行:
main
函数执行过程中,CPU根据指令流执行机器码,通过栈传递函数参数、保存返回地址,通过堆动态分配内存(如malloc
),通过系统调用(如write
、open
)与内核交互。 - 程序终止:
main
函数返回后,_start
获取返回值,调用exit
系统传,将返回值传递给内核;若程序异常终止(如段错误),内核会收到信号并终止进程。
辅助工具与调试
Linux提供了多种工具用于分析ELF文件的结构和执行过程:
readelf -h a.out
:查看ELF头信息,确认入口地址、文件类型等;readelf -S a.out
:查看节区头表,列出所有节区及其属性;readelf -l a.out
:查看程序头表,分析加载时的段映射;ldd a.out
:列出动态链接的依赖库及其路径;objdump -d a.out
:反汇编.text
节区,查看机器指令;strace ./a.out
:跟踪程序执行过程中的系统调用。
相关问答FAQs
Q1:为什么动态链接的可执行文件需要动态链接器,而静态链接的不需要?
A1:动态链接的可执行文件在编译时未将依赖库的代码合并,仅保留了符号引用和重定位信息,因此需要在运行时由动态链接器加载共享库、解析符号并重定位地址,静态链接的可执行文件在编译时已将所有依赖库的代码合并到自身,无需额外链接,可直接由内核加载并执行,动态链接的优势是节省内存(多个进程共享同一份库文件)和更新方便(库文件更新后无需重新编译程序),静态链接的优势是独立性强(不依赖系统库,可在不同环境运行)。
Q2:ELF文件中的.bss
节区为什么不占用文件空间,但加载时需要内存?
A2:.bss
节区用于存储未初始化的全局变量和静态变量,这些变量在程序运行时默认值为0,由于未初始化,编译器在生成ELF文件时不会为.bss
分配实际的数据空间(因此.bss
节区的sh_size
为0),仅记录其大小和内存对齐要求,加载时,内核根据.bss
节区的大小在内存中分配一片连续区域并清零,确保程序访问这些变量时初始值为0,这种设计节省了ELF文件的存储空间,同时保证了程序的正确性。
原创文章,发布者:酷番叔,转转请注明出处:https://cloud.kd.cn/ask/32471.html