本文为<x86汇编语言:从实模式到保护模式> 第17章笔记编程
中断和异常的做用是指示系统中的某个地方发生一些事件, 须要引发处理器(包括正在执行中的程序和任务)的注意. 当中断和异常发生时, 典型的结果是迫使处理器将控制从当前正在执行的程序或任务转移到另外一个历程或任务中去. 该例程叫作中断处理程序, 或者异常处理程序. 若是是一个任务, 则发生任务切换.数组
1. 中断(Interrupt)缓存
中断包括硬件中断和软中断.并发
硬件中断是由外围硬件设备发出的中断信号引起的, 以请求处理器提供服务. 当I/O接口发出中断请求时, 会被像8259A和I/O APIC这样的中断控制器收集, 并发送处处理器. 硬件中断彻底是随机产生的, 与处理器的执行并不一样步. 当中断发生时, 处理器要先执行完当前的指令, 而后才对中断进行处理.ide
软中断是由 int n 指令引起的中断处理, n是中断号或者叫类型吗.性能
2. 异常(Exception)测试
异常就是16位汇编中的内部中断. 它们是处理器内部产生的中断, 表示在指令执行的过程当中遇到了错误的情况. 当处理器执行一条非法指令, 或者因条件不具有, 指令不能正常执行时, 将引起这种类型的中断. 以上所列都是异常状况, 因此内部中断又叫异常或者异常中断. 好比在执行除法指令 div/idiv 时, 遇到了被0除的状况(除数是0); 在好比, 使用jmp指令发起任务切换时, 指令的操做数不是一个有效的TSS描述符选择子.spa
异常分为三种, 第一种是程序错误异常, 指处理器在执行指令的过程当中, 检测到了程序中的错误, 并由此而引起的异常.操作系统
第二种是软件引起的异常. 这类异常一般由into, int3和bound指令主动发起. 这些指令容许在指令流的当前点上检查实施异常处理的条件是否知足. 举个例子, into指令在执行时, 将检查EFLAGS的OF标志, 若是知足为1的条件, 则引起异常.指针
第三种是机器检查异常. 这种异常是处理器型号相关的, 也就是说, 每种处理器都不太同样. 不管如何, 处理器提供了一种对硬件芯片内部和总线处理进行检查的机制, 当检测到错误是, 将引起异常.
根据异常状况的性质和严重性, 异常又分为如下三种, 并分别实施不一样的处理.
中断和异常发生时, 处理器将挂起当前正在执行的任务或者过程, 而后执行中断和异常处理程序. 返回时, 处理器恢复程序或者任务的执行, 并且被打断的程序或任务的执行不失连续性, 除非遇到了一个终止类型的异常. 对于某些异常, 处理器在转入异常处理程序以前, 会在当前栈中压入一个成为错误代码的数值, 帮助程序进一步诊断异常产生的位置和缘由. 下表列出了Intel处理器在保护模式下的中断和异常.
向量 | 助记 | 描述 | 类型 | 错误代码 | 来源 |
0 | #DE | 除法错 | 故障 | 无 | div或idiv指令 |
1 | #DB | 保留 | |||
2 | - | NMI | 中断 | 无 | 不可屏蔽的外部中断 |
3 | #BP | 断点 | 陷阱 | 无 | int3指令 |
4 | #OF | 溢出 | 陷阱 | 无 | into指令 |
5 | #BR | 对数组的引用超出边界 | 故障 | 无 | bound指令 |
6 | #UD | 无效或未定义的操做码 | 故障 | 无 | ud2指令, 或保护的操做码 |
7 | #NM | 设备不可用(无数学协处理器) | 故障 | 无 | 浮点或者wait/fwait指令 |
8 | #DF | 双重故障 | 终止 | 有(0) | 任何会产生异常的指令, NMI或者硬件中断 |
9 | 协处理器段超越(保留). 协处理器执行浮点运算时, 至少有两个操做数不在一个段内(跨段) | 故障 | 无 | 浮点指令 | |
10 | #TS | 无效TSS | 故障 | 有 | 任务切换或访问TSS |
11 | #NP | 段不存在 | 故障 | 有 | 加载段寄存器或者访问系统段 |
12 | #SS | 栈段故障 | 故障 | 有 | 栈操做或者加载段寄存器SS |
13 | #GP | 常规保护 | 故障 | 有 | 任何内存引用或其余保护异常 |
14 | #PF | 页故障 | 故障 | 有 | 任何内存引用 |
15 | - | 由Intel处理器保留, 不能使用 | 无 | ||
16 | #MF | x87 FPU(浮点处理单元)浮点处理错误 | 故障 | 无 | x87 FPU浮点指令或wait/fwait指令 |
17 | #AC | 对齐检查 | 故障 | 有(0) | 任何内存数据引用 |
18 | #MC | 机器检查 | 终止 | 无 | 错误代码(若是有的话)和来源是处理器型号相关的 |
19 | #XM | SIMD(单指令多数据)浮点异常 | 故障 | 无 | sse/sse2/sse3浮点指令 |
20~31 |
Intel公司保留, 建议不要使用 | ||||
32~255 | 用户自定义的中断 | 中断 | 外部中断, 或者int n指令 |
当中断和异常发生时, NMI和异常的向量是由处理器自动给出的; 硬件的向量是由I/O中断控制器芯片送给处理器的; 软中断的向量是由指令中的操做数给出的. 从80486以后开始, 处理器内部通常集成了浮点运算部件x87FPU, 再也不须要安装独立的数学协处理器, 因此有些和浮点运算有关的异常可能不会产生(好比向量为9的协处理器段超越故障). wait和fwait指令用于主处理器和浮点处理部件(FPU)之间的同步, 它们应当放在浮点指令以后, 以捕捉任何我浮点异常.
bound r16, m16 bound r32, m32
该指令用于检查数组的索引是否在边界以内, 目的操做数包含了数组索引, 源操做数必须指向内存位置, 那里包含两个成对出现的字或双字, 分别是数组索引的下限和上限. 若是执行bound时, 数组的索引小于下标的下限, 或者大于下标的上限, 则产生异常.
ud2指令是从Pentium Pro处理器开始引入的, 它只有操做码而没有操做数, 执行该指令时, 会引起一个无效操做码异常. 该指令没有别的用处, 典型的用于软件测试. 尽管异常是该指令故意引起的, 可是, 在转入异常处理程序时, 压入栈中的指令指针是指向该指令的, 而非下一条指令.
在实模式下, 位于内存最低端的1KB内存, 是中断向量表IVT, 定义了256种中断的入口地址, 包括16位段地址和16位段内偏移量. 当中断发生时, 处理器要么自发产生一个中断向量, 要么从int n 指令中获得中断向量, 或者从外部的中断控制器接受一个中断向量. 而后, 它将该向量做为索引访问中断向量表. 具体作法是, 乘以4, 做为表内偏移量访问中断向量表, 从中取得中断处理过程的段地址和偏移地址, 并转到那里执行.
在保护模式下, 处理器对中断的管理是类似的, 但并不是使用传统的中断向量表来保存中断处理过程的地址, 而是中断描述符表(Interrupt Descriptor Table: IDT). 顾名思义, 这个表里, 保存的是和中断处理过程有关的描述符, 包括中断门, 陷阱门和任务门.
任务门的格式在<任务切换>中有说, 中断门和陷阱的格式以下图所示.
事实上, 调用门, 任务门, 中断门和陷阱门的描述符很是类似, 从大的方面来讲, 由于都用于实施控制转移, 故都包括16位的目标代码段选择子, 以及32位的段内偏移量. 由上图可知, 中断门和陷阱门仅仅有一比特的差异. 中断门和陷阱门描述符只容许存放在IDT内, 任务门能够位于GDT, LDT和IDT中.
和实模式下的中断向量表(IVT)不一样, 保护模式下的IDT不要求必须位于内存的最低端. 事实上, 在处理器内部, 有一个48位的中断描述符表寄存器(Interrupt Descriptor Table Register:IDTR), 保存着中断描述符表在内存中的线性基地址和界限. 以下图所示, 和GDT同样, 由于整个系统中只须要一个IDT就够了, 因此, GDTR与IDTR不像LDTR和TR, 没有也不须要选择器部分.
这就意味着, IDT能够位于内存中的任何地方, 只要IDTR指向了它, 整个中断系统就能够正常工做. 为了利用高速缓存使处理器的工做性能最大化, 建议IDT的基地址是8字节对齐的(地址的数值可以被8整除). 处理器复位时, IDTR的基地址部分为0, 界限部分为0xffff. 16位的表界限值意味着IDT和GDT, LDT同样, 表的大小可使64KB, 可是, 事实上, 由于处理器只能识别256种中断, 故一般只是用2KB, 其余空余的槽位应当将描述符的P位清0. 最后, 与GDT不一样的是, IDT中的第一个描述符也是有效的.
如上图所示, 在保护模式下, 当中断和异常发生时, 处理器用中断向量乘以8的结果去访问IDT, 从中取得对应的描述符. 由于IDT在内存中的位置是由IDTR指示的, 因此这很容易作到.
注意, 上图没有考虑分页, 也没有考虑门描述符是任务门的状况, 由于任务门的处理比较特殊. 中断门和陷阱门中有目标代码段描述符的选择子, 以及段内偏移量, 取决于选择子的TI位 , 处理器访问GDT或者LDT, 取出目标代码段的描述符. 接着, 从目标代码段的描述符中取得目标代码段所在的基地址, 再同门描述符中的偏移量相加, 就获得了中断处理程序的32位线性地址. 若是没有开启分页功能, 该线性地址就是物理地址; 不然, 送页部件转换成物理地址. 注意, 当处理器用中断向量访问IDT时, 要访问的位置超出了IDT的界限, 则产生常规保护异常(#GP).
和经过调用门实施控制转移同样, 处理器要对中断和异常处理程序进行特权级保护. 当目标代码段描述符的特权级(能够用门描述符中的段选择子, 从GDT或LDT中找到)低于当前特权级CPL时, 即, 在数值上,
CPL < 目标代码段的DPL
时, 不容许将控制转移到中断或异常处理程序, 违反此规则将引起常规保护异常(#GP).
不过, 中断和异常处理程序的特权级保护也有一些特别之处. 具体表如今:
CPL <= 门描述符的DPL这主要是为了防止低段特技的软件经过软中断指令访问一些只为内核服务的例程, 如页故障处理. 相反地, 对于硬件中断和处理器检测到异常状况而引起的中断处理, 不检查门的DPL.
中断和异常是随机产生的, 不可预测的. 可是, 有一点能够肯定的, 即, 它老是发生在某个任务内, 是在某个任务正在进行的时候产生的, 即便整个系统内只有一个任务. 当中断和异常发生时, 任务可能正在特权级0的全局空间(内核)中执行, 也可能正在特权级为3的局部空间执行. 所以, 当处理器将控制转移到中断或异常处理程序时, 若是处理程序运行在较高的特权级上(数值上较低的), 那么, 将转换栈:
中断门和陷阱门区别不大, 经过中断门进入中断处理程序时, EFLAGS寄存器的IF位被处理器清零, 以禁止嵌套的中断, 当中断返回时, 将从栈中恢复EFLAGS寄存器的原始状态. 陷阱中断的优先级较低, 当经过陷阱门进入中断处理程序时, EFLAGS寄存器的IF位不变, 以容许其余中断优先处理.
EFLAGS寄存器的IF位仅影响硬件中断, 对NMI, 异常和int n形式的软件中断不起做用.
当中断和异常发生时, 若是很具中断向量从IDT中找到的描述符是任务门, 则不是进行通常的中断处理过程, 而是发起任务切换. 以下图所示, 这是经过中断发起任务切换的原理.
具体的说, 在中断中使用任务门能够得到如下好处:
固然, 和通常的中断处理过程相比, 利用中断发起任务切换也有不利的一面, 那就是速度很慢, 笔记要保存大量的机器状态, 并进行一系列的特权级和内存访问的检查. 因中断和异常而发起任务切换时, 再也不保存CS, EIP的状态, 可是, 在任务切换工做完成后, 处理器要把错误代码压入新任务的栈中(若是有错误代码的话).
任务是不可重入的, 所以, 在进行中断任务以后和执行iret指令以前, 必须关中断, 以防止因相同的中断再此发生而产生常规保护异常(#GP);
做为对任务门的保护, 和中断门, 陷阱门同样, 只对经过int3, int n和into指令发起的任务切换实施特权级检查, 即, 只有在数值上符合如下条件, 才容许经过以上指令发起任务切换:
CPL <= 任务门的DPL
在其余异常和硬件中断的状况下, 不检查任务门的特权级. 另外, 因为任务切换, 不对目标代码段的特权级别进行检查.
有些异常产生时, 处理器会在异常处理程序或中断任务的栈中压入一个错误代码, 一般, 这意味着异常和特定的段选择子或中断向量有关.
以下图所示, 压入栈中的错误代码是32位的, 但高16位不用.
EXT位的意思是, 异常是由外部事件引起的. 此位置位时, 表示异常是由NMI, 硬件中断等引起的.
IDT位用于指示描述符的位置. 为1时, 表示段选择子的索引部分(错误代码的位15~3)是指向中断描述符表的; 为0时, 表示段选择子的索引部分指向GDT或者LDT.
TI位仅在IDT位是0的状况下才有意义. 此位是0时, 表示段选择子的索引部分指向GDT, 不然, 指向LDT.
段选择子的索引部分用于指示GDT/LDT内的段描述符, 或者IDT内的门描述符, 它就是咱们平时所用的段选择子的高13位.
有时候, 错误代码多是全零(空), 这表示异常的产生并不是因为引用了一个特定的段. 固然, 也可能确实是在引用一个段时发生的, 并且因为那个段的描述符是空描述符.
注意, 当经过iret指令从中断处理程序返回时, 处理器并不会自动弹出错误代码. 所以, 对那些有异常代码的异常处理程序来讲, 在执行iret以前必须先从栈中移去错误代码.
对于外部异常(经过处理器引脚触发), 以及用软中断指令int n引起的异常, 处理器不会压入错误代码, 即便它本来是一个有错误代码的异常.
一旦设置了中断描述符表, 并加载了IDTR寄存器(用lidt指令), 处理器的中断机制就开始起做用了. 但如今还不宜开放硬件中断. 在保护模式下, 若是计算机系统的可编程中断控制器芯片仍是8259A, 那就得从新进行初始化, 事实上, 8259A并无过期, 在单处理器系统中, 它依然健在. 从新初始化的缘由是其主片的中断向量和处理器的异常向量冲突. 计算机启动以后, 主片的中断向量为0x08~0x0F, 从片的中断向量为0x70~0x77, 在以8086位处理器的系统中, 这没有什么问题, 在32位处理器上, 0x08~0x0f已经被处理器用作异常向量.
好在8259A(以及I/O APIC)都是可编程的, Intel公司建议, 中断向量0x20~0xff是用户能够自由分配的部分. 那么, 咱们能够设置8259A的主片, 把它的中断向量改为0x20~0x27.
对8259A编程须要使用初始化命令字(Initialize Command Word: ICW), 以设置它的工做方式, 共有4个初始化命令字. 分别是ICW1~ICW4, 都是单字节命令.ICW1用于设置中断请求的触发方式, 以及级联的芯片数量; ICW2用于设置每一个芯片的中断向量; ICW3用于指定用哪一个引脚实现芯片的级联; ICW4用于控制芯片的工做方式.
主片的端口是0x20和0x21, 从片的端口是0xa0和0xa1, 要发送初始化命令字给8259A, 对于主片来讲, 须要先向0x20端口发送ICW1, 而对于从片来讲, 这个端口是0xa0. 这是一个标志, 每次8259A接到ICW1时, 都意味着一个新的初始化过程开始了.
从0x20/0xa0端口接受命令字ICW1后, 8259A期待从0x21/0xa1端口接受命令字ICW2, 可是, 它是否接受ICW3和ICW4, 还要看ICW1的内容. 以下图所示, ICW1的位0决定了是否有ICW4命令, 位1指示是否为多片级联. 若是是多片级联, 那么, 一定有ICW3命令. 这样一来, 8259A就知道, 在接受了ICW2以后, 是否还要在相同的端口(0x21/0xa1)上依次再接受ICW3和ICW4.
注意, 上图中, 深色的比特位表示它被保留, 或者不用, 使用途中所标注的固定值(0或1); 有些虽然不是深色, 但也标注了固定值(0或1),这些位是有意义的, 能够设置或改变, 具体含义可参考芯片手册.
开启也功能时, 处理器的页部件要把线性地址转换成物理地址, 而访问页目录表和页表是至关费时的. 所以, 把页表项预先存放处处理器中, 能够加考哪一个地址转换速度. 为此, 处理器专门构造了一个特殊的高速缓存装置, 叫作转换后援缓冲器(Translation Lookaside Buffer:TLB). 事实上, 对该缓冲器的命名可谓五花八门, 从"转换旁路缓冲器", "转换后备缓冲区"到"快表"不一而足.
如上图所示, 这是TLB的结构, 它分为两大部分, 第一部分是标记, 其内容为线性地址的高20位; 第二部分是页表数据, 包括属性, 访问权和页物理地址的高20位. 在分页模式下, 当段部件发出一个线性地址时, 处理器用线性地址的高20位来查找TLB(用线性地址的高20位和TLB中的标记比对查找), 若是找到匹配项(命中), 则直接使用数据部分物理地址做为转换用的地址; 若是检索不成功(不中), 则处理器还得花时间访问内存中的页目录表和页表, 找到那个页表项, 而后将它填写到TLB中, 以备后用. TLB容量不大, 若是它装满了, 则必须淘汰掉那些用的较少的项目.
TLB中的属性位来自页表项, 好比D位等; 访问权来自页目录项和对应的页表项, 好比RW位和US位等. 问题是, 就RW位和US位来讲, 页目录项和页表项都有着两位, 以哪个为准呢? 在分页机制中, 对页的访问控制按最严格的访问权进行. 对于某个线性地址, 若是其页目录项的RW为0而其也表项的RW为1, 则按RW位是0执行. 也就是说, TLB中的访问权, 是页目录项和页表项中, 对应访问权的逻辑与.
处理器仅仅缓存那些P位是1的页表项, 并且, TLB的工做和CR3的PCD和PWT位无关, 不受这两位影响. 另外, 对于页表项的修改不会同时反映到TLB中, 是的, 这是很糟糕的, 若是内从中的页表项已经修改, 但TLB中对应的条目尚未更新, 那么, 转换后的物理地址一定是错误的.
能够将CR3寄存器的内容都出, 再原样写入, 这样就会是的TLB中的全部条目失效. 固然, 这是比较直接的办法. 当任务切换时, 由于要重新任务中的CR3寄存器域加载页目录项, 也会隐式得致使TLB中的全部条目无效. 注意, 上述方法对于那些标记为全局(G位 = 1)的页表项来讲无效, 不起做用.
也能够用指令invlpg来刷新TLB中的单个条目. 固然, 要作到这一点, 必须指定一个线性地址, 处理器用给出的线性地址搜索TLB, 找到那个条目, 而后从新加载它. invlpg指令格式:
invlpg m
该指令的操做数是一个内存地址, 当指令执行时, 处理器首先肯定该线性地址位于哪一个页内, 而后刷新相应的TLB条目. 它的操做数之因此是内存地址, 而不是要给当即数, 是应为, TLB是一个附加的硬件机构, 只有在处理器正常访问内存时才会致使它的填充和更新, 所以, 处理器用一个访问内存的操做来促使TLB条目的更新会更方便. invlpg是特权指令, 当前特权级必须为0. 该指令不影响任何标志位.