一.前置知识
正常触发系统调?时,?户态通过syscall指令触发系统调?, 跳转到系统调???的汇编代码。syscall指令触发entry_SYSCALL_64并以sysret或iret返回系统调?
系统调?陷?内核态,从?户态堆栈转换到内核态堆栈,然后把相应的CPU关键的现场栈顶寄存器、指令指针寄存器、标志寄存器等保存到内核堆栈,保存现场。系统调???的汇编代码还会通过系统调?号执?系统调?内核处理函数, 最后恢复现场和系统调?返回将CPU关键现场栈顶寄存器、指令指针寄存器、 标志寄存器等从内核堆栈中恢复到对应寄存器中,并回到?户态int $0x80或 syscall指令之后的下?条指令的位置继续执?。
以上是一般系统调用的过程,fork以及execve也一样有这些步骤,以下分析他们执行过程中特别的地方。
下面是一个简化的bash程序
int main()
{
pid_t pid=fork();
if(pid==-1)
{
perror("fork");
return -1;
}
else if(pid==0) //子进程执行的代码
{
execlp("/bin/ls", "ls", NULL);
}
else //父进程执行的代码
{
wait(NULL);
printf("father done\n");
}
return 0;
}
由上面代码可以看出,在bash的执行过程中会调用fork和execlp,虽然bash执行过程中执行的是56号(64位)系统调用clone,而不是57号(64位)系统调用fork,不过他们最终都是调用的do_fork。所以其实不需要专门写程序触发fork以及execve,直接在bash执行任何一条命令就可以触发fork和execlp系统调用。
二.分析fork
直接运行内核
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"
在bash中执行ls命令,并且对于_do_fork进行断点,可以看到程序执行到了_do_fork。
_do_fork具体进程的创建?概就是把当前进程的描述符等相关进程资 源复制?份,从?产??个?进程,并根据?进程的需要对复制的进 、程描述符做?些修改,然后把创建好的?进程放?运?队列。
在_do_fork中可以看到调用了copy_process用于复制进程描述符和执?时所需的其他数据结构 ,调用wake_up_new_task将?进程添加到就绪队列 。
copy_process中调用了dup_task_struct复制当前进程(?进程)描述符task_struct和copy_thread_tls初始化?进程内核栈。
在调用copy_thread_tls的时候,在gdb中使用bt命令可以看到此时的调用栈。
如下图所示,fork和其他系统调?不同之处是它在陷?内核态之后有两次返回,第?次返回到原来的?进程的位置继续向下执?,这和其他的系统调?是?样的。
第二次是子进程被创建出来后,开始执行的地方是ret_from_fork,所以在子进程内核堆栈里存入了struct inactive_task_frame frame用于系统调用返回的上下文。
三.分析execve系统调用
execve和其他系统调?不同之处是加载完新的可执?程序之后已经覆盖了原来?进程的上下?环境。
同之前所说,在bash输入命令如ls的过程中execve便会被触发。
通过调用栈可以看出execve的调用关系为:
__x64_sys_execve -> do_execve() –> do_execveat_common() -> __do_execve_?le -> exec_binprm()-> search_binary_handler() -> load_elf_binary() -> start_thread()
**
do_execve中将加载可执??件,把当前进程的可执?程序给覆盖掉。当execve系统调?返回时,返回的已经不是原来的那个可执?程序了,?是新的可执?程序。execve返回的是新的可执? 程序执?的起点,静态链接的可执??件也就是main函数的?致位置,动态链接的可执??件还需 要ld链接好动态链接库再从main函数开始执?。
四.Linux系统的一般执行过程
进程调度的时机?般都是中断处理后和中断返回前的时机点进行,只有内核线程可以直接调?schedule函数主动发起进程调度和进程切换。
进程调度的时机主要根据中断上下文的切换是还是进程上下文的切换是分为两类
中断上下文发生的进程调度是指:用户进程上下?中主动调?特定的系统调用进?中断上下?,系统调用返回用户态之前进行进程调度。或者内核线程或可中断的中断处理程序,执行过程中发?中断进?中断上下文,在中断返回前进行进程调度。
进程上下文发生的进程调度是指内核线程主动调?schedule函数进?进程调度。
中断上下?和进程上下?的?个关键区别是堆栈切换的方法。中断是由CPU实现的,所以中断上下?切换过程中最关键的栈顶寄存器sp和指令指针寄存器ip是由CPU协助完成的;进程切换是由内核实现的,所以进程上下?切换过程中最关键的栈顶寄存器sp切换是通过进程描述符的thread.sp实现的,指令指针寄存器ip的切换是在内核堆栈切换的基础上巧妙利?call/ret指令实现的。
以系统调用作为特殊的中断简要总结如下(64位下):
(1)正在运行的?户态进程X。
(2)发生中断(包括异常、系统调用等),CPU完成load cs:rip(entry of a speci?c ISR),即跳转到中断处理程序??。
(3)中断上下文切换,具体包括如下?点:
swapgs指令保存现场,可以理解CPU通过swapgs指令给当前CPU寄存器状态做了?个快照。
rsp point to kernel stack,加载当前进程内核堆栈栈顶地址到RSP寄存器。快速系统调?是由系统调用入口处的汇编代码实现?户堆栈和内核堆栈的切换。
save cs:rip/ss:rsp/r?ags:将当前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/r?ags,从Y进程的内核堆栈中弹出(3)中对应的压栈内容。此时完 成了中断上下?的切换,即从进程Y的内核态返回到进程Y的?户态。注意快速系统调?返回sysret与iret的处理略有不同。
(8)继续运行用户态进程Y。