当前位置: 首页 > news >正文

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

在上一篇文章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。到此为止,整个中断的过程就结束了。

相关文章:

  • 评论发布信息可插入QQ表情
  • 数据中心操作人员:艰难地在针对VM构建的基础设施上运行容器
  • Event 营销热点日历
  • 使用SuperSocket快速建立Socket服务
  • JSON中JObject和JArray的修改
  • 思维的框架
  • 《数据分析实战 基于EXCEL和SPSS系列工具的实践》一3.3 耗时耗力的数据整理过程...
  • eclipse+tomcat配置远程debug调整
  • myeclipse 10 j安装了JDK1.7,java编译器无法选择到1.7的问题
  • JAVA注解引发的思考
  • 基于 Web 的 Go 语言 IDE - Wide 1.1.0 公布!
  • pageoffice相关问题整理
  • less学习笔记三
  • Dwg,png,jpg,Dxf格式转换
  • 文件备份同步rsync
  • 【刷算法】从上往下打印二叉树
  • 30秒的PHP代码片段(1)数组 - Array
  • Android交互
  • C# 免费离线人脸识别 2.0 Demo
  • css系列之关于字体的事
  • IOS评论框不贴底(ios12新bug)
  • js ES6 求数组的交集,并集,还有差集
  • overflow: hidden IE7无效
  • PermissionScope Swift4 兼容问题
  • React组件设计模式(一)
  • Spark VS Hadoop:两大大数据分析系统深度解读
  • SpiderData 2019年2月23日 DApp数据排行榜
  • 后端_MYSQL
  • 基于组件的设计工作流与界面抽象
  • 力扣(LeetCode)965
  • 通信类
  • 我是如何设计 Upload 上传组件的
  • 移动端 h5开发相关内容总结(三)
  • 原生js练习题---第五课
  • 源码之下无秘密 ── 做最好的 Netty 源码分析教程
  • 远离DoS攻击 Windows Server 2016发布DNS政策
  • 怎么将电脑中的声音录制成WAV格式
  • Salesforce和SAP Netweaver里数据库表的元数据设计
  • ​​​​​​​Installing ROS on the Raspberry Pi
  • # 透过事物看本质的能力怎么培养?
  • #14vue3生成表单并跳转到外部地址的方式
  • #中的引用型是什么意识_Java中四种引用有什么区别以及应用场景
  • (16)Reactor的测试——响应式Spring的道法术器
  • (pytorch进阶之路)CLIP模型 实现图像多模态检索任务
  • (二)学习JVM —— 垃圾回收机制
  • (附源码)apringboot计算机专业大学生就业指南 毕业设计061355
  • (附源码)python房屋租赁管理系统 毕业设计 745613
  • (附源码)ssm考试题库管理系统 毕业设计 069043
  • (附源码)基于ssm的模具配件账单管理系统 毕业设计 081848
  • (过滤器)Filter和(监听器)listener
  • (经验分享)作为一名普通本科计算机专业学生,我大学四年到底走了多少弯路
  • (转)Android学习笔记 --- android任务栈和启动模式
  • (转)IIS6 ASP 0251超过响应缓冲区限制错误的解决方法
  • *** 2003
  • .net 写了一个支持重试、熔断和超时策略的 HttpClient 实例池