保护模式中断






保护模式下Intel 80x86 CPU 硬件中断过程

 

大学学操作系统原理的时候,感觉进程和文件似乎是介绍最多的两块内容,但慢慢发现,要想理解清楚操作系统的工作机制,如果按知识学习的先后顺序排,中断应当是比较靠前的才对,只有理解了中断的机制,才有可能真正理解进程,文件系统,设备等等其他的概念。

中断实际上为OS里很多概念的具体实现提供了一个基本的保证,比如进程的调度、设备的访问、用户态和内核态的切换,各种异常的处理等等都需要中断的参与,甚至连对临界资源的安全访问也需要中断的支持,这足以说明中断是可以作为一个了解操作系统工作原理和具体机制的入口点的,而不仅仅只是作为相关书籍里独立的一个章节来理解。

       由于中断的机制和实现是和硬件密切相关的,这里只总结一下Intel 80x86系列CPU的中断在硬件层面上处理过程。

 

1. 中断和异常的概念区别

       Intel的官方文档[1]里将中断和异常理解为两种中断当前程序执行的不同机制。这是中断和异常的共同点。不同点在于:

       中断(interrupt)是异步的事件,典型的比如由I/O设备触发;

 异常(exception)是同步的事件,典型的比如处理器执行某条指令时发现出错了等等。

       中断又可以分为可屏蔽中断和非可屏蔽中断,异常又分为故障、陷阱和异常中止3种,它们的具体区别很多书籍和官方文档都解释的比较清楚这里不再赘述。

关于它们的区别有两点是需要注意的:

1)平常所说的屏蔽中断是不包括异常的,即异常不会因为CPU的IF位被清(关中断,指令:cli)而受影响,比如缺页异常,即使关了中断也会触发CPU的处理。

2)通常说的int 80h这种系统调用使用的中断方式实际上硬件上是理解为异常处理的,因此也不会被屏蔽掉,这也很好理解,int 80h这种中断方式是程序里主动触发的,对于CPU来说属于同步事件,因此也就属于异常的范畴。

 

2. 中断(异常)处理过程

       需要明确的一点是CPU对于中断和异常的具体处理机制本质上是完全一致的,即:

当CPU收到中断或者异常的信号时,它会暂停执行当前的程序或任务,通过一定的机制跳转到负责处理这个信号的相关处理程序中,在完成对这个信号的处理后再跳回到刚才被打断的程序或任务中。这里只描述保护模式下的处理过程,搞清楚了保护模式下的处理过程(更复杂),实模式下的处理机制也就容易理解了。

 

具体的处理过程如下:

0)中断响应的事前准备:

系统要想能够应对各种不同的中断信号,总的来看就是需要知道每种信号应该由哪个中断服务程序负责以及这些中断服务程序具体是如何工作的。系统只有事前对这两件事都知道得很清楚,才能正确地响应各种中断信号和异常。

[a] 系统将所有的中断信号统一进行了编号(一共256个:0~255),这个号称为中断向量,具体哪个中断向量表示哪种中断有的是规定好的,也有的是在给定范围内自行设定的。  

中断向量和中断服务程序的对应关系主要是由IDT(中断向量表)负责。操作系统在IDT中设置好各种中断向量对应的中断描述符(一共有三类中断门描述符:任务门、中断门和陷阱门),留待CPU查询使用。而IDT本身的位置是由idtr保存的,当然这个地址也是由OS填充的。

下面的示意图显示了IDT的基本结构和IDTR是如何指示IDT的位置和长度的:

 

 

 

[b] 中断服务程序具体负责处理中断(异常)的代码是由软件,也就是操作系统实现的,这部分代码属于操作系统内核代码。也就是说从CPU检测中断信号到加载中断服务程序以及从中断服务程序中恢复执行被暂停的程序,这个流程基本上是硬件确定下来的,而具体的中断向量和服务程序的对应关系设置和中断服务程序的内容是由操作系统确定的。

 

1)CPU检查是否有中断/异常信号

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

对于异常和系统调用那样的软中断,因为中断向量是直接给出的,所以和通过IRQ(中断请求)线发送的硬件中断请求不同,不会再专门去取其对应的中断向量。

 

2)根据中断向量到IDT表中取得处理这个向量的中断程序的段选择符

       CPU根据得到的中断向量到IDT表里找到该向量对应的中断描述符,中断描述符里保存着中断服务程序的段选择符。

 

3)根据取得的段选择符到GDT中找相应的段描述符

       CPU使用IDT查到的中断服务程序的段选择符从GDT中取得相应的段描述符,段描述符里保存了中断服务程序的段基址和属性信息,此时CPU就得到了中断服务程序的起始地址。

       这里,CPU会根据当前cs寄存器里的CPL和GDT的段描述符的DPL,以确保中断服务程序是高于当前程序的,如果这次中断是编程异常(如:int 80h系统调用),那么还要检查CPL和IDT表中中断描述符的DPL,以保证当前程序有权限使用中断服务程序,这可以避免用户应用程序访问特殊的陷阱门和中断门[3]

