• 进程切换时栈的变化


      每个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位置指向

  • 相关阅读:
    存储过程
    输入http://localhost/,apache出现You don't have permission to access/on this server.的提示,如何解决?
    搭建内网的NTP时间服务器
    cobbler自动化安装系统
    Linux三剑客之awk最佳实践
    ansible学习笔记
    2021.05.07 多线程之可重入锁
    2021.05.08 easyExcel简单读写
    2021.05.03 Java常用文件路径
    2021.05.04 二维码生成与解析
  • 原文地址:https://www.cnblogs.com/chaozhu/p/6283495.html
Copyright © 2020-2023  润新知