在arm linux中,进程的运行处于两种模式之一,要么在用户空间运行(用户模式USR_MODE),要么在内核空间运行(SVC_MODE)。在内核空间时,处于特权模式,在用户空间时,处于普通模式。
用户空间运行时和内核空间运行时,所用的堆栈是不同的。
本文基于linux-3.11.1代码来学习这两种运行空间下,堆栈是如何运作的(切换)。
linux-3.11.1/kernel/fork.c
SYSCALL_DEFINE0(fork) -> do_fork() -> copy_process() -> copy_thread()
本文基于ARM来学习,这里copy_thread()定义在linux-3.11.1/arch/arm/kernel/process.c
367行设置task的内核空间的栈指针为childregs.
linux-3.11.1/arch/arm/include/asm/process.h
linux-3.11.1/arch/arm/include/asm/thread_info.h
linux-3.11.1/include/linux/sched.h
task->stack 为thread_info,在task_struct之后,故,内核态堆栈的位置如下:
注意这里,pt_regs中的所有寄存器,除了r0外的其他寄存器的值,对于非内核线程,全部拷贝自父进程,r0寄存器设为0,即子进程fork()的返回值(pid)为0.
copy_thread()除了设置了内核态栈的地方后,还设置了pc指针为ret_from_fork. 注意,此时这个新创建的task并没有run,只是被创建了,等到schedule()到自己时才真正开始run。
假设,某个时候,系统的schedule()被出发,且选到了这个新创建的task来运行。
linux-3.11.1/kernel/sched/core.c
schedule() -> __schedule() -> context_switch() -> context_switch()
context_switch()调用switch_mm切换进程页表, 然后调用switch_to()加载task的pc, sp等寄存器。
linux-3.11.1/arch/arm/include/asm/switch_to.h
linux-3.11.1/arch/arm/kernel/entry-armv.S
708行,r4指向新创建的task的thread_info中的cpu_context_save结构,即r4 = &task.stack. cpu_context, 所以717行加载这个结构里面保存的各寄存器的值后,sp指向了前面图示的位置,pc则指向的是ret_from_fork。
新创建的进程运行时,执行ret_from_fork处指令。
linux-3.11.1/arch/arm/kernel/entry-common.S
ret_from_fork跳转到ret_slow_syscall处执行:
linux-3.11.1/arch/arm/kernel/entry-common.S
ret_slow_syscall又调用restore_user_regs.
linux-3.11.1/arch/arm/kernel/entry-common.S
restore_user_regs调用load_user_sp_lr,将sp切到pt_regs.sp,lr切到pt_regs.lr.
177行,先切到system模式,system模式和user模式共享sp_usr;然后180行从内核态堆栈rd后面的pt_regs.sp中取出用户态堆栈,并设置到sp寄存器(sp_usr寄存器,由于此时是用户态模式,sp为sp_usr,而非sp_svc);接着181行取出用户态的lr,并设置到lr_usr,这样,一旦模式切换到用户态, move pc, lr,pc就是用户态堆栈中保存的lr了,即用户空间下一条指令。
做完这些后,184行,重新切回SVC模式,此后sp为sp_svc.
然后ldmdb加载sp的数据到r0-r12寄存器(r0在前面copy_thread()的时候设置为0 了childregs->ARM_r0 = 0).
最后(305行)通过movs pc, lr指令,返回fork()的下一条指令。特权模式在298行的时候就恢复为fork()之前的值了。
由此可见,fork()后,进程使用的用户空间栈是共享父进程的pt_regs.sp,指令也是共享父进程的pt_regs.pc.
fork()子进程后,我们通过exec函数来执行子进程程序。
先来看系统调用出发的SWI异常:
linux-3.11.1/arch/arm/kernel/entry-common.S
注意,此时为SVC模式, sp为sp_svc,vector_swi首先保存现场,拿到系统调用号,368行加载系统调用表sys_call_table,379调用系统调用程序,这里为sys_execve()。
这里有一个疑惑,sp_svc栈寄存器不需要恢复操作的吗?
答案是,是的,sp_svc只有在schedule()进程切换的时候,调用__switch_to时,才会被更新为新task.stack.cpu_context.sp。
exec函数就是替换当前进程的可执行程序(elf文件)。
SYSCALL_DEFINES(execve) -> do_execve() -> do_execve_common() -> search_binary_handler() -> load_elf_binary()
linux-3.11.1/fs/exec.c
load_elf_binary()用于解析和加载elf可执行文件到内存中。
linux-3.11.1/fs/binfmt_elf.c
load_elf_binary()调用setup_arg_pages()设置bprm->p(这个就是用户空间堆栈的位置),这里STACK_TOP为2G. randomize_stack_top(STACK_TOP)为2G-16M.
linux-3.11.1/fs/binfmt_elf.c
bprm->p在__bprm_mm_init()初始化为STACK_TOP_MAX(2G).
linux-3.11.1/fs/binfmt_elf.c
linux-3.11.1/arch/arm/include/asm/process.h
所以,bprm->p = 2G – 16M.
load_elf_binary()最后调用start_thread().
linux-3.11.1/fs/binfmt_elf.c
这里elf_entry()就是elf文件中的ENTRY()指示的地址,即main().
linux-3.11.1/arch/arm/include/asm/process.h
start_thread()更新了当前进程的pt_regs中的寄存器pc, sp, cpsr.这样,下次schedule的时候,就是运行pc指向的elf_entry处的函数(main)了。
这样,当exec()系统调用返回时,regs->ARM_pc, regs->ARM_sp就会被加载到对应的寄存器中,完成了子进程可执行程序的替换。
下图显示的为用户态栈的分布: