execve 系统调用


execve 系统调用


我们前面提到了, fork, vfork 等复制出来的进程是父进程的一个副本, 那么如何我们想加载新的程序, 可以通过 execve 来加载和启动新的程序。

x86 架构下, 其实还实现了一个新的 exec 的系统调用叫做 execveat(自 linux-3.19 后进入内核)

syscalls,x86: Add execveat() system call

exec()函数族


exec 函数一共有六个,其中 execve 为内核级系统调用,其他(execl,execle,execlp,execv,execvp)都是调用 execve 的库函数。

1
2
3
4
5
6
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,
                  ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);123456

ELF 文件格式以及可执行程序的表示


ELF 可执行文件格式


Linux 下标准的可执行文件格式是 ELF.ELF(Executable and Linking Format)是一种对象文件的格式,用于定义不同类型的对象文件(Object files)中都放了什么东西、以及都以什么样的格式去放这些东西。它自最早在 System V 系统上出现后,被 xNIX 世界所广泛接受,作为缺省的二进制文件格式来使用。

但是 linux 也支持其他不同的可执行程序格式, 各个可执行程序的执行方式不尽相同, 因此 linux 内核每种被注册的可执行程序格式都用 linux_bin_fmt 来存储, 其中记录了可执行程序的加载和执行函数

同时我们需要一种方法来保存可执行程序的信息, 比如可执行文件的路径, 运行的参数和环境变量等信息,即 linux_bin_prm 结构

struct linux_bin_prm 结构描述一个可执行程序


linux_binprm 是定义在include/linux/binfmts.h中, 用来保存要要执行的文件相关的信息, 包括可执行程序的路径, 参数和环境变量的信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
* This structure is used to hold the arguments that are used when loading binaries.
*/
struct linux_binprm {
    char buf[BINPRM_BUF_SIZE];  // 保存可执行文件的头128字节
#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; /* current top of mem , 当前内存页最高地址*/
    unsigned int
            cred_prepared:1,/* true if creds already prepared (multiple
                             * preps happen for interpreters) */
            cap_effective:1;/* true if has elevated effective capabilities,
                             * false if not; except for init which inherits
                             * its parent's caps anyway */
#ifdef __alpha__
    unsigned int taso:1;
#endif
    unsigned int recursion_depth; /* only for search_binary_handler() */
    struct file * file;         /*  要执行的文件  */
    struct cred *cred;      /* new credentials */
    int unsafe;             /* how unsafe this exec is (mask of LSM_UNSAFE_*) */
    unsigned int per_clear; /* bits to clear in current->personality */
    int argc, envc;     /*  命令行参数和环境变量数目  */
    const char * filename;  /* Name of binary as seen by procps, 要执行的文件的名称  */
    const char * interp;    /* Name of the binary really executed. Most
                               of the time same as filename, but could be
                               different for binfmt_{misc,script} 要执行的文件的真实名称,通常和filename相同  */
    unsigned interp_flags;
    unsigned interp_data;
    unsigned long loader, exec;
};12345678910111213141516171819202122232425262728293031323334353637

struct linux_binfmt 可执行程序的结构


linux 支持其他不同格式的可执行程序, 在这种方式下, linux 能运行其他操作系统所编译的程序, 如 MS-DOS 程序, 活 BSD Unix 的 COFF 可执行格式, 因此 linux 内核用 struct linux_binfmt 来描述各种可执行程序。

linux 内核对所支持的每种可执行的程序类型都有个 struct linux_binfmt 的数据结构,定义如下

linux_binfmt 定义在include/linux/binfmts.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/*
  * 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 */
 };123456789101112

其提供了 3 种方法来加载和执行可执行程序

  • load_binary

    通过读存放在可执行文件中的信息为当前进程建立一个新的执行环境

  • load_shlib

    用于动态的把一个共享库捆绑到一个已经在运行的进程, 这是由 uselib()系统调用激活的

  • core_dump

    在名为 core 的文件中, 存放当前进程的执行上下文. 这个文件通常是在进程接收到一个缺省操作为”dump”的信号时被创建的, 其格式取决于被执行程序的可执行类型

