可执行程序的装载

本文详细介绍了从C源代码到可执行文件的过程,解释了ELF文件格式及其组成部分,探讨了Linux系统中execve系统调用的工作原理,以及它是如何加载并执行新的程序。

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

杨金龙 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

实验环境:https://www.shiyanlou.com/courses/195

基础知识

先以 helloworld.c 程序为例,搞清楚可执行文件是如何生成的:

#include <stdio.h>
int main(void)
{
    printf("hello, world!\n");
    return 0;
}

1.预处理,处理代码中的宏定义和 include 文件,并做语法检查

gcc -E helloworld.c -o helloworld.cpp

2.编译,生成汇编代码

gcc -S helloworld.cpp -o helloworld.s

3.汇编,生成汇编代码

gcc -c helloworld.s -o helloworld.o

4.链接,生成可执行文件

gcc helloworld.o -o helloworld

具体过程可以用下面的图片表示,各种文件格式之间的关系如下:

这里写图片描述

参考资料:https://wenku.baidu.com/view/a04ab40b581b6bd97f19eaa6.html

ELF 文件格式

ELF 格式:可执行和可链接格式 (Executable and Linkable Format) 是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储的标准文件格式。它又包括三种文件格式:

  1. 可重定位文件,如:.o 文件,包含代码和数据,可以被链接成可执行文件或共享目标文件,静态链接库属于这类。
  2. 可执行文件,如:/bin/bash 文件,包含可直接执行的程序,没有扩展名。
  3. 共享目标文件,如:.so 文件,包含代码和数据,可以跟其他可重定位文件和共享目标文件链接产生新的目标文件,也可以跟可执行文件结合作为进程映像的一部分。

ELF 文件由 ELF header 和文件数据组成,文件数据包括:

  • Program header table, 程序头:描述段信息

  • .text, 代码段:保存编译后得到的指令数据

  • .data, 数据段:保存已经初始化的全局静态变量和局部静态变量

  • Section header table, 节头表:链接与重定位需要的数据

这里写图片描述

这里写图片描述

链接

  链接,是收集和组织程序所需的不同代码和数据的过程,以便程序能被装入内存并被执行。一般分为两步:1.空间与地址分配,2.符号解析与重定位。一般有两种类型,一是静态链接,二是动态链接。

  • 空间与地址分配
      扫描所有的输入目标文件,获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步中,连接器将能获得所有输入如目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度和位置,并建立映射关系。
  • 符号解析与重定位
      使用上面一步中收集的所有信息,读取输入文件中的段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。事实上,这一步是链接过程的核心,特别是重定位过程。

      使用静态链接的好处是,依赖的动态链接库较少(这句话有点绕),对动态链接库的版本更新不会很敏感,具有较好的兼容性;不好地方主要是生成的程序比较大,占用资源多。使用动态链接的好处是生成的程序小,占用资源少。动态链接分为可执行程序装载时动态链接和运行时动态链接。

      当用户启动一个应用程序时,它们就会调用一个可执行和链接格式映像。Linux 中 ELF 支持两种类型的库:静态库包含在编译时静态绑定到一个程序的函数。动态库则是在加载应用程序时被加载的,而且它与应用程序是在运行时绑定的。

代码分析

sys_execve内部会解析可执行文件格式。代码在内核中/linux-3.18.6/fs/exec.c中。sys_execve调用顺序:
do_execve -> do_execve_common -> exec_binprm

int do_execve(struct filename *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
struct user_arg_ptr argv = { .ptr.native = __argv };
struct user_arg_ptr envp = { .ptr.native = __envp };
return do_execve_common(filename, argv, envp);
}