如下图显示了从中断向量到GDT中相应中断服务程序起始位置的定位方式:

 

 

 

4)CPU根据特权级的判断设定即将运行的中断服务程序要使用的栈的地址

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

 

6)保护当前程序的现场

       CPU开始利用栈保护被暂停执行的程序的现场:依次压入当前程序使用的eflags,cs,eip,errorCode(如果是有错误码的异常)信息。

官方文档[1]给出的栈变化的示意图如下:

 

 

 

7)跳转到中断服务程序的第一条指令开始执行

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

 

8)中断服务程序处理完毕,恢复执行先前中断的程序

       在每个中断服务程序的最后,必须有中断完成返回先前程序的指令,这就是iret(或iretd)。程序执行这条返回指令时,会从栈里弹出先前保存的被暂停程序的现场信息,即eflags,cs,eip重新开始执行。如果存在特权级转换还会弹出ss和esp,这样也意味着栈也被切换回原先使用的栈了。

这里有个地方需要注意:如果此次处理的是带有错误码(errorCode)的异常,CPU在恢复先前程序的现场时,并不会弹出errorCode,也就是说CPU似乎忘记了曾经压过一个errorCode入栈,因此要求相关的中断服务程序在调用iret返回之前需要主动弹出errorCode。

 

 

 

参考书目:

[1] Intel 64 and IA-32 Architectures Software Developers Manual Volume 1 Basic Architecture

[2] 《微型计算机接口技术及应用》,华中科技大学出版社,刘乐善 主编

[3] 《深入理解Linux内核》,第三版,中国电力出版社






前面讲到了实模式下用int 15h得到内存信息,然后在保护模式下把它们显示出来。保护模式下中断机制发生了很大的变化,原来的中断向量表被IDT(Interrupt Descriptor Table,中断描述符表)代替,实模式下能用的BIOS中断在保护模式下已经不能用了。IDT可以将每一个中断向量和一个描述符对应起来。IDT中的描述符可以是中断门描述符、陷阱门描述符、任务门描述符。尽管IDT在形式上与实模式下的向量表非常不同,但它也是一种向量表。

中断门和陷阱门的作用机理几乎一样,只不过使用调用门时用call指令,而在这里我们使用int指令。

每一种中断(异常)都会对应一个中断向量号,而这个向量号通过IDT就与相应的中断处理程序对应起来。保护模式的中断和异常见下表:

 

向量号助记符描述类型出错码
0#DE除法错FaultDIV和IDIV指令
1#DB调试异常Fault/Trap任何代码和数据的访问
2非屏蔽中断Interrupt非屏蔽外部中断
3#BP调试断点Trap指令INT 3
4#OF溢出Trap指令INTO
5#BR越界Fault指令BOUND
6#UD无效(未定义)操作码Fault指令UD2或无效指令
7#NM设备不可用(无数学协处理器)Fault浮点或WAIT/FWAIT指令
8#DF双重错误Abort有(0)所有能产生异常或NMI或INTR
的指令
9 协处理器段越界(保留)Fault浮点指令(386后不再处理此
异常)
10#TS无效TSSFault任务切换或访问TSS时
11#NP段不存在Fault加载段寄存器或访问系统段时
12#SS堆栈段错误Fault堆栈操作或加载SS时
13#GP常规保护错误Fault内存或其他保护检验
14#PF页错误Fault内存访问
15Intel保留,未使用   
16#MFx87FPU浮点错(数学错)Faultx87FPU浮点指令或WAIT/FWAIT指令
17#AC对齐检验Fault有(0)内存中的数据访问(486开始支持)
18#MCMachine CheckAbort错误码(若有的话)和源依赖于
具体模式(奔腾CPU开始支持)
19#XFSIMD浮点异常FaultSSE和SSE2浮点指令(奔腾三
开始支持)
20~31Inter保留,未使用   
32~255用户定义中断Interrupt 外部中断或int n指令

对于异常的三种类型解释如下:

 

Fault(错误)——可被更正的异常,更正后程序可继续执行。当一个fault发生时,处理器会把产生fault的指令之前的状态保存起来。异常处理程序的返回地址将会是产生fault的指令,而不是其后的那条指令。

Trap(陷阱)——一种在发生trap的指令执行后立即被报告的异常,它也允许程序或任务不失连续性地继续执行。异常处理程序的返回地址将会是产生trap的指令之后的那条指令。

