Linux内核启动(init)与程序执行(execve)深度解析:从kernel_init到load_elf_binary

引言:Linux系统启动的"最后一公里"

当我们按下电源键,BIOS进行自检,引导加载程序(GRUB)将内核映像载入内存,然后内核开始执行。从start_kernelrest_init,内核完成了大量的初始化工作:初始化中断、内存管理、调度器、文件系统......然而,直到用户空间的第一个进程 ------ init 进程被启动之前,系统仍然处于"内核态独占"的状态。用户空间的第一个进程的诞生,标志着操作系统真正走向可用。

那么,内核是如何从内核线程转变为执行用户空间/sbin/init的呢?execve系统调用在这个过程里扮演了什么角色?ELF二进制文件又是如何被识别、加载并最终执行的?本文将以Linux内核源码(以当前主流版本v6.x为例)中的关键函数为线索,逐层深入解析从kernel_initload_elf_binary的完整流程。

我们将重点剖析以下几个核心环节:

  1. kernel_init :作为init内核线程的主体函数,负责释放初始化内存、尝试启动用户态init

  2. run_init_process / try_to_run_init_process :封装内核态的execve调用。

  3. kernel_execve:内核空间执行用户程序的总入口。

  4. bprm_execveexec_binprmsearch_binary_handler:二进制格式处理器的查找与执行。

  5. load_elf_binary:ELF加载器,承担将ELF可执行文件映射到进程地址空间的核心工作。

通过本文,你将不仅了解init进程的启动细节,还将掌握execve在内核中的实现脉络,以及ELF格式在Linux内核中是如何被解析和处理的。


一、kernel_init:init线程的诞生

Linux内核启动的最后阶段,rest_init()函数会创建两个内核线程:init(PID=1)和kthreadd(PID=2)。其中init线程的主体函数正是kernel_init。该函数定义在init/main.c中,我们看到的源码片段正是其核心逻辑。

c

复制代码
static int __ref kernel_init(void *unused)
{
    int ret;

    /* Wait until kthreadd is all set-up. */
    wait_for_completion(&kthreadd_done);

    kernel_init_freeable();
    /* need to finish all async __init code before freeing the memory */
    async_synchronize_full();

    system_state = SYSTEM_FREEING_INITMEM;
    kprobe_free_init_mem();
    ftrace_free_init_mem();
    kgdb_free_init_mem();
    exit_boot_config();
    free_initmem();
    mark_readonly();

    /*
     * Kernel mappings are now finalized - update the userspace page-table
     * to finalize PTI.
     */
    pti_finalize();

    system_state = SYSTEM_RUNNING;
    numa_default_policy();

    rcu_end_inkernel_boot();

    do_sysctl_args();

    if (ramdisk_execute_command) {
        ret = run_init_process(ramdisk_execute_command);
        if (!ret)
            return 0;
        pr_err("Failed to execute %s (error %d)\n",
               ramdisk_execute_command, ret);
    }

    /* ... fallback to other init paths ... */
}

1.1 等待kthreadd准备就绪

wait_for_completion(&kthreadd_done); 确保内核的kthreadd守护线程(负责创建其他内核线程)已经完全初始化。这是一个典型的"完成量"(completion)同步机制,保证后续可能依赖kthreadd的功能不会在它未就绪时被调用。

1.2 kernel_init_freeable():可抢占的初始化

kernel_init_freeable() 是一个简化的初始化阶段,它释放了大部分"可抢占"的资源,例如:

  • 启动init进程前挂载根文件系统(prepare_namespace())。

  • 初始化SMP、ACPI等。

  • 最终会调用do_basic_setup()来完成各种驱动和子系统的初始化。

该函数本身可能调用schedule(),因此它被标记为__init(代码段在初始化后会被释放),并且可以被抢占。这就与kernel_init的其他部分区分开来。

1.3 异步__init代码同步与内存释放

async_synchronize_full(); 会等待所有异步初始化的__init函数执行完毕。一些驱动或子系统使用async_schedule()进行并发初始化,这里必须等待它们完成后,才能安全地释放__init所占用的内存。

接着,系统状态被设为SYSTEM_FREEING_INITMEM,随后依次调用:

  • kprobe_free_init_mem()ftrace_free_init_mem()kgdb_free_init_mem():释放特定子系统(kprobe、ftrace、kgdb)的初始化代码和数据段。

  • exit_boot_config():处理引导配置(bootconfig)的清理。

  • free_initmem():释放所有__init__initdata等标记的内存区域。这些内存页一旦释放,就可以被系统重新利用。

  • mark_readonly():将内核只读数据段标记为只读,保护内核关键数据不被修改。

1.4 PTI最终化与系统运行状态

pti_finalize() 完成**页表隔离(Page Table Isolation,PTI)**的最终设置。PTI是缓解Meltdown漏洞的一种机制,这里确保用户态页表不再包含内核敏感信息。

system_state = SYSTEM_RUNNING; 标志着内核已经完全过渡到正常运行状态。在这之后,内核大部分子系统都认为系统已经"running"。

numa_default_policy(); 设置NUMA内存访问默认策略为"本地分配优先"。

rcu_end_inkernel_boot(); 通知RCU子系统内核引导阶段结束,允许RCU进入更高效的运作模式。

do_sysctl_args(); 解析内核命令行中sysctl相关的参数(如sysctl.vm.swappiness=...),并应用到ProcFS的/proc/sys目录下。

1.5 尝试启动init进程:多个途径

ramdisk_execute_command 变量存储了通过命令行参数rdinit=指定的init程序。大多数发行版不显式设置,则默认为/init(位于initrd中)。如果指定且执行成功,则kernel_init返回0,init线程退出(实际上由于run_init_process内部调用了kernel_execve,当前内核线程会蜕变成用户态进程,不再返回)。如果失败,会输出错误日志。

接下来,如果execute_command存在(由init=命令行参数指定,如init=/sbin/myinit),则尝试执行它。如果也失败,则panic ------ 因为这是用户显式要求的init程序,失败意味着系统无法继续。

如果没指定init=,内核会尝试一系列默认路径:

  • CONFIG_DEFAULT_INIT(编译时配置,通常为空)

  • /sbin/init

  • /etc/init

  • /bin/init

  • /bin/sh

所有这些尝试都通过run_init_processtry_to_run_init_process完成。

最后如果全部失败,调用panic,系统挂起。

关键点run_init_process内部调用的是kernel_execve,这是一个内核态版本的execve。它不会返回(除非出错)。因此,一旦成功,PID=1的线程就会变成用户空间的init进程,内核的init线程就"消失"了,取而代之的是用户空间进程。


二、run_init_processtry_to_run_init_process:封装内核execve

这两个函数非常简单,但它们的作用是设置好argvenvp然后调用kernel_execve

c

复制代码
static int run_init_process(const char *init_filename)
{
    const char *const *p;

    argv_init[0] = init_filename;
    pr_info("Run %s as init process\n", init_filename);
    pr_debug("  with arguments:\n");
    for (p = argv_init; *p; p++)
        pr_debug("    %s\n", *p);
    pr_debug("  with environment:\n");
    for (p = envp_init; *p; p++)
        pr_debug("    %s\n", *p);
    return kernel_execve(init_filename, argv_init, envp_init);
}

argv_init是一个全局的char *数组,通常只包含第一个元素:init文件的路径,并以NULL结尾。envp_init是环境变量数组,包含了HOME=/TERM=linux等基本环境。在kernel_init_freeable()阶段,这些环境变量被初始化。

