一、实验要求
- 以fork和execve系统调用为例分析中断上下文的切换
- 分析execve系统调用中断上下文的特殊之处
- 分析fork子进程启动执行时进程上下文的特殊之处
- 以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程
二、实验过程
2.1 fork系统调用
fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。子进程使用相同的pc(程序计数器),相同的CPU寄存器,在父进程中使用的相同打开文件。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。如果fork函数出现错误,则返回一个负值。如果初始参数或者传入的变量不同,父子进程也可以做不同的事。
一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
2.1.1 编写程序,使用fork() 函数
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <sys/types.h> 5 #include <sys/wait.h> 6 7 int main(int argc, char* argv[]) 8 { 9 int pid; 10 11 pid = fork(); 12 if(pid<0) 13 { 14 //error 15 fprintf(stderr,"For Failed"); 16 exit(-1); 17 } 18 else if(pid==0) 19 { 20 //child 21 printf("this is child process \n"); 22 } 23 else 24 { 25 //parent 26 printf("this is Parent process \n"); 27 wait(NULL); 28 printf("child complete \n"); 29 } 30 return 0; 31 }
2.1.2 编译后执行
1 gcc -o fork fork.c -static 2 ./fork
2.1.3 反汇编
1 objdump -S fork -o fork.s 2 查看fock的系统调用号
查/linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl
表得到内核函数__x64_sys_clone,
在 /linux/kernel/fork.c
中,发现,__x64_sys_clone
是调用了内核中的_do_fork
函数。
2.1.4 打断点
在__x64_sys_clone
,_do_fork
,cpoy_process
,dup_task_struct
,copy_thread_tls
下断点,shell下运行fork
可执行文件,查看此时函数栈
继续运行,在断点处输入bt,查看详细的调用过程。__x64_sys_clone —> __se_sys_clone —> __do_sys_clone —> _do_fork —> copy_process —> copy_thread_tls
2.2 execve系统调用
2.2.1 编写程序,调用execve
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <sys/types.h> 5 #include <sys/wait.h> 6 7 int main() { 8 int pid; 9 pid = fork(); 10 if (pid < 0) 11 { 12 fprintf(stderr, "Fork Failed\n"); 13 exit(-1); 14 } 15 else if (pid == 0) 16 {<br> // execlp是对系统调用execve的一层封装 17 execlp("/bin/ls", "ls", NULL); 18 printf("ls command run finished\n"); 19 } 20 else 21 { 22 wait(NULL); 23 printf("Child Completed\n"); 24 exit(0); 25 } 26 return 0; 27 }
2.2.2 打断点
通过调用栈可以看出execve的调用关系为:
__x64_sys_execve -> do_execve() –> do_execveat_common() -> __do_execve_?le -> exec_binprm()-> search_binary_handler() -> load_elf_binary() -> start_thread()
2.2.3 do_execve()
1 SYSCALL_DEFINE3(execve, 2 const char __user *, filename, 3 const char __user *const __user *, argv, 4 const char __user *const __user *, envp) 5 { 6 return do_execve(getname(filename), argv, envp); 7 }
2.2.4 do_execveat_common()
1 int do_execve(struct filename *filename, 2 const char __user *const __user *__argv, 3 const char __user *const __user *__envp) 4 { 5 struct user_arg_ptr argv = { .ptr.native = __argv }; 6 struct user_arg_ptr envp = { .ptr.native = __envp }; 7 return do_execveat_common(AT_FDCWD, filename, argv, envp, 0); 8 }
2.2.5 __do_execve_file()
1 static int do_execveat_common(int fd, struct filename *filename, 2 struct user_arg_ptr argv, 3 struct user_arg_ptr envp, 4 int flags) 5 { 6 return __do_execve_file(fd, filename, argv, envp, flags, NULL); 7 }
2.2.6 exec_binprm()
1 struct linux_binprm *bprm; 2 struct files_struct *displaced; 3 int retval; 4 5 if (IS_ERR(filename)) 6 return PTR_ERR(filename); 7 8 /* 9 * We move the actual failure in case of RLIMIT_NPROC excess from 10 * set*uid() to execve() because too many poorly written programs 11 * don‘t check setuid() return code. Here we additionally recheck 12 * whether NPROC limit is still exceeded. 13 */ 14 if ((current->flags & PF_NPROC_EXCEEDED) && 15 atomic_read(¤t_user()->processes) > rlimit(RLIMIT_NPROC)) { 16 retval = -EAGAIN; 17 goto out_ret; 18 } 19 20 /* We‘re below the limit (still or again), so we don‘t want to make 21 * further execve() calls fail. */ 22 current->flags &= ~PF_NPROC_EXCEEDED; 23 24 retval = unshare_files(&displaced); 25 if (retval) 26 goto out_ret; 27 28 retval = -ENOMEM; 29 bprm = kzalloc(sizeof(*bprm), GFP_KERNEL); //创建了一个结构体bprm, 把环境变量和命令行参数都复制到结构体中 30 if (!bprm) 31 goto out_files; 32 33 retval = prepare_bprm_creds(bprm); 34 if (retval) 35 goto out_free; 36 37 check_unsafe_exec(bprm); 38 current->in_execve = 1; 39 40 if (!file) 41 file = do_open_execat(fd, filename, flags); 42 retval = PTR_ERR(file); 43 if (IS_ERR(file)) 44 goto out_unmark; 45 46 sched_exec(); 47 48 bprm->file = file; 49 if (!filename) { 50 bprm->filename = "none"; 51 } else if (fd == AT_FDCWD || filename->name[0] == ‘/‘) { 52 bprm->filename = filename->name; 53 } else { 54 if (filename->name[0] == ‘\0‘) 55 pathbuf = kasprintf(GFP_KERNEL, "/dev/fd/%d", fd); 56 else 57 pathbuf = kasprintf(GFP_KERNEL, "/dev/fd/%d/%s", 58 fd, filename->name); 59 if (!pathbuf) { 60 retval = -ENOMEM; 61 goto out_unmark; 62 } 63 /* 64 * Record that a name derived from an O_CLOEXEC fd will be 65 * inaccessible after exec. Relies on having exclusive access to 66 * current->files (due to unshare_files above). 67 */ 68 if (close_on_exec(fd, rcu_dereference_raw(current->files->fdt))) 69 bprm->interp_flags |= BINPRM_FLAGS_PATH_INACCESSIBLE; 70 bprm->filename = pathbuf; 71 } 72 bprm->interp = bprm->filename; 73 74 retval = bprm_mm_init(bprm); 75 if (retval) 76 goto out_unmark; 77 78 retval = prepare_arg_pages(bprm, argv, envp); 79 if (retval < 0) 80 goto out; 81 82 retval = prepare_binprm(bprm); 83 if (retval < 0) 84 goto out; 85 86 retval = copy_strings_kernel(1, &bprm->filename, bprm); 87 if (retval < 0) 88 goto out; 89 90 bprm->exec = bprm->p; 91 retval = copy_strings(bprm->envc, envp, bprm); //把传入的shell上下文复制到bprm中 92 93 if (retval < 0) 94 goto out; 95 96 retval = copy_strings(bprm->argc, argv, bprm); //把传入的命令行参数复制到bprm中 97 98 if (retval < 0) 99 goto out; 100 101 would_dump(bprm, bprm->file); 102 103 retval = exec_binprm(bprm); //准备交给真正的可执行文件加载器了 104 if (retval < 0) 105 goto out; 106 107 /* execve succeeded */ 108 current->fs->in_exec = 0; 109 current->in_execve = 0; 110 rseq_execve(current); 111 acct_update_integrals(current); 112 task_numa_free(current, false); 113 free_bprm(bprm); 114 kfree(pathbuf); 115 if (filename) 116 putname(filename); 117 if (displaced) 118 put_files_struct(displaced); 119 return retval; 120 121 out: 122 if (bprm->mm) { 123 acct_arg_size(bprm, 0); 124 mmput(bprm->mm); 125 } 126 127 out_unmark: 128 current->fs->in_exec = 0; 129 current->in_execve = 0; 130 131 out_free: 132 free_bprm(bprm); 133 kfree(pathbuf); 134 135 out_files: 136 if (displaced) 137 reset_files_struct(displaced); 138 out_ret: 139 if (filename) 140 putname(filename); 141 return retval; 142 }
2.2.7 search_binary_handler()
1 static int exec_binprm(struct linux_binprm *bprm) 2 { 3 pid_t old_pid, old_vpid; 4 int ret; 5 6 /* Need to fetch pid before load_binary changes it */ 7 old_pid = current->pid; 8 rcu_read_lock(); 9 old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent)); 10 rcu_read_unlock(); 11 12 ret = search_binary_handler(bprm); 13 if (ret >= 0) { 14 audit_bprm(bprm); 15 trace_sched_process_exec(current, old_pid, bprm); 16 ptrace_event(PTRACE_EVENT_EXEC, old_vpid); 17 proc_exec_connector(current); 18 } 19 20 return ret; 21 }
2.2.8 load_binary()
1 int search_binary_handler(struct linux_binprm *bprm) 2 { 3 bool need_retry = IS_ENABLED(CONFIG_MODULES); 4 struct linux_binfmt *fmt; 5 int retval; 6 7 /* This allows 4 levels of binfmt rewrites before failing hard. */ 8 if (bprm->recursion_depth > 5) 9 return -ELOOP; 10 11 retval = security_bprm_check(bprm); 12 if (retval) 13 return retval; 14 15 retval = -ENOENT; 16 retry: 17 read_lock(&binfmt_lock); 18 list_for_each_entry(fmt, &formats, lh) { 19 if (!try_module_get(fmt->module)) 20 continue; 21 read_unlock(&binfmt_lock); 22 23 bprm->recursion_depth++; 24 retval = fmt->load_binary(bprm); 25 bprm->recursion_depth--; 26 27 read_lock(&binfmt_lock); 28 put_binfmt(fmt); 29 if (retval < 0 && !bprm->mm) { 30 /* we got to flush_old_exec() and failed after it */ 31 read_unlock(&binfmt_lock); 32 force_sigsegv(SIGSEGV); 33 return retval; 34 } 35 if (retval != -ENOEXEC || !bprm->file) { 36 read_unlock(&binfmt_lock); 37 return retval; 38 } 39 } 40 read_unlock(&binfmt_lock); 41 42 if (need_retry) { 43 if (printable(bprm->buf[0]) && printable(bprm->buf[1]) && 44 printable(bprm->buf[2]) && printable(bprm->buf[3])) 45 return retval; 46 if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) < 0) 47 return retval; 48 need_retry = false; 49 goto retry; 50 } 51 52 return retval; 53 }
2.2.9 start_thread()
1 start_thread(structpt_regs *regs, unsigned long new_ip, unsigned long new_sp) 2 { 3 set_user_gs(regs, 0); 4 regs->fs = 0; 5 regs->ds = __USER_DS; 6 regs->es = __USER_DS; 7 regs->ss = __USER_DS; 8 regs->cs = __USER_CS; 9 regs->ip = new_ip; 10 regs->sp = new_sp; 11 regs->flags = X86_EFLAGS_IF; 12 /* 13 * force it to the iret return path by makingit look as if there was 14 * some work pending. 15 */ 16 set_thread_flag(TIF_NOTIFY_RESUME); 17 }
2.2.10 execve系统调用的过程
1)execve系统调用陷入内核,并传入命令行参数和shell上下文环境
2)execve陷入内核的第一个函数:do_execve,do_execve封装命令行参数和shell上下文
3)do_execve调用do_execve_common,do_execve_common打开ELF文件并把所有的信息一股脑的装入linux_binprm结构体
4)do_execve_common中调用search_binary_handler,寻找解析ELF文件的函数
5)search_binary_handler找到ELF文件解析函数load_elf_binary
6)load_elf_binary解析ELF文件,把ELF文件装入内存,修改进程的用户态堆栈(主要是把命令行参数和shell上下文加入到用户态堆栈),修改进程的数据段代码段
7)load_elf_binary调用start_thread修改进程内核堆栈(特别是内核堆栈的ip指针)
8)进程从execve返回到用户态后ip指向ELF文件的main函数地址,用户态堆栈中包含了命令行参数和shell上下文环境
2.3 分析execve系统调用中断上下文的特殊之处
2.3.1 和普通系统系统调用对比
当前的可执?程序在执?,执?到execve系统调?时陷?内核态,在内核???do_execve加载可执??件,把当前进程的可执?程序给覆盖掉
。当execve系统调?返回 时,返回的已经不是原来的那个可执?程序了,?是新的可执?程序。
execve返回的是新的可执?程序执?的起点,静态链接的可执??件也就是main函数的?致位置,动态链接的可执??件还需 要ld链接好动态链接库再从main函数开始执?。
Linux系统?般会提供了execl、execlp、execle、execv、execvp和execve
等6个?以加载执? ?个可执??件的库函数,这些库函数统称为exec函数,差异在于对命令?参数和环境变量参数 的传递?式不同。
exec
函数都是通过execve
系统调?进?内核,对应的系统调?内核处理函数为sys_execve
或__x64_sys_execve
,它们都是通过调?do_execve
来具体执?加载可执??件的?作。
2.3.2 整体的调?的递进关系
sys_execve()或__x64_sys_execve -> // 内核处理函数 do_execve() –> // 系统调用函数 do_execveat_common() -> // 系统调用函数 __do_execve_?le -> exec_binprm()-> // 根据读入文件头部,寻找该文件的处理函数 search_binary_handler() -> load_elf_binary() -> // 加载elf文件到内存中 start_thread() // 开始新进程
2.4 分析fork子进程启动执行时进程上下文的特殊之处
2.4.1 和普通系统系统调用对比
1)正常的?个系统调?都是陷?内核态,再返回到?户态,然后继续执?系统调?后的下?条指令。
2)fork和其他系统调?不同之处是它在陷?内核态之后有两次返回
,第?次返回到原来的?进程的位置继续向下执?,这和其他的系统调?是?样的。
3)在?进程中fork也返回了?次,会返回到?个特 定的点——ret_from_fork,通过内核构造的堆栈环境,它可以正常系统调?返回到?户态
2.4.2 do_fork()主要源码
1 long do_fork(unsigned long clone_flags, 2 unsigned long stack_start, 3 unsigned long stack_size, 4 int __user *parent_tidptr, 5 int __user *child_tidptr) 6 { 7 struct task_struct *p; 8 int trace = 0; 9 long nr; 10 11 <strong>p = copy_process(clone_flags, stack_start, stack_size, 12 child_tidptr, NULL, trace); 13 </strong> 14 if (!IS_ERR(p)) { 15 struct completion vfork; 16 struct pid *pid; 17 18 trace_sched_process_fork(current, p); 19 20 pid = get_task_pid(p, PIDTYPE_PID); 21 nr = pid_vnr(pid); 22 23 if (clone_flags & CLONE_PARENT_SETTID) 24 put_user(nr, parent_tidptr); 25 26 if (clone_flags & CLONE_VFORK) { 27 p->vfork_done = &vfork; 28 init_completion(&vfork); 29 get_task_struct(p); 30 } 31 32 <strong>wake_up_new_task(p); 33 </strong> 34 if (clone_flags & CLONE_VFORK) { 35 if (!wait_for_vfork_done(p, &vfork)) 36 ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid); 37 } 38 39 put_pid(pid); 40 } else { 41 nr = PTR_ERR(p); 42 } 43 return nr; 44 }
2.4.3 do_fork()主要做的工作
1)调用了 copy_process 函数,复制当前进程产生子进程,并且传入关键参数为子进程设置响应进程上下文;
2)调用 wake_up_new_task 函数,将子进程放入调度队列中,从而有机会 CPU 调度并得以运行。
2.4.4 copy_process() 主要做的工作
1)调用 dup_task_struct 复制一份task_struct结构体,作为子进程的进程描述符;
2)初始化与调度有关的数据结构,调用了sched_fork,这里将子进程的state设置为TASK_RUNNING;
3)复制所有的进程信息,包括fs、信号处理函数、信号、内存空间(包括写时复制)等;
4)调用copy_thread,这里设置了子进程的堆栈信息;
5)为子进程分配一个pid。
2.5.5 copy_thread() 主要做的工作
1)对子进程的thread.sp赋值,即子进程 esp 寄存器的值;
2)将父进程的寄存器信息复制给子进程;
3)将子进程的eax寄存器值设置为0,所以fork调用在子进程中的返回值为0;
4)子进程从ret_from_fork开始执行,所以它的地址赋给thread.ip,也就是将来的eip寄存器。
2.5 以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程
2.5.1 中断上下文
为了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行
跟进程上下文不同,中断上下文切换并不涉及到进程的用户态。所以,即便中断过程打断了一个正处于用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必需的状态,包括CPU寄存器、内核堆栈、硬件中断参数等。
对同一个CPU来说,中断处理比进程拥有更高的优先级,所以中断上下文切换并不会与进程上下文切换同时发生。同样道理,由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便尽可能快的执行结束。
中断是由软硬件触发中断,查找IDT表内相应中断门,SAVE_ALL宏在栈中保存中断处理程序可能会使用的所有CPU寄存器(eflags、cs、eip、ss、esp已由硬件自动保存),并将栈顶地址保存到eax寄存器中来形成。然后中断处理程序调用do_IRQ(pt_regs*)函数,查找irq_desc数组来执行具体的中断逻辑。
2.5.2 进程上下文
进程则是资源拥有的基本单位,进程切换是由内核实现的,所以进程上下?切换过程中最关键的栈顶寄存器sp切换是通过进程描述符的thread.sp实现的,指令指针寄存器ip的切换是在内核堆栈切换的基础上巧妙利?call/ret指令实现的。 切换进程需要在
不同的进程间切换。但?般进程上下?切换是嵌套到中断上下?切换中的,?如前述系统调?作为?种中断先陷?内核,即发?中断保存现场和系统调?处理过程。其中调?了schedule函数发?进程上下?切换,当系统调?返回到?户态时会恢复现场,?此完成了保存现场和恢复现场,即完成了中断上下?切换。
进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。因此进程的上下文切换就比系统调用时多了一步:在保存当前进程的内核状态和CPU寄存器之前,需要先把该进程的虚拟内存、栈等保存下来;而加载下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。
2.5.3 分析linux系统的一般执行过程
以32位x86系统结构linux-3.18.6为例,分析正在运行的用户态进程X切换到用户态进程Y的过程
(1)正在运行的用户态进程X。 (2)发生中断(包括异常、系统调用等),CPU完成以下动作。 save cs:eip/ss:esp/eflags:当前CPU上下文压入进程X的内核堆栈。 load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack):加载当前进程内核堆栈相关信息,跳转到中断处理程序,即中断执行路径的起点。 (3)SAVE_ALL,保存现场,此时完成了中断上下文切换,即从进程X的用户态到进程X的内核态。 (4)中断处理过程中或中断返回前调用了schedule函数,其中的switch_to做了关键的进程上下文切换。将当前进程X的内核堆栈切换到进程调度算法选出来的next进程 (本例假定为进程Y)的内核堆栈,并完成了进程上下文所需的EIP等寄存器状态切换。
(5)标号1,即前述3.18.6内核的swtich_to代码第50行“”1:\t“ ”(地址为switch_to中的“$1f”),之后开始运行进程Y(这里进程Y曾经通过以上步骤被切换出去,因此可以从标号1继续执行) (6)restore_all,恢复现场,与(3)中保存现场相对应。注意这里是进程Y的中断处理过程中,而(3)中保存现场是在进程X的中断处理过程中,因为内核堆栈从进程X切换到进程Y了
(7)iret - pop cs:eip/ss:esp/eflags,从Y进程的内核堆栈中弹出(2)中硬件完成的压栈内容。此时完成了中断上下文的切换,即从进程Y的内核态返回到进程Y的用户态。 (8)继续运行用户态进程Y。
(1)正在运行的用户态进程X。 (2)发生中断(包括异常、系统调用等),CPU完成load cs:rip(entry of a specific ISR),即跳转到中断处理程序入口。 (3)中断上下文切换,具体包括如下几点: swapgs指令保存现场,可以理解CPU通过swapgs指令给当前CPU寄存器状态做了一个快照。 rsp point to kernel stack,加载当前进程内核堆栈栈顶地址到RSP寄存器。快速系统调用是由系统调用入口处的汇编代码实现用户堆栈和内核堆栈的切换。 save cs:rip/ss:rsp/rflags:将当前CPU关键上下文压入进程X的内核堆栈,快速系统调用是由系统调用入口处的汇编代码实现的。 此时完成了中断上下文切换,即从进程X的用户态到进程X的内核态。 (4)中断处理过程中或中断返回前调用了schedule函数,其中完成了进程调度算法选择next进程、进程地址空间切换、以及switch_to关键的进程上下文切换等。 (5)switch_to调用了__switch_to_asm汇编代码做了关键的进程上下文切换。将当前进程X的内核堆栈切换到进程调度算法选出来的next进程(本例假定为进程Y)的内核堆栈,并完成了进程上下文所需的指令指针寄存器状态切换。之后开始运行进程Y(这里进程Y曾经通过以上步骤被切换出去,因此可以从switch_to下一行代码继续执行)。 (6)中断上下文恢复,与(3)中断上下文切换相对应。注意这里是进程Y的中断处理过程中,而(3)中断上下文切换是在进程X的中断处理过程中,因为内核堆栈从进程X切换到进程Y了。 (7)为了对应起?中断上下文恢复的最后一步单独拿出来(6的最后一步即是7)iret - pop cs:rip/ss:rsp/rflags,从Y进程的内核堆栈中弹出(3)中对应的压栈内容。此时完成了中断上下文的切换 (8)继续运行用户态进程Y。
通过分别总结分析32位x86系统结构linux-3.18.6和x86-64系统结构linux-5.4.34 的中断(系统调用)处理过程及其中的进程上下文切换,大致上可以想象出 Linux系统中的一般执行过程在Linux系统中反复执行,其中的关键点如下。
•
•