每个task的栈分成用户栈和内核栈两部分。每个task的内核栈是8k。内核栈与current宏紧密相关,栈低地址是thread_info,栈高地址是task可以实际使用的栈空间。这样设计的目的在于屏蔽栈指针esp的低13位就可以得到thread_info,从而得到thread_info->task,也就是我们的current宏。从上面的描述可以看出,这8k栈必须在物理上连续,并且要8k地址对齐(注1)。linux内核栈与current宏的关系见图1。
图1 内核栈与current宏
pt_regs中的寄存器顺序是固定的。
下面通过图2分析一下栈是如何切换的。当cpu由ring3(用户态)变成ring0(内核态)时,用户栈切换到内核栈。过程如下:
- 在发生中断、异常时前,程序运行在用户态,ESP指向的是Interrupted Procedure's Stack,即用户栈。
- 运行下一条指令前,检测到中断(x86不会在指令执行没有指向完期间响应中断)。从TSS中取出esp0字段(esp0代表的是内核栈指针,特权级0)赋给ESP,所以此时ESP指向了Handler's Stack,即内核栈。
- cpu控制单元将用户堆栈指针(TSS中的ss,sp字段,这代表的是用户栈指针)压入栈,ESP已经指向内核栈,所以入栈指的的是入内核栈。
- cpu控制单元依次压入EFLAGS、CS、EIP、Error Code(如果有的话)。此时内核栈指针ESP位置见图4中的ESP After Transfer to Handler。
图2 stack usage with priviledge change
这里需要做个额外说明,我们这里的场景是从用户态进入内核态,所以图4是描绘得是有特权级变化时硬件控制单元自动压栈的一些寄存器。如果没有特权级变化,硬件控制单元自动压栈的寄存器见图3。
图3 stack usage with no priviledge change
图2、3区别在于如果没有发生特权级变化,硬件控制单元不会压栈SS、ESP寄存器,这2个寄存器共占用8个内存单元,如果不在内核栈高端地址处保留8个bytes,将会导致pt_regs->SS、pt_regs->ESP访问到内核栈顶端以外的地址处,也就是与内核栈高端地址相邻的另一个页中,导致缺页异常,这是一个内核bug。高端地址保留8个bytes,pt_regs->SS、pt_regs->ESP会访问到保留的8个字节单元,虽然其中的值是无效的,但是不会触发内核异常。
其他的寄存器是软件方式保存到栈上的,软件压栈的代码在linux-2.6.24/arch/x86/kernel/entry_32.S中,见SAVE_ALL宏:
1 #define SAVE_ALL 2 cld; 3 pushl %fs; 4 CFI_ADJUST_CFA_OFFSET 4; 5 /*CFI_REL_OFFSET fs, 0;*/ 6 pushl %es; 7 CFI_ADJUST_CFA_OFFSET 4; 8 /*CFI_REL_OFFSET es, 0;*/ 9 pushl %ds; 10 CFI_ADJUST_CFA_OFFSET 4; 11 /*CFI_REL_OFFSET ds, 0;*/ 12 pushl %eax; 13 CFI_ADJUST_CFA_OFFSET 4; 14 CFI_REL_OFFSET eax, 0; 15 pushl %ebp; 16 CFI_ADJUST_CFA_OFFSET 4; 17 CFI_REL_OFFSET ebp, 0; 18 pushl %edi; 19 CFI_ADJUST_CFA_OFFSET 4; 20 CFI_REL_OFFSET edi, 0; 21 pushl %esi; 22 CFI_ADJUST_CFA_OFFSET 4; 23 CFI_REL_OFFSET esi, 0; 24 pushl %edx; 25 CFI_ADJUST_CFA_OFFSET 4; 26 CFI_REL_OFFSET edx, 0; 27 pushl %ecx; 28 CFI_ADJUST_CFA_OFFSET 4; 29 CFI_REL_OFFSET ecx, 0; 30 pushl %ebx; 31 CFI_ADJUST_CFA_OFFSET 4; 32 CFI_REL_OFFSET ebx, 0; 33 movl $(__USER_DS), %edx; 34 movl %edx, %ds; 35 movl %edx, %es; 36 movl $(__KERNEL_PERCPU), %edx; 37 movl %edx, %fs
另外还有一个问题是thread_struct中的sp和sp0两个地址的区别
1 struct thread_struct { 2 unsigned long rsp0; 3 unsigned long rsp; 4 unsigned long userrsp; /* Copy from PDA */ 5 unsigned long fs; 6 unsigned long gs; 7 unsigned short es, ds, fsindex, gsindex; 8 /* Hardware debugging registers */ 9 unsigned long debugreg0; 10 unsigned long debugreg1; 11 unsigned long debugreg2; 12 unsigned long debugreg3; 13 unsigned long debugreg6; 14 unsigned long debugreg7; 15 /* fault info */ 16 unsigned long cr2, trap_no, error_code; 17 /* floating point info */ 18 union i387_union i387 __attribute__((aligned(16))); 19 /* IO permissions. the bitmap could be moved into the GDT, that would make 20 switch faster for a limited number of ioperm using tasks. -AK */ 21 int ioperm; 22 unsigned long *io_bitmap_ptr; 23 unsigned io_bitmap_max; 24 /* cached TLS descriptors. */ 25 u64 tls_array[GDT_ENTRY_TLS_ENTRIES]; 26 } __attribute__((aligned(16)));
在解释这2个字段之前,先看看copy_thread函数,代码在linux-2.6.24/arch/x86/kernel/process_32.c中。
1 int copy_thread(int nr, unsigned long clone_flags, unsigned long esp, 2 unsigned long unused, 3 struct task_struct * p, struct pt_regs * regs) 4 { 5 struct pt_regs * childregs; 6 struct task_struct *tsk; 7 int err; 8 9 childregs = task_pt_regs(p); 10 *childregs = *regs; 11 childregs->eax = 0; 12 childregs->esp = esp; 13 14 p->thread.esp = (unsigned long) childregs; 15 p->thread.esp0 = (unsigned long) (childregs+1); 16 17 p->thread.eip = (unsigned long) ret_from_fork; 18 19 savesegment(gs,p->thread.gs); 20 21 tsk = current;
先解释一下task_pt_regs,在前面的描述中,内核栈高地址部分压入了通用寄存器及用户栈指针信息,这些寄存器作为一个整体pt_regs存放在栈高地址部分(内核struct pt_regs结构)。task_pt_regs返回的就是pt_regs的起始地址。
1 #define THREAD_SIZE_LONGS (THREAD_SIZE/sizeof(unsigned long)) 2 #define KSTK_TOP(info) 3 ({ 4 unsigned long *__ptr = (unsigned long *)(info); 5 (unsigned long)(&__ptr[THREAD_SIZE_LONGS]); 6 }) 7 8 /* 9 * The below -8 is to reserve 8 bytes on top of the ring0 stack. 10 * This is necessary to guarantee that the entire "struct pt_regs" 11 * is accessable even if the CPU haven't stored the SS/ESP registers 12 * on the stack (interrupt gate does not save these registers 13 * when switching to the same priv ring). 14 * Therefore beware: accessing the xss/esp fields of the 15 * "struct pt_regs" is possible, but they may contain the 16 * completely wrong values. 17 */ 18 #define task_pt_regs(task) 19 ({ 20 struct pt_regs *__regs__; 21 __regs__ = (struct pt_regs *)(KSTK_TOP(task_stack_page(task))-8); 22 __regs__ - 1; 23 })
KSTK_TOP(task_stack_page(task)返回内核栈高端地址处的地址值,其中-8表示从高端地址处往下偏移8个字节,参考图1。
那么什么需要保留8个字节呢?这是在2005年提交的一个patch,为了解决一个bug:
commit 5df240826c90afdc7956f55a004ea6b702df9203 [PATCH] fix crash in entry.S restore_all Fix the access-above-bottom-of-stack crash.
对于这个bug我的理解是:在图5中,如果没有特权级变化(比如说在内核态中,来了一个中断),硬件控制单元是不会压栈保存SS、ESP寄存器的,如果不保留8个字节,那么我们看到的内核栈见图4:
图4 内核栈没保存8个字节空间
图中左边内核栈中pt_regs并不含有右边红字寄存器xss、esp的值,此时,如果代码访问pt_regs->xss或者pt_regs->esp,必然访问到内核栈顶端的虚线框地址单元处,而这两个单元不属于内核栈范围,所以会导致crash。保留8 bytes内存单元,虽然避免了crash,但是需要注意如果没有特权级变化,读到的xss、esp的值是无效的。根据copy_thread函数中sp与sp0的处理方法,可以知道sp与sp0的内存位置如图5:
图5 sp与sp0位置指向