一、以fork和execve系统调用为例分析中断上下文的切换
1、fork函数
头文件:#include<unistd.h>,#include<sys/types.h>
函数原型:pid_t fork( void);
返回值: 若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1
函数说明:在Unix/Linux中用fork函数创建一个新的进程。进程是由当前已有进程调用fork函数创建,分叉的进程叫子进程,创建者叫父进程。该函数的特点是调用一次,返回两次,一次是在父进程,一次是 在
子进程。两次返回的区别是子进程的返回值为0,父进程的返回值是新子进程的ID。子进程与父进程继续并发运行。如果父进程继续创建更多的子进程,子进程之间是兄弟关系,同样子进程也可以创建自己的子
进程,这样可以建立起定义关系的进程之间的一种层次关系。
2、execve函数
函数定义:int execve(const char *filename, char *const argv[ ], char *const envp[ ]);
返回值:函数执行成功时没有返回值,执行失败时的返回值为-1.
函数说明:execve()用来执行参数filename字符串所代表的文件路径,第二个参数是利用数组指针来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组。exec
函数一共有六个,其中execve为内核级系统调用,其他(execl,execle,execlp,execv,execvp)都是调用execve的库函数。
3、编写分析fork函数和execve函数系统调用的代码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main(int argc, char * argv[]) { int pid; /* fork another process */ pid = fork(); if (pid < 0) { /* error occurred */ fprintf(stderr, "Fork Failed!"); exit(-1); } else if (pid == 0) { /* child process */ execlp("/bin/ls", "ls", NULL); } else { /* parent process */ /* parent will wait for the child to complete*/ wait(NULL); printf("Child Complete!"); exit(0); } }
代码中包括fork函数和execve函数,可以同时进行debug,了解函数的调用过程。代码运行结果如下
4、根据上次的实验,将编写的函数打包到rootfs中,运行qemu,进行debug,对代码进行分析。
(1)分析fork调用
查看fork的系统调用号,在相应的地方打上断点
继续运行,在断点处输入bt,查看详细的调用过程。__x64_sys_clone —> __se_sys_clone —> __do_sys_clone —> _do_fork —> copy_process —> copy_thread_tls
继续运行,结果如下
(2)分析execve函数调用
过程和fork函数类似,首先打断点,运行测试程序
程序在断点处停止,输入bt,查看系统的调用过程。__x64_sys_execve —> __se_sys_execve —> __do_sys_execve —> _do_execve —> do_execveat_common —> __do_execve_file
继续运行,结果如下
5、至此,我们根据debug的结果,知道了fork函数和execve函数的系统调用过程,下面通过阅读源码,进行详细的分析。
二、分析execve系统调用中断上下文和其特殊之处
1、根据debug的结果,execve函数首先进行系统调用,调用函数位于linux-5.4.34/fs/exec.c中,通过传入参数,调用do_execve()函数
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); }
2、do_execve()函数将参数转换成相应的结构体,然后调用do_execveat_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); }
3、do_execveat_common()函数继续调用__do_execve_file()函数
static int do_execveat_common(int fd, struct filename *filename, struct user_arg_ptr argv, struct user_arg_ptr envp, int flags) { return __do_execve_file(fd, filename, argv, envp, flags, NULL); }
4、__do_execve_file()比较复杂,主要的工作是把前面函数传入的参数复制到bprm结构体中,接着调用exec_binprm()进行可执行文件的加载
struct linux_binprm *bprm; 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(¤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; retval = unshare_files(&displaced); if (retval) goto out_ret; retval = -ENOMEM; bprm = kzalloc(sizeof(*bprm), GFP_KERNEL); //创建了一个结构体bprm, 把环境变量和命令行参数都复制到结构体中 if (!bprm) goto out_files; retval = prepare_bprm_creds(bprm); if (retval) goto out_free; check_unsafe_exec(bprm); current->in_execve = 1; if (!file) file = do_open_execat(fd, filename, flags); retval = PTR_ERR(file); if (IS_ERR(file)) goto out_unmark; sched_exec(); bprm->file = file; if (!filename) { bprm->filename = "none"; } else if (fd == AT_FDCWD || filename->name[0] == '/') { bprm->filename = filename->name; } else { if (filename->name[0] == '