注意到try_to_run_init_process仅仅是对run_init_process的一层封装,区别在于当返回-ENOENT(文件不存在)时不会打印错误信息,只有文件存在但无法执行时才报警。这是为了避免在尝试多个默认路径时输出大量误导性的错误。


三、kernel_execve:内核空间的execve

kernel_execve是用户态execve系统调用的内核态版本。它直接接收内核空间的文件名、argv、envp(均位于内核内存),构造struct linux_binprm,然后调用底层的加载逻辑。

代码片段如下:

c

复制代码
int kernel_execve(const char *kernel_filename,
                  const char *const *argv, const char *const *envp)
{
    struct filename *filename;
    struct linux_binprm *bprm;
    int fd = AT_FDCWD;
    int retval;

    /* It is non-sense for kernel threads to call execve */
    if (WARN_ON_ONCE(current->flags & PF_KTHREAD))
        return -EINVAL;

    filename = getname_kernel(kernel_filename);
    if (IS_ERR(filename))
        return PTR_ERR(filename);

    bprm = alloc_bprm(fd, filename, 0);
    if (IS_ERR(bprm)) {
        retval = PTR_ERR(bprm);
        goto out_ret;
    }

    retval = count_strings_kernel(argv);
    if (WARN_ON_ONCE(retval == 0))
        retval = -EINVAL;
    if (retval < 0)
        goto out_free;
    bprm->argc = retval;

    retval = count_strings_kernel(envp);
    if (retval < 0)
        goto out_free;
    bprm->envc = retval;

    retval = bprm_stack_limits(bprm);
    if (retval < 0)
        goto out_free;

    retval = copy_string_kernel(bprm->filename, bprm);
    if (retval < 0)
        goto out_free;
    bprm->exec = bprm->p;

    retval = copy_strings_kernel(bprm->envc, envp, bprm);
    if (retval < 0)
        goto out_free;

    retval = copy_strings_kernel(bprm->argc, argv, bprm);
    if (retval < 0)
        goto out_free;

    retval = bprm_execve(bprm);
out_free:
    free_bprm(bprm);
out_ret:
    putname(filename);
    return retval;
}

3.1 禁止内核线程调用execve

WARN_ON_ONCE(current->flags & PF_KTHREAD)确保内核线程不会执行execve。因为内核线程没有用户态地址空间,执行用户程序没有意义。而我们的init线程虽然名义上是内核线程,但它被特殊处理------在kernel_init中,它还未成为真正的"内核线程"?实际上init线程是由kthread_create创建的,PF_KTHREAD标志是存在的。但仔细观察:kernel_init在调用run_init_process之前,已经执行了大量初始化,且current->flags包含PF_KTHREAD。内核为何允许此处执行execve?这是因为在execve过程中,如果发现当前是内核线程,会调用unshare_files等方式转换状态,并且最终会清除PF_KTHREAD标志。但代码片段中的WARN_ON_ONCE提示"non-sense",实际上这个检查是合理的,因为正常情况下内核线程不应当调用execve。不过对于init线程,它是一个特例,实际上是先调用kernel_execve,然后在bprm_execveprepare_bprm_creds等函数中会处理这种情况。实际上,在较新的内核中,该检查确实存在 ,但init会被特殊处理吗?我们继续看bprm_execve内部的prepare_bprm_credsbegin_new_exec,它们会调用unshare_filesflush_old_exec,并最终清除PF_KTHREAD标志。但无论如何,kernel_execve一开始的检查会触发警告,实际上我看到的内核源码中该检查是存在的 ,但kernel_init确实是一个内核线程,却成功调用了kernel_execve。可能有如下原因:kernel_init函数指针被__ref修饰,表明它可能引用被释放的内存,但更重要的是,在调用kernel_execve时,当前进程已经被标记为"可执行的",或者在较早的版本中没有这个检查。查阅最新内核,kernel_execve已经改为直接调用do_execve,而没有这个警告。我们分析的代码片段可能是历史版本或特定架构的修改。但为了尊重提供的源码,我们保留该描述:这是一个防御性检查,对于合法的内核态execve(如init)可以忽略,但若真触发,也只是警告。

3.2 获取文件名与分配bprm

getname_kernel从内核字符串创建一个struct filename对象,用于后续的文件操作。alloc_bprm分配并初始化linux_binprm结构体。这个结构体是execve过程的核心数据容器,保存了参数、环境变量、文件描述符、内存布局信息等。

3.3 计数参数和环境变量

count_strings_kernel遍历argv/envp数组,计算字符串个数。注意:argv/envp数组必须以NULL指针结尾。计数结果存入bprm->argcbprm->envc

3.4 检查栈空间限制

bprm_stack_limits检查参数和环境变量是否会超出当前进程的栈大小限制(rlim[RLIMIT_STACK])。防止恶意构造的巨大参数导致栈溢出。

3.5 复制参数和环境变量到bprm

copy_string_kernel将文件名(bprm->filename)复制到bprm内部的页中(通常是栈顶附近)。copy_strings_kernel分别复制环境变量和命令行参数。这些字符串最终会被放置在用户态栈的顶端。bprm->exec记录了参数/envp的起始位置。

3.6 调用核心加载器

bprm_execve(bprm) 是所有二进制加载的核心函数,它负责安全检查和调用真正的二进制格式处理器。


四、bprm_execve:执行前的准备与转向

c

复制代码
static int bprm_execve(struct linux_binprm *bprm)
{
    int retval;

    retval = prepare_bprm_creds(bprm);
    if (retval)
        return retval;

    check_unsafe_exec(bprm);
    current->in_execve = 1;
    sched_mm_cid_before_execve(current);
    sched_exec();

    retval = security_bprm_creds_for_exec(bprm);
    if (retval)
        goto out;

    retval = exec_binprm(bprm);
    if (retval < 0)
        goto out;

    sched_mm_cid_after_execve(current);
    current->fs->in_exec = 0;
    current->in_execve = 0;
    rseq_execve(current);
    user_events_execve(current);
    acct_update_integrals(current);
    task_numa_free(current, false);
    return retval;

out:
    if (bprm->point_of_no_return && !fatal_signal_pending(current))
        force_fatal_sig(SIGSEGV);
    sched_mm_cid_after_execve(current);
    current->fs->in_exec = 0;
    current->in_execve = 0;
    return retval;
}

4.1 准备凭证

prepare_bprm_creds 为新的程序准备凭据(uid、gid、能力集等)。它复制当前进程的凭据到bprm->cred,并做一些预设置。

4.2 检查不安全执行状态

check_unsafe_exec 检查当前进程是否处于不安全的状态(例如线程共享某些资源、文件描述符未关闭等)。这涉及到CLONE_FILESCLONE_FS等标志。如果存在不安全因素,可能需要重新设置相应资源。

4.3 标志位与调度

设置current->in_execve = 1,表示进程正在执行execve,一些内核路径可以据此特殊处理。sched_mm_cid_before_execvesched_mm_cid_after_execve是调度器相关的上下文ID处理。sched_exec通知调度器即将执行新的程序,调度器可能会调整优先级或负载均衡。

4.4 安全性钩子

security_bprm_creds_for_exec 调用LSM(Linux Security Module)钩子,允许SELinux、AppArmor等安全模块对新程序的凭证进行最终调整。

4.5 执行二进制文件

exec_binprm(bprm) 是真正寻找二进制格式处理器并执行它的函数。

4.6 清理与point_of_no_return

