在Linux系统上,一个进程有两种不同的栈,一种是用户栈,另一种是内核栈。
用户栈
用户栈就是应用程序直接使用的栈。如下图所示,它位于应用程序的用户进程空间的最顶端。
当用户程序逐级调用函数时,用户栈从高地址向低地址方向扩展,每次增加一个栈帧,一个栈帧中存放的是函数的参数、返回地址和局部变量等,所以栈帧的长度是不定的。
用户栈的栈底靠近进程空间的上边缘,但一般不会刚好对齐到边缘,出于安全考虑,会在栈底与进程上边缘之间插入一段随机大小的隔离区。这样,程序在每次运行时,栈的位置都不同,这样黑客就不大容易利用基于栈的安全漏洞来实施攻击。
用户栈的伸缩对于应用程序来说是透明的,应用程序不需要自己去管理栈,这是操作系统提供的功能。应用程序在刚刚启动的时候(由fork()系统调用复制出新的进程),新的进程其实并不占有任何栈的空间。当应用程序中调用了函数需要压栈时,会触发一个page fault,内核在处理这个异常里会发现进程需要新的栈空间,于是建立新的VMA并映射内存给用户栈。
![](http://www.2ndmoon.net/weblog/wp-content/uploads/2013/08/user_space_layout.png)
内核栈
内核栈对于应用程序是不可见的,因为它位于内核空间中。在应用程序执行过程中,如果发生异常、中断或系统调用的话,应用程序会被暂停,系统进入内核态,转去执行异常响应等代码,这个时候所使用的栈就是内核栈。
与用户栈相比,内核栈的尺寸要小得多。在32位Linux系统上,用户栈最多可以扩展到64M,但内核栈最多也只有8K字节,而且有时为了提高内存利用率还常常把内核栈配置成4K。其实即使是只有4K,在绝大多数情况下也仍然是够用的,因为这里只是给内核代码使用的,栈不会很大。
每个进程在内核空间中都拥有一个对应的内核栈,而且这个栈是在进程fork的时候就预留出来的。以下是创建内核栈的代码(Kernel 2.6.35 版本):
[c]
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
......
ti = alloc_thread_info(tsk);
......
tsk->stack = ti;
......
}
[/c]
内核栈的结构比较精巧,内核使用一个联合体来定义内核栈:
[c]
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
[/c]
其中thread_info中存放了进程/线程(内核不大区分进程与线程)的一些数据,其中包括指向task_struct结构的指针。数组stack即内核栈,stack占据8K/4K(依配置不同)空间,是这个联合体的主要部分。
这样,一个实际的内核栈的结构将如下图所示。由于栈总是由高地址向低地址延伸的,所以栈底位于thread_union联合体的最末端,而thread_info结构则位于thread_union联合体的开始处,而且所占用的空间比较少。只要不出现内核栈特别大的极端情况,栈与thread_info可以互不干扰。
![](http://www.2ndmoon.net/weblog/wp-content/uploads/2013/08/kernel_stack.png)
为什么要设计成这样的结构呢?原因就在于,使用这种结构可以在系统进入内核态时很方便地取得当前进程的信息。如果不用这种方式的话,取得task_struct将是一个比较麻烦的事情。
不管系统因为什么原因进入内核态,最后都要切换到SVC模式做主要的异常处理。在进入SVC模式时,SP/R13寄存器所指向的位置就正好是当前进程的内核栈。通过简单的对齐操作,就可以拿到thread_union即thread_info结构的指针,从中又可以得到最重要的task_struct的指针,这个进程的所有信息就都有了。
以下两个函数即分别用于从SP寄存器取得当前进程的thread_info,以及进一步取得task_struct结构的内容。
[c]
static inline struct thread_info *current_thread_info(void)
{
register unsigned long sp asm ("sp");
return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));
}
static inline struct task_struct *get_current(void)
{
return current_thread_info()->task;
}
[/c]
关于SP寄存器,这里有一个问题值得澄清一下,前面提到“在进入SVC模式时,SP/R13寄存器所指向的位置就正好是当前进程的内核栈”,原因是什么呢?
在当前进程即当前被异常或中断所暂停的这个进程,是在上一次发生进程调度(schedule())的时候被调入的,当时在“上下文切换”(context_switch())完成的时候,当前这个进程可以说已经被调入了CPU,系统当时所处的模式也是SVC模式。当进程高度完成,CPU从SVC模式切换到USR模式时候,SVC模式下的SP寄存器已经指向了当前进程的内核栈。所以当再次切换到SVC模式时,进程还是这个进程,SP也还是指向这个内核栈。
其实ARM处理器的每一种模式下都有自己独立的SP/R13寄存器。当CPU在不同的模式间切换的时候所看到的寄存器内容都是不同的。Linux对于各种模式的使用策略是:SVC和USR两种模式是可以稳定工作的模式;在其它的模式下都是不稳定的,会尽快切换到稳定的模式去工作。在SVC模式下,SP寄存器总是指向内核栈;在USR模式下,SP寄存器总是指向用户栈;那么,其它模式下,SP又指向哪里呢?
其它模式下,Linux对于SP寄存器的维护很简单。在系统启动阶段,cpu_init()函数会被调用,其中有对其它模式下SP寄存器的初始化操作:
[c]
struct stack {
u32 irq[3];
u32 abt[3];
u32 und[3];
} ____cacheline_aligned;
static struct stack stacks[NR_CPUS];
void cpu_init(void)
{
unsigned int cpu = smp_processor_id();
struct stack *stk = &stacks[cpu];
__asm__ (
"msr cpsr_c, %1\n\t"
"add r14, %0, %2\n\t"
"mov sp, r14\n\t"
"msr cpsr_c, %3\n\t"
"add r14, %0, %4\n\t"
"mov sp, r14\n\t"
"msr cpsr_c, %5\n\t"
"add r14, %0, %6\n\t"
"mov sp, r14\n\t"
"msr cpsr_c, %7"
:
: "r" (stk),
PLC (PSR_F_BIT | PSR_I_BIT | IRQ_MODE),
"I" (offsetof(struct stack, irq[0])),
PLC (PSR_F_BIT | PSR_I_BIT | ABT_MODE),
"I" (offsetof(struct stack, abt[0])),
PLC (PSR_F_BIT | PSR_I_BIT | UND_MODE),
"I" (offsetof(struct stack, und[0])),
PLC (PSR_F_BIT | PSR_I_BIT | SVC_MODE)
: "r14");
}
[/c]
可以看到,Linux为IRQ\ABT\UND3种模式的SP寄存器指定了相应的栈,但是这个栈很小很小,只有12个字节。但这已经足够了,在中断处理最初那部分相应模式的代码vector_XXX中,linux只保存r0, lr和spsr三个32位的数据,这正好需要12个字节。