所有的 linux_binfmt 对象都处于一个链表中, 第一个元素的地址存放在 formats 变量中, 可以通过调用 register_binfmt()和 unregister_binfmt()函数在链表中插入和删除元素, 在系统启动期间, 为每个编译进内核的可执行格式都执行 registre_fmt()函数. 当实现了一个新的可执行格式的模块正被装载时, 也执行这个函数, 当模块被卸载时, 执行 unregister_binfmt()函数.

当我们执行一个可执行程序的时候, 内核会 list_for_each_entry 遍历所有注册的 linux_binfmt 对象, 对其调用 load_binrary 方法来尝试加载, 直到加载成功为止.

execve 加载可执行程序的过程


内核中实际执行 execv()或 execve()系统调用的程序是 do_execve(),这个函数先打开目标映像文件,并从目标文件的头部(第一个字节开始)读入若干(当前 Linux 内核中是 128)字节(实际上就是填充 ELF 文件头,下面的分析可以看到),然后调用另一个函数 search_binary_handler(),在此函数里面,它会搜索我们上面提到的 Linux 支持的可执行文件类型队列,让各种可执行程序的处理程序前来认领和处理。如果类型匹配,则调用 load_binary 函数指针所指向的处理函数来处理目标映像文件。在 ELF 文件格式中,处理函数是 load_elf_binary 函数,下面主要就是分析 load_elf_binary 函数的执行过程(说明:因为内核中实际的加载需要涉及到很多东西,这里只关注跟 ELF 文件的处理相关的代码):

sys_execve() > do_execve() > do_execveat_common > search_binary_handler() > load_elf_binary()

execve 的入口函数 sys_execve


描述 定义 链接
系统调用号(体系结构相关) 类似与如下的形式 #define **NR_execve 117 **SYSCALL(117, sys_execve, 3) arch/对应体系结构/include/uapi/asm/unistd.h, line 265
入口函数声明 asmlinkage long sys_execve(const char **user *filename, const char **user *const __user *argv, const char **user *const **user *envp); include/linux/syscalls.h, line 843
系统调用实现 SYSCALL_DEFINE3(execve, const char **user*, filename, const char **user *const **user* , argv, const char **user const __user , envp) { return do_execve(getname(filename), argv, envp); } fs/exec.v 1710

execve 系统调用的的入口点是体系结构相关的 sys_execve, 该函数很快将工作委托给系统无关的 do_execve 函数

1
2
3
4
5
6
7
SYSCALL_DEFINE3(execve,
                const char __user *, filename,
                const char __user *const __user *, argv,
                const char __user *const __user *, envp)
{
    return do_execve(getname(filename), argv, envp);
}1234567

通过参数传递了寄存集合和可执行文件的名称(filename), 而且还传递了指向了程序的参数 argv 和环境变量 envp 的指针

参数 描述
filename 可执行程序的名称
argv 程序的参数
envp 环境变量

指向程序参数 argv 和环境变量 envp 两个数组的指针以及数组中所有的指针都位于虚拟地址空间的用户空间部分。因此内核在当问用户空间内存时, 需要多加小心, 而__user 注释则允许自动化工具来检测时候所有相关事宜都处理得当

do_execve 函数

do_execve 的定义在 fs/exec.c 中,参见 http://lxr.free-electrons.com/source/fs/exec.c?v=4.5#L1628

更早期实现 linux-2.4 linux-3.18 引入 execveat 之前 do_execve 实现 linux-3.19~至今引入 execveat 之后 do_execve 实现 do_execveat 的实现
代码过长, 没有经过 do_execve_common 的封装 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_execveat_common(AT_FDCWD, filename, argv, envp, 0); } 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_execveat_common(AT_FDCWD, filename, argv, envp, 0); } int do_execveat(int fd, struct filename *filename, const char __user *const **user ***argv, const char **user *const **user ***envp, int flags) { struct user_arg_ptr argv = { .ptr.native = **argv }; struct user_arg_ptr envp = { .ptr.native = __envp }; return do_execveat_common(fd, filename, argv, envp, flags); }