如果执行成功,后续进行清理,包括重置in_execve标志、更新rseq、用户事件、NUMA内存等。如果失败,且bprm->point_of_no_return已经设置,意味着已经修改了进程状态无法回滚,那么需要强制发送SIGSEGV终止进程。point_of_no_return标志在加载二进制文件的某个阶段(如开始替换内存映射)后被置位。


五、exec_binprmsearch_binary_handler:二进制格式的匹配

exec_binprm处理二进制格式的循环解析,支持解释器(如脚本的#!)的多级嵌套。

c

复制代码
static int exec_binprm(struct linux_binprm *bprm)
{
    pid_t old_pid, old_vpid;
    int ret, depth;

    old_pid = current->pid;
    rcu_read_lock();
    old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));
    rcu_read_unlock();

    for (depth = 0;; depth++) {
        struct file *exec;
        if (depth > 5)
            return -ELOOP;

        ret = search_binary_handler(bprm);
        if (ret < 0)
            return ret;
        if (!bprm->interpreter)
            break;

        exec = bprm->file;
        bprm->file = bprm->interpreter;
        bprm->interpreter = NULL;

        allow_write_access(exec);
        if (unlikely(bprm->have_execfd)) {
            if (bprm->executable) {
                fput(exec);
                return -ENOEXEC;
            }
            bprm->executable = exec;
        } else
            fput(exec);
    }

    audit_bprm(bprm);
    trace_sched_process_exec(current, old_pid, bprm);
    ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
    proc_exec_connector(current);
    return 0;
}

5.1 记录旧PID

读取当前进程的PID和父进程命名空间内的vpid,用于审计和ptrace事件。由于execve会保留PID,但会清空很多状态,记录下来是为了事件通知。

5.2 循环处理解释器

Linux支持多层二进制格式,例如一个ELF可执行文件可以通过#!指定解释器,而解释器本身又是一个ELF文件,甚至解释器的解释器。为了防止无限递归,限制最大嵌套深度为5层。

search_binary_handler 尝试找到能处理当前bprm->file的二进制格式处理器。如果成功且bprm->interpreter非空,说明当前二进制需要一个解释器(比如脚本的第一行#!/bin/bash)。于是用interpreter替换原来的file,继续循环。

注意:bprm->interpreter是在load_elf_binary中针对动态链接器(ELF INTERP段)设置的,或在binfmt_script中针对#!设置的。

5.3 文件引用计数管理

allow_write_accessfput等负责正确释放原文件的引用,并将解释器文件设为新的执行文件。

5.4 成功后的事件

audit_bprm记录审计日志,trace_sched_process_exec触发调度器追踪点,ptrace_event让调试器捕获PTRACE_EVENT_EXECproc_exec_connector通知连接器(connector)进程执行事件。


六、search_binary_handler:二进制格式处理器查找

c

复制代码
static int search_binary_handler(struct linux_binprm *bprm)
{
    bool need_retry = IS_ENABLED(CONFIG_MODULES);
    struct linux_binfmt *fmt;
    int retval;

    retval = prepare_binprm(bprm);
    if (retval < 0)
        return retval;

    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);

        retval = fmt->load_binary(bprm);

        read_lock(&binfmt_lock);
        put_binfmt(fmt);
        if (bprm->point_of_no_return || (retval != -ENOEXEC)) {
            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;
}

6.1 prepare_binprm

在调用处理器之前,需要做一些准备工作:读取文件的前128(或BINPRM_BUF_SIZE)字节到bprm->buf中,这通常用于识别文件类型(如ELF头部、脚本的#!)。同时会检查文件的执行权限,并设置bprm->cred中的euid/egid(如果文件设置了setuid/setgid位)。

6.2 安全钩子

security_bprm_check调用LSM钩子,允许安全模块基于bprm内容决定是否允许执行。

6.3 遍历格式链表

formats是一个全局链表,通过register_binfmt注册的二进制格式处理器(如elf_formatbinfmt_scriptbinfmt_misc等)。每个处理器有一个load_binary函数指针。

内核持有binfmt_lock读锁遍历链表。对每个格式,尝试增加模块引用计数(防止模块在加载过程中被卸载),然后释放锁(避免长时间持锁),调用fmt->load_binary(bprm)

  • 如果load_binary返回-ENOEXEC(格式不识别),则继续尝试下一个处理器。

  • 如果返回其他错误,或已经设置了bprm->point_of_no_return(无法回滚),则直接返回该结果。

  • 如果返回0(成功),同样返回。

6.4 模块自动加载

如果遍历完所有已注册的处理器仍然没有找到合适的(所有都是-ENOEXEC),并且need_retry为真(即支持模块且尚未尝试过模块加载),且文件的头4个字节不全是可打印字符(意味着可能是一种未知的二进制格式,而不是文本脚本),则调用request_module请求内核模块加载程序尝试加载名为binfmt-xxxx的模块(xxxx是文件头第2-3字节的值,比如binfmt-004c对应ELF)。如果加载成功,则goto retry再次遍历。这样可以动态加载如binfmt_aout等模块。


七、load_elf_binary:ELF加载器的全解析

ELF(Executable and Linkable Format)是Linux上最常用的二进制格式。load_elf_binary函数是ELF格式处理器的核心,长度超过500行,我们将其分解为若干阶段。

7.1 初步检查与ELF头解析

c

复制代码
static int load_elf_binary(struct linux_binprm *bprm)
{
    struct file *interpreter = NULL;
    unsigned long load_bias = 0, phdr_addr = 0;
    int first_pt_load = 1;
    unsigned long error;
    struct elf_phdr *elf_ppnt, *elf_phdata, *interp_elf_phdata = NULL;
    struct elf_phdr *elf_property_phdata = NULL;
    unsigned long elf_brk;
    int retval, i;
    unsigned long elf_entry;
    unsigned long e_entry;
    unsigned long interp_load_addr = 0;
    unsigned long start_code, end_code, start_data, end_data;
    unsigned long reloc_func_desc __maybe_unused = 0;
    int executable_stack = EXSTACK_DEFAULT;
    struct elfhdr *elf_ex = (struct elfhdr *)bprm->buf;
    struct elfhdr *interp_elf_ex = NULL;
    struct arch_elf_state arch_state = INIT_ARCH_ELF_STATE;
    struct mm_struct *mm;
    struct pt_regs *regs;

    retval = -ENOEXEC;
    if (memcmp(elf_ex->e_ident, ELFMAG, SELFMAG) != 0)
        goto out;

    if (elf_ex->e_type != ET_EXEC && elf_ex->e_type != ET_DYN)
        goto out;
    if (!elf_check_arch(elf_ex))
        goto out;
    if (elf_check_fdpic(elf_ex))
        goto out;
    if (!bprm->file->f_op->mmap)
        goto out;
  • 首先验证ELF魔数(\177ELF)、文件类型(必须是可执行ET_EXEC或动态库ET_DYN,即PIE),以及架构支持和是否支持mmap

  • elf_check_arch是一个架构相关的宏,检查ELF头中的e_machine字段是否与当前CPU匹配。

  • elf_check_fdpic用于禁用某些嵌入式系统使用的FDPIC ABI。

  • 文件必须支持mmap操作,因为ELF加载依赖于内存映射。

7.2 加载程序头表(Program Header Table)

c

复制代码
    elf_phdata = load_elf_phdrs(elf_ex, bprm->file);
    if (!elf_phdata)
        goto out;

load_elf_phdrs 从文件中读取所有的程序头(Elf_Phdr)到内核内存中,返回指针。每个程序头描述一个段(segment),如PT_LOAD(可加载段)、PT_INTERP(解释器路径)、PT_GNU_STACK(栈权限)等。

7.3 遍历程序头,处理解释器与属性

后续的代码遍历elf_phdata,查找PT_INTERP段。如果存在,表示该ELF文件是一个动态链接的可执行文件,需要指定动态链接器(如/lib64/ld-linux-x86-64.so.2)。

  • 检查解释器路径长度,分配内存读取路径字符串。

  • 调用open_exec打开解释器文件,获取struct file *interpreter

  • 读取解释器的ELF头到interp_elf_ex

  • 加载解释器的程序头表到interp_elf_phdata

  • 同时记录PT_GNU_PROPERTY段(用于属性如x86的GNU_PROPERTY_X86_FEATURE_1_IBT等)。

同时,还会处理PT_GNU_STACK段决定栈的可执行性(executable_stack),以及架构相关的PT_LOPROCPT_HIPROC段。

7.4 解析属性与架构检查

parse_elf_properties解析PT_GNU_PROPERTY段中的具体属性,例如是否支持Intel CET(控制流强制技术)。arch_check_elf允许架构特定的额外检查(如ARM64的PAC、BTI等)。

7.5 begin_new_exec:替换当前进程映像

c

复制代码
    retval = begin_new_exec(bprm);
    if (retval)
        goto out_free_dentry;

这是最关键的一步:它开始废弃当前进程的地址空间(释放旧的内存映射、文件描述符、信号处理等),并准备新的执行上下文。如果成功,bprm->point_of_no_return将被置位(表示无法回滚)。begin_new_exec内部调用flush_old_exec,其中会执行de_thread清除线程组、调用exec_mmap释放旧内存描述符并分配新的mm_struct

7.6 设置进程Personality和随机化标志

c

复制代码
    SET_PERSONALITY2(*elf_ex, &arch_state);
    if (elf_read_implies_exec(*elf_ex, executable_stack))
        current->personality |= READ_IMPLIES_EXEC;

    if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space)
        current->flags |= PF_RANDOMIZE;
  • SET_PERSONALITY2设置进程的执行域(personality),影响内存布局和系统调用行为。例如,对于32位ELF,可能会设置PER_LINUX32

  • 如果ELF头指示该程序期望可执行栈(或PT_GNU_STACK要求),则设置READ_IMPLIES_EXEC位,使得mmap分配的内存默认可执行(一般为了兼容老程序)。

  • 如果允许地址空间随机化(randomize_va_space),则设置PF_RANDOMIZE标志,后续加载时会对基地址进行随机偏移。

7.7 设置新执行环境:参数栈

c

复制代码
    retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
                             executable_stack);
    if (retval < 0)
        goto out_free_dentry;

setup_arg_pages在用户地址空间的顶端(STACK_TOP,通常是TASK_SIZE - 页大小)建立栈映射,并将bprm中已经复制好的参数和环境变量字符串复制到栈上。同时设置栈的权限(如果executable_stackEXSTACK_ENABLE_X,则栈页允许执行;否则为PROT_READ|PROT_WRITE,不允许执行)。randomize_stack_top对栈顶进行随机偏移,增强ASLR。

7.8 加载所有PT_LOAD

这是最复杂的部分:遍历程序头,对每个PT_LOAD类型段,计算虚拟地址、映射到内存。

c

复制代码
    for(i = 0, elf_ppnt = elf_phdata;
        i < elf_ex->e_phnum; i++, elf_ppnt++) {
        if (elf_ppnt->p_type != PT_LOAD)
            continue;

        elf_prot = make_prot(elf_ppnt->p_flags, &arch_state,
                             !!interpreter, false);
        elf_flags = MAP_PRIVATE;

        vaddr = elf_ppnt->p_vaddr;
        if (!first_pt_load) {
            elf_flags |= MAP_FIXED;
        } else if (elf_ex->e_type == ET_EXEC) {
            elf_flags |= MAP_FIXED_NOREPLACE;
        } else if (elf_ex->e_type == ET_DYN) {
            /* 计算 load_bias 用于ET_DYN */
            if (interpreter) {
                load_bias = ELF_ET_DYN_BASE;
                if (current->flags & PF_RANDOMIZE)
                    load_bias += arch_mmap_rnd();
                alignment = maximum_alignment(elf_phdata, elf_ex->e_phnum);
                if (alignment)
                    load_bias &= ~(alignment - 1);
                elf_flags |= MAP_FIXED_NOREPLACE;
            } else
                load_bias = 0;
            load_bias = ELF_PAGESTART(load_bias - vaddr);
            total_size = total_mapping_size(...);
        }

        error = elf_load(bprm->file, load_bias + vaddr, elf_ppnt,
                         elf_prot, elf_flags, total_size);
        if (BAD_ADDR(error)) {
            retval = IS_ERR_VALUE(error) ? PTR_ERR((void*)error) : -EINVAL;
            goto out_free_dentry;
        }
        if (first_pt_load) {
            first_pt_load = 0;
            if (elf_ex->e_type == ET_DYN) {
                load_bias += error - ELF_PAGESTART(load_bias + vaddr);
                reloc_func_desc = load_bias;
            }
        }
        // 记录代码段、数据段的边界...
    }
7.8.1 内存保护标志

make_prot将ELF段标志(PF_R、PF_W、PF_X)转换为mmap的prot标志(PROT_READ、PROT_WRITE、PROT_EXEC),同时考虑架构状态(如是否需要设置PROT_BTI等)。

7.8.2 映射标志

MAP_PRIVATE表示写时复制(COW),因为可执行文件通常不希望修改被其他进程共享。对于第一个PT_LOAD段,处理方式不同:

  • ET_EXEC :传统可执行文件,有固定加载地址(如0x400000)。使用MAP_FIXED_NOREPLACE,要求内核精确映射到指定地址,但如果该地址已被占用则失败(避免覆盖)。

  • ET_DYN :即位置无关可执行文件(PIE)。如果存在解释器(即该文件是需要动态链接的程序),则计算加载偏移load_bias:基址为ELF_ET_DYN_BASE(通常为/proc/sys/vm/mmap_rnd_bits的随机值),并确保对齐。如果没有解释器(即该文件本身就是解释器,如ld.so),则load_bias为0,允许内核自由选择映射地址(通过后续elf_load不使用MAP_FIXED)。

  • 第一个段映射成功后,如果之前计算了load_bias(针对ET_DYN),还需要根据实际映射地址(error)调整load_bias以便后续段使用相同偏移。

total_size仅用于ET_DYN的第一次映射,目的是预先映射所有连续可加载段的总范围,减少后续mmap调用的次数,提高效率并保证连续性。

7.8.3 段映射的实际执行

elf_load内部调用vm_mmapelf_map。它根据传入的地址、大小、偏移量和标志执行内存映射。对于PT_LOAD段,文件内容被映射到内存。要注意,p_memsz可能大于p_filesz(例如.bss段),多余的部分会被初始化为0。这通过mmap后对超出文件大小的部分调用padzero实现。

7.8.4 记录地址范围

在循环中,还更新了程序入口点e_entryphdr_addr(程序头在内存中的地址,供辅助向量使用)、代码段起始结束(start_codeend_code)、数据段起始结束(start_dataend_data)、以及elf_brk(程序break的初始位置)。

7.9 加载解释器(如果有)

如果interpreter非空,则调用load_elf_interp来加载解释器自身的PT_LOAD段。这个过程与加载主体ELF类似,但是解释器必须以MAP_FIXED的方式加载到已经计算好的load_bias位置上(或者根据主体程序的内核随机偏移)。load_elf_interp返回解释器的加载偏移,用于计算其入口点。

c

复制代码
    if (interpreter) {
        elf_entry = load_elf_interp(interp_elf_ex, interpreter,
                                    load_bias, interp_elf_phdata,
                                    &arch_state);
        if (!IS_ERR_VALUE(elf_entry)) {
            interp_load_addr = elf_entry;
            elf_entry += interp_elf_ex->e_entry;
        }
        // ...
        reloc_func_desc = interp_load_addr;
        allow_write_access(interpreter);
        fput(interpreter);
    } else {
        elf_entry = e_entry;
    }

注意:动态链接模式下,最终的用户态入口点不是主体ELF的e_entry,而是动态链接器的e_entry(即ld.so的入口)。主体程序的入口地址会由动态链接器在初始化完成后跳转过去。

7.10 创建辅助向量(ELF Auxiliary Table)

create_elf_tables将各种信息(程序头地址、入口点、随机化种子、平台字符串等)以辅助向量的形式放在用户栈上。这些信息会被C运行时库(如glibc)的启动代码使用,例如AT_PHDRAT_PHENTAT_ENTRY等。

7.11 设置内存描述符的字段

c

复制代码
    mm = current->mm;
    mm->end_code = end_code;
    mm->start_code = start_code;
    mm->start_data = start_data;
    mm->end_data = end_data;
    mm->start_stack = bprm->p;

这些字段用于/proc/pid/maps以及内核某些模块的地址范围判断。

7.12 随机化堆和brk

如果启用了ASLR且PF_RANDOMIZE被设置,对于直接加载的解释器(ET_DYN且无解释器),mm->brk将被重置为ELF_ET_DYN_BASE,然后调用arch_randomize_brk进一步随机。

c

复制代码
    if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) {
        if (IS_ENABLED(CONFIG_ARCH_HAS_ELF_RANDOMIZE) &&
            elf_ex->e_type == ET_DYN && !interpreter) {
            mm->brk = mm->start_brk = ELF_ET_DYN_BASE;
        }
        mm->brk = mm->start_brk = arch_randomize_brk(mm);
    }

