可执行文件的装载

本文详细介绍了Linux系统中ELF可执行文件的加载过程,包括虚拟地址空间的创建、映射关系的建立以及CPU指令寄存器的设置。此外,还深入探讨了ELF文件的段和节结构,以及Linux内核如何实现ELF文件的加载。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    这里,我们从OS的角度来阐述一个可执行文件如何被装载,并且同时在进程中执行。一个可执行文件从装载到执行,最开始只需要做三件事情:
  • 创建一个独立的虚拟地址空间
  • 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系
  • 将CPU指令寄存器设置成可执行文件的入口地址,启动运行
Linux下,创建虚拟地址空间只是分配一个页目录就可以了,甚至不设置页映射关系。当程序发生页错误时,OS将从物理内存中分配一个物理页,然后将“缺页”读取到内存中,在设置缺页的虚拟页和物理页的映射关系。显然,当OS捕获到缺页错误时,它应知道程序目前所需要的页在可执行文件中的哪个位置。这就是虚拟空间与可执行文件之间的映射关系,也是传统意义上的“装载”过程。这种映射关系只是保存在操作系统内部的一个数据结构。Linux将进程虚拟空间中的一个段叫做虚拟内存区域。将CPU指令寄存器设置为可执行文件入口,从进程角度来看,这一步可以简单地认为OS执行了一条跳转指令。
    上述步骤执行完以后,其实可执行文件的真正指令和数据都没有被转载到内存中,操作系统只是通过可执行文件头部的信息建立起可执行文件和进程虚拟之间的数据结构而已。当CPU开始执行这个地址的指令时,发现该页面是个空页面,于是它就认为这是一个页错误。CPU将控制权交给OS,OS根据映射关系找到空页面所在的VMA,计算出相应的页面在可执行文件中的偏移,然后在物理内存中分配一个物理页面,将进程中该虚拟页与分配的物理页之间建立映射关系,然后把控制权交给进程继续执行。
    OS在装载时,只关心一些跟装载相关的问题,最主要的是段的权限。在ELF文件中,段的权限往往只有为数不多的几种组合:
  • 以代码段为代表的权限为可读可执行的段
  • 以数据段和BSS段为代表的权限为可读可写的段
  • 以只读数据段为代表的权限为只读的段
OS给出一个简单的方案,对于相同权限的段,把它们合并到一起当作一个段进行映射。ELF可执行文件引入了一个概念“segment”,一个“segment”包含一个或多个属性类似的“section”。这样做的好处是可以很明显的减少页面内部碎片,从而节省了内存空间。下面是一个很小的程序:
//SectionMapping

#include <stdlib.h>
int main()
{
    while(1)
        sleep(1000);
    return 0;
}
    
使用静态链接的方式将其编译成可执行文件,得到一个elf文件
$ gcc -static SectionMapping.c -o SectionMapping.elf
    使用readelf查看该elf文件的section: $ readelf -S SectionMapping.elf
    也可以使用readelf命令查看ELF的“Segment”,描述“segment”的结构叫程序头( Program Header),它描述了ELF文件该如何被OS映射到进程的虚拟空间: $ readelf -l SectionMapping.elf

    从装载的角度,我们只关心“LOAD”类型的Segment。Segment[00]是可读可执行的,统一被映射到VMA0;Segment[01]是可读写的,统一被映射到VMA1。
    
    操作系统通过给进程空间划分出一个个VMA来管理进程的虚拟空间,基本原则是将相同权限属性的、有相同映像文件的映射成一个VMA;一个进程基本上可以分为如下几种VMA区域:
  • 代码VMA,权限可读、可执行;有映像文件
  • 数据VMA,权限可读写、不可执行;有映像文件
  • 堆VMA,权限可读写、不可执行;无映像文件,匿名,可向上扩展
  • 栈VMA,权限可读写、不可执行;无映像文件,匿名,可向下扩展
Linux下,我们可以通过查看/proc来查看进程的虚拟空间分布:
$ ./SectionMapping.elf &
[1] 3061
$ cat /proc/3061/maps

    我们从权限属性可以看出VMA1可执行,映射到代码段等;VMA2同VMA1有映像文件,可读写不可执行,VMA2映射到数据段等;VMA3为堆VMA;VMA4为栈VMA。

