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