Linux系统上,一个进程有两种不同的栈,一种是用户栈,另一种是内核栈。用户栈

用户栈就是应用程序直接使用的栈。如右图所示,它坐落应用程序的用户进程空间的最顶端。

当用户程序逐级调用函数时,用户栈从高地址向低地址方向扩充,每次降低一个栈帧,一个栈帧中储存的是函数的参数、返回地址和局部变量等linux内核打印调用栈,所以栈帧的厚度是不定的。

用户栈的栈底紧靠进程空间的上边沿,但通常不会正好对齐到边沿,出于安全考虑linux 常用命令,会在栈底与进程上边沿之间插入一段随机大小的隔离区。这样,程序在每次运行时linux内核打印调用栈,栈的位置都不同,这样黑客就不大容易借助基于栈的安全漏洞来施行功击。

linux内核打印调用栈_linux内核栈_内核打印命令

用户栈的伸缩对于应用程序来说是透明的,应用程序不须要自己去管理栈,这是操作系统提供的功能。应用程序在刚才启动的时侯(由fork()系统调用复制出新的进程),新的进程也许并不占有任何栈的空间。当应用程序中调用了函数须要压栈时,会触发一个pagefault,内核在处理这个异常里会发觉进程须要新的栈空间,于是完善新的VMA并映射显存给用户栈。

内核栈

内核栈对于应用程序是不可见的,由于它坐落内核空间中。在应用程序执行过程中,假如发生异常、中断或系统调用的话,应用程序会被暂停,系统步入内核态,转去执行异常响应等代码,这个时侯所使用的栈就是内核栈。

内核打印命令_linux内核打印调用栈_linux内核栈

与用户栈相比,内核栈的规格要小得多。在32位Linux系统上,用户栈最多可以扩充到64M,但内核栈最多也只有8K字节,并且有时为了提升显存借助率还经常把内核栈配置成4K。虽然虽然是只有4K,在绝大多数情况下也依然是够用的,由于这儿只是给内核代码使用的,栈不会很大。

每位进程在内核空间中都拥有一个对应的内核栈,但是这个栈是在进程fork的时侯就预留出来的。以下是创建内核栈的代码(Kernel2.6.35版本):

[c]staticstructtask_struct*dup_task_struct(structtask_struct*orig){structtask_struct*tsk;structthread_info*ti;……ti=alloc_thread_info(tsk);……tsk->stack=ti;……}[/c]

内核栈的结构比较精致,内核使用一个联合体来定义内核栈:

内核打印命令_linux内核打印调用栈_linux内核栈

[c]unionthread_union{structthread_infothread_info;unsignedlongstack[THREAD_SIZE/sizeof(long)];};[/c]

其中thread_info中储存了进程/线程(内核不大区分进程与线程)的一些数据,其中包括指向task_struct结构的表针。字段stack即内核栈,stack抢占8K/4K(依配置不同)空间,是这个联合体的主要部份。

这样,一个实际的内核栈的结构将如右图所示。因为栈总是由高地址向低地址延展的,所以栈底坐落thread_union联合体的最末端,而thread_info结构则坐落thread_union联合体的开始处,并且所占用的空间比较少。只要不出现内核栈非常大的极端情况,栈与thread_info可以互不干扰。

为何要设计成这样的结构呢?缘由就在于,使用这些结构可以在系统步入内核态时很便捷地取得当前进程的信息。倘若不用这些方法的话,取得task_struct将是一个比较麻烦的事情。

内核打印命令_linux内核打印调用栈_linux内核栈

不管系统由于哪些缘由步入内核态,最后都要切换到SVC模式做主要的异常处理。在步入SVC模式时,SP/R13寄存器所指向的位置就刚好是当前进程的内核栈。通过简单的对齐操作,就可以领到thread_union即thread_info结构的表针,从中又可以得到最重要的task_struct的表针,这个进程的所有信息就都有了。

以下两个函数即分别用于从SP寄存器取得当前进程的thread_info,以及进一步取得task_struct结构的内容。

[c]staticinlinestructthread_info*current_thread_info(void){registerunsignedlongspasm(“sp”);return(structthread_info*)(sp&~(THREAD_SIZE-1));}staticinlinestructtask_struct*get_current(void){returncurrent_thread_info()->task;}[/c]

关于SP寄存器,这儿有一个问题值得澄清一下,上面提及“在步入SVC模式时,SP/R13寄存器所指向的位置就刚好是当前进程的内核栈”,缘由是哪些呢?

在当前进程即当前被异常或中断所暂停的这个进程,是在上一次发生进程调度(schedule())的时侯被调入的,当时在“上下文切换”(context_switch())完成的时侯,当前这个进程可以说早已被调入了CPU,系统当时所处的模式也是SVC模式。当进程高度完成,CPU从SVC模式切换到USR模式时侯,SVC模式下的SP寄存器早已指向了当前进程的内核栈。所以当再度切换到SVC模式时,进程还是这个进程,SP也还是指向这个内核栈。

内核打印命令_linux内核打印调用栈_linux内核栈

虽然ARM处理器的每一种模式下都有自己独立的SP/R13寄存器。当CPU在不同的模式间切换的时侯所见到的寄存器内容都是不同的。Linux对于各类模式的使用策略是:SVC和USR两种模式是可以稳定工作的模式;在其它的模式下都是不稳定的,会尽早切换到稳定的模式去工作。在SVC模式下,SP寄存器总是指向内核栈;在USR模式下,SP寄存器总是指向用户栈;这么,其它模式下linux c,SP又指向那里呢?

其它模式下,Linux对于SP寄存器的维护很简单。在系统启动阶段,cpu_init()函数会被调用,其中有对其它模式下SP寄存器的初始化操作:

[c]structstack{u32irq[3];u32abt[3];u32und[3];}____cacheline_aligned;staticstructstackstacks[NR_CPUS];voidcpu_init(void){unsignedintcpu=smp_processor_id();structstack*stk=&stacks[cpu];__asm__(“msrcpsr_c,%1nt””addr14,%0,%2nt””movsp,r14nt””msrcpsr_c,%3nt””addr14,%0,%4nt””movsp,r14nt””msrcpsr_c,%5nt””addr14,%0,%6nt””movsp,r14nt””msrcpsr_c,%7″::”r”(stk),PLC(PSR_F_BIT|PSR_I_BIT|IRQ_MODE),”I”(offsetof(structstack,irq[0])),PLC(PSR_F_BIT|PSR_I_BIT|ABT_MODE),”I”(offsetof(structstack,abt[0])),PLC(PSR_F_BIT|PSR_I_BIT|UND_MODE),”I”(offsetof(structstack,und[0])),PLC(PSR_F_BIT|PSR_I_BIT|SVC_MODE):”r14”);}[/c]

可以看见,Linux为IRQABTUND3种模式的SP寄存器指定了相应的栈,然而这个栈很小很小,只有12个字节。但这早已足够了,在中断处理最初那部份相应模式的代码vector_XXX中,linux只保存r0,lr和spsr三个32位的数据,这刚好须要12个字节。

Tagged:
Author

这篇优质的内容由TA贡献而来

刘遄

《Linux就该这么学》书籍作者,RHCA认证架构师,教育学(计算机专业硕士)。

发表回复