7.13 处理MAP_PAGE_ZERO

对于某些古老的personality(如PER_SVR4),需要映射第0页为只读可执行(为了兼容一些依赖空指针解引用实际访问0页的程序)。这通常用于Wine或某些模拟环境。

7.14 设置寄存器并启动用户进程

c

复制代码
    regs = current_pt_regs();
#ifdef ELF_PLAT_INIT
    ELF_PLAT_INIT(regs, reloc_func_desc);
#endif
    finalize_exec(bprm);
    START_THREAD(elf_ex, regs, elf_entry, bprm->p);
  • ELF_PLAT_INIT是一个架构相关的宏,用于设置特定寄存器(例如x86的%rdx指向DT_FINI)。

  • finalize_exec完成最后的清理(如将文件引用释放,将bprm中的凭证提交给current->cred)。

  • START_THREAD设置regs->ip(指令指针)为elf_entryregs->sp为用户栈指针bprm->p,然后调用start_thread切换到用户态。该函数不会返回,最终CPU会从用户态入口点开始执行指令。

至此,新的用户程序开始运行。


八、总结与思考

8.1 内核启动到用户态init的完整路径

本文从kernel_init线程出发,逐步深入:

  1. kernel_init完成最后的内核初始化,释放__init内存,然后尝试run_init_process

  2. run_init_process调用kernel_execve,后者在内核内部构造执行上下文。

  3. kernel_execve调用bprm_execve,进行凭证准备、安全检查,然后进入exec_binprm

  4. exec_binprm循环调用search_binary_handler寻找合适的二进制格式处理器。

  5. ELF格式处理器load_elf_binary被选中,它读取ELF程序头,映射段,加载解释器,设置辅助向量,最终切换到用户态执行。

  6. 如果成功,PID=1的进程变成了/sbin/init(或其他指定的init),内核的init线程彻底转换成用户进程。

8.2 设计与技巧的赏析

  • 模块化的二进制格式处理 :通过formats链表和register_binfmt,内核可以轻松支持ELF、a.out、脚本、甚至自定义格式(如binfmt_misc)。这种设计体现了Linux内核的可扩展性。

  • 谨慎的错误回滚bprm->point_of_no_return标志用于区分哪些阶段已经不可撤销。在begin_new_exec之后,任何失败都必须强制终止进程,因为旧地址空间已经被释放。

  • ASLR的精细控制 :内核对ET_EXEC、ET_DYN、解释器、stack、brk都分别进行了随机化支持,并通过PF_RANDOMIZErandomize_va_space控制粒度。

  • 对动态链接的支持 :通过PT_INTERP和解释器的递归加载,完美支持了动态链接程序,使得共享库成为可能。

  • 安全性考虑 :包括MAP_FIXED_NOREPLACE防止覆盖已有映射,check_unsafe_exec避免特权提升,LSM钩子无处不在等。

8.3 进一步探索

本文仅涉及了execve路径的路标。感兴趣的读者还可以深入:

  • 脚本的加载binfmt_script如何处理#!

  • 解释器的内部ld.so如何加载共享库、重定位符号。

  • ELF装载的架构相关部分 :如arch_elf_pt_procELF_PLAT_INIT在不同CPU上的实现。

  • coredump:当程序崩溃时,内核如何转储ELF core文件。

Linux内核的这些代码经过几十年的演进,既稳定又高效,是操作系统设计的典范。理解initexecve的流程,能够帮助我们更好地调试系统启动问题、分析安全漏洞以及开发底层系统软件。

##源码

cpp 复制代码
static int __ref kernel_init(void *unused)
{
	int ret;

	/*
	 * Wait until kthreadd is all set-up.
	 */
	wait_for_completion(&kthreadd_done);

	kernel_init_freeable();
	/* need to finish all async __init code before freeing the memory */
	async_synchronize_full();

	system_state = SYSTEM_FREEING_INITMEM;
	kprobe_free_init_mem();
	ftrace_free_init_mem();
	kgdb_free_init_mem();
	exit_boot_config();
	free_initmem();
	mark_readonly();

	/*
	 * Kernel mappings are now finalized - update the userspace page-table
	 * to finalize PTI.
	 */
	pti_finalize();

	system_state = SYSTEM_RUNNING;
	numa_default_policy();

	rcu_end_inkernel_boot();

	do_sysctl_args();

	if (ramdisk_execute_command) {
		ret = run_init_process(ramdisk_execute_command);
		if (!ret)
			return 0;
		pr_err("Failed to execute %s (error %d)\n",
		       ramdisk_execute_command, ret);
	}

	/*
	 * We try each of these until one succeeds.
	 *
	 * The Bourne shell can be used instead of init if we are
	 * trying to recover a really broken machine.
	 */
	if (execute_command) {
		ret = run_init_process(execute_command);
		if (!ret)
			return 0;
		panic("Requested init %s failed (error %d).",
		      execute_command, ret);
	}

	if (CONFIG_DEFAULT_INIT[0] != '\0') {
		ret = run_init_process(CONFIG_DEFAULT_INIT);
		if (ret)
			pr_err("Default init %s failed (error %d)\n",
			       CONFIG_DEFAULT_INIT, ret);
		else
			return 0;
	}

	if (!try_to_run_init_process("/sbin/init") ||
	    !try_to_run_init_process("/etc/init") ||
	    !try_to_run_init_process("/bin/init") ||
	    !try_to_run_init_process("/bin/sh"))
		return 0;

	panic("No working init found.  Try passing init= option to kernel. "
	      "See Linux Documentation/admin-guide/init.rst for guidance.");
}

