ucore操作系统实验笔记 - 重新理解中断

RdouTyping

在上一篇文章ucore操作系统实验笔记 - Lab1中,我已经比较详细地记录了中断的使用。那篇文章关于中断的重点是如何使用IDT、中断描述符和中断向量表等。这篇文章我将把重点放到另外一个地方,也就是中断的过程中如何保存和恢复现场。

CPU接收到中断信号后会做什么

  1. CPU在执行完当前程序的每一条指令后,都会去确认在执行刚才的指令过程中中断控制器(如:8259A)是否发送中断请求过来,如果有那么CPU就会在相应的时钟脉冲到来时从总线上读取中断请求对应的中断向量;

  2. CPU根据得到的中断向量(以此为索引)到IDT中找到该向量对应的中断描述符,中断描述符里保存着中断服务例程的段选择子;

  3. CPU使用IDT查到的中断服务例程的段选择子从GDT中取得相应的段描述符,段描述符里保存了中断服务例程的段基址和属性信息,此时CPU就得到了中断服务例程的起始地址,并跳转到该地址;

  4. CPU会根据CPL和中断服务例程的段描述符的DPL信息确认是否发生了特权级的转换。比如当前程序正运行在用户态,而中断程序是运行在内核态的,则意味着发生了特权级的转换,这时CPU会从当前程序的TSS信息(该信息在内存中的起始地址存在TR寄存器中)里取得该程序的内核栈地址,即包括内核态的ss和esp的值,并立即将系统当前使用的栈切换成新的内核栈。这个栈就是即将运行的中断服务程序要使用的栈。紧接着就将当前程序使用的用户态的ss和esp压到新的内核栈中保存起来;

  5. CPU需要开始保存当前被打断的程序的现场(即一些寄存器的值),以便于将来恢复被打断的程序继续执行。这需要利用内核栈来保存相关现场信息,即依次压入当前被打断程序使用的eflags,cs,eip,errorCode(如果是有错误码的异常)信息;

  6. CPU利用中断服务例程的段描述符将其第一条指令的地址加载到cs和eip寄存器中,开始执行中断服务例程。这意味着先前的程序被暂停执行,中断服务程序正式开始工作。

上面这些内容是我从ucore实验指导书上直接摘抄下来的,在之前那篇文章中,我主要关注前3步和最后一步,这篇文章,我将关注第4、5步。

特权级转换的检测

我个人觉得第4、5步应该是发生在CPU跳转到ISR(中断服务例程)之前,所以把第3步放在第5步的后面更合适,之后我会解释为什么我这么觉得。当CPU获取到IDT中的中断描述符后,会对特权级的转换进行一次检测,具体检测如下图所示:

特权级转换的检测

当CPU获取了中断描述符后,CPU会用中断描述符的DPL和当前段选择子的CPL进行比较,从而判断是否需要进行特权级的转换。同时,它还会做一些列的检测工作,比如对于硬中断而言,CPL一定要大于等于DPL,因为特权级是向着更高特权级或者平级转换的。而对于软中断而言,转换后的特权级不能超过转换前的特权级,这是为了防止用户代码随意触发中断。对于CPL和DPL不同的情况,我们需要使用TSS来对内核栈进行切换,关于TSS的内容我之后会单独开篇文章。

内核栈的变化

第4、5步一个重要的功能就是向内核栈中压入各种寄存器。压入这些寄存器既可以起到保存现场的作用,又能让ISR知道中断的各种信息,所以这两步是很重要的。我们来看看哪些寄存器是CPU必须压入内核栈的:

内核栈的变化

这是发生中断并且特权级转换后栈空间变化的示意图,对于不发生特权级转换的中断,有两个地方不同,第一,它只用到一个栈,也就是说Procedure和Handler用的是同一个栈;第二,CPU不需要压入SS和ESP。除此之外,这两种情况都需要压入CS,EIP和Error Code(如果有的话)。之所以我说第3步应该在第5步后,原因就在这里,如果先跳到了ISR,那么压入的EIP就是ISR中的EIP了,并不是中断前的EIP,因此我们应该在第3步前完成步骤4和5。

Trapframe和ISR

除了CPU要压入的各种寄存器,我们还需要压入其他一些寄存器用于保存现场和提供给ISR中断信息。在ucore中,我们使用结构体trapframe来将保存的寄存器传给ISR。下面就先来看看trapframe:

/* registers as pushed by pushal */
struct pushregs {
    uint32_t reg_edi;
    uint32_t reg_esi;
    uint32_t reg_ebp;
    uint32_t reg_oesp;          /* Useless */
    uint32_t reg_ebx;
    uint32_t reg_edx;
    uint32_t reg_ecx;
    uint32_t reg_eax;
};

struct trapframe {
    struct pushregs tf_regs;
    uint16_t tf_gs;
    uint16_t tf_padding0;
    uint16_t tf_fs;
    uint16_t tf_padding1;
    uint16_t tf_es;
    uint16_t tf_padding2;
    uint16_t tf_ds;
    uint16_t tf_padding3;
    uint32_t tf_trapno;
    /* below here defined by x86 hardware */
    uint32_t tf_err;
    uintptr_t tf_eip;
    uint16_t tf_cs;
    uint16_t tf_padding4;
    uint32_t tf_eflags;
    /* below here only when crossing rings, such as from user to kernel */
    uintptr_t tf_esp;
    uint16_t tf_ss;
    uint16_t tf_padding5;
} __attribute__((packed));

其中pushregs中的寄存器都是pushal中需要压入栈的所有寄存器。有了这个数据结构后,我们就可以在中断后获取中断的信息,并将它传给ISR,ISR会根据传入的trapframe来进行相应的操作。
下面我们来看看如何给trapframe赋值,如何将trapframe传给ISR:

.globl vector2
vector2:
  pushl $0
  pushl $2
  jmp __alltraps

上面这段代码是中断向量2,在第6步时CPU会执行这里的指令。它首先压入0和2,0是error code(对于没有error code的中断,ISR会压入0作为error code;如果中断有error code,这里就不会压入0),2是中断向量号。注意,在这之前,CPU已经压入了EFLAGS,CS,EIP和Error Code(如果有的话)。在压入error code和中断向量号后,CPU跳到__alltraps,__alltraps会将所有中断需要保存的寄存器存到内核栈,然后将此时栈顶的地址($esp)作为参数传给trap(),trap()会将此时栈中压入的各种寄存器整体当成trapframe来处理。trap()会会根据trapframe中的内容,对中断进行相应的处理。

.text
.globl __alltraps
__alltraps:
    # push registers to build a trap frame
    # therefore make the stack look like a struct trapframe
    pushl %ds
    pushl %es
    pushl %fs
    pushl %gs
    pushal

这段代码将所有中断需要保存的寄存器压入内核栈。

 # load GD_KDATA into %ds and %es to set up data segments for kernel
    movl $GD_KDATA, %eax
    movw %ax, %ds
    movw %ax, %es

这段代码将此时的数据段和附加段设置为内核的数据段(ISR是位于kernel的)。

 # push %esp to pass a pointer to the trapframe as an argument to trap()
    pushl %esp

    # call trap(tf), where tf=%esp
    call trap

这段代码先将%esp的值压入内核栈,%esp的值将作为函数trap()的参数,然后我们再call trap。通过向栈中压入各种寄存器的信息并且将栈顶的地址作为trapframe的地址,我们完成了对trapframe的赋值。trap()函数接收到trapframe后就可以根据中断类型做出相应处理了。我们来看看此时栈中的情况:

内核栈

因为栈是从高地址向低地址生长的,因此,栈中蓝色部分EFLAGS地址最高,EDI地址最低。这个和trapframe中的元素也是吻合的,tf_eflags地址最高(如何不考虑tf_esp, tf_ss),而reg_edi地址最低。因此我们可以通过Old ESP这个地址,把栈中蓝色部分当成trapframe来处理。

# pop the pushed stack pointer
    popl %esp

    # return falls through to trapret...
.globl __trapret
__trapret:
    # restore registers from stack
    popal

    # restore %ds, %es, %fs and %gs
    popl %gs
    popl %fs
    popl %es
    popl %ds

    # get rid of the trap number and error code
    addl $0x8, %esp
    iret

当trap()运行结束后,我们需要将寄存器恢复到中断前的状态。在这里,我们只需要将内核栈中的内容分别弹出,并保存到相应的寄存器即可。最后,通过调用iret指令来恢复EIP,CS和EFLAGS。如果还存在特权级的转化,我们还需要弹出之前保存的SS和ESP。到此为止,整个中断的过程就结束了。

阅读 4.5k

编程之道
这个专栏都是我自己在编程方面的经验总结,主要是C和Linux相关内容,希望大家多多指教!
1k 声望
93 粉丝
0 条评论
1k 声望
93 粉丝
文章目录
宣传栏