说说中断上下文的切换

来源:互联网 发布:郑州学java 编辑:程序博客网 时间:2024/06/15 16:23

在深入理解linux内核课程结束之际,我觉得对中断过程的理解,太浅薄。于是又重新将孟宁老师的系统中断部分再一次的听讲了一遍,并希望能够从中理解更多有关中断的处理过程。

系统中断

在课程中,老师以系统中断为例子讲解。这里我也从系统中断开篇。
当应用程序在应用层调用系统调用时,比如调用了read函数,那么经过库函数sys_call,将会通过int 0x80进入到内核中。此时会发生什么呢?
首先,因为是中断过程,硬件会保存部分现场。硬件保存的内容包括哪些呢?

  1. 用户态栈的栈顶值
  2. 当时的状态字
  3. 当时的cs:eip的值

保存的位置在栈中保存。保存到了用户的栈上。
如果保存到了用户的栈上,那么此时sp的值到底还是不是指向栈顶的位置呢?也许是,也许不是。
特别值得提出的是,对sp的保存,也保存到了栈中,这是如何完成的呢?在扩展中进一步的分析。

理解与扩展

通过上述的硬件保存的值,我们可以知道保存了栈顶值,从而可以推测出在内核态中栈顶值将会是另外一个,也就说,内核中仍然有一个栈,是和用户态中的栈不是一起的。
保存了状态值。这个状态是在eflag寄存器的值。看来这个寄存器的值对硬件是十分重要的。虽然在软件编程的过程中,我们不需要涉及到,但是他的值可能会影响很多程序的走向。保存eflag的值,接下来更换新的eflag的时候,将会指示当前程序的模式是内核模式了。
保存eip的值,当我们返回时将会继续执行应用程序,只要把他弹出来即可。这里进一步的分析。cs:eip的值,在应用层指向了用户空间的代码段。那么保存之后,将会指向内核的代码段中的语句。cs:eip的值更新后,将会自动的指向内核空间。cs:eip的值肯定会被更新到0xC000 000之上的位置中。
刚才我们看到int 0x80保存这些值到栈中,如何保存的呢? 老师在课程中讲到是硬件完成的。硬件是如何完成的呢?不知道。这需要查芯片手册估计才能明确。
另外,int 0x80保存了sp的值到栈中,但是没有保存bp的值。是不需要保存吗?显然不是,因为用户空间的栈和内核空间的栈是两个部分。现在推论不是硬件保存的,而是软件保存的,在后文中应该可以看到。此处存疑。

软件保存现场

我们的硬件保存现场之后,在进入中断处理函数中,第一件事情是,SAVE_ALL做的事情。又继续保存现场。

那么为什么这里还需要保存现场呢?
原因是在硬件保存现场中,保存的寄存器的值还不够,还要进一步的保存。

在代码中我们找到了SAVE_ALL的调用之处:
代码位置:
arch/x86/kernel/entry.s

# system call handler stubENTRY(system_call)    RING0_INT_FRAME         # can't unwind into user space anyway    ASM_CLAC    pushl_cfi %eax          # save orig_eax    SAVE_ALL    GET_THREAD_INFO(%ebp)# system call tracing in operation/emulation    testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)    jnz syscall_trace_entry    cmpl $(NR_syscalls), %eax    jae syscall_badsyssyscall_call:    call *sys_call_table(,%eax,4)syscall_after_call:    movl %eax,PT_EAX(%esp)      # store the return value

代码的分析以后再分析。
贴出SAVE_ALL的实现:

.macro SAVE_ALL    cld    PUSH_GS    pushl_cfi %fs    /*CFI_REL_OFFSET fs, 0;*/    pushl_cfi %es    /*CFI_REL_OFFSET es, 0;*/    pushl_cfi %ds    /*CFI_REL_OFFSET ds, 0;*/    pushl_cfi %eax    CFI_REL_OFFSET eax, 0    pushl_cfi %ebp    CFI_REL_OFFSET ebp, 0    pushl_cfi %edi    CFI_REL_OFFSET edi, 0    pushl_cfi %esi    CFI_REL_OFFSET esi, 0    pushl_cfi %edx    CFI_REL_OFFSET edx, 0    pushl_cfi %ecx    CFI_REL_OFFSET ecx, 0    pushl_cfi %ebx    CFI_REL_OFFSET ebx, 0    movl $(__USER_DS), %edx    movl %edx, %ds    movl %edx, %es    movl $(__KERNEL_PERCPU), %edx    movl %edx, %fs    SET_KERNEL_GS %edx.endm