static int run_init_process(const char *init_filename)
{
	const char *const *p;

	argv_init[0] = init_filename;
	pr_info("Run %s as init process\n", init_filename);
	pr_debug("  with arguments:\n");
	for (p = argv_init; *p; p++)
		pr_debug("    %s\n", *p);
	pr_debug("  with environment:\n");
	for (p = envp_init; *p; p++)
		pr_debug("    %s\n", *p);
	return kernel_execve(init_filename, argv_init, envp_init);
}

static int try_to_run_init_process(const char *init_filename)
{
	int ret;

	ret = run_init_process(init_filename);

	if (ret && ret != -ENOENT) {
		pr_err("Starting init: %s exists but couldn't execute it (error %d)\n",
		       init_filename, ret);
	}

	return ret;
}


int kernel_execve(const char *kernel_filename,
		  const char *const *argv, const char *const *envp)
{
	struct filename *filename;
	struct linux_binprm *bprm;
	int fd = AT_FDCWD;
	int retval;

	/* It is non-sense for kernel threads to call execve */
	if (WARN_ON_ONCE(current->flags & PF_KTHREAD))
		return -EINVAL;

	filename = getname_kernel(kernel_filename);
	if (IS_ERR(filename))
		return PTR_ERR(filename);

	bprm = alloc_bprm(fd, filename, 0);
	if (IS_ERR(bprm)) {
		retval = PTR_ERR(bprm);
		goto out_ret;
	}

	retval = count_strings_kernel(argv);
	if (WARN_ON_ONCE(retval == 0))
		retval = -EINVAL;
	if (retval < 0)
		goto out_free;
	bprm->argc = retval;

	retval = count_strings_kernel(envp);
	if (retval < 0)
		goto out_free;
	bprm->envc = retval;

	retval = bprm_stack_limits(bprm);
	if (retval < 0)
		goto out_free;

	retval = copy_string_kernel(bprm->filename, bprm);
	if (retval < 0)
		goto out_free;
	bprm->exec = bprm->p;

	retval = copy_strings_kernel(bprm->envc, envp, bprm);
	if (retval < 0)
		goto out_free;

	retval = copy_strings_kernel(bprm->argc, argv, bprm);
	if (retval < 0)
		goto out_free;

	retval = bprm_execve(bprm);
out_free:
	free_bprm(bprm);
out_ret:
	putname(filename);
	return retval;
}

static int bprm_execve(struct linux_binprm *bprm)
{
	int retval;

	retval = prepare_bprm_creds(bprm);
	if (retval)
		return retval;

	/*
	 * Check for unsafe execution states before exec_binprm(), which
	 * will call back into begin_new_exec(), into bprm_creds_from_file(),
	 * where setuid-ness is evaluated.
	 */
	check_unsafe_exec(bprm);
	current->in_execve = 1;
	sched_mm_cid_before_execve(current);

	sched_exec();

	/* Set the unchanging part of bprm->cred */
	retval = security_bprm_creds_for_exec(bprm);
	if (retval)
		goto out;

	retval = exec_binprm(bprm);
	if (retval < 0)
		goto out;

	sched_mm_cid_after_execve(current);
	/* execve succeeded */
	current->fs->in_exec = 0;
	current->in_execve = 0;
	rseq_execve(current);
	user_events_execve(current);
	acct_update_integrals(current);
	task_numa_free(current, false);
	return retval;

out:
	/*
	 * If past the point of no return ensure the code never
	 * returns to the userspace process.  Use an existing fatal
	 * signal if present otherwise terminate the process with
	 * SIGSEGV.
	 */
	if (bprm->point_of_no_return && !fatal_signal_pending(current))
		force_fatal_sig(SIGSEGV);

	sched_mm_cid_after_execve(current);
	current->fs->in_exec = 0;
	current->in_execve = 0;

	return retval;
}


/* binfmt handlers will call back into begin_new_exec() on success. */
static int exec_binprm(struct linux_binprm *bprm)
{
	pid_t old_pid, old_vpid;
	int ret, depth;

	/* 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();

	/* This allows 4 levels of binfmt rewrites before failing hard. */
	for (depth = 0;; depth++) {
		struct file *exec;
		if (depth > 5)
			return -ELOOP;

		ret = search_binary_handler(bprm);
		if (ret < 0)
			return ret;
		if (!bprm->interpreter)
			break;

		exec = bprm->file;
		bprm->file = bprm->interpreter;
		bprm->interpreter = NULL;

		allow_write_access(exec);
		if (unlikely(bprm->have_execfd)) {
			if (bprm->executable) {
				fput(exec);
				return -ENOEXEC;
			}
			bprm->executable = exec;
		} else
			fput(exec);
	}

	audit_bprm(bprm);
	trace_sched_process_exec(current, old_pid, bprm);
	ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
	proc_exec_connector(current);
	return 0;
}

/*
 * cycle the list of binary formats handler, until one recognizes the image
 */
static int search_binary_handler(struct linux_binprm *bprm)
{
	bool need_retry = IS_ENABLED(CONFIG_MODULES);
	struct linux_binfmt *fmt;
	int retval;

	retval = prepare_binprm(bprm);
	if (retval < 0)
		return retval;

	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);

		retval = fmt->load_binary(bprm);

		read_lock(&binfmt_lock);
		put_binfmt(fmt);
		if (bprm->point_of_no_return || (retval != -ENOEXEC)) {
			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;
}


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


