进程切换时栈的变化

  每个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(内核态)时,用户栈切换到内核栈。过程如下:

  1. 在发生中断、异常时前,程序运行在用户态,ESP指向的是Interrupted Procedure's Stack,即用户栈。
  2. 运行下一条指令前,检测到中断(x86不会在指令执行没有指向完期间响应中断)。从TSS中取出esp0字段(esp0代表的是内核栈指针,特权级0)赋给ESP,所以此时ESP指向了Handler's Stack,即内核栈。
  3. cpu控制单元将用户堆栈指针(TSS中的ss,sp字段,这代表的是用户栈指针)压入栈,ESP已经指向内核栈,所以入栈指的的是入内核栈。
  4. 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位置指向

 

posted @ 2017-01-13 17:18  penghan  阅读(2945)  评论(0编辑  收藏  举报