博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
linux的用户态堆栈(sp_usr)和内核态堆栈(sp_svc)
阅读量:4056 次
发布时间:2019-05-25

本文共 3094 字,大约阅读时间需要 10 分钟。

在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就会被加载到对应的寄存器中,完成了子进程可执行程序的替换。

下图显示的为用户态栈的分布:

转载地址:http://rtlci.baihongyu.com/

你可能感兴趣的文章
通过C++反射实现C++与任意脚本(lua、js等)的交互(二)
查看>>
利用清华镜像站解决pip超时问题
查看>>
微信小程序开发全线记录
查看>>
CCF 分蛋糕
查看>>
解决python2.7中UnicodeEncodeError
查看>>
小谈python 输出
查看>>
Django objects.all()、objects.get()与objects.filter()之间的区别介绍
查看>>
python:如何将excel文件转化成CSV格式
查看>>
机器学习实战之决策树(一)
查看>>
机器学习实战之决策树二
查看>>
[LeetCode By Python]7 Reverse Integer
查看>>
[LeetCode By Python]121. Best Time to Buy and Sell Stock
查看>>
[LeetCode By Python]136. Single Number
查看>>
Android/Linux 内存监视
查看>>
Android2.1消息应用(Messaging)源码学习笔记
查看>>
iphone开发之SDK研究(待续)
查看>>
计算机网络复习要点
查看>>
Variable property attributes or Modifiers in iOS
查看>>
NSNotificationCenter 用法总结
查看>>
C primer plus 基础总结(一)
查看>>