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

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

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

  1. CPU在执行完当前程序的每一条指令后,都会去确认在执行刚才的指令过程中中断控制器(如:8259A)是否发送中断请求过来,如果有那么CPU就会在相应的时钟脉冲到来时从总线上读取中断请求对应的中断向量;
  1. CPU根据得到的中断向量(以此为索引)到IDT中找到该向量对应的中断描述符,中断描述符里保存着中断服务例程的段选择子;
  2. CPU使用IDT查到的中断服务例程的段选择子从GDT中取得相应的段描述符,段描述符里保存了中断服务例程的段基址和属性信息,此时CPU就得到了中断服务例程的起始地址,并跳转到该地址;
  3. CPU会根据CPL和中断服务例程的段描述符的DPL信息确认是否发生了特权级的转换。比如当前程序正运行在用户态,而中断程序是运行在内核态的,则意味着发生了特权级的转换,这时CPU会从当前程序的TSS信息(该信息在内存中的起始地址存在TR寄存器中)里取得该程序的内核栈地址,即包括内核态的ss和esp的值,并立即将系统当前使用的栈切换成新的内核栈。这个栈就是即将运行的中断服务程序要使用的栈。紧接着就将当前程序使用的用户态的ss和esp压到新的内核栈中保存起来;
  4. CPU需要开始保存当前被打断的程序的现场(即一些寄存器的值),以便于将来恢复被打断的程序继续执行。这需要利用内核栈来保存相关现场信息,即依次压入当前被打断程序使用的eflags,cs,eip,errorCode(如果是有错误码的异常)信息;
  5. 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。到此为止,整个中断的过程就结束了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容