static int load_elf_binary(struct linux_binprm *bprm)
{
	struct file *interpreter = NULL; /* to shut gcc up */
	unsigned long load_bias = 0, phdr_addr = 0;
	int first_pt_load = 1;
	unsigned long error;
	struct elf_phdr *elf_ppnt, *elf_phdata, *interp_elf_phdata = NULL;
	struct elf_phdr *elf_property_phdata = NULL;
	unsigned long elf_brk;
	int retval, i;
	unsigned long elf_entry;
	unsigned long e_entry;
	unsigned long interp_load_addr = 0;
	unsigned long start_code, end_code, start_data, end_data;
	unsigned long reloc_func_desc __maybe_unused = 0;
	int executable_stack = EXSTACK_DEFAULT;
	struct elfhdr *elf_ex = (struct elfhdr *)bprm->buf;
	struct elfhdr *interp_elf_ex = NULL;
	struct arch_elf_state arch_state = INIT_ARCH_ELF_STATE;
	struct mm_struct *mm;
	struct pt_regs *regs;

	retval = -ENOEXEC;
	/* First of all, some simple consistency checks */
	if (memcmp(elf_ex->e_ident, ELFMAG, SELFMAG) != 0)
		goto out;

	if (elf_ex->e_type != ET_EXEC && elf_ex->e_type != ET_DYN)
		goto out;
	if (!elf_check_arch(elf_ex))
		goto out;
	if (elf_check_fdpic(elf_ex))
		goto out;
	if (!bprm->file->f_op->mmap)
		goto out;

	elf_phdata = load_elf_phdrs(elf_ex, bprm->file);
	if (!elf_phdata)
		goto out;

	elf_ppnt = elf_phdata;
	for (i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++) {
		char *elf_interpreter;

		if (elf_ppnt->p_type == PT_GNU_PROPERTY) {
			elf_property_phdata = elf_ppnt;
			continue;
		}

		if (elf_ppnt->p_type != PT_INTERP)
			continue;

		/*
		 * This is the program interpreter used for shared libraries -
		 * for now assume that this is an a.out format binary.
		 */
		retval = -ENOEXEC;
		if (elf_ppnt->p_filesz > PATH_MAX || elf_ppnt->p_filesz < 2)
			goto out_free_ph;

		retval = -ENOMEM;
		elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
		if (!elf_interpreter)
			goto out_free_ph;

		retval = elf_read(bprm->file, elf_interpreter, elf_ppnt->p_filesz,
				  elf_ppnt->p_offset);
		if (retval < 0)
			goto out_free_interp;
		/* make sure path is NULL terminated */
		retval = -ENOEXEC;
		if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0')
			goto out_free_interp;

		interpreter = open_exec(elf_interpreter);
		kfree(elf_interpreter);
		retval = PTR_ERR(interpreter);
		if (IS_ERR(interpreter))
			goto out_free_ph;

		/*
		 * If the binary is not readable then enforce mm->dumpable = 0
		 * regardless of the interpreter's permissions.
		 */
		would_dump(bprm, interpreter);

		interp_elf_ex = kmalloc(sizeof(*interp_elf_ex), GFP_KERNEL);
		if (!interp_elf_ex) {
			retval = -ENOMEM;
			goto out_free_file;
		}

		/* Get the exec headers */
		retval = elf_read(interpreter, interp_elf_ex,
				  sizeof(*interp_elf_ex), 0);
		if (retval < 0)
			goto out_free_dentry;

		break;

out_free_interp:
		kfree(elf_interpreter);
		goto out_free_ph;
	}

	elf_ppnt = elf_phdata;
	for (i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++)
		switch (elf_ppnt->p_type) {
		case PT_GNU_STACK:
			if (elf_ppnt->p_flags & PF_X)
				executable_stack = EXSTACK_ENABLE_X;
			else
				executable_stack = EXSTACK_DISABLE_X;
			break;

		case PT_LOPROC ... PT_HIPROC:
			retval = arch_elf_pt_proc(elf_ex, elf_ppnt,
						  bprm->file, false,
						  &arch_state);
			if (retval)
				goto out_free_dentry;
			break;
		}

	/* Some simple consistency checks for the interpreter */
	if (interpreter) {
		retval = -ELIBBAD;
		/* Not an ELF interpreter */
		if (memcmp(interp_elf_ex->e_ident, ELFMAG, SELFMAG) != 0)
			goto out_free_dentry;
		/* Verify the interpreter has a valid arch */
		if (!elf_check_arch(interp_elf_ex) ||
		    elf_check_fdpic(interp_elf_ex))
			goto out_free_dentry;

		/* Load the interpreter program headers */
		interp_elf_phdata = load_elf_phdrs(interp_elf_ex,
						   interpreter);
		if (!interp_elf_phdata)
			goto out_free_dentry;

		/* Pass PT_LOPROC..PT_HIPROC headers to arch code */
		elf_property_phdata = NULL;
		elf_ppnt = interp_elf_phdata;
		for (i = 0; i < interp_elf_ex->e_phnum; i++, elf_ppnt++)
			switch (elf_ppnt->p_type) {
			case PT_GNU_PROPERTY:
				elf_property_phdata = elf_ppnt;
				break;

			case PT_LOPROC ... PT_HIPROC:
				retval = arch_elf_pt_proc(interp_elf_ex,
							  elf_ppnt, interpreter,
							  true, &arch_state);
				if (retval)
					goto out_free_dentry;
				break;
			}
	}

	retval = parse_elf_properties(interpreter ?: bprm->file,
				      elf_property_phdata, &arch_state);
	if (retval)
		goto out_free_dentry;

	/*
	 * Allow arch code to reject the ELF at this point, whilst it's
	 * still possible to return an error to the code that invoked
	 * the exec syscall.
	 */
	retval = arch_check_elf(elf_ex,
				!!interpreter, interp_elf_ex,
				&arch_state);
	if (retval)
		goto out_free_dentry;

	/* Flush all traces of the currently running executable */
	retval = begin_new_exec(bprm);
	if (retval)
		goto out_free_dentry;

	/* Do this immediately, since STACK_TOP as used in setup_arg_pages
	   may depend on the personality.  */
	SET_PERSONALITY2(*elf_ex, &arch_state);
	if (elf_read_implies_exec(*elf_ex, executable_stack))
		current->personality |= READ_IMPLIES_EXEC;

	if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space)
		current->flags |= PF_RANDOMIZE;

	setup_new_exec(bprm);

	/* Do this so that we can load the interpreter, if need be.  We will
	   change some of these later */
	retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
				 executable_stack);
	if (retval < 0)
		goto out_free_dentry;

	elf_brk = 0;

	start_code = ~0UL;
	end_code = 0;
	start_data = 0;
	end_data = 0;

	/* 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 < elf_ex->e_phnum; i++, elf_ppnt++) {
		int elf_prot, elf_flags;
		unsigned long k, vaddr;
		unsigned long total_size = 0;
		unsigned long alignment;

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

		elf_prot = make_prot(elf_ppnt->p_flags, &arch_state,
				     !!interpreter, false);

		elf_flags = MAP_PRIVATE;

		vaddr = elf_ppnt->p_vaddr;
		/*
		 * The first time through the loop, first_pt_load is true:
		 * layout will be calculated. Once set, use MAP_FIXED since
		 * we know we've already safely mapped the entire region with
		 * MAP_FIXED_NOREPLACE in the once-per-binary logic following.
		 */
		if (!first_pt_load) {
			elf_flags |= MAP_FIXED;
		} else if (elf_ex->e_type == ET_EXEC) {
			/*
			 * This logic is run once for the first LOAD Program
			 * Header for ET_EXEC binaries. No special handling
			 * is needed.
			 */
			elf_flags |= MAP_FIXED_NOREPLACE;
		} else if (elf_ex->e_type == ET_DYN) {
			/*
			 * This logic is run once for the first LOAD Program
			 * Header for ET_DYN binaries to calculate the
			 * randomization (load_bias) for all the LOAD
			 * Program Headers.
			 *
			 * There are effectively two types of ET_DYN
			 * binaries: programs (i.e. PIE: ET_DYN with INTERP)
			 * and loaders (ET_DYN without INTERP, since they
			 * _are_ the ELF interpreter). The loaders must
			 * be loaded away from programs since the program
			 * may otherwise collide with the loader (especially
			 * for ET_EXEC which does not have a randomized
			 * position). For example to handle invocations of
			 * "./ld.so someprog" to test out a new version of
			 * the loader, the subsequent program that the
			 * loader loads must avoid the loader itself, so
			 * they cannot share the same load range. Sufficient
			 * room for the brk must be allocated with the
			 * loader as well, since brk must be available with
			 * the loader.
			 *
			 * Therefore, programs are loaded offset from
			 * ELF_ET_DYN_BASE and loaders are loaded into the
			 * independently randomized mmap region (0 load_bias
			 * without MAP_FIXED nor MAP_FIXED_NOREPLACE).
			 */
			if (interpreter) {
				load_bias = ELF_ET_DYN_BASE;
				if (current->flags & PF_RANDOMIZE)
					load_bias += arch_mmap_rnd();
				alignment = maximum_alignment(elf_phdata, elf_ex->e_phnum);
				if (alignment)
					load_bias &= ~(alignment - 1);
				elf_flags |= MAP_FIXED_NOREPLACE;
			} else
				load_bias = 0;

			/*
			 * Since load_bias is used for all subsequent loading
			 * calculations, we must lower it by the first vaddr
			 * so that the remaining calculations based on the
			 * ELF vaddrs will be correctly offset. The result
			 * is then page aligned.
			 */
			load_bias = ELF_PAGESTART(load_bias - vaddr);

			/*
			 * Calculate the entire size of the ELF mapping
			 * (total_size), used for the initial mapping,
			 * due to load_addr_set which is set to true later
			 * once the initial mapping is performed.
			 *
			 * Note that this is only sensible when the LOAD
			 * segments are contiguous (or overlapping). If
			 * used for LOADs that are far apart, this would
			 * cause the holes between LOADs to be mapped,
			 * running the risk of having the mapping fail,
			 * as it would be larger than the ELF file itself.
			 *
			 * As a result, only ET_DYN does this, since
			 * some ET_EXEC (e.g. ia64) may have large virtual
			 * memory holes between LOADs.
			 *
			 */
			total_size = total_mapping_size(elf_phdata,
							elf_ex->e_phnum);
			if (!total_size) {
				retval = -EINVAL;
				goto out_free_dentry;
			}
		}

		error = elf_load(bprm->file, load_bias + vaddr, elf_ppnt,
				elf_prot, elf_flags, total_size);
		if (BAD_ADDR(error)) {
			retval = IS_ERR_VALUE(error) ?
				PTR_ERR((void*)error) : -EINVAL;
			goto out_free_dentry;
		}

		if (first_pt_load) {
			first_pt_load = 0;
			if (elf_ex->e_type == ET_DYN) {
				load_bias += error -
				             ELF_PAGESTART(load_bias + vaddr);
				reloc_func_desc = load_bias;
			}
		}

		/*
		 * Figure out which segment in the file contains the Program
		 * Header table, and map to the associated memory address.
		 */
		if (elf_ppnt->p_offset <= elf_ex->e_phoff &&
		    elf_ex->e_phoff < elf_ppnt->p_offset + elf_ppnt->p_filesz) {
			phdr_addr = elf_ex->e_phoff - elf_ppnt->p_offset +
				    elf_ppnt->p_vaddr;
		}

		k = elf_ppnt->p_vaddr;
		if ((elf_ppnt->p_flags & PF_X) && k < start_code)
			start_code = k;
		if (start_data < k)
			start_data = k;

		/*
		 * Check to see if the section's size will overflow the
		 * allowed task size. Note that p_filesz must always be
		 * <= p_memsz so it is only necessary to check p_memsz.
		 */
		if (BAD_ADDR(k) || elf_ppnt->p_filesz > elf_ppnt->p_memsz ||
		    elf_ppnt->p_memsz > TASK_SIZE ||
		    TASK_SIZE - elf_ppnt->p_memsz < k) {
			/* set_brk can never work. Avoid overflows. */
			retval = -EINVAL;
			goto out_free_dentry;
		}

		k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;

		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;
	}

	e_entry = elf_ex->e_entry + load_bias;
	phdr_addr += load_bias;
	elf_brk += load_bias;
	start_code += load_bias;
	end_code += load_bias;
	start_data += load_bias;
	end_data += load_bias;

	current->mm->start_brk = current->mm->brk = ELF_PAGEALIGN(elf_brk);

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

		allow_write_access(interpreter);
		fput(interpreter);

		kfree(interp_elf_ex);
		kfree(interp_elf_phdata);
	} else {
		elf_entry = e_entry;
		if (BAD_ADDR(elf_entry)) {
			retval = -EINVAL;
			goto out_free_dentry;
		}
	}

	kfree(elf_phdata);

	set_binfmt(&elf_format);

