Exec
execve 系统调用
execve 系统调用
我们前面提到了, fork, vfork 等复制出来的进程是父进程的一个副本, 那么如何我们想加载新的程序, 可以通过 execve 来加载和启动新的程序。
x86 架构下, 其实还实现了一个新的 exec 的系统调用叫做 execveat(自 linux-3.19 后进入内核)
exec()函数族
exec 函数一共有六个,其中 execve 为内核级系统调用,其他(execl,execle,execlp,execv,execvp)都是调用 execve 的库函数。
|
|
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中, 用来保存要要执行的文件相关的信息, 包括可执行程序的路径, 参数和环境变量的信息
|
|
struct linux_binfmt 可执行程序的结构
linux 支持其他不同格式的可执行程序, 在这种方式下, linux 能运行其他操作系统所编译的程序, 如 MS-DOS 程序, 活 BSD Unix 的 COFF 可执行格式, 因此 linux 内核用 struct linux_binfmt 来描述各种可执行程序。
linux 内核对所支持的每种可执行的程序类型都有个 struct linux_binfmt 的数据结构,定义如下
linux_binfmt 定义在include/linux/binfmts.h中
|
|
其提供了 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 函数
|
|
通过参数传递了寄存集合和可执行文件的名称(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_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 实现的,依次执行以下操作:
- 调用 unshare_files()为进程复制一份文件表
- 调用 kzalloc()分配一份 structlinux_binprm 结构体
- 调用 open_exec()查找并打开二进制文件
- 调用 sched_exec()找到最小负载的 CPU,用来执行该二进制文件
- 根据获取的信息,填充 structlinux_binprm 结构体中的 file、filename、interp 成员
- 调用 bprm_mm_init()创建进程的内存地址空间,为新程序初始化内存管理.并调用 init_new_context()检查当前进程是否使用自定义的局部描述符表;如果是,那么分配和准备一个新的 LDT
- 填充 structlinux_binprm 结构体中的 argc、envc 成员
- 调用 prepare_binprm()检查该二进制文件的可执行权限;最后,kernel_read()读取二进制文件的头 128 字节(这些字节用于识别二进制文件的格式及其他信息,后续会使用到)
- 调用 copy_strings_kernel()从内核空间获取二进制文件的路径名称
- 调用 copy_string()从用户空间拷贝环境变量和命令行参数
- 至此,二进制文件已经被打开,struct linux_binprm 结构体中也记录了重要信息, 内核开始调用 exec_binprm 执行可执行程序
- 释放 linux_binprm 数据结构,返回从该文件可执行格式的 load_binary 中获得的代码
定义在fs/exec.c
|
|
exec_binprm 识别并加载二进程程序
每种格式的二进制文件对应一个 struct linux_binprm 结构体,load_binary 成员负责识别该二进制文件的格式;
内核使用链表组织这些 structlinux_binfmt 结构体,链表头是 formats。
接着 do_execveat_common()继续往下看:
调用 search_binary_handler()函数对 linux_binprm 的 formats 链表进行扫描,并尝试每个 load_binary 函数,如果成功加载了文件的执行格式,对 formats 的扫描终止。
|
|
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 |
参考
-
No backlinks found.