下面简介下Linux内核装载ELF过程:
    首先在用户层面,bash进程会调用fork()系统调用创建一个新的进程,然后新的进程调用execve()系统调用执行指定的ELF文件,原先的bash进程继续返回等待刚启动的新进程结束,然后继续等待用户输入命令。
    在进入execve()系统调用之后,Linux内核就开始进行真正的装在工作。在内核中,execve()系统调用相应的入口时sys_execve(),内部调用do_execve()。do_execve()会首先查找被执行的文件,如果找到文件,则读取文件的前128字节,然后调用search_binary_handle()去搜索和匹配合适的可执行文件装在处理过程。Linux中所有被支持的可执行文件格式都有相应的装在处理过程,search_binary_handle()会通过判断文件头部的魔数确定文件的格式,并调用相应的装载处理过程;这里我们只关心ELF可执行文件的装载,load_elf_binary(),它的主要套路是:
  1. 检查ELF可执行文件的有效性;
  2. 寻找动态链接的“.interp”段,设置动态链接路径
  3. 根据ELF的程序头表的描述,对ELF文件进行映射
  4. 初始化ELF进程环境
  5. 将系统调用的返回地址修改为ELF文件的入口点;入口点取决于程序的链接方式,静态链接ELF文件的头文件中e_entry所指的地址,动态链接ELF文件则入口点为动态链接器
