6.3.6. ELF

6.3.6.1. 加载

从编译/链接和运行的角度看,应用程序和库程序的链接有两种方式。 一种是固定的、静态的链接,在编译时将要用到的库函数的代码从代码库中抽取出来,链接进应用软件中。另一种是动态链接,在编译阶段并不完成跟库函数的链接,到程序时才把链接库的映像装入用户空间并加以定位。

其中ELF的载入在Linux内核中完成,动态链接的实现在用户空间中由 ld-linux.so 来完成,解释器的启动由内核负责。

6.3.6.2. execve

在用户层面,shell进行会调用 fork() 系统调用创建一个新进程,新进程调用 execve() 系统调用执行指定的ELF文件。

在内核中, execve 系统调用的相应入口是 sys_execve ,在执行 sys_execve 之后,内核会调用 do_execve / search_binary_handle / load_elf_binary 等函数来完成加载。 do_execve 相关代码如下:

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

static int do_execve_common(struct filename *filename,
                struct user_arg_ptr argv,
                struct user_arg_ptr envp)
{
    // 选择最小负载的CPU,以执行新程序
    sched_exec();

    // 填充 linux_binprm 结构体
    retval = prepare_binprm(bprm);

    // 拷贝文件名、命令行参数、环境变量
    retval = copy_strings_kernel(1, &bprm->filename, bprm);
    retval = copy_strings(bprm->envc, envp, bprm);
    retval = copy_strings(bprm->argc, argv, bprm);

    // 调用 search_binary_handler 扫描formats链表,根据不同的文本格式,选择不同的load函数
    retval = exec_binprm(bprm);
}

其中结构体 linux_binprm 部分定义是:

struct linux_binprm {
    char buf[BINPRM_BUF_SIZE];
#ifdef CONFIG_MMU
    struct vm_area_struct *vma;
    unsigned long vma_pages;
#else
    ...
    unsigned interp_flags;
    unsigned interp_data;
    unsigned long loader, exec;
};

6.3.6.3. 注册机制

Linux支持不同格式的可执行程序,这些程序用 linux_binfmt 来描述,其定义在 include/linux/binfmts.h 中。

/*
 * This structure defines the functions that are used to load the binary formats that linux accepts.
 */
struct linux_binfmt {
    struct list_head lh;
    struct module *module;
    int (*load_binary)(struct linux_binprm *);
    int (*load_shlib)(struct file *);
    int (*core_dump)(struct coredump_params *cprm);
    unsigned long min_coredump; /* minimal dump size */
} __randomize_layout;

所有的 linux_binfmt 对象都处于一个链表中,第一个元素的地址存放在 formats 变量中, 可以通过调用 register_binfmt()unregister_binfmt() 函数在链表中插入和删除元素。在系统启动期间,为每个编译进内核的可执行格式都执行 register_binfmt() 函数。

当执行程序的时候,内核打开目标映像文件,并从目标文件的头部读入若干字节,并调用 search_binary_handler 遍历所有注册的 linux_binfmt 对象,对其调用 load_binary 方法来尝试加载,直到加载成功为止。

search_binary_handler 的部分代码如下:

int search_binary_handler(struct linux_binprm *bprm)
{
    // 遍历formats链表
    list_for_each_entry(fmt, &formats, lh) {
        if (!try_module_get(fmt->module))
            continue;
        read_unlock(&binfmt_lock);
        bprm->recursion_depth++;

        // 应用每种格式的load_binary方法
        retval = fmt->load_binary(bprm);
        read_lock(&binfmt_lock);
        put_binfmt(fmt);
        bprm->recursion_depth--;
        // ...
    }
    return retval;
}

6.3.6.4. Load ELF

在ELF文件格式中,处理函数是 load_elf_binary 函数,流程如下:

  • 填充并且检查目标程序ELF头部
    • 是否 \x7fELF 开头

    • 映像的类型是否为 ET_EXEC

  • load_elf_phdrs 加载目标程序的程序头表
    • 执行程序必须至少有一个段

    • 所有段大小之和不能超过64k

  • 如果需要动态链接, 则寻找和处理解释器段
    • “解释器” 段的类型为 PT_INTERP ,可通过 readelf -l 查看

    • “解释器” 段实际上是一个字符串,即解释器的文件位置

    • 通常为 /lib/ld-linux.so.2 / /lib64/ld-linux-x86-64.so.2

  • 检查并读取解释器的程序表头

  • 装入目标程序的段 segment

  • 填写程序的入口地址
    • 如果需要装入解释器
      • 通过 load_elf_interp 装入映像

      • 设置用户空间的入口地址为 load_elf_interp() 的返回值

      • load_elf_interp() 的返回值为解释器映像的入口地址

    • 如果不需要装入解释器
      • 入口地址设置为目标映像本身的入口地址

  • create_elf_tables 填写目标文件的参数环境变量等必要信息
    • 准备 argc envc 等变量

  • start_thread 宏修改 eip / esp ,准备进入新的程序入口

load_elf_binary 的部分代码如下:

static int load_elf_binary(struct linux_binprm *bprm)
{
    ...

    /* Now we do a little grungy work by mmapping the ELF image into
       the correct location in memory. */
    for(i = 0, elf_ppnt = elf_phdata;
        i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
        int elf_prot = 0, elf_flags, elf_fixed = MAP_FIXED_NOREPLACE;
        unsigned long k, vaddr;
        unsigned long total_size = 0;

        if (elf_ppnt->p_type != PT_LOAD)
            continue;
         ...
}