/*
* sys_execve() executes a new program.
*/
static int do_execve_common(struct filename *filename,
   struct user_arg_ptr argv,
   struct user_arg_ptr envp)
{
   struct linux_binprm *bprm;
   struct file *file;
   struct files_struct *displaced;
   int retval;
if (IS_ERR(filename))
   return PTR_ERR(filename);
/*
* We move the actual failure in case of RLIMIT_NPROC excess from
* set*uid() to execve() because too many poorly written programs
* don't check setuid() return code.  Here we additionally recheck
* whether NPROC limit is still exceeded.
*/
   if ((current->flags & PF_NPROC_EXCEEDED) &&
   atomic_read(&current_user()->processes) > rlimit(RLIMIT_NPROC)) {
   retval = -EAGAIN;
   goto out_ret;
   }
/* We're below the limit (still or again), so we don't want to make
* further execve() calls fail. */
   current->flags &= ~PF_NPROC_EXCEEDED;
retval = unshare_files(&displaced);
   if (retval)
   goto out_ret;
retval = -ENOMEM;
   bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
   if (!bprm)
   goto out_files;
retval = prepare_bprm_creds(bprm);
   if (retval)
   goto out_free;
check_unsafe_exec(bprm);
   current->in_execve = 1;
file = do_open_exec(filename);
   retval = PTR_ERR(file);
   if (IS_ERR(file))
   goto out_unmark;
sched_exec();
bprm->file = file;
   bprm->filename = bprm->interp = filename->name;
retval = bprm_mm_init(bprm);
   if (retval)
   goto out_unmark;
bprm->argc = count(argv, MAX_ARG_STRINGS);
   if ((retval = bprm->argc) < 0)
   goto out;
bprm->envc = count(envp, MAX_ARG_STRINGS);
   if ((retval = bprm->envc) < 0)
   goto out;
retval = prepare_binprm(bprm);
   if (retval < 0)
   goto out;
retval = copy_strings_kernel(1, &bprm->filename, bprm);
   if (retval < 0)
   goto out;
bprm->exec = bprm->p;
   retval = copy_strings(bprm->envc, envp, bprm);
   if (retval < 0)
   goto out;
retval = copy_strings(bprm->argc, argv, bprm);
   if (retval < 0)
   goto out;
retval = exec_binprm(bprm);
   if (retval < 0)
   goto out;
/* execve succeeded */
   current->fs->in_exec = 0;
   current->in_execve = 0;
   acct_update_integrals(current);
   task_numa_free(current);
   free_bprm(bprm);
   putname(filename);
   if (displaced)
   put_files_struct(displaced);
   return retval;
out:
   if (bprm->mm) {
   acct_arg_size(bprm, 0);
   mmput(bprm->mm);
   }
out_unmark:
   current->fs->in_exec = 0;
   current->in_execve = 0;
out_free:
   free_bprm(bprm);
out_files:
   if (displaced)
   reset_files_struct(displaced);
out_ret:
   putname(filename);
   return retval;
}

static int exec_binprm(struct linux_binprm *bprm)
{
   pid_t old_pid, old_vpid;
   int ret;
/* Need to fetch pid before load_binary changes it */
   old_pid = current->pid;
   rcu_read_lock();
   old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));
   rcu_read_unlock();
ret = search_binary_handler(bprm);
   if (ret >= 0) {
   audit_bprm(bprm);
   trace_sched_process_exec(current, old_pid, bprm);
   ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
   proc_exec_connector(current);
   }

search_binary_handler寻找符合文件格式对应的解析模板,如下:(对于给定的文件名,根据文件头部信息寻找对应的文件格式处理模块)

/*
* cycle the list of binary formats handler, until one recognizes the image
*/
int search_binary_handler(struct linux_binprm *bprm)
{
   bool need_retry = IS_ENABLED(CONFIG_MODULES);
   struct linux_binfmt *fmt;
   int retval;
/* This allows 4 levels of binfmt rewrites before failing hard. */
   if (bprm->recursion_depth > 5)
   return -ELOOP;
retval = security_bprm_check(bprm);
   if (retval)
   return retval;
retval = -ENOENT;
retry:
   read_lock(&binfmt_lock);
   list_for_each_entry(fmt, &formats, lh) {
   if (!try_module_get(fmt->module))
   continue;
   read_unlock(&binfmt_lock);
   bprm->recursion_depth++;
   retval = fmt->load_binary(bprm);
   read_lock(&binfmt_lock);
   put_binfmt(fmt);
   bprm->recursion_depth--;
   if (retval < 0 && !bprm->mm) {
   /* we got to flush_old_exec() and failed after it */
   read_unlock(&binfmt_lock);
   force_sigsegv(SIGSEGV, current);
   return retval;
   }
   if (retval != -ENOEXEC || !bprm->file) {
   read_unlock(&binfmt_lock);
   return retval;
   }
   }
   read_unlock(&binfmt_lock);
if (need_retry) {
   if (printable(bprm->buf[0]) && printable(bprm->buf[1]) &&
   printable(bprm->buf[2]) && printable(bprm->buf[3]))
   return retval;
   if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) < 0)
   return retval;
   need_retry = false;
   goto retry;
   }