在这段代码中,保存了相关寄存器的值。
他们一次是:ES,DS,EAX,EBP,EDI,ESI,EDX,ECX,EBX等等。从这里寄存器的顺序可以知道压栈的最后压入的是ebx,这里压入的栈是内核栈。

问题

什么时候切换到内核栈的呢?通篇代码,没有看到什么时候切换栈到了内核栈中。
个人理解,什么时候将sp指向了内核中的sp呢?内核中的sp的位置在何处呢?
在开始,硬件只是保存了sp的值是不是除了保存sp的值之外,还将sp的值给更新了呢?更新成为了指向内核栈的栈顶的位置。因为只有这样,SAVE_ALL的时候,才会是保存的用户的寄存器的值,保存到了内核栈中。这样才能说得通。
目前没有查到,但是所有信息都在内核态中这个结论是和其他人说的一致的。
在老师的讲义中,老师提到了int 0x80的具体细节:
第一步硬件细节:
把cs:eip的值存储内核栈中,
把ss:esp的值存储内核栈中,
把eflags的值存储内核栈中,
第二步硬件细节:
更新cs:eip的值为0x80的中断服务函数
第三步硬件细节:
将ss:esp的值指向了内核栈中的栈顶位置。
第四步软件细节:
SAVE_ALL
在SAVE_ALL中我们看到了都是pushl的操作,其实是将这些值都存储进入了内核栈中。并且存储的部分寄存器的值,将会起到输入参数的功能。
当中断服务函数完成后,
第五步软件细节:
RESTORE_ALL
与SAVE_ALL的值是对应的。
第六步软件细节:
iret
这个iret与我们函数的ret是不同的,他是与上述第一步的硬件的细节相互对应的。
将会从内核栈中将:
eflags
ss:esp
cs:eip
这三个值依次恢复到寄存器中,并换回到用户态中来。

pop %esp问题

这里就是pop的过程
那么pop %esp的时候,是否能够完成呢?
pop的过程是
将值弹出到sp中,然后对sp的值+2.显然,这里恢复出来的sp的值不是存储在栈中的值而是栈中的值加上2.
但是,这是不是意味着我们这里的做法是错误的呢?也不一定。因为我们前面的压栈,如果是和这个对应的话,那么就是对的。
在硬件存储用户空间的sp的时候,将sp的值首先减去2,然后在存储到内核栈中,这样在iret的时候直接pop才不会出错。

ebp的问题

在内核中没有ebp的问题。因为在内核中认为ebp的值是和esp的值是一致的,也就是说,每次进入到内核空间的时候,当前的进程的内核栈是空的。sp的值就是内核ebp的值。

entry.s中内核栈指示:

0(%esp) - %ebx *   4(%esp) - %ecx *   8(%esp) - %edx *   C(%esp) - %esi *  10(%esp) - %edi *  14(%esp) - %ebp *  18(%esp) - %eax *  1C(%esp) - %ds *  20(%esp) - %es *  24(%esp) - %fs *  28(%esp) - %gs      saved iff !CONFIG_X86_32_LAZY_GS *  2C(%esp) - orig_eax *  30(%esp) - %eip *  34(%esp) - %cs *  38(%esp) - %eflags *  3C(%esp) - %oldesp *  40(%esp) - %oldss *

问题

在最后一个esp中,如果esp被pop出来的话,oldss将不能被拿到了啊?不能理解?

解惑

在课堂讨论中,向老师提出了此问题。老师给出的明确的答复是,int指令iret指令虽然干了很多事情,但是他们是原子的操作,是不会被打断的。是纯粹的硬件完成的。没有我个人理解的先后次序,也就不存在上述的疑问了。

现在的问题是,内核的sp如何知道他的位置的呢?因为内核栈是我们在fork的时候创建的。这是我还不懂的地方,需要继续查找。

参考

此外在这边文档中讲解的要比我的更加清楚
http://blog.chinaunix.net/xmlrpc.php?r=blog/article&id=4739291&uid=14528823

0 0