Linux内核中ELF可执行文件的装载/l…

本文详细介绍了Linux系统中ELF文件的加载过程,包括如何解析ELF文件头、程序头表,以及如何根据这些信息计算出进程空间中各区域的位置。重点分析了load_elf_binary()函数的工作流程。

在Linux系统中运行一个可执行的ELF文件时,内核首先需要识别这个文件,然后解析并装载它以构建进程的内存空间,最后切换到新的进程来运行。

在fs/binfmt_elf.c中定义了函数load_elf_binary()和load_elf_library()分别用于装载和解析ELF格式的可执行文件和动态连接库。下面来研究一下在load_elf_binary()中做了哪些事情,一个新的进程的内存空间是布局是怎样计算出来的。

下图是一个典型的Linux程序的进程空间模型。一个进程的虚拟空间包含以下区域(从地址由底到高):

  1. 代码区(text):存放可执行的代码;
  2. 数据区(data):存放经过初始化的数据;
  3. 数据区(bss):bss区存放的也是数据,不过在这里的数据是没有初始它的,而且是全局的。即那些在代码里面声明了但是没有赋初始值全局变量。未初始化全局变量在ELF文件中不占有存储空间,但是在内存空间里必须占有一席之地。
  4. 堆(heap):进程运行期间动态分配内存的区域,当进程需要分配更多的内存时,它将向上扩展;
  5. 栈(stack):进程的栈,它的扩展方向与堆刚好相反,当有新的函数调用时,它将向下扩展。

ELF文件装载的最终目的有两个:

  1. 确定各个区域的边界:text区的起始和终止位置,data区的起始和终止位置,bss区的起始和终止位置,heap和stack的起始位置(它们的终止位置是动态变化的)。
  2. 把text区和data区的内容做mmap映射:ELF文件的内容不会被真地拷贝到内存,只有当真正需要的时候,内核才会通过page fault的形式把文件内存复制到内存中去。

下面来一步步分析load_elf_binary()函数的代码。
load_elf_binary()函数有两个参数:



[c]
static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs)
[/c]

其中第一个参数bprm含有很多装载二进制文件所需的信息,结构如下:



[c]
struct linux_binprm {
char buf[BINPRM_BUF_SIZE];
#ifdef CONFIG_MMU
struct vm_area_struct *vma;
unsigned long vma_pages;
#else
# define MAX_ARG_PAGES 32
struct page *page[MAX_ARG_PAGES];
#endif
struct mm_struct *mm;
unsigned long p;
unsigned int
cred_prepared:1,
cap_effective:1;
#ifdef __alpha__
unsigned int taso:1;
#endif
unsigned int recursion_depth;
struct file * file;
struct cred *cred;
int unsafe;
unsigned int per_clear;
int argc, envc;
const char * filename;
const char * interp;
unsigned interp_flags;
unsigned interp_data;
unsigned long loader, exec;
};
[/c]

此结构的第一个成员buf中含有ELF文件开关处的一段内容,共128个字节,这段内容包含了ELF文件头和程序头表,这些信息足够用于构造进程的虚拟空间结构。
接下来,首先读入ELF文件头,其数据结构定义为:



[c]
typedef struct elf32_hdr{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
[/c]

然后对文件头做一些必要的检查,比如文件类型(magic number)、体系结构等:



[c]
if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
goto out;

if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
goto out;
if (!elf_check_arch(&loc->elf_ex))
goto out;
if (!bprm->file->f_op || !bprm->file->f_op->mmap)
goto out;
[/c]

接下来需要读入程序头表(段表),ELF文件头elf32_hdr结构中的e_phoff即是程序头表在文件中的位置偏移量,e_phnum是程序头表中表项的个数,也就是文件中段的数目。



[c]
size = loc->elf_ex.e_phnum * sizeof(struct elf_phdr);
retval = -ENOMEM;
elf_phdata = kmalloc(size, GFP_KERNEL);
retval = kernel_read(bprm->file, loc->elf_ex.e_phoff,
(char *)elf_phdata, size);
[/c]

读入的程序头表存放在elf_phdata中。
在逐个解析段表项之前,先初始它各个段的起始位置和终止位置:



[c]
elf_ppnt = elf_phdata;
elf_bss = 0;
elf_brk = 0;

start_code = ~0UL;
end_code = 0;
start_data = 0;
end_data = 0;
[/c]

在开始解析之前,还需要查看ELF文件中是否指明了具体的解析器,如果是的话,需要装入这个解析器程序,用它来解析ELF文件:



[c]
for (i = 0; i < loc->elf_ex.e_phnum; i ) {
if (elf_ppnt->p_type == PT_INTERP) {
[/c]

本文不考虑这种情况,所以略过外部解析器相关的代码。
下面开始一段非常重要的代码,这里开始计算进程空间各个区的位置:



[c]
for(i = 0, elf_ppnt = elf_phdata;
i < loc->elf_ex.e_phnum; i , elf_ppnt ) {
int elf_prot = 0, elf_flags;
unsigned long k, vaddr;

if (elf_ppnt->p_type != PT_LOAD)
continue;
[/c]

这个循环将遍历所有段表项,并且只处理那些可装载的段。



[c]
if (elf_ppnt->p_flags & PF_R)
elf_prot |= PROT_READ;
if (elf_ppnt->p_flags & PF_W)
elf_prot |= PROT_WRITE;
if (elf_ppnt->p_flags & PF_X)
elf_prot |= PROT_EXEC;

elf_flags = MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE;

vaddr = elf_ppnt->p_vaddr;
......
error = elf_map(bprm->file, load_bias vaddr, elf_ppnt,
elf_prot, elf_flags, 0);
[/c]

上面的代码,首先读取了这一段的读写权限,然后又读入了这一段在进程空间中的地址p_vaddr。接着,把此段的内容做了elf_map映射,即从文件中映射到了进程空间中,但要注意的是,映射到进程空间中的地址不只是vaddr,这里还加了一个偏移量load_bias,这个偏移量将被加在所有段的映射上,只所以设计这样一个偏移量,是为满足段的位置随机化的需要。如果没有打开随机化这一功能的话,load_bias的值将保持为0。

下面的一段代码用于计算代码区和数据区的开始位置:



[c]
             k = elf_ppnt->p_vaddr;
if (k < start_code)
start_code = k;
if (start_data < k)
start_data = k;

[/c]

由于代码区在进程空间的最前面,如果当前映射的这一段的开始位置还位于当前的代码区之前,那么代码区的开始位置应该还要向前移,至少移到这一段的位置上。

而如果当前映射的这一段的开始位置还位于当前的数据区之后,那么数据区的开始位置还应该向后移,至少移到这一段的位置上。这是因为数据区在可装载的段的最后,不应该有哪个段的位置比较数据区还靠后。

接下来的代码是用于计算几个区的结束位置:



[c]
              k = elf_ppnt->p_vaddr   elf_ppnt->p_filesz;

if (k > elf_bss)
elf_bss = k;
if ((elf_ppnt->p_flags & PF_X) && end_code < k)
end_code = k;
if (end_data < k)
end_data = k;
k = elf_ppnt->p_vaddr elf_ppnt->p_memsz;
if (k > elf_brk)
elf_brk = k;

[/c]

elf_bss变量记录的是BSS区的开始位置。BSS区排在所有可加载段的后面,即它的开始处也就是最后一个可加载段的结尾处。所以总是把当前加载段的结尾与它相比,如果当前加载段的结尾比较靠后的话,则还需要把BSS区往后推。

elf_brk变量记录的是堆(heap)的上边界,现在进程还没有运行起来,没有从堆上面申请内存,所以堆的大小是0,堆的上边界与下边界重合,而堆的位置还在BSS之后,即它的开始位置应该是BSS区的结构位置。

一般情况下,一个程序头的p_memsz与p_filesz如果不一样大小的话,其差值应是未初始化全局变量的大小,这段空间应归入BSS区。上面代码中两个k值的计算正是考虑到这一点,所以第二次k值(BRK)的计算是把BSS区大小也计算在内的。

最后,为所有计算出的区起止位置加上随机化偏移量:



[c]
 loc->elf_ex.e_entry  = load_bias;
elf_bss = load_bias;
elf_brk = load_bias;
start_code = load_bias;
end_code = load_bias;
start_data = load_bias;
end_data = load_bias;

[/c]

进程空间中各区域起止位置的计算到此完成。

<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、付费专栏及课程。

余额充值