return retval;
}
EXPORT_SYMBOL(search_binary_handler);

对于ELF格式的可执行文件fmt->load_binary(bprm); 执行的应该是load_elf_binary其内部是和ELF文件格式解析的部分需要和ELF文件格式标准结合起来阅读。load_elf_binary在/linux-3.18.6/fs/binfmt_elf.c文件中,代码太长,在此不贴出,这个函数主要作用就是在函数的最后根据链接种类启动一个起点为新的可执行程序的入口的进程。其中的关键代码如下:

...
if (elf_interpreter) {   //动态链接
    unsigned long interp_map_addr = 0;

    elf_entry = load_elf_interp(&loc->interp_elf_ex,
                    interpreter,
                    &interp_map_addr,
                    load_bias);
    if (!IS_ERR((void *)elf_entry)) {
        /*
         * load_elf_interp() returns relocation
         * adjustment
         */
        interp_load_addr = elf_entry;
        elf_entry += loc->interp_elf_ex.e_entry;
    }
    if (BAD_ADDR(elf_entry)) {
        retval = IS_ERR((void *)elf_entry) ?
                (int)elf_entry : -EINVAL;
        goto out_free_dentry;
    }
    reloc_func_desc = interp_load_addr;

    allow_write_access(interpreter);
    fput(interpreter);
    kfree(elf_interpreter);
} else {   //静态链接
    elf_entry = loc->elf_ex.e_entry;
    if (BAD_ADDR(elf_entry)) {
        retval = -EINVAL;
        goto out_free_dentry;
    }
}
...
start_thread(regs, elf_entry, bprm->p);

ELF文件格式结构体:

static struct linux_binfmt elf_format = {
    .module     = THIS_MODULE,
    .load_binary    = load_elf_binary,
    .load_shlib = load_elf_library,
    .core_dump  = elf_core_dump,
    .min_coredump   = ELF_EXEC_PAGESIZE,
};

load_elf_binary 的最后调用 start_thread 函数。修改 int 0x80 压入内核堆栈的 EIP,当 load_elf_binary 执行完毕,返回至 do_execve 再返回至 sys_execve 时,系统调用的返回地址,即 EIP 寄存器,已经被改写成了被装载的 ELF 程序的入口地址了。

追踪过程

配置实验环境https://www.shiyanlou.com/courses/running

rm menu -rf //删除menu

git clone https://github.com/mengning/menu.git //下载克隆新的menu

cd menu //切换到menu目录下

mv test_exce.c test.c //把test_exce.c 改成 test.c ,这里是应为Makefile里面用的是test.c

vi test.c //查看一下test.c的内容

vi makefile //查看一下Makefile的内容

make rootfs //启动系统

这里写图片描述
启动gdb

gdb -tui  //启动gdb
file linux-3.18.6/vmlinux //读取符号表
target remote:1234  //设置端口

这里写图片描述

设置断点

这里写图片描述
实验截图
这里写图片描述

总结

1、可执行程序的产生:

C语言代码–>编译器预处理–>编译成汇编代码–>汇编器编译成目标代码–>链接成可执行文件,再由操作系统加载到内存中执行。

2、ELF格式中主要有3种可执行文件:可重定位文件.o,可执行文件,共享目标文件。

3、ELF可执行文件会被默认映射到0x8048000这个地址。

4、命令行参数和环境变量是如何进入新程序的堆栈的?

Shell程序–>execve–>sys_execve,然后在初始化新程序堆栈时拷贝进去。

先函数调用参数传递,再系统调用参数传递。

5、当前程序执行到execve系统调用时陷入内核态,在内核中用execve加载可执行文件,把当前进程的可执行文件覆盖掉,execve系统调用返回到新的可执行程序的起点。

6、动态链接库的装载过程是一个图的遍历过程,

ELF格式中的.interp和.dynamic需要依赖动态链接器来解析,entry返回到用户态时不是返回到可执行程序规定的起点,返回到动态链接器的程序入口。

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

余额充值