一、execve 系统调用
1.1 简介
NAME
execve - execute program
SYNOPSIS
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
filename 参数是要执行的程序的路径。argv 参数是一个字符串数组,包含了要传递给新程序的参数列表。数组的第一个元素应该是程序名,后面的元素是参数。数组的最后一个元素必须为 NULL,以指示参数列表的结束。envp 参数是一个字符串数组,包含了新程序将使用的环境变量列表。
1.1.1进程布局
当调用 execve() 时,当前进程的代码、数据和堆栈(text, data, bss, 和stack)都会被替换为新程序的代码、数据和堆栈。新程序开始执行时,它会接管当前进程的控制权,并继承当前进程的所有打开的文件描述符、进程 ID、进程组 ID 等属性。
如下如所示:
备注:.bss段不存在磁盘中,加载进程是为.bss段分配对应的内存空间。
在 Linux ELF(Executable and Linkable Format)文件中,.bss 段是一种特殊的段,用于存储未初始化或者初始化为0的全局变量和静态变量。.bss 段实际上并不存储任何数据,而是在程序加载时被操作系统初始化为零值。
如果有.bss段,那么elf文件的 PT_LOAD 的segment(带有RW flag)的 MemSiz 大小大于FileSiz,差值就是.bss段的大小:
# readelf -l /usr/libexec/gvfsd
Elf file type is DYN (Shared object file)
Entry point 0x4540
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
......
LOAD 0x0000000000008918 0x0000000000009918 0x0000000000009918
0x00000000000006f8 0x0000000000000748 RW 0x1000
0x748 - 0x6f8 = 0x50
# readelf -S /usr/libexec/gvfsd
There are 30 section headers, starting at offset 0x91b8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
......
[26] .bss NOBITS 000000000000a010 00009010
0000000000000050 0000000000000000 WA 0 0 8
......
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
可以看到.bss段的大小就是 0x50。
1.1.2 execve函数组
如果 execve() 调用成功,它将不会返回,因为当前进程已经被替换为新程序。如果 execve() 调用失败,它将返回 -1,并设置一个错误码,可以使用 perror() 函数将错误信息输出到标准错误流中。
exec 是一组函数:
包含 p 的函数(execvp, execlp)会在 PATH 路径下面寻找程序;
不包含 p 的函数需要输入程序的全路径;
包含 v 的函数(execv, execvp, execve)以数组的形式接收参数;
包含 l 的函数(execl, execlp, execle)以列表的形式接收参数;
包含 e 的函数(execve, execle)以数组的形式接收环境变量。
图片来自极客时间:趣谈Linux操作系统
1.1.3 ld
# cat /proc/1308/maps
55f9541eb000-55f9541ee000 r--p 00000000 08:05 59512518 /usr/libexec/gvfsd
55f9541ee000-55f9541f2000 r-xp 00003000 08:05 59512518 /usr/libexec/gvfsd
55f9541f2000-55f9541f4000 r--p 00007000 08:05 59512518 /usr/libexec/gvfsd
55f9541f4000-55f9541f5000 r--p 00008000 08:05 59512518 /usr/libexec/gvfsd
55f9541f5000-55f9541f6000 rw-p 00009000 08:05 59512518 /usr/libexec/gvfsd
55f955370000-55f9553ce000 rw-p 00000000 00:00 0 [heap]
7f2e94000000-7f2e94021000 rw-p 00000000 00:00 0
7f2e94021000-7f2e98000000 ---p 00000000 00:00 0
......
7f2ea3a46000-7f2ea3a58000 r--p 00000000 08:05 60162979 /usr/lib/x86_64-linux-gnu/gvfs/libgvfscommon.so
7f2ea3a58000-7f2ea3a72000 r-xp 00012000 08:05 60162979 /usr/lib/x86_64-linux-gnu/gvfs/libgvfscommon.so
7f2ea3a72000-7f2ea3a7f000 r--p 0002c000 08:05 60162979 /usr/lib/x86_64-linux-gnu/gvfs/libgvfscommon.so
7f2ea3a7f000-7f2ea3a85000 r--p 00038000 08:05 60162979 /usr/lib/x86_64-linux-gnu/gvfs/libgvfscommon.so
7f2ea3a85000-7f2ea3a86000 rw-p 0003e000 08:05 60162979 /usr/lib/x86_64-linux-gnu/gvfs/libgvfscommon.so
7f2ea3a86000-7f2ea3a90000 r--p 00000000 08:05 60162980 /usr/lib/x86_64-linux-gnu/gvfs/libgvfsdaemon.so
7f2ea3a90000-7f2ea3aa5000 r-xp 0000a000 08:05 60162980 /usr/lib/x86_64-linux-gnu/gvfs/libgvfsdaemon.so
7f2ea3aa5000-7f2ea3aaf000 r--p 0001f000 08:05 60162980 /usr/lib/x86_64-linux-gnu/gvfs/libgvfsdaemon.so
7f2ea3aaf000-7f2ea3ab0000 ---p 00029000 08:05 60162980 /usr/lib/x86_64-linux-gnu/gvfs/libgvfsdaemon.so
7f2ea3ab0000-7f2ea3ab1000 r--p 00029000 08:05 60162980 /usr/lib/x86_64-linux-gnu/gvfs/libgvfsdaemon.so
7f2ea3ab1000-7f2ea3ab2000 rw-p 0002a000 08:05 60162980 /usr/lib/x86_64-linux-gnu/gvfs/libgvfsdaemon.so
7f2ea3ab2000-7f2ea3ab4000 rw-p 00000000 00:00 0
7f2ea3ab4000-7f2ea3ab5000 r--p 00000000 08:05 59509063 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f2ea3ab5000-7f2ea3ad8000 r-xp 00001000 08:05 59509063 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f2ea3ad8000-7f2ea3ae0000 r--p 00024000 08:05 59509063 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f2ea3ae1000-7f2ea3ae2000 r--p 0002c000 08:05 59509063 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f2ea3ae2000-7f2ea3ae3000 rw-p 0002d000 08:05 59509063 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f2ea3ae3000-7f2ea3ae4000 rw-p 00000000 00:00 0
7ffda1580000-7ffda15a1000 rw-p 00000000 00:00 0 [stack]
7ffda15b6000-7ffda15ba000 r--p 00000000 00:00 0 [vvar]
7ffda15ba000-7ffda15bc000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
execve 加载新的进程是execve()和ld合作完成的:
1、execve()加载exe文件和ld(/usr/lib/x86_64-linux-gnu/ld-2.31.so)到进程地址空间。然后跳到ld入口处,把控制权交给ld。
2、ld负责加载exe依赖的动态库文件和进行动态链接。例如libc.so、libgvfscommon.so等等。
如下图所示:
图片来自于:https://blog.csdn.net/pwl999/article/details/109289451
1.1.4 stack
在把控制权移交给ld之前,execve()在用户堆栈中已经构造好了内容,具体内容如下:
1.2 示例
execve()执行filename指向的程序。filename必须是二进制可执行文件,或者是以以下形式的一行开头的脚本:
#! interpreter [optional-arg]
比如:
#!/bin/bash
#!/usr/bin/env python
备注:脚本文件对于shell脚本,第一行开头可以没有 #!/bin/bash ,因为会使用当前终端默认的Bash。
argv是传递给新程序的参数字符串数组。按照惯例,这些字符串中的第一个应该包含与正在执行的文件相关联的文件名。envp是一个字符串数组,通常形式为key=value,这些字符串作为环境传递给新程序。argv和envp都必须以NULL指针终止。
当被调用程序的主函数定义为:
int main(int argc, char *argv[], char *envp[])
execve()不会在成功时返回,并且调用进程的文本、数据、bss和堆栈会被加载的程序的文本覆盖。
# cat shell1.sh
#!/bin/bash
echo $0
echo "My script PID is $$"
echo "Please enter a string:"
read input_string
echo "You entered: $input_string"
用./运行脚本文件,传递给新程序第一个参数是脚本文件名。
# strace ./shell1.sh
execve("./shell1.sh", ["./shell1.sh"], 0x7ffcd24d2d30 /* 33 vars */) = 0
用指定脚本解释器运行文件,传递给新程序第一个参数是脚本解释器的文件名,第二个参数是脚本文件名。
# strace bash shell1.sh
execve("/usr/bin/bash", ["bash", "shell1.sh"], 0x7ffc3095a3c8 /* 33 vars */) = 0
以下是一个简单的c语言使用示例:
#include <stdio.h>
#include <unistd.h>
int process_exec(char *command, char *args[])
{
execvp(command, args); // 执行命令
printf("execvp failed\n"); // 如果execvp执行失败,则输出错误信息
return 0;
}
int main (int argc, char *argv[])
{
if(argc < 2){
printf("please input Correct arg\n");
return 0;
}
printf("process exec\n");
process_exec(argv[1], &argv[1]);
return 0;
}
1.3 exec与fork
在调用fork()的情况下,会创建一个新的子进程,该子进程是父进程的克隆。当一个进程执行exec时,不会创建新的进程,调用进程将被传递作为第一个参数的程序覆盖。在大多数情况下,fork系统调用后面会紧跟着新创建的子进程中的exec调用。典型的用例是这样的:一个进程执行fork系统调用,创建一个新的子进程,然后子进程执行exec调用来执行特定的程序。因此,fork和exec通常是一起使用的。没有fork,exec的用途有限。而没有exec,fork几乎没有什么用途。
对于Linux 通常是 fork + exec组合,如下图所示:
一个进程的内存地址空间布局:
二、源码解析
2.1 execve
相关的系统调用有两个:execve 和 execveat(高版本才有该系统调用)
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);
}
SYSCALL_DEFINE5(execveat,
int, fd, const char __user *, filename,
const char __user *const __user *, argv,
const char __user *const __user *, envp,
int, flags)
{
int lookup_flags = (flags & AT_EMPTY_PATH) ? LOOKUP_EMPTY : 0;
return do_execveat(fd,
getname_flags(filename, lookup_flags, NULL),
argv, envp, flags);
}
static 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);
}
static 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_execveat_common 函数。
execve 系统调用的主要流程:
execve
-->do_execve
-->do_execveat_common
-->bprm_execve
-->exec_binprm
-->search_binary_handler
-->load_binary
2.2 do_execveat_common
2.2.1 简介
static int do_execveat_common(int fd, struct filename *filename,
struct user_arg_ptr argv,
struct user_arg_ptr envp,
int flags)
{
struct linux_binprm *bprm;
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(¤t_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;
bprm = alloc_bprm(fd, filename);
if (IS_ERR(bprm)) {
retval = PTR_ERR(bprm);
goto out_ret;
}
retval = count(argv, MAX_ARG_STRINGS);
if (retval < 0)
goto out_free;
bprm->argc = retval;
retval = count(envp, MAX_ARG_STRINGS);
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(bprm->envc, envp, bprm);
if (retval < 0)
goto out_free;
retval = copy_strings(bprm->argc, argv, bprm);
if (retval < 0)
goto out_free;
retval = bprm_execve(bprm, fd, filename, flags);
out_free:
free_bprm(bprm);
out_ret:
putname(filename);
return retval;
}
fd 参数是文件描述符,指定要执行的程序的文件。如果 fd 为负数,则表示使用 filename 参数指定的路径名。
filename 参数是 struct filename 类型的指针,表示要执行的程序的路径名。如果 fd 参数为负数,则需要使用 filename 参数指定路径名;否则,该参数将被忽略。
argv 参数是 struct user_arg_ptr 类型的指针,表示要传递给新程序的参数列表。
envp 参数是 struct user_arg_ptr 类型的指针,表示新程序将使用的环境变量列表。
flags 参数是一个整数,表示要执行的程序的标志。
函数首先检查 filename 参数的有效性,如果无效则返回错误代码。然后,它检查当前进程是否已经超过了 RLIMIT_NPROC 限制,如果超过了限制,则返回 EAGAIN 错误代码。如果进程仍然在限制内,则将当前进程的 PF_NPROC_EXCEEDED 标志清除。
接下来,函数使用 alloc_bprm() 函数为 bprm 变量分配内存,并将 fd 和 filename 参数传递给该函数。bprm 变量是 linux_binprm 类型的结构体,用于存储新程序的相关信息,包括参数列表、环境变量列表、限制信息等。
然后,函数调用 count() 函数来计算参数列表和环境变量列表中的字符串数量,如果数量超过了限制则返回错误代码。
然后,函数将参数列表和环境变量列表的字符串复制到 bprm 变量中。这里使用了 copy_strings() 和 copy_string_kernel() 函数来完成复制操作。
最后,函数调用 bprm_execve() 函数来执行新程序,并将 bprm 变量、fd 和 filename 参数传递给该函数。如果执行成功,则 bprm_execve() 函数不会返回;否则,将返回错误代码。
2.2.1.1 copy_string_kernel
/*
* Copy and argument/environment string from the kernel to the processes stack.
*/
int copy_string_kernel(const char *arg, struct linux_binprm *bprm)
{
(1)
int len = strnlen(arg, MAX_ARG_STRLEN) + 1 /* terminating NUL */;
unsigned long pos = bprm->p;
(2)
if (len == 0)
return -EFAULT;
if (!valid_arg_len(bprm, len))
return -E2BIG;
(3)
/* We're going to work our way backwards. */
arg += len;
bprm->p -= len;
if (IS_ENABLED(CONFIG_MMU) && bprm->p < bprm->argmin)
return -E2BIG;
(4)
while (len > 0) {
unsigned int bytes_to_copy = min_t(unsigned int, len,
min_not_zero(offset_in_page(pos), PAGE_SIZE));
struct page *page;
char *kaddr;
pos -= bytes_to_copy;
arg -= bytes_to_copy;
len -= bytes_to_copy;
page = get_arg_page(bprm, pos, 1);
if (!page)
return -E2BIG;
kaddr = kmap_atomic(page);
flush_arg_page(bprm, pos & PAGE_MASK, page);
memcpy(kaddr + offset_in_page(pos), arg, bytes_to_copy);
flush_kernel_dcache_page(page);
kunmap_atomic(kaddr);
put_arg_page(page);
}
return 0;
}
EXPORT_SYMBOL(copy_string_kernel);
copy_string_kernel用于将内核空间的字符串复制到目标进程的栈中,通常用于将参数或环境变量从内核传递到新进程的地址空间。其主要任务包括:
字符串长度验证:确保字符串长度不超过限制(MAX_ARG_STRLEN)。
栈空间检查:确保目标栈空间足够容纳字符串。
逐页复制:将字符串按页复制到目标进程的栈中。
(1)字符串长度计算
strnlen:计算字符串长度,上限为MAX_ARG_STRLEN(通常为128KB)。
+1:包含字符串的终止符\0。
/*
* These are the maximum length and maximum number of strings passed to the
* execve() system call. MAX_ARG_STRLEN is essentially random but serves to
* prevent the kernel from being unduly impacted by misaddressed pointers.
* MAX_ARG_STRINGS is chosen to fit in a signed 32-bit integer.
*/
#define MAX_ARG_STRLEN (PAGE_SIZE * 32)
(2)长度验证
空字符串检查:若长度为0,返回-EFAULT(无效地址)。
长度限制检查:调用valid_arg_len验证字符串长度是否超出限制。
static bool valid_arg_len(struct linux_binprm *bprm, long len)
{
return len <= MAX_ARG_STRLEN;
}
(3)栈位置调整
pos:保存当前栈位置。
arg += len:将字符串指针移动到末尾(便于反向复制)。
bprm->p -= len:更新栈指针,预留空间。
栈下限检查:若栈指针低于argmin(栈的最小地址),返回-E2BIG(参数过大)。
(4)逐页复制字符串
计算复制字节数:
bytes_to_copy为len、offset_in_page(pos)和PAGE_SIZE的最小值。
确保复制操作不跨越页边界。
获取目标页:
调用get_arg_page获取目标页,若失败返回-E2BIG。
映射页到内核空间:
使用kmap_atomic将页映射到内核虚拟地址空间。
刷新页缓存:
调用flush_arg_page确保页缓存一致性。
复制数据:
使用memcpy将字符串复制到目标页的指定偏移。
刷新数据缓存:
调用flush_kernel_dcache_page确保数据写入内存。
释放页映射:
使用kunmap_atomic解除页映射。
释放页引用:
调用put_arg_page释放页引用。
2.2.2 struct linux_binprm
struct linux_binprm 结构体:
/*
* This structure is used to hold the arguments that are used when loading binaries.
*/
struct linux_binprm {
......
struct vm_area_struct *vma;
unsigned long vma_pages;
......
struct mm_struct *mm;
unsigned long p; /* current top of mem */
unsigned long argmin; /* rlimit marker for copy_strings() */
unsigned int
/* Should an execfd be passed to userspace? */
have_execfd:1,
/* Use the creds of a script (see binfmt_misc) */
execfd_creds:1,
/*
* Set by bprm_creds_for_exec hook to indicate a
* privilege-gaining exec has happened. Used to set
* AT_SECURE auxv for glibc.
*/
secureexec:1,
/*
* Set when errors can no longer be returned to the
* original userspace.
*/
point_of_no_return:1;
#ifdef __alpha__
unsigned int taso:1;
#endif
struct file *executable; /* Executable to pass to the interpreter */
struct file *interpreter;
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} */
const char *fdpath; /* generated filename for execveat */
unsigned interp_flags;
int execfd; /* File descriptor of the executable */
unsigned long loader, exec;
struct rlimit rlim_stack; /* Saved RLIMIT_STACK used during exec. */
char buf[BINPRM_BUF_SIZE];
} __randomize_layout;
它用于存储加载可执行文件时所需的参数,包括程序的参数列表、环境变量列表、限制信息等。下面是对该结构体的一些解释:
vma 和 vma_pages 成员变量用于存储新程序的地址空间信息。vma 是一个指向 vm_area_struct 结构体的指针,该结构体用于描述一个虚拟内存区域;vma_pages 是一个无符号长整型变量,表示新程序占用的虚拟内存页数。
mm 成员变量是一个指向 mm_struct 结构体的指针,用于表示进程的内存映射信息。
p 成员变量是一个无符号长整型变量,表示新程序的内存布局的顶部位置。
argmin 成员变量是一个无符号长整型变量,表示 RLIMIT_STACK 限制的标记位置。
have_execfd、execfd_creds 和 secureexec 成员变量是用于表示一些特殊情况的标志位。
point_of_no_return 成员变量是一个标志位,用于表示在执行新程序时是否可以返回错误给原始用户空间。
executable、interpreter 和 file 成员变量是指向 file 结构体的指针,分别表示要执行的程序文件、解释器文件和当前进程的执行文件。
cred 成员变量是一个指向 cred 结构体的指针,表示新程序的执行凭证。
unsafe 成员变量是一个整型变量,用于表示执行新程序的安全级别。
per_clear 成员变量是一个无符号整型变量,表示在执行新程序时需要清除的当前进程的 personality 标志位。
argc 和 envc 成员变量分别表示新程序的参数数量和环境变量数量。
filename、interp 和 fdpath 成员变量分别表示新程序的名称、解释器的名称和在执行 execveat() 系统调用时生成的文件名。
interp_flags 成员变量是一个无符号整型变量,表示解释器的标志位。
execfd 成员变量是一个整型变量,表示要执行的程序文件的文件描述符。
loader 和 exec 成员变量分别表示解释器和新程序的入口地址。
rlim_stack 成员变量是一个 rlimit 结构体,表示新程序的栈空间大小限制。
buf 成员变量是一个字符数组,用于存储新程序的代码和数据。
这个结构体是 execve() 系统调用的底层实现所需的参数集合,它会在内核中的加载可执行文件时被使用。
2.2.3 alloc_bprm
do_execveat_common
-->alloc_bprm
-->bprm_mm_init
-->mm_alloc
-->__bprm_mm_init
(1)
static struct linux_binprm *alloc_bprm(int fd, struct filename *filename)
{
struct linux_binprm *bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
int retval = -ENOMEM;
if (!bprm)
goto out;
if (fd == AT_FDCWD || filename->name[0] == '/') {
bprm->filename = filename->name;
} else {
if (filename->name[0] == '\0')
bprm->fdpath = kasprintf(GFP_KERNEL, "/dev/fd/%d", fd);
else
bprm->fdpath = kasprintf(GFP_KERNEL, "/dev/fd/%d/%s",
fd, filename->name);
if (!bprm->fdpath)
goto out_free;
bprm->filename = bprm->fdpath;
}
bprm->interp = bprm->filename;
retval = bprm_mm_init(bprm);
if (retval)
goto out_free;
return bprm;
out_free:
free_bprm(bprm);
out:
return ERR_PTR(retval);
}
用于为 execve() 系统调用分配 linux_binprm 结构体的函数 alloc_bprm() 的实现。
fd 和 filename 参数分别表示要执行的程序文件的文件描述符和路径名。
函数首先使用 kzalloc() 函数为 bprm 变量分配内存,并将其初始化为零。如果分配失败,则返回错误代码 ENOMEM。
然后函数检查 fd 和 filename->name 的值,以确定要执行的程序文件的路径名。如果 fd 为 AT_FDCWD 或者 filename->name 的第一个字符是 /,则表示使用 filename->name 作为路径名;否则,需要生成一个 /dev/fd/<fd>/name> 的路径名,并将其存储到 bprm->fdpath 变量中。
最后,函数调用 bprm_mm_init() 函数来初始化 linux_binprm 结构体中与进程地址空间相关的成员变量。如果初始化失败,则函数释放 bprm 变量,并返回错误代码;否则,返回 bprm 变量的指针。
(2)
其中 bprm_mm_init 函数:
/*
* Create a new mm_struct and populate it with a temporary stack
* vm_area_struct. We don't have enough context at this point to set the stack
* flags, permissions, and offset, so we use temporary values. We'll update
* them later in setup_arg_pages().
*/
static int bprm_mm_init(struct linux_binprm *bprm)
{
int err;
struct mm_struct *mm = NULL;
bprm->mm = mm = mm_alloc();
err = -ENOMEM;
if (!mm)
goto err;
/* Save current stack limit for all calculations made during exec. */
task_lock(current->group_leader);
bprm->rlim_stack = current->signal->rlim[RLIMIT_STACK];
task_unlock(current->group_leader);
err = __bprm_mm_init(bprm);
if (err)
goto err;
return 0;
err:
if (mm) {
bprm->mm = NULL;
mmdrop(mm);
}
return err;
}
用于初始化 linux_binprm 结构体中的 mm 成员变量的函数 bprm_mm_init() 的实现。
函数首先调用 mm_alloc() 函数分配一个新的 mm_struct 结构体,并将其存储到 bprm->mm 成员变量中。如果分配失败,则返回错误代码 ENOMEM。
然后函数调用 task_lock() 和 task_unlock() 函数来锁定当前进程的领导进程,并将当前进程的信号栈的大小限制(RLIMIT_STACK)存储到 bprm->rlim_stack 成员变量中。
最后,函数调用 __bprm_mm_init() 函数来为新 mm_struct 结构体分配并初始化一个用于临时栈的 vm_area_struct 结构体。如果初始化失败,则函数释放 mm 结构体,并返回错误代码;否则,返回 0 表示成功初始化。
该函数的作用是为 execve() 系统调用创建一个新的 mm_struct 结构体,并为其分配和初始化一个用于临时栈的 vm_area_struct 结构体。在执行新程序之前,内核需要使用该结构体来准备新程序的执行环境。
在 Linux 中,execve() 系统调用用于执行一个新的程序文件。当用户进程调用 execve() 系统调用时,内核会创建一个新的地址空间,并加载新程序的代码和数据。在这个过程中,内核需要使用 linux_binprm 结构体来存储一些参数,例如程序的参数列表、环境变量列表、限制信息等。linux_binprm 结构体中的 mm 成员变量则用于表示新程序的地址空间信息,包括程序的代码、数据、堆栈等。
bprm_mm_init() 函数是 execve() 系统调用的底层实现之一,其主要作用是创建一个新的 mm_struct 结构体,并为其分配和初始化一个用于临时栈的 vm_area_struct 结构体。在 __bprm_mm_init() 函数中,内核将会使用该临时栈来执行新程序的入口代码,直到能够分配和初始化真正的用户栈。在执行 execve() 系统调用时,内核会使用 linux_binprm 结构体中的信息来准备新程序的执行环境,包括程序的参数、环境变量、限制信息、进程的地址空间等。
(3)
其中__bprm_mm_init 函数:
static int __bprm_mm_init(struct linux_binprm *bprm)
{
bprm->p = PAGE_SIZE * MAX_ARG_PAGES - sizeof(void *);
return 0;
}
函数将临时栈的起始地址设置为 PAGE_SIZE * MAX_ARG_PAGES - sizeof(void *),其中 MAX_ARG_PAGES 定义了用于存储程序参数和环境变量的最大页面数(通常为 32),sizeof(void *) 是为了在栈底留出空间来存储 NULL 指针,以便在程序参数列表的结尾添加一个 NULL 作为结束标记。
函数返回 0 表示初始化成功。
该函数的作用是为新的 mm_struct 结构体分配和初始化一个用于临时栈的 vm_area_struct 结构体,并初始化 linux_binprm 结构体中的 p 成员变量,用于表示临时栈的起始地址。在执行 execve() 系统调用时,内核会使用该临时栈来执行新程序的入口代码,直到能够分配和初始化真正的用户栈。
2.4 bprm_execve
2.4.1 简介
(1)
/*
* sys_execve() executes a new program.
*/
static int bprm_execve(struct linux_binprm *bprm,
int fd, struct filename *filename, int flags)
{
struct file *file;
int retval;
retval = prepare_bprm_creds(bprm);
if (retval)
return retval;
check_unsafe_exec(bprm);
current->in_execve = 1;
file = do_open_execat(fd, filename, flags);
retval = PTR_ERR(file);
if (IS_ERR(file))
goto out_unmark;
sched_exec();
bprm->file = file;
/*
* Record that a name derived from an O_CLOEXEC fd will be
* inaccessible after exec. This allows the code in exec to
* choose to fail when the executable is not mmaped into the
* interpreter and an open file descriptor is not passed to
* the interpreter. This makes for a better user experience
* than having the interpreter start and then immediately fail
* when it finds the executable is inaccessible.
*/
if (bprm->fdpath && get_close_on_exec(fd))
bprm->interp_flags |= BINPRM_FLAGS_PATH_INACCESSIBLE;
/* 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;
/* execve succeeded */
current->fs->in_exec = 0;
current->in_execve = 0;
rseq_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_sigsegv(SIGSEGV);
out_unmark:
current->fs->in_exec = 0;
current->in_execve = 0;
return retval;
}
函数首先调用 prepare_bprm_creds() 函数,用于准备新程序的执行凭证(credentials)。如果准备失败,则返回相应的错误代码。
然后函数调用 check_unsafe_exec() 函数,用于检查新程序是否是不安全的。如果检查失败,则返回相应的错误代码。
接着函数调用 do_open_execat() 函数,打开新程序的可执行文件,并将打开的文件对象存储到 bprm->file 成员变量中。如果打开失败,则返回相应的错误代码。
然后函数调用 sched_exec() 函数,用于在进程切换之前执行一些必要的清理和刷新操作。
然后函数调用 security_bprm_creds_for_exec() 函数,用于为新程序的执行凭证设置安全属性。如果设置失败,则返回相应的错误代码。
接着函数调用 exec_binprm() 函数,用于执行新程序。如果执行失败,则返回相应的错误代码。
如果 execve() 系统调用成功执行,则函数更新一些信息(例如文件系统状态、进程状态等),并返回相应的返回值。
(2)
execve() 系统调用用于执行一个新的程序文件。当用户进程调用 execve() 系统调用时,内核会创建一个新的地址空间,并加载新程序的代码和数据。在这个过程中,内核需要使用 linux_binprm 结构体来存储一些参数,例如程序的参数列表、环境变量列表、限制信息等。linux_binprm 结构体中的 file 成员变量则用于表示新程序的可执行文件的文件对象。
bprm_execve() 函数是 execve() 系统调用的底层实现之一,其主要作用是打开新程序的可执行文件,并执行新程序。在执行 execve() 系统调用时,内核会使用 linux_binprm 结构体中存储的一些参数来设置新程序的执行环境。在执行 execve() 系统调用后,内核会将当前进程的地址空间替换为新程序的地址空间,并开始执行新程序。
在执行 execve() 系统调用前,bprm_execve() 函数会调用一些必要的函数来准备新程序的执行环境,例如准备新程序的执行凭证、检查新程序是否是不安全的、打开新程序的可执行文件、设置新程序的执行凭证安全属性等。如果执行成功,则函数会更新一些信息(例如文件系统状态、进程状态等),并返回相应的返回值。如果执行失败,则函数会返回相应的错误代码。
2.4.2 do_open_execat
static struct file *do_open_execat(int fd, struct filename *name, int flags)
{
struct file *file;
int err;
struct open_flags open_exec_flags = {
.open_flag = O_LARGEFILE | O_RDONLY | __FMODE_EXEC,
.acc_mode = MAY_EXEC,
.intent = LOOKUP_OPEN,
.lookup_flags = LOOKUP_FOLLOW,
};
if ((flags & ~(AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH)) != 0)
return ERR_PTR(-EINVAL);
if (flags & AT_SYMLINK_NOFOLLOW)
open_exec_flags.lookup_flags &= ~LOOKUP_FOLLOW;
if (flags & AT_EMPTY_PATH)
open_exec_flags.lookup_flags |= LOOKUP_EMPTY;
file = do_filp_open(fd, name, &open_exec_flags);
if (IS_ERR(file))
goto out;
/*
* may_open() has already checked for this, so it should be
* impossible to trip now. But we need to be extra cautious
* and check again at the very end too.
*/
err = -EACCES;
if (WARN_ON_ONCE(!S_ISREG(file_inode(file)->i_mode) ||
path_noexec(&file->f_path)))
goto exit;
err = deny_write_access(file);
if (err)
goto exit;
if (name->name[0] != '\0')
fsnotify_open(file);
out:
return file;
exit:
fput(file);
return ERR_PTR(err);
}
用于打开新程序的可执行文件的函数 do_open_execat() 的实现:
函数首先创建一个 open_flags 结构体 open_exec_flags,用于指定打开文件的标志。其中 open_exec_flags.open_flag 指定打开文件的标志为 O_LARGEFILE | O_RDONLY | __FMODE_EXEC,表示打开一个大文件、只读文件和可执行文件。open_exec_flags.acc_mode 指定打开文件的访问权限为 MAY_EXEC,表示允许执行该文件。open_exec_flags.intent 指定打开文件的意图为 LOOKUP_OPEN,表示打开已经存在的文件。open_exec_flags.lookup_flags 指定打开文件的查找标志为 LOOKUP_FOLLOW,表示遵循符号链接。
函数检查 flags 参数是否合法。如果不合法,则返回相应的错误代码。如果 flags 参数包含 AT_SYMLINK_NOFOLLOW 标志,则取消 open_exec_flags.lookup_flags 中的 LOOKUP_FOLLOW 标志,表示不遵循符号链接。如果 flags 参数包含 AT_EMPTY_PATH 标志,则将 open_exec_flags.lookup_flags 中的 LOOKUP_EMPTY 标志设置为 1,表示查找空路径名(即当前工作目录)。
函数调用 do_filp_open() 函数,打开新程序的可执行文件,并返回打开的文件对象。如果打开失败,则返回相应的错误代码。
函数检查打开的文件是否为普通文件,并检查文件是否具有执行权限。如果检查失败,则释放打开的文件并返回相应的错误代码。
函数调用 deny_write_access() 函数,用于防止其他进程修改打开的文件。如果防止失败,则释放打开的文件并返回相应的错误代码。
如果打开的文件对象是一个常规文件,并且具有执行权限,则函数调用 fsnotify_open() 函数,用于通知文件系统有一个新文件被打开。
函数返回打开的文件对象。
该函数的作用是打开新程序的可执行文件,并返回打开的文件对象。在执行 execve() 系统调用时,内核需要打开新程序的可执行文件,并将打开的文件对象存储到 linux_binprm 结构体中的 file 成员变量中,以便在执行新程序时能够加载文件中的代码和数据。
2.5 exec_binprm
(1)
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;
}
函数首先获取当前进程的 PID 和其父进程的 PID,以便在执行新程序失败时可以输出相应的错误信息。
函数使用一个循环来查找新程序的解释器,并将其替换为新程序的可执行文件。在每一轮循环中,函数调用 search_binary_handler() 函数来查找新程序的处理程序,并返回相应的错误代码或者0。如果查找成功,则函数将找到的处理程序赋值给 linux_binprm 结构体的 interpreter 成员变量,并将新程序的可执行文件赋值给 linux_binprm 结构体的 file 成员变量。如果查找失败,则函数返回相应的错误代码。
如果找到了解释器,则函数使用 allow_write_access() 函数允许写访问新程序的可执行文件。然后函数检查是否存在可执行文件描述符,如果存在,则将可执行文件对象存储到 linux_binprm 结构体的 executable 成员变量中。如果不存在,则释放可执行文件对象。
如果没有找到解释器,则函数调用 audit_bprm() 函数,用于记录新程序的执行事件。然后函数调用 trace_sched_process_exec() 函数,用于跟踪新程序的执行情况。接着函数调用 ptrace_event() 函数,用于向父进程发送 PTRACE_EVENT_EXEC 事件。最后函数调用 proc_exec_connector() 函数,用于通知进程之间的连接器有新程序的执行事件发生。
函数返回0,表示执行成功。
(2)
exec_binprm() 是 Linux 内核中用于执行新程序的函数。它的作用是搜索新程序的解释器(如果需要的话),并设置新程序的执行环境。该函数会检查传入的 linux_binprm 结构体中的参数和可执行文件信息,并在必要时查找解释器来执行指定的程序。如果有解释器,则会将解释器替换为可执行文件,并允许对可执行文件进行写访问。如果没有解释器,则会直接执行可执行文件。
在函数执行过程中,它会调用 search_binary_handler() 函数来查找处理程序,并使用 audit_bprm() 函数记录新程序的执行事件,使用 trace_sched_process_exec() 函数跟踪新程序的执行情况,使用 ptrace_event() 函数向父进程发送 PTRACE_EVENT_EXEC 事件,以及使用 proc_exec_connector() 函数通知进程之间的连接器有新程序的执行事件发生。
如果执行成功,则函数返回0。否则,它会返回相应的错误代码。
2.6 search_binary_handler
(1)
#define printable(c) (((c)=='\t') || ((c)=='\n') || (0x20<=(c) && (c)<=0x7e))
/*
* 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;
}
函数首先调用 prepare_binprm() 函数来准备执行新程序所需的参数和环境变量。如果准备失败,则函数返回对应的错误代码。
函数然后调用 security_bprm_check() 函数来检查执行新程序的安全性。如果检查失败,则函数返回对应的错误代码。
security_bprm_check() 是 Linux 中的一个 LSM 钩子函数,用于在执行二进制文件之前检查二进制文件的安全性。
函数使用一个循环来遍历二进制格式处理程序的列表,并使用 try_module_get() 函数获取每个处理程序。如果获取失败,则函数跳过该处理程序。如果获取成功,则函数调用处理程序的 load_binary() 函数来加载新程序,并将返回值存储在 retval 变量中。
如果新程序加载成功,则函数释放处理程序并返回该函数的返回值。如果新程序加载失败,则函数释放处理程序,并继续遍历列表,直到找到一个成功加载新程序的处理程序。
如果所有的处理程序都不能成功加载新程序,则函数使用 request_module() 函数来请求加载相应的内核模块。如果请求成功,则函数再次遍历处理程序列表来查找能够成功加载新程序的处理程序。如果请求失败,则函数返回对应的错误代码。
如果新程序的前四个字节中包含不可打印的字符,则函数会尝试重新加载处理程序列表。如果重新加载成功,则函数再次遍历处理程序列表来查找能够成功加载新程序的处理程序。如果重新加载失败,则函数返回对应的错误代码。
(2)
search_binary_handler() 是 Linux 内核中用于搜索二进制格式处理程序的函数。它的主要作用是根据可执行文件的前四个字节来确定其文件格式,并在二进制格式处理程序列表中查找相应的处理程序。如果找到相应的处理程序,则使用该处理程序来加载可执行文件。如果找不到相应的处理程序,则尝试通过加载内核模块来扩展列表,并再次查找相应的处理程序。
在函数执行过程中,它会调用 prepare_binprm() 函数来准备 linux_binprm 结构体,以便能够读取可执行文件中的前四个字节。然后,它会调用 security_bprm_check() 函数来检查可执行文件的安全性。接下来,它会循环遍历二进制格式处理程序列表,并尝试使用每个处理程序来加载可执行文件。如果处理程序能够成功加载可执行文件,则函数将返回相应的返回值。如果处理程序不能成功加载可执行文件,则函数将尝试使用另一个处理程序,并继续循环遍历列表。如果在列表中找不到适当的处理程序,则函数尝试通过加载内核模块来扩展列表,并再次循环遍历列表。
在函数执行过程中,它还会使用 try_module_get() 函数来获取每个处理程序的内核模块,并使用 list_for_each_entry() 函数来循环遍历处理程序列表。如果要加载的可执行文件的前四个字节包含不可打印的字符,则函数会尝试重新加载处理程序列表,并再次查找相应的处理程序。
如果函数能够找到适当的二进制格式处理程序,则会使用该处理程序来加载可执行文件,并将返回值作为函数的返回值。如果函数不能找到适当的二进制格式处理程序,则会返回相应的错误代码。
2.7 load_binary
2.7.1 struct linux_binfmt
(1)
/*
* 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;
这段代码定义了 struct linux_binfmt 结构体,该结构体用于定义可以加载 Linux 可执行文件格式的函数。
struct linux_binfmt 结构体包含以下成员变量:
lh:一个 struct list_head 结构体,用于将 struct linux_binfmt 结构体添加到双向链表中。
module:一个指向 struct module 结构体的指针,表示加载该二进制格式所需的内核模块。
load_binary:一个指向函数的指针,用于加载指定的二进制格式。
load_shlib:一个指向函数的指针,用于加载共享库。
core_dump:一个指向函数的指针,用于生成核心转储文件。
min_coredump:一个无符号长整型变量,表示生成的核心转储文件的最小大小。
__randomize_layout 是 GCC 扩展的属性,用于指示编译器随机化结构体的布局,以增强程序的安全性。
在 Linux 内核中,struct linux_binfmt 用于定义二进制格式处理程序的接口。每个二进制格式处理程序都必须实现 struct linux_binfmt 中的函数,以便能够加载指定的二进制格式。具体来说,load_binary 函数用于加载可执行文件,load_shlib 函数用于加载共享库,core_dump 函数用于生成核心转储文件。min_coredump 变量用于指示生成的核心转储文件的最小大小。module 变量指向加载该二进制格式所需的内核模块。lh 变量用于将 struct linux_binfmt 结构体添加到双向链表中,以便内核可以遍历所有已安装的二进制格式处理程序。
(2)
extern void __register_binfmt(struct linux_binfmt *fmt, int insert);
/* Registration of default binfmt handlers */
static inline void register_binfmt(struct linux_binfmt *fmt)
{
__register_binfmt(fmt, 0);
}
/* Same as above, but adds a new binfmt at the top of the list */
static inline void insert_binfmt(struct linux_binfmt *fmt)
{
__register_binfmt(fmt, 1);
}
extern void unregister_binfmt(struct linux_binfmt *);
static LIST_HEAD(formats);
static DEFINE_RWLOCK(binfmt_lock);
void __register_binfmt(struct linux_binfmt * fmt, int insert)
{
BUG_ON(!fmt);
if (WARN_ON(!fmt->load_binary))
return;
write_lock(&binfmt_lock);
insert ? list_add(&fmt->lh, &formats) :
list_add_tail(&fmt->lh, &formats);
write_unlock(&binfmt_lock);
}
EXPORT_SYMBOL(__register_binfmt);
void unregister_binfmt(struct linux_binfmt * fmt)
{
write_lock(&binfmt_lock);
list_del(&fmt->lh);
write_unlock(&binfmt_lock);
}
EXPORT_SYMBOL(unregister_binfmt);
__register_binfmt() 函数用于将指定的二进制格式处理程序添加到内核的二进制格式处理程序列表中。该函数接受两个参数:fmt 是一个指向 struct linux_binfmt 结构体的指针,表示要添加的二进制格式处理程序;insert 是一个整数值,如果为非零,则表示将二进制格式处理程序添加到列表的开头。
register_binfmt() 函数是 __register_binfmt() 函数的一个包装器,它将 insert 参数设置为零,以将二进制格式处理程序添加到列表的末尾。
insert_binfmt() 函数也是 __register_binfmt() 函数的一个包装器,它将 insert 参数设置为非零,以将二进制格式处理程序添加到列表的开头。
unregister_binfmt() 函数用于从内核的二进制格式处理程序列表中删除指定的二进制格式处理程序。该函数接受一个指向 struct linux_binfmt 结构体的指针作为参数,表示要删除的二进制格式处理程序。
这些函数用于将二进制格式处理程序添加到内核中,以便能够加载指定的二进制格式。在 Linux 内核中,二进制格式处理程序使用 struct linux_binfmt 结构体定义,每个处理程序都必须实现该结构体中的函数。通过调用 register_binfmt() 或 insert_binfmt() 函数,可以将处理程序添加到内核的二进制格式处理程序列表中。通过调用 unregister_binfmt() 函数,可以从该列表中删除处理程序。
2.7.2 load_binary
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);
这里说明一些常用的 binary :
(1)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,
};
static int __init init_elf_binfmt(void)
{
register_binfmt(&elf_format);
return 0;
}
static void __exit exit_elf_binfmt(void)
{
/* Remove the COFF and ELF loaders. */
unregister_binfmt(&elf_format);
}
(2)Script
static struct linux_binfmt script_format = {
.module = THIS_MODULE,
.load_binary = load_script,
};
static int __init init_script_binfmt(void)
{
register_binfmt(&script_format);
return 0;
}
static void __exit exit_script_binfmt(void)
{
unregister_binfmt(&script_format);
}
(3)misc
static struct linux_binfmt misc_format = {
.module = THIS_MODULE,
.load_binary = load_misc_binary,
};
static struct file_system_type bm_fs_type = {
.owner = THIS_MODULE,
.name = "binfmt_misc",
.init_fs_context = bm_init_fs_context,
.kill_sb = kill_litter_super,
};
MODULE_ALIAS_FS("binfmt_misc");
static int __init init_misc_binfmt(void)
{
int err = register_filesystem(&bm_fs_type);
if (!err)
insert_binfmt(&misc_format);
return err;
}
static void __exit exit_misc_binfmt(void)
{
unregister_binfmt(&misc_format);
unregister_filesystem(&bm_fs_type);
}
# ls /proc/sys/fs/binfmt_misc/
register status
misc_format 是一个二进制格式处理程序,用于加载一些特殊的二进制格式文件。
(4)a.out
static int load_aout_binary(struct linux_binprm *);
static int load_aout_library(struct file*);
static struct linux_binfmt aout_format = {
.module = THIS_MODULE,
.load_binary = load_aout_binary,
.load_shlib = load_aout_library,
};
static int __init init_aout_binfmt(void)
{
register_binfmt(&aout_format);
return 0;
}
static void __exit exit_aout_binfmt(void)
{
unregister_binfmt(&aout_format);
}
static int load_aout_binary(struct linux_binprm ) 和 static int load_aout_library(struct file) 是两个函数,用于加载 a.out 二进制格式文件和共享库文件。
static struct linux_binfmt aout_format = {…} 定义了一个名为 aout_format 的 struct linux_binfmt 结构体变量,并使用花括号 {…} 来初始化其中的成员变量。
aout_format.module = THIS_MODULE 表示将当前内核模块的指针赋值给 aout_format 结构体的 module 成员变量。这是因为要使用 load_aout_binary() 和 load_aout_library() 函数来加载 a.out 二进制格式文件和共享库文件,这两个函数定义在当前内核模块中。
aout_format.load_binary = load_aout_binary 表示将 load_aout_binary() 函数的地址赋值给 aout_format 结构体的 load_binary 成员变量。这样,在加载 a.out 二进制格式文件时,内核就会使用 load_aout_binary() 函数来处理该文件。
aout_format.load_shlib = load_aout_library 表示将 load_aout_library() 函数的地址赋值给 aout_format 结构体的 load_shlib 成员变量。这样,在加载 a.out 共享库文件时,内核就会使用 load_aout_library() 函数来处理该文件。
这段代码定义了一个名为 aout_format 的二进制格式处理程序,用于加载 a.out 二进制格式文件和共享库文件。在内核启动时,可以使用 register_binfmt() 函数将 aout_format 添加到内核的二进制格式处理程序列表中,以便能够加载这些 a.out 格式的二进制文件。
三、execve 缺页中断
fork 之后如果要执行新的程序,那么就需要执行 execve 这个系统调用。它的主要作用是加载可执行程序并运行。execve 的作用是使当前进程执行一个新的可执行程序,
这里简单说下execve 的执行步骤如下所示:
(1)清空页表,这样整个进程中的页都变成不存在了,一旦访问这些页,就会发生页中断;
(2)打开待加载执行的文件,在内核中创建代表这个文件的 struct file 结构;
(3)加载和解析文件头,文件头里描述了这个可执行文件一共有多少 section;
(4)创建相应的 vma 来描述代码段,数据段,并且将文件的各个 section 与这些内存区域建立映射关系;
(5)如果当前加载的文件还依赖其他共享库文件,则找到这个共享库文件,并跳转到第 2 步继续处理这个共享库文件;
(6)最后跳转到可执行程序的入口处执行。
execve 的实现并不负责将文件内容加载到物理页中,它只建立了这种文件 section,与内存区域的映射关系就结束了。真正负责加载文件内容的是缺页中断,接下来,我们就看看缺页中断是如何加载物理页的。
在 execve 的执行步骤中,我们讲了,内核为可执行程序创建一个 vma 结构体实例,然后将它的 vm_file 属性设成第 2 步所打开的文件,这就建立起了内存区域和文件的映射关系。这个内核区域的区间首地址、区间尾地址和控制权限,都是由第 3 步解析的信息决定的。例如.text 段被加载到的内存首地址,也就是链接时所决定的起始地址,它就决定了内存代码段的起始地址。
struct vm_area_struct {
......
struct file * vm_file; /* File we map to (can be NULL). */
......
}
由于第 1 步把页表都清空了,这就导致 CPU 在加载指令时会发现代码段是缺失的,此时就会产生缺页中断。
Linux 内核用于处理缺页中断的函数是 do_page_fault,如果内核检查,当前出现缺页中断的虚拟地址所在的内存区域 vma(虚拟地址落在该内存区域的 vm_start 和 vm_end 之间)存在文件映射 (vm_file 不为空),那就可以通过虚拟内存地址计算文件中的偏移,这就定位到了内存所缺的页对应到文件的哪一段。然后内核就启动磁盘 IO,将对应的页从磁盘加载进内存。一次缺页中断就这样被解决了。
可执行程序的加载不是一次性完成的,而是由缺页中断根据需要,将文件的内容以页为单位加载进内存的,一次只会加载一页。
参考资料
Linux 5.13