Linux 进程启动 execve 系统调用内核源码解析
文章目录
- 一、execve 系统调用
- 1.1 简介
- 1.2 示例
- 二、源码解析
- 2.1 execve
- 2.2 do_execveat_common
- 2.2.1 简介
- 2.2.2 struct linux_binprm
- 2.2.3 alloc_bprm
- 2.4 bprm_execve
- 2.4.1 简介
- 2.4.2 do_open_execat
- 2.5 exec_binprm
- 2.6 search_binary_handler
- 2.7 load_binary
- 2.7.1 struct linux_binfmt
- 2.7.2 load_binary
- 参考资料
一、execve 系统调用
1.1 简介
NAMEexecve - execute programSYNOPSIS#include int execve(const char *filename, char *const argv[], char *const envp[]);
filename 参数是要执行的程序的路径。argv 参数是一个字符串数组,包含了要传递给新程序的参数列表。数组的第一个元素应该是程序名,后面的元素是参数。数组的最后一个元素必须为 NULL,以指示参数列表的结束。envp 参数是一个字符串数组,包含了新程序将使用的环境变量列表。
当调用 execve() 时,当前进程的代码、数据和堆栈(text, data, bss, 和stack)都会被替换为新程序的代码、数据和堆栈。新程序开始执行时,它会接管当前进程的控制权,并继承当前进程的所有打开的文件描述符、进程 ID、进程组 ID 等属性。
如果 execve() 调用成功,它将不会返回,因为当前进程已经被替换为新程序。如果 execve() 调用失败,它将返回 -1,并设置一个错误码,可以使用 perror() 函数将错误信息输出到标准错误流中。
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/bashecho $0echo "My script PID is $$"echo "Please enter a string:"
read input_stringecho "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
#include 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;}
二、源码解析
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.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;
#endifstruct 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. Mostof the time same as filename, but could bedifferent 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);elsebprm->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/
最后,函数调用 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;} elsefput(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 格式的二进制文件。
参考资料
Linux 5.13
Chatgpt 3.5
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