我们可以看到不同时期的演变, 早期的代码 do_execve 就直接完成了自己的所有工作, 后来 do_execve 会调用更加底层的 do_execve_common 函数, 后来 x86 架构下引入了新的系统调用 execveat, 为了使代码更加通用, do_execveat_common 替代了原来的 do_execve_common 函数

早期的 do_execve 流程如下, 基本无差别, 可以作为参考

do_execve函数的流程
do_execve函数的流程

程序的加载 do_execve_common 和 do_execveat_common


早期 linux-2.4 中直接由 do_execve 实现程序的加载和运行

linux-3.18 引入 execveat 之前 do_execve 调用 do_execve_common 来完成程序的加载和运行

linux-3.19~至今引入 execveat 之后 do_execve 调用 do_execveat_common 来完成程序的加载和运行

在 Linux 中提供了一系列的函数,这些函数能用可执行文件所描述的新上下文代替进程的上下文。这样的函数名以前缀 exec 开始。所有的 exec 函数都是调用了 execve()系统调用。

sys_execve 接受参数:1.可执行文件的路径 2.命令行参数字符串 3.环境变量字符串

sys_execve 是调用 do_execve 实现的。do_execve 则是调用 do_execveat_common 实现的,依次执行以下操作:

  1. 调用 unshare_files()为进程复制一份文件表
  2. 调用 kzalloc()分配一份 structlinux_binprm 结构体
  3. 调用 open_exec()查找并打开二进制文件
  4. 调用 sched_exec()找到最小负载的 CPU,用来执行该二进制文件
  5. 根据获取的信息,填充 structlinux_binprm 结构体中的 file、filename、interp 成员
  6. 调用 bprm_mm_init()创建进程的内存地址空间,为新程序初始化内存管理.并调用 init_new_context()检查当前进程是否使用自定义的局部描述符表;如果是,那么分配和准备一个新的 LDT
  7. 填充 structlinux_binprm 结构体中的 argc、envc 成员
  8. 调用 prepare_binprm()检查该二进制文件的可执行权限;最后,kernel_read()读取二进制文件的头 128 字节(这些字节用于识别二进制文件的格式及其他信息,后续会使用到)
  9. 调用 copy_strings_kernel()从内核空间获取二进制文件的路径名称
  10. 调用 copy_string()从用户空间拷贝环境变量和命令行参数
  11. 至此,二进制文件已经被打开,struct linux_binprm 结构体中也记录了重要信息, 内核开始调用 exec_binprm 执行可执行程序
  12. 释放 linux_binprm 数据结构,返回从该文件可执行格式的 load_binary 中获得的代码

定义在fs/exec.c

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/*
 * sys_execve() executes a new program.
 */