About(终止)——不总是报告精确异常发生位置,它不允许程序或任务继续执行,而是用来报告严重错误。

中断产生的原因有两种:

外部中断(即由硬件产生的)——需要建立硬件中断与向量号之间的对应关系。分为可屏蔽中断和不可屏蔽中断两种。不可屏蔽中断由CPU的NMI引脚接收,可屏蔽中断则由CPU的INTR引脚接收。可屏蔽中断与CPU的关系是通过对可编程中断控制器8259A(可以认为是中断机制中所有外围设备的一个代理,这个代理不但可以根据优先级在同时发生中断的设备中选择应该处理的请求,而且可以通过对其寄存器的设置来屏蔽或打开相应的中断)建立起来的。

内部中断(由int n产生)——类似于调用门的使用,n即为向量号。

设置8259A:

有两片级联的8259A与CPU相连,每个8259A有8根中断信号线,两片级联公可以挂接15个不同的外设。对8259A进行设置,可以使这些外设发出的中断请求与中断向量对应起来。在BIOS初始化它的时候,IRQ0~IRQ7被设置为对应向量号08h~0Fh,但保护模式下这些向量号已被占用,所以需要重新设置。8259A是可编程中断控制器,对它的设置可通过向相应端口写入特定的ICW(Initialization Command Word)来实现。主8259A对应的端口地址是20h和21h,从8259A对应的端口地址是A0h和A1h。初始化过程(不能颠倒顺序):往端口20h或A0h写入ICW1->往端口21h或A1h写入ICW2->往端口21h或A1h写入ICW3->往端口21h或A1h写入ICW4(注:2、3、4的写入的端口相同)。

还有一个东西叫做OCW(Operation Control Word),共有OCW1、OCW2、OCW3共三个,用于屏蔽或打开外部中断(向21h或A1h写入OCW1即可,实际上OCW1被写入了中断屏蔽寄存器IMR中,当一个中断到达,IMR会判断此中断是否应被丢弃)或发送EOI给8259A以通知它中断处理结束。

对于EOA,每一次中断处理结束,需要发送一个EOI给8259A,以便继续接受中断。发送EOI通过向20h或A0h端口写OCW2实现。

可屏蔽中断与NMI的区别在于是否受到IF位的影响,而8259A的中断屏蔽寄存器(IMR)也影响着中断是否会被响应。所以外部可屏蔽中断受IF位(需为1)和IMR位(需为0)的影响。

实际应用中,中断的产生大多带有特权级变换,规则与用call指令调用一个调用门的规则完全一样。如果中断或异常发生时没有特权级变换,则eflags、cs、eip、将依次被压入堆栈,如果有出错码,则出错码最后被压栈。如果有特权级变换,ss和esp被压入内层堆栈,然后是eflags、cs、eip、出错码(若有的话)。总之不管怎样,中断或异常发生时,堆栈都会发生变化。

从中断或异常返回时必须使用指令iretd,与ret的区别就在于它同时改变eflags的值。需要注意的是,只有当CPL为0时,eflags中的IOPL域才会改变,而且只有当CPL小于等于IOPL时,IF才会改变。还要注意的是,iretd执行的时候Error Code不会被自动从堆栈中弹出,所以执行它之前要先将它从栈中清除掉。

还有一点,中断门和陷阱门有一个小区别。由中断门向量引起的中断会复位IF,因为可以避免其他中断干扰当前中断的处理,随后的iret指令会从堆栈上回复IF的原值;而通过陷阱门产生的中断不会改变IF。



下面的内容是保护模式下的I/O,内容很少~

I/O敏感指令:in、ins、out、outs、cli、sti,它们只有在CPL小于等于IOPL时才能执行,如果低特权级的指令试图访问这些I/O敏感指令将会导致常规保护错误(#GP)。

可以改变IOPL的指令只有popf和iretd,但只有运行在ring0的程序才能将其改变,低特权级的无法改变,但也不会产生异常(会维持原样)。指令popf也可用来改变IF(就像执行了cli和sti)。但在这种情况下,popf也变成了I/O敏感指令,只有CPL小于等于IOPL时,popf才能改变IF,否则保持原样(不会产生异常)。

I/O位图基址是一个以TSS的地址为基址的偏移,指向的便是I/O许可位图。它的每一位表示一个字节的端口地址是否可用(0为可用,1为不可用)。由于每个任务都可以有单独的TSS,所以每个任务可以有它单独的I/O许可位图。I/O许可位图必须以0FFh结尾。如果I/O位图基址大于或等于TSS段界限,则表示没有I/O许可位图,如果CPL小于等于IOPL,则所有I/O指令都会引起异常。I/O许可位图的使用使得即便在同一特权级下不同的任务也可以有不同的I/O访问权限。

 


  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值