当sys_execve()从内核态返回到用户态时,EIP寄存器直接跳转到ELF程序入口地址,程序开始执行,装载完成。
<think>我们正在详细讨论可执行文件装载过程,特别是ELF文件的加载、内存映射和动态链接库处理。根据用户提供的引用和之前的问题,我们需要整合信息,构建一个详细的装载过程解析。 关键点: 1. 触发装载:用户执行程序(如./program)触发execve系统调用。 2. 打开并验证文件:操作系统打开文件,检查ELF魔数(0x7f 45 4C 46)等。 3. 解析ELF文件头:获取入口点、程序头表(Program Header Table)位置和大小、动态链接器路径等。 4. 创建进程地址空间:释放旧地址空间(如果是替换当前进程),创建新的虚拟地址空间,初始化页表。 5. 加载段到内存:根据程序头表将各个段(如.text, .data)映射到内存,处理.bss段(清零)。 6. 动态链接处理:如果存在动态链接,加载动态链接器(如ld-linux.so),并执行重定位和初始化。 特别关注引用[1]中提到的load_elf_binary函数(Linux内核中处理ELF装载的核心函数),引用[2]中关于装载流程的步骤,引用[4]中关于段(segment)映射到虚拟内存区域(VMA)的描述。 我们将按照以下结构组织回答: - 触发装载 - ELF文件验证 - 解析ELF文件头 - 创建新地址空间 - 加载段到内存(内存映射) - 动态链接处理 - 设置程序入口 同时,我们将使用Mermaid图表来展示内存映射的布局,并给出一个装载过程的代码级概述(以Linux内核的load_elf_binary为例)。 注意:由于用户要求详细,我们会深入解释每个步骤,但避免过于底层的内核细节,专注于ELF装载相关部分。</think># 可执行文件装载过程详解 ## 一、触发装载 当用户执行`./program`或通过图形界面启动程序时: 1. Shell解析命令并调用`fork()`创建新进程 2. 新进程通过`execve()`系统调用请求内核装载可执行文件 3. 内核验证文件权限和类型后开始装载流程[^2] ```c // Linux内核execve系统调用入口 SYSCALL_DEFINE3(execve, ...) { return do_execve(getname(filename), ...); } ``` ## 二、ELF文件验证与解析 内核通过`load_elf_binary`函数处理装载过程: ### 1. 文件头验证 ```c // 内核源码片段 fs/binfmt_elf.c static int load_elf_binary(struct linux_binprm *bprm) { // 读取ELF前128字节 loc->elf_ex = *((struct elfhdr *)bprm->buf); // 验证魔数 0x7F+'E'+'L'+'F' if (memcmp(elf_ex.e_ident, ELFMAG, SELFMAG) != 0) goto out; } ``` 验证通过后继续解析关键字段[^1][^3]: | 字段 | 描述 | 作用 | |------|------|------| | e_entry | 程序入口地址 | 设置程序计数器 | | e_phoff | 程序头表偏移 | 定位段信息 | | e_phnum | 程序头数量 | 确定段数量 | ### 2. 程序头表解析 程序头表(Program Header Table)描述**内存段**信息: ```c struct elf32_phdr { Elf32_Word p_type; // 段类型 (LOAD, DYNAMIC等) Elf32_Off p_offset; // 文件内偏移 Elf32_Addr p_vaddr; // 虚拟地址 Elf32_Word p_filesz; // 文件内大小 Elf32_Word p_memsz; // 内存中大小 Elf32_Word p_flags; // 权限标志 (R/W/X) }; ``` 关键段类型: - `PT_LOAD`:需装载的段(代码/数据) - `PT_DYNAMIC`:动态链接信息 - `PT_INTERP`:动态链接器路径 ## 三、内存映射与地址空间创建 ### 1. 创建新地址空间 ```c // 释放旧地址空间 flush_old_exec(bprm); // 创建新页表 current->mm->pgd = pgd_alloc(mm); ``` 进程虚拟地址空间布局初始化: ```mermaid graph TD A[0x00000000] --> B[保留区] B --> C[.text段] C --> D[.data段] D --> E[.bss段] E --> F[堆] F --> G[共享库] G --> H[栈] H --> I[内核空间] ``` ### 2. 段装载与内存映射 对每个`PT_LOAD`类型段执行: ```c // 计算对齐后的内存大小 unsigned long size = ELF_PAGEOFFSET(phdr->p_vaddr) + phdr->p_filesz; // 建立文件到内存的映射 error = vm_mmap(bprm->file, phdr->p_vaddr & PAGE_MASK, size, prot, MAP_FIXED|MAP_PRIVATE, phdr->p_offset); ``` 映射特点[^4]: - 使用**写时复制**(Copy-On-Write)技术 - .bss段映射为匿名页(初始化为0) - 只读段共享物理页(如多个进程运行同一程序) ## 四、动态链接处理 ### 1. 动态链接器加载 若存在`PT_INTERP`段: ```c // 读取动态链接器路径(如/lib64/ld-linux-x86-64.so.2) elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL); // 装载动态链接器 load_elf_interp(&loc->interp_elf_ex, interpreter, &interp_load_addr); ``` ### 2. 重定位与符号解析 动态链接器执行: 1. 装载所有依赖库(`.so`文件) 2. 解析符号引用(通过`.dynsym`和`.dynstr`节) 3. 重定位全局偏移表(GOT): ```c // GOT表项重定位示例 *got_entry = symbol_address; ``` 4. 延迟绑定(Lazy Binding):通过PLT实现函数首次调用时解析 ### 3. 初始化执行 动态链接器完成后的操作: ```c // 设置入口地址 start_thread(regs, elf_entry, bprm->p); // 跳转到程序入口(通常为_start) regs->ip = elf_entry; ``` ## 五、完整装载流程 ```mermaid sequenceDiagram participant User participant Kernel participant Linker participant Memory User->>Kernel: execve("./program") Kernel->>Kernel: 验证ELF魔数 Kernel->>Kernel: 解析程序头表 Kernel->>Memory: 创建虚拟地址空间 loop 每个PT_LOAD段 Kernel->>Memory: 建立文件-内存映射 end alt 存在PT_INTERP Kernel->>Linker: 加载动态链接器 Linker->>Linker: 装载依赖库 Linker->>Memory: 重定位GOT/PLT end Kernel->>User: 返回用户态执行 ``` ## 六、关键数据结构 Linux装载过程核心数据结构: ```c struct linux_binfmt { // 可执行文件格式处理 struct list_head lh; int (*load_binary)(struct linux_binprm *); }; struct elf_phdr { // 程序头 Elf32_Word p_type; Elf32_Off p_offset; Elf32_Addr p_vaddr; Elf32_Word p_filesz; Elf32_Word p_memsz; Elf32_Word p_flags; }; struct mm_struct { // 内存描述符 pgd_t * pgd; // 页全局目录 unsigned long start_code, end_code; // 代码段范围 unsigned long start_data, end_data; // 数据段范围 unsigned long start_brk, brk; // 堆范围 unsigned long start_stack; // 栈起始 }; ``` ## 七、特殊场景处理 1. **位置无关代码(PIC)** - 共享库使用`-fPIC`编译 - 通过GOT实现地址无关访问 2. **ASLR(地址空间随机化)** ```c // 内核随机化加载地址 if (current->flags & PF_RANDOMIZE) load_bias += arch_mmap_rnd(); ``` 3. **大页支持** 使用`MAP_HUGETLB`标志映射2MB/1GB大页 ## 总结 可执行文件装载过程是操作系统核心功能: 1. 通过**内存映射**技术高效加载代码/数据段 2. **动态链接**实现库共享和延迟绑定 3. **虚拟地址空间**隔离保障进程安全 4. **写时复制**优化内存使用效率 理解装载机制对性能优化、安全加固(如ASLR)和容器技术(namespace隔离)有重要意义[^1][^4]。 --- ## 相关问题 1. 动态链接器(ld-linux)的具体工作流程是怎样的? 2. 如何查看进程的内存映射布局(使用/proc/<pid>/maps)? 3. 全局偏移表(GOT)和过程链接表(PLT)如何协同实现延迟绑定? 4. 位置无关代码(PIC)的实现原理及其在安全中的应用? 5. 容器技术(如Docker)如何利用命名空间修改装载行为? [^1]: ELF文件装载的核心函数是`load_elf_binary`,负责解析文件头和程序头表 [^2]: 可执行文件装载过程包括文件验证、地址空间创建和段映射 [^3]: ELF文件格式通过程序头表描述内存段信息 [^4]: 可执行文件通过匿名虚拟内存区域(VMA)映射到进程地址空间
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值