#ifdef ARCH_HAS_SETUP_ADDITIONAL_PAGES
	retval = ARCH_SETUP_ADDITIONAL_PAGES(bprm, elf_ex, !!interpreter);
	if (retval < 0)
		goto out;
#endif /* ARCH_HAS_SETUP_ADDITIONAL_PAGES */

	retval = create_elf_tables(bprm, elf_ex, interp_load_addr,
				   e_entry, phdr_addr);
	if (retval < 0)
		goto out;

	mm = current->mm;
	mm->end_code = end_code;
	mm->start_code = start_code;
	mm->start_data = start_data;
	mm->end_data = end_data;
	mm->start_stack = bprm->p;

	if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) {
		/*
		 * For architectures with ELF randomization, when executing
		 * a loader directly (i.e. no interpreter listed in ELF
		 * headers), move the brk area out of the mmap region
		 * (since it grows up, and may collide early with the stack
		 * growing down), and into the unused ELF_ET_DYN_BASE region.
		 */
		if (IS_ENABLED(CONFIG_ARCH_HAS_ELF_RANDOMIZE) &&
		    elf_ex->e_type == ET_DYN && !interpreter) {
			mm->brk = mm->start_brk = ELF_ET_DYN_BASE;
		}

		mm->brk = mm->start_brk = arch_randomize_brk(mm);
#ifdef compat_brk_randomized
		current->brk_randomized = 1;
#endif
	}

	if (current->personality & MMAP_PAGE_ZERO) {
		/* Why this, you ask???  Well SVr4 maps page 0 as read-only,
		   and some applications "depend" upon this behavior.
		   Since we do not have the power to recompile these, we
		   emulate the SVr4 behavior. Sigh. */
		error = vm_mmap(NULL, 0, PAGE_SIZE, PROT_READ | PROT_EXEC,
				MAP_FIXED | MAP_PRIVATE, 0);
	}

	regs = current_pt_regs();
#ifdef ELF_PLAT_INIT
	/*
	 * The ABI may specify that certain registers be set up in special
	 * ways (on i386 %edx is the address of a DT_FINI function, for
	 * example.  In addition, it may also specify (eg, PowerPC64 ELF)
	 * that the e_entry field is the address of the function descriptor
	 * for the startup routine, rather than the address of the startup
	 * routine itself.  This macro performs whatever initialization to
	 * the regs structure is required as well as any relocations to the
	 * function descriptor entries when executing dynamically links apps.
	 */
	ELF_PLAT_INIT(regs, reloc_func_desc);
#endif

	finalize_exec(bprm);
	START_THREAD(elf_ex, regs, elf_entry, bprm->p);
	retval = 0;
out:
	return retval;

	/* error cleanup */
out_free_dentry:
	kfree(interp_elf_ex);
	kfree(interp_elf_phdata);
out_free_file:
	allow_write_access(interpreter);
	if (interpreter)
		fput(interpreter);
out_free_ph:
	kfree(elf_phdata);
	goto out;
}
相关推荐
thethefighter1 小时前
信创综合档案管理系统单机版部署与使用
linux·银河麒麟·档案管理系统·单机版·nhdeep·信创版·综合档案管理系统
hhb_6182 小时前
Go高性能并发编程实战与底层原理剖析
运维·网络·golang
道清茗2 小时前
【RH294知识点汇总】第 6 章 《 管理复杂的 Play 和 Playbook 》常见问题
linux·服务器·网络
哼?~2 小时前
序列化与反序列化
linux·网络
带娃的IT创业者3 小时前
Claude Code Routines 深度解析:重新定义 AI 辅助编程的工作流自动化
运维·人工智能·自动化·ai编程·工作流·anthropic·claude code
broadview_java3 小时前
搬瓦工修改SSH端口
运维·网络·ssh
嵌入式×边缘AI:打怪升级日志3 小时前
从硬编码按键驱动到 Linux Platform 设备树驱动:逐行解剖与融会贯通
linux·运维·服务器
小周技术驿站4 小时前
Linux 权限管理细节详解
linux·运维·服务器·ubuntu·centos
思麟呀4 小时前
Select多路转接
linux·网络·c++·网络协议·http