static int do_execveat_common(int fd, struct filename *filename,
                          struct user_arg_ptr argv,
                          struct user_arg_ptr envp,
                          int flags)
{
    char *pathbuf = NULL;
    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;

    //  1.  调用unshare_files()为进程复制一份文件表;
    retval = unshare_files(&displaced);
    if (retval)
            goto out_ret;

    retval = -ENOMEM;

    //  2、调用kzalloc()在堆上分配一份structlinux_binprm结构体;
    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;

    //  3、调用open_exec()查找并打开二进制文件;
    file = do_open_execat(fd, filename, flags);
    retval = PTR_ERR(file);
    if (IS_ERR(file))
            goto out_unmark;

    //  4、调用sched_exec()找到最小负载的CPU,用来执行该二进制文件;
    sched_exec();

    //  5、根据获取的信息,填充structlinux_binprm结构体中的file、filename、interp成员;
    bprm->file = file;
    if (fd == AT_FDCWD || filename->name[0] == '/') {
            bprm->filename = filename->name;
    } else {
            if (filename->name[0] == '\0')
                    pathbuf = kasprintf(GFP_TEMPORARY, "/dev/fd/%d", fd);
            else
                    pathbuf = kasprintf(GFP_TEMPORARY, "/dev/fd/%d/%s",
                                        fd, filename->name);
            if (!pathbuf) {
                    retval = -ENOMEM;
                    goto out_unmark;
            }
            /*
             * Record that a name derived from an O_CLOEXEC fd will be
             * inaccessible after exec. Relies on having exclusive access to
             * current->files (due to unshare_files above).
             */
            if (close_on_exec(fd, rcu_dereference_raw(current->files->fdt)))
                    bprm->interp_flags |= BINPRM_FLAGS_PATH_INACCESSIBLE;
            bprm->filename = pathbuf;
    }
    bprm->interp = bprm->filename;

    //  6、调用bprm_mm_init()创建进程的内存地址空间,并调用init_new_context()检查当前进程是否使用自定义的局部描述符表;如果是,那么分配和准备一个新的LDT;
    retval = bprm_mm_init(bprm);
    if (retval)
            goto out_unmark;

    //  7、填充structlinux_binprm结构体中的命令行参数argv,环境变量envp
    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;

    //  8、调用prepare_binprm()检查该二进制文件的可执行权限;最后,kernel_read()读取二进制文件的头128字节(这些字节用于识别二进制文件的格式及其他信息,后续会使用到);
    retval = prepare_binprm(bprm);
    if (retval < 0)
            goto out;

    //  9、调用copy_strings_kernel()从内核空间获取二进制文件的路径名称;
    retval = copy_strings_kernel(1, &bprm->filename, bprm);
    if (retval < 0)
            goto out;

    bprm->exec = bprm->p;

    //  10.1、调用copy_string()从用户空间拷贝环境变量
    retval = copy_strings(bprm->envc, envp, bprm);
    if (retval < 0)
            goto out;

    //  10.2、调用copy_string()从用户空间拷贝命令行参数;
    retval = copy_strings(bprm->argc, argv, bprm);
    if (retval < 0)
            goto out;
    /*
        至此,二进制文件已经被打开,struct linux_binprm结构体中也记录了重要信息;

        下面需要识别该二进制文件的格式并最终运行该文件
    */
    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);
    kfree(pathbuf);
    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);
    kfree(pathbuf);

out_files:
    if (displaced)
            reset_files_struct(displaced);
out_ret:
    putname(filename);
    return retval;
}

exec_binprm 识别并加载二进程程序


每种格式的二进制文件对应一个 struct linux_binprm 结构体,load_binary 成员负责识别该二进制文件的格式;

内核使用链表组织这些 structlinux_binfmt 结构体,链表头是 formats。

接着 do_execveat_common()继续往下看:

调用 search_binary_handler()函数对 linux_binprm 的 formats 链表进行扫描,并尝试每个 load_binary 函数,如果成功加载了文件的执行格式,对 formats 的扫描终止。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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);
    }

    return ret;
}123456789101112131415161718192021

search_binary_handler 识别二进程程序


这里需要说明的是,这里的 fmt 变量的类型是 struct linux_binfmt *, 但是这一个类型与之前在 do_execveat_common()中的 bprm 是不一样的,

定义在fs/exec.c

/_ * 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;
123456789

retry: read_lock(&binfmt_lock);

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

        // 遍历formats链表
        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;
123456789101112131415161718192021222324252627282930313233343536

}

load_binary 加载可执行程序


我们前面提到了,linux 内核支持多种可执行程序格式, 每种格式都被注册为一个 linux_binfmt 结构, 其中存储了对应可执行程序格式加载函数等

格式 linux_binfmt 定义 load_binary load_shlib core_dump
a.out aout_format load_aout_binary load_aout_library aout_core_dump
flat style executables flat_format load_flat_binary load_flat_shared_library flat_core_dump
script 脚本 script_format load_script
misc_format misc_format load_misc_binary
em86 em86_format load_format
elf_fdpic elf_fdpic_format load_elf_fdpic_binary elf_fdpic_core_dump
elf elf_format load_elf_binary load_elf_binary elf_core_dump

参考

linux 可执行文件的加载和运行(转)

linux 上应用程序的执行机制

linux 可执行文件创建 学习笔记