系统的启动过程:
任何一台计算机,在开机后,它要做的第一件事情就是引导(Booting),通过引导,计算机为自身搭建好运行环境,为以后OS的启动与运行做好准备.首先,我们来看看一台计算机是如何引导自身的.在机器加电后,电源供电稳定后,电源会传给8284A时钟生成器一个"Power Good"低电位信号,随后8284A会输出有效的RESET信号,使CPU复位,这时CS:IP = FFFF:0000.CPU在这里执行一条jmp far addr类指令,跳转到实际BIOS映射代码的位置,开始执行BIOS代码.
上述是机器在加电后的启动过程,大家都知道计算机的启动是分为冷启动与热启动的,那么对于热启动,其过程又是怎样的呢?其实热启动只不过是将键盘中断程序置复位标志为1234h,然后再跳转到BIOS处执行,其主要是省去了在自检过程中对存储器的检测.
在跳转到BIOS后,首先会先关闭中断,然后开始自检(POST)工作,这个自检主要检测计算机最基本设备的运转状态.其主要包括对,CPU内部寄存器测试,BIOS芯片字节的检查,8237 DMA控制器测试,基本32K RAM检测等最基本内容.由于被检测设备在系统运行中的重要性,因此在此过程中,BIOS一旦检测到任何异常,都将判为致命性错误,系统将被停机.
通过上面的自检后,BIOS开始初始化8259可编程中断控制器,并设置BIOS的8个主要中断向量(int 10h-int 17h),然后初始化并测试CRT视频接口以及显示内存(对于热启动这一步将跳过),在确认正常后,执行其内部的显示卡标准驱动程序(注意这里的驱动跟安装操作系统下的驱动是不一样的),这段代码会存放在C0000h,其主要目的是初始化显示卡.然后BIOS会打印显卡信息.
接着BIOS开始检查其他设备,其包括对8259中断控制器测试,8253定时器测试,键盘复位和卡键测试,扩展I/O测试,设置硬件中断向量,扩展RAM测试(这里的RAM测试会检测除0-32K以外的整个RAM空间,对于热启动同样也会跳过这一阶段),.然后BIOS会搜索其他设备的ROM,如果找到,则会执行它们.接着测试ROM-BASIC的字节检查,测试磁盘驱动器(如:FDC等),测试打印机端口和RS-232,并设置他们的地址.
然后打开NMI(不可屏蔽中断),最后就是调用Int 19h进行自举.这一阶段的自检如果发生错误,系统会判断其为一般性错误,并显示出相应的提示信息.在此过程中,BIOS会将检测收集到的数据保存在内存低1K--2K的区域,并将BIOS中断向量表,以及BIOS程序运行所需要的stack保存在内存低0K--1K的地方.
下面就是系统自举工作了,系统调用int 19h进行自举,寻找启动设备,如:软驱,硬盘,光驱等等.找到后系统读取启动设备的0号逻辑扇区(如是软盘就读取0面0道1扇区的整个内容),并将读取的内容放到内存地址为0000:7C00的地方.
当然,如果找不到启动设备,BIOS就会调用Int 18h,并给出相应的提示信息,然后进入ROM-BASIC.(有些机器会在等待一段时间后自动进入CMOS.如:很久以前海洋的AMD 386DX/40主板)至此,BIOS的引导程序结束,CPU开始执行0000:7C00处的代码.在这里需要说明一下的是,BIOS的引导程序是与操作系统无关的,但随后CPU开始执行的代码就开始与操作系统存在较大的相关性了,因此对于不同的操作系统,下面这一部分可能会存在着较大的不同.不过,从目的上来讲,它们是相同的,即都是为将要运行OS的内核(Kernel)作准备.
进入这一部分的首要工作就是执行启动设备的引导程序.硬盘与软盘的对于引导程序的存放结构是不同的.硬盘有一个叫做MBR(Master Boot Record)的扇区,系统会首先执行它,以判断那个分区是启动分区,并读入该分 区的第一个扇区,并执行.并且在这个扇区中还存放着硬盘分区表(DPT),这个表的地位相当重要,因为它包含了各个分区的诸如:分区类型,起始位置,结束位置等重要参数.下面我们来详细介绍一下MBR的结构.MBR的结构分为三部分,首先是可执行代码,占446个字节,然后是4个分区表,每个占16个字节,共64个字节,最后是签字AA55H.
下表列出了分区信息的详细内容:
? 长度 描述
0 字节 分区状态0:非活动分区,80h活动分区(可引导)
1 字节 分区起始头
2 字 分区起始扇区和起始柱
4 字节 分区类型
5 字节 分区中止头
6 字 分区中止扇区和中止柱
8 双字 分区起始绝对扇区
0Ch 双字 分区扇区数
然后我们开始介绍MBR中的可执行代码部分:
首先,程序会检测MBR的签字是否合法,即判断其最后字是否为AA55h.通过后,将自身移动到内存中的其他地方,以备将来在此装入引导分区的Boot扇区.然后,程序检查四个分区的分区状态,找出活动分区,并将该分区的Boot扇区读入到0000:7C00h处,并检查其签字是否合法,在通过后,程序跳转到0000:7C00h处执行,即将控制权交给活动分区的Boot程序;对于软盘则没有那么复杂,软盘的第一个扇区就是它的Boot区,系统自举时将直接将其读入到0000:7C00h处并执行.
从功能上来讲软盘与硬盘的Boot区是相同的,其任务都是将OS的内核(Kernel)读入到内存并执行.但具体来看,由于绝大多数OS的Kernel是以文件形式存放在磁盘上的,要读取它就要涉及到对文件系统的操作,这使得它们在实现上又是很不相同的.因此,对于Boot区的分析我们将放在后面的内容中具体介绍.
2.保护模式简述
最早的Intel系列的CPU只存在一种操作模式,即现在所说的实模式(Real Mode,以下简称RM).在Intel推出80286之后,为了增强CPU的处理能力,同时也为了适应当时的软件开发需求,Intel提出了保护模式(Protected Mode,以下简称PM),但在80286下的PM由于CPU本身设计的问题,并没有使其发挥出很大的功效.在80386推出之后,Intel完善了CPU的设计形成了最终的IA-32架构,并提出了另一种模式系统管理模式(System Management Mode).本章我们的讨论就围绕着这三种模式进行展开,并重点讨论PM.
首先,对这三种模式做一简单概述.
RM:此模式是主机在加电或复位后自动进入的模式,在此模式下其可以执行16位指令,并可以切换到PM或者SMM.
PM:在此模式下,CPU能够支持其自身的32位特性,使自身处于最高性能表现.这些特性主要包括:
1. 最大可访问4GB内存空间.事实上,在RM下通过一些未公开的特性,也可以达到同样的效果,但其对于代码段 和堆栈空间却是无效的.况且后面的所有特性都是基于PM的,对RM没有效果.
2. 虚拟存储.处于PM下的CPU其内存管理单元(MMU)支持这项特性.前面我已经说到,在PM下CPU最大寻址空 间为4GB,而在实际中,我们并没有如此大的物理内存空间.因此通过MMU,可以将外存设备(如:硬盘)的一部分 空间模拟成物理内存进行使用.
3. 地址映射.即MMU可以在地址使用前对其进行转换,即所谓的映射.
4. 改进的分段机制.本文后面将对此进行重点论述.
5. 内存保护与任务保护.即在PM状态下,引入了权限机制.通过权限控制可以达到保护相关代码和数据的目的.
6. 改进的寻址模式.在RM下,只有常数,BX或BP,SI或DI可以用来形成地址,而在RM下可以通过任意寄存器进行寻 址,并且可以包含一个为2,4或8的比例因子.
7. 多任务支持.在PM下,CPU提供了特殊的机制能够进行快速的上下文切换.
SMM:该模式为操作系统实现特定平台指定的功能提供了一种有效的机制.
值得注意的是,在PM下,CPU允许在受保护的情况下,执行RM程序,这个特性被称为虚拟8086模式(Virtual-8086 Mode),但其本质上却不是真正的RM.
对于三种模式关系的形象解释可以通过下图来描绘:
正如上面所说的,只有在PM下,CPU才能充分发挥其自身的所有特性,而计算机在启动之后,默认的CPU操作模式却是RM.因此摆在我们面前的一个主要问题就是如何在RM与PM之间相互切换.那么如何在RM和PM之间相互切换呢.核心步骤其实很简单,只要改变CPU中的CR0寄存器中PE标志位的值,就可以实现.在PE=1时,CPU进入PM,而在PE = 0时,则进入RM.但这仅仅是整个切换过程中的一小部分,在进入保护模式之前我们还需要做很多事情,其中最关键的就是建立好一个被称为GDT的表.
在谈到GDT之前,我们先回顾一下,在RM中,内存中寻址的方式---段:偏移量.其中段(Segment)表明了一个基地址,其最大长度固定为64KB(FFFFH),即16bit数所能表示的最大数值.而偏移量(Offset),就是指在指定段内的位置.由此可见,通过段+偏移量这种表示方式,就可以表示出内存中的绝对地址.需要指出的是,在CPU实际处理过程中,CPU会将段寄存器的 值左移动4位,再与偏移量相加,形成地址,放入20位的总线当中.
在PM中,对于段模式来讲,上面的寻址方式,在大部分上仍然是适用的.但由于PM是工作在32位下的,因此上面的各个值,也就都相应的变成了32位.与RM不同的是,在PM下,一个段的长度不再固定,其可以在CPU允许的规则下任意设置.并且CPU为段模式提供了保护机制,即增加了对自身的访问权限.因此在PM下,对于一个段,需要有三个变量给于描述,即基地址,段界限和访问权限.
事实上,CPU将这三个值保存为一个64位长的段描述符.但出于兼容性的考虑,Intel并没有将段寄存器改为64位可用--虽然,段寄存器在事实上确是64位,但对于程序来讲,高于16位的部分却是不可见的--因此,我们需要另一种方法去存放这些数据.Intel选择了将这些段描述符统统存入到一个全局数组中的方法,在访问段时,向相应的段寄存器填入该数组的下标值来实现间接引用.这个全局数组就称其为GDT(全局描述符表).由于GDT可以存放在内存中的任何位置,因此要引用它,就必须知道他的入口地址.Intel为我们提供了GDTR寄存器和LGDT指令.其中GDTR寄存器存放的是GDT的入口地址(32位)和其界限(16位),共48位.这里的入口地址是一个线性地址,界限则是表的字节长度减一.可见该表最多可以长达64KB,存储8192条描述符号,而LGDT指令的作用就是将GDT装载到放入GDTR寄存器当中.
顾名思义,GDT是全局描述符,因此其在内存中存在且仅存在一个,并且它的存在对于所有的任务来讲,都是可见的.显然,这种做法对于多任务来讲是不易管理的.因此,Intel又引入了LDT(局部描述符表),该描述符与GDT不同之处在于,LDT在系统中可以有许多个,但每个任务只允许有一个LDT,且其只能该任务自身可见.其与GDT的主要关系在于,每一个LDT都会作为一个段,存入GDT中.由于CPU在任何时刻只能执行一个任务的代码,因此存储LDT所需要的寄存器也就只需要一个,Intel将其命名为LDTR,与GDT相同,Intel为装入LDT设置了LLDT指令.与GDT不同的是,LLDT指令的操作数却是一个16位的段选择子,即前面说到的要装入的LDT在GDT中的索引值.这里需要指出的是,LDT并不是必须的,你的程序可以选择使用,或者不使用它.
前面提到了一个新概念--段选择子.我们说段选择子是要引用段在GDT或LDT中的索引值,其实这种说法并不正确.因为段选择子除了含有索引值以外,它还包含了其他内容.
段选择子的结构如下图:
由于是从Word文档中复制过来,表格无法显示,详情请查阅相关文档。
段选择子是一个16位的数据结构,其包含了三部分内容.其中,其高13位正是前面所说的索引值,TI用来指定是在GDT中索引,还是在LDT中索引(0 = GDT, 1 = LDT),RPL则是用来指明请求特权级的.
谈到这里,我们就已经阐明了在PM的段模式下,如何引用一个内存地址.首先,将段选择子装入相应的段寄存器中,然后CPU会自动根据段选择子找到相应的段描述符,并找出基地址,最后在加上偏移量,就得到了所需要的内存地址.
在本文开始的部分,我已经说过GDT是进入 PM所必需的数据结构,下面就详细的来讨论一下如何设置好GDT,并将其装入相应的寄存器.
首先必须注意的两点是:
(1).GDT中的第一个描述符必须是空,即全为0.在程序中这个描述符不能用来进行内存访问,否则将产生General Protection异常.(2).由于GDT中的描述符都是64位长,因此为了让CPU的访问速度达到最快,需要将GDT的入口地址以8字节对齐,即放入8的倍数的位置.
下面,开始设置进入PM后的代码段和数据段的描述符.
其格式如下:
G - 粒度
D/B - 大小(0 = 16位段; 1 = 32位段)
D - 保留
AVL - 用户定义
P - 段是否存在 DPL - 描述符特权级
注意P位,这个位确定了段是否存在.这是什么意思呢.当该位被清除时,如果存在任务要访问这个段.那么CPU会产生一个错误,并会从外存(如:硬盘)中调入该段并再次尝试.当该位被清除时,描述符中的0到39位和48到63位能够包含任意值.你也可以用这些空间来存储该段在磁盘空间中的地址.
还有就是A位,CPU会在对其所在段写入数据后,将该位置1,这样在做段的磁盘交换时,可以决定是否将该段写入磁盘.
下面要说的就是G位.你会发现,在描述符中段界限仅仅为20位.那么其如何能够设置成1MB到4GB之间的范围呢.这里G位其了重要的作用.当G位被清零时,界限域就是段的最大合法偏移.而如果G位被置成1,那么会把描述符中的段界限左移12位形成32位界限,再将低12位全部填1.这样,实际上就能够指定1MB以下的任意长度,和以4K到4GB为单位的长度.
假定要在进入PM后,使代码段和数据段能够访问全部线性空间,于是可将GDT设置为:
gdt dd 00000000h, 00000000h ;空
gdt.Code32 dd 0000ffffh, 00cf9a00h ;代码段 读/执行 4GB空间 基地址=0 粒度=4096,386
gdt.Data32 dd 0000ffffh, 00cf9200h ;数据段 读/写 4GB空间 基地址=0 粒度=4096,386
这里你会发现GD中不同的描述符指向了同一块内存空间.这在系统中是允许的.在实际应用中,这也是经常要使用到的,例如:操作系统可以将一个可执行文件装入数据段,然后再从同一位置开始执行.
在设置好GDT以后,需要将其装入相应的寄存器中.前面说过GDTR寄存器包含两段内容,因此我们需要先算出GDT的绝对物理地址.
GDTR的具体内容
gdtr dw gdtr - gdt - 1;界限
dd gdt;前面GDT的地址
实现代码如下:
mov eax, ds
shl eax, 4
add [gdtr+2], eax ; 生成绝对物理地址
lgdt [gdtr] ; 将gdtr装入寄存器
到此,就完成了进入保护模式的最主要工作,可以进入PM模式了.
实现代码如下:
mov eax, cr0
or al, 1
mov cr0, eax ; 修改CR0寄存器,置PE = 1
jmp dword 8:_premain32 ; 8为选择子
你可能会问为什么要在代码的最后添加一个jmp语句.这是因为,我们必须要清除CPU的指令预取队列(流水线),并以此来设置CS段寄存器. 不过这仅仅是其一,还有一个重要的原因就是,我前面谈到的那个段寄存器大于16位的不可见部分.需要指出的是,Intel对于这一点是未公开的,因此我下面对该问题的讨论仅仅是由推断得出来的.事实上,当我们执行一条装载CS寄存器的指令时,操作数被装入了寄存器的可见部分,而CPU会自动根据操作数,去设置其不可见部分.CS段寄存器所处的状态与当前在哪个模式下并无关系.在刚刚进PM后,CS仍然认为当前处于16位段,即当前的地址仍是16位地址.因此,必须通过装载一32位指令去切换到32位段,这也就是jmp在这里的意义.
虽然程序已经进入了PM,但需要做的事情还远没有做完,因为我们还没有配置IDT,即中断描述符表,而要理解这个表又要牵涉到许多内容,因此,我将在后面的文章中详细介绍,这里就不多谈了.
Intel之所以将这个模式起名为保护模式,其来源就在于特权保护.在PM下,每一个任务都拥有自己的特权级(PL).Intel将其分为四个级别,由零到三.数字越低级别则越高.例如:PL3级的程序对于一些特定的指令,如:HLT,LGDT,LLDT等,没有执行的权限,并且其也不允许访问拥有高特权级程序的数据.
在PM下,I/O的访问同样也受到了特权保护.在EFLAGS中存在一个IOPL域.这个域的值决定了能够执行I/O操作的最低权限.例如,当IOPL为3的时候,表明所有特权级的程序都能够执行I/O操作.这个域的值仅允许PL0级的程序进行修改,其他级的程序修改无效.
同样,数据访问在PM下也是受到保护的.当数据段寄存器要被加载时,CPU会将该段的描述符特权级(DPL)与一个被称为有效特权级(EPL)的数值进行比较,如果DPL不小于EPL,则允许装载寄存器,否则将会产生一个错误.这里的EPL就是选择子的RPL和程序当前特权级(CPL)的数值较大的那一个.
对于堆栈,则有些不同,访问SS寄存器,其DPL要求必须和CPL相等.
关于保护模式,本篇文章就介绍到的这里.对于保护模式的其他重要特性,如:分页操作,多任务处理等,由于内容很多,几乎每一块内容都能当成一个专题来讲,因此我将在以后的文章中对此进行详细讨论.
中断和异常
学过8086/8088汇编的人肯定对于中断这个概念都不陌生.在80386中,这个概念在一定程度上发生了变化,并引入了"异常"这个新概念.本篇文章就是围绕在操作系统开发中涉及到中断和异常的讨论.
中断
中断在系统中是由外部事件所引起的,如:一次I/O操作的结束.其产生与CPU当前所执行的指令没有关系.从是否能够被屏蔽来划分,可将其分为两类,即可屏蔽中断与不可屏蔽中断,其中前者由CPU的INTR引脚接收信号,后者由NMI引脚接收信号.
由于产生中断的中断源并不单一,因此在INTR接收中断的时候,同样还要接收一个8位的中断向量号,以判断是谁发出的中断请求.CPU对于某个中断向量号是由谁发出原则上并没有规定.但在实际系统中,为了避免产生冲突,部分中断向量号都有自己固定的中断源,这一工作是由可编程中断控制器(PIC)完成的.在80386系统上,该PIC是8259A.该芯片功能十分强大,不仅能向CPU提供中断向量号,还可以自主处理中断请求的优先级.每个8259A芯片可以支持8个中断请求信号,并可进行级连.对于8259A的介绍,我们将在文章的后半部分进行.
对于是否屏蔽可屏蔽中断可以通过8259A有选择地控制,也可以通过CPU的CLI和STI指令实现.CLI和STI指令可以设置EFLAGS寄存器的IF位,如果该位被清除,则CPU会禁止外部中断传递信号给INTR引脚.但对于CPU内部异常和NMI该位不起作用.在执行这两条指令时,必须要保证当前CPL小于等于IOPL,否则会引起通用保护故障.当然虽然NMI是不可屏蔽中断,但通过将CMOS端口(0x70)中第7位置1这种手段也可以将NMI也屏蔽掉的,当然需要打开该中断将该位清零就可以了.
实现代码如下:
#define PORT_CMOS 0x70
void disable_NMI(){
byte val;
val = inportb(PORT_CMOS);
outportb(PORT_CMOS, val | 0x80);}
void enable_NMI(){
byte val;
val = inportb(PORT_CMOS);
outportb(PORT_CMOS, val & 0x7F);}
异常
异常是在CPU执行指令期间遇到非法指令所产生的.因此异常与当前指令存在着关系,例如:除零,特权级不正确等等,都会触发异常.80386可以识别多种不同的异常,并以不同的中断向量号来标示它们.在异常发生时,CPU就根据原先设定好的中断向量号转到不同的中断处理程序(ISR)执行.
根据是由是否可恢复和恢复点位置不同将异常划分为三种.它们是故障(Fault),陷阱(Trap)和中止(Abort).
80386认为故障是可以排除的,因此在CPU遇到引起故障的指令的时候,会保存当前的CS和EIP值,并转去执行故障处理程序.在故障排除后,执行IRET指令回到刚在引发故障的位置,重新执行刚才触发故障的指令.例如,当程序企图装入一个不存在的段时将会引发一个故障,这时操作系统会将该段装入,并重新进行刚才的操作.
陷阱与故障的区别主要在于,在执行陷阱处理程序之前,系统会保存CS和EIP的值为引起陷阱的下一条要执行指令所在的位置.例如,软中断就是典型的陷阱.
中止是在系统发生严重错误时产生的.在引起中止后,当前执行的程序不能被恢复执行.并且系统在接受到中止后,中止处理程序要重新建立各种系统表.引起这类错误的主要原因是系统表的数据不一致或者非法等.
下表列出了80386在保护模式下的中断和异常
向量号 名称 异常类型 出错代码 相关指令
0 除法错 故障 无 DIV,IDIV
1 调试 故障/陷阱 无 调试状态下
3 断点 陷阱 无 INT 3
4 溢出 陷阱 无 INTO
5 界限检查 故障 无 BOUND
6 无效操作码 故障 无 非法指令
7 无80X87 故障 无 ESC, WAIT再无协处理器情况下
8 双重故障 中止 有 任何指令
9 NPX 中止 无 ESC的操作数超过段尾
0AH 无效TSS 故障 有 JMP、CALL、IRET或中断
0BH 段不存在 故障 有 装载段寄存器的指令
0CH 堆栈段异常 故障 有 任何使用SS寄存器的访问
0DH 通用保护 故障 有 任何内存访问
0EH 页异常 故障 有 任何内存访问
10H 协处理器出错 故障 无 ESC, WAIT
11H-0FFH 软中断 陷阱 无 INT n
这里一些异常在发生时会将错误码压入堆栈,这些错误码都是导致错误的选择子.对于错误代码为0的异常,其除了表示空选择子导致的错误外,当CPU不能确定时也会返回该数值.
这里需要指出的是,对于异常0DH,它的产生没有一个准确的原因,但通常来讲是基于以下几个原因:
1.试图用一个超过段界限的偏移量访问段
2.将一个不可执行的段装入CS寄存器
3.将一个只执行段装入除CS寄存器外的其他段寄存器
4.写只读段
5.使用空选择子
6.转换到一个忙任务
7.中断描述表
8.门描述符
在系统中除了存储段描述符和系统段描述符外,还有一类门描述符.这种描述符是用来描述控制转移的入口点.任务内特权级的改变和任务间且换都是通过这种描述符实现的.其中,门描述符共分为4种分别是,调用门(CallGates),陷阱门(Trap Gates),中断门(Interrupt Gates)和任务门(TaskGates).由于本章内容主要是涉及中断和异常,因此,在这里我们将对陷阱门和中断门进行详细介绍,对于另两种门会简要的做一介绍.
陷阱门和中断门
这两种门是用来描述中断和异常的入口的.其只能出现在IDT中(对于IDT后面将有详细描述),不能出现在GDT和LDT中.
这两种门的格式如下表:
中断门描述符
由于是从Word文档中复制过来,表格无法显示,详情请查阅相关文档。
陷阱门描述符
由于是从Word文档中复制过来,表格无法显示,详情请查阅相关文档。
注:DPL - 描述符特权级
P - 门有效标志
D - 门规模(1 = 32位; 0 - 16位)
这里的段选择子用来查找GDT和IDT,得到一个代码段描述符,并最终得到代码段的基地址,再加上图中的偏移量就能够得到中断处理程序的入口了.由于中断处理程序是在当前任务的上下文中运行的,因此可能会出现中断处理程序与被中断程序特权级不一致的问题,这时就会发生堆栈切换.对于由软中断所产生的中断和异常CPU要求,CPL必须小于等于门的DPL.
在整个中断处理程序中,CPU会将TF置成0,以禁止中断处理程序单步执行,并将NT置成0,以在使用IRET指令返回时是回到同一个任务.对于中断门和陷阱门,其就在于对EFLAGS寄存器中IF标志的处理方法不同,当调用中断门时,IF被清除.而调用陷阱门时则不对IF进行处理.
在从中断处理程序返回的过程中,如果当初是通过陷阱门或中断门进入的,则从堆栈顶弹出EIP和CS,以及EFLAGS.然后根据CS寄存器选择子的RPL字段确定返回后的特权级.值得注意的是,如果RPL为一个内层特权级,则将会产生通用保护故障.对于需要提供出错误码的中断处理程序,则必须先人为地从堆栈中弹出出错误码,在执行IRET指令返回.
进入中断和异常还可以通过任务门,即将中断处理程序作为一个任务进行处理,使用该方法即将中断处理程序当成一个任务来看待,对于这种方式的具体操作在以后的文章中会有讨论.而对于调用门由于其只能出现在GDT和LDT中,因此与我们这里讨论的中断和异常无关.
中断描述表(IDT)
前面我们提到了一个叫做IDT的表,这个表的作用实际上与在实模式下的IVT(中断向量表)相同.不过在具体内容上IDT要比IVT丰富的多,在IDT中装载的是我们前面介绍过的门描述符,而不仅仅向IVT那样仅包含一个中断处理程序的地址.
IDT是由门描述符组成的一个数组,每个门描述符对应一个中断/异常向量.像全局描述符(GDT)一样,在系统中IDT也仅存在一个.其可以保存在内存中的任何位置,CPU通过访问IDTR寄存器获取IDT的位置.IDTR的长度为48位,其中包括保存IDT的32位线性地址和16位的大小.对于IDTR寄存器的操作包含两个指令,一个是LIDT,另一个SIDT.LIDT用来将指定的IDT所在线性地址和其长度装入IDTR寄存器.而SIDT则是将IDTR寄存器的内容读出.值得注意的是,LIDT仅能在CPL为0时执行,而SIDT则不受此限制,可以运行在任何特权级下.
当系统发生中断或异常时,CPU会以所产生的中断向量号为索引去查找IDT,通过找到的门描述符,转到中断处理程序处执行.
下面是设置中断描述表的代码
#define PARAM_1 ebp+8+4*0
_LIDT:
push ebp
mov ebp, esp
mov eax, [PARAM_1]
lidt [eax]
pop ebp
ret
_SIDT:
push ebp
mov ebp, esp
mov eax, [PARAM_1]
sidt [eax]
pop ebp
ret
#define ACS_PRESENT 0x80
#define ACS_INT 0x0E
#define ACS_INT_GATE (ACS_INT | ACS_PRESENT)
#define ACS_DPL_0 0x00
#define ACS_DPL_1 0x20
#define ACS_DPL_2 0x40
#define ACS_DPL_3 0x60
//IDTR结构
typedef struct IDT_REG {
word limit;
dword base;}IDT_REG;
//中断描述符
typedef struct INT_DESCRIPTOR{
word offs0_15;
word sel;
byte paramcnt;
byte attrs;
word offs16_31;}INT_DESCRIPTOR;
//设置中断描述表
static void setup_IDT(){
dword i;
//清空IDT
memset (&idt, 0, sizeof(idt));
// Int 0Dh - 通用保护故障
idt[0x0D].offs0_15 = ((dword)(&isr_0D_wrapper))&0xFFFF;
idt[0x0D].offs16_31 = ((dword)(&isr_0D_wrapper)) >> 16;
idt[0x0D].sel = 8;
idt[0x0D].paramcnt = 0;
idt[0x0D].attrs = ACS_INT_GATE;
// Int 0Eh - 页面错误
idt[0x0E].offs0_15 = ((dword)(&isr_0E_wrapper))&0xFFFF;
idt[0x0E].offs16_31 = ((dword)(&isr_0E_wrapper)) >> 16;
idt[0x0E].sel = 8;
idt[0x0E].paramcnt = 0;
idt[0x0E].attrs = ACS_INT_GATE;
// IRQ0...0Fh 设置0x20为起始中断向量号,从0x20到0x2F的初始化
//省略若干项
idt[0x26].offs0_15 = ((dword)(&fd_handler))&0xFFFF;
idt[0x26].offs16_31 = ((dword)(&fd_handler)) >> 16;
idt[0x26].attrs = ACS_INT_GATE | ACS_DPL_1;
idt[0x26].sel = 8;
idt[0x26].paramcnt = 0;
idt[0x2E].offs0_15 = ((dword)(&ide_interrupt))&0xFFFF;
idt[0x2E].offs16_31 = ((dword)(&ide_interrupt)) >> 16;
idt[0x2E].attrs = ACS_INT_GATE | ACS_DPL_1;
idt[0x2E].sel = 8;
idt[0x2E].paramcnt = 0;
// SYS_INT 操作系统中断调用
idt[0x30].offs0_15 = ((dword)(&isr_30_wrapper))&0xFFFF;
idt[0x30].offs16_31 = ((dword)(&isr_30_wrapper)) >> 16;
idt[0x30].sel = 8;
idt[0x30].paramcnt = 0;
idt[0x30].attrs = ACS_INT_GATE | ACS_DPL_3;
idtr.base = (dword) &idt;
idtr.limit = sizeof(idt)-1;
LIDT(&idtr);}
中断优先级
在系统发生中断或异常时,为了能够尽快处理紧急或重要的事务,系统将它们按类型赋予了不同的优先级.CPU在处理时总是优先处理优先级最高的中断或异常,而对于同一级别的中断或异常,则按照先进先出(FIFO)的原则处理.
当CPU在处理中断或异常时,如果又产生了其他的中断或异常,这时CPU会检查产生的中断或异常的优先级是否比当前处理的要高,如果是的话,则CPU会保存当前中断处理的上下文,然后转去处理优先级最高的那个中断或者异常.对于那些未接收处理的异常,系统则将它们扔掉.而未接受处理的中断则将保持悬挂状态.系统之所以这样做,主要是基于以下原因,即硬件的异常永远为最高优先级,所以其永远不会被丢弃.因此丢弃的异常都是由软件所引起,而这些异常由于未被解决,所以在系统处理完当前中断或异常后,会重新执行这些引起异常的点,再次触发异常,并等待解决.
下表按照优先级由高到低的顺序列出了中断和异常类型:
中断/异常类型
调试故障
80386响应 其它故障
中断/异常 陷阱指令INT n和INTO
的优先级 调试陷阱
NMI中断
INTR中断
8259A
在前面我们已经看到,在CPU处理的各种中断中,有很大一部分是来自外部硬件设备的中断,这些中断通过可编程控制器(PIC)控制.在IBMPC兼容机上该控制器为Intel 8259A芯片.
单个8259A芯片最多可以连接8个中断源,但由于可以最多将9个该芯片级连,因此,其最多可以接受64个中断源.在IBMPC机上采用2个8259A芯片级连,最多支持15个中断源.这两个芯片一个叫做Master,另一个叫做Slave.之所以这么称呼是因为,由于CPU只具有INTR这一个中断线,所以Slave必须连级到Master上,占用MasterPIC的IRQ2,将IRQ9重定向到IRQ2上.
8259A芯片处理中断的过程,主要是通过芯片内3个内部寄存器进行的.这三个寄存器分别为IMR,IRR和ISR其中,IMR用作过滤被屏蔽的中断,IRR用来存放被悬挂的中断并等待进一步处理,ISR用来保存CPU正在处理的中断.
另外8259A芯片还有一个叫做优先级仲裁的单元.该单元的作用是在8259A同时接受到多个中断时,根据各个中断的优先级,挑选具有最高优先级的中断传递给CPU处理.
在大致介绍这几个单元后,下面我们来看一些8259A在处理中断时的具体过程.
首先,外部中断请求(IR0到IR7)传输到IMR,IMR根据此中断请求是否被屏蔽,以决定是将其丢弃,还是放入IRR中等待进一步处理.当8259A等待到一个中断时机时,优先级仲裁单元会从所有放入IRR中的中断请求中挑出一个优先级最高的中断,传递给CPU处理.值得注意的是中断优先级是随着中断请求号降低而提高的.在CPU的INTR引脚接收到8259A发送过来的信号后,CPU会暂停执行下一条指令,并向8259A发送一个INTA信号.在8259A接收到该信号后,就会将ISR中代表该中断的位置1,并将IRR中相应的位清零.以表示该中断正在被CPU处理.接着CPU会向8259A再发送一个INTA信号,向其请求中断向量号.这时,8259A会根据先前设置好的起始向量号再加上中断请求号计算出中断向量号,并将其放入数据总线中.这时候,如果8259A的EOI通知被设定为自动模式,那么8259A就会自动将ISR中刚才置1的位清零.在CPU获得该中断向量号后,就会转去调用该中断服务程序.在处理完该中断后如果8259A的EOI通知被设定为人工模式,则还要向8259A发送一个EOI.通常来讲,这一工作往往是在中断服务程序中完成.在8259A接收到该EOI通知后,就会将ISR中刚才置1的位清零.
以上就是8259A处理一个中断的整个过程的简述.由于中断请求存在着优先级,因此,如果在一个中断处理期间,8259A又收到了新的中断请求,则首先跟当前处理的优先级进行比较,如果新到的中断请求的优先级高于当前处理的中断请求,则马上处理新到的中断请求,否则则将新到的中断请求放入IRR.
对于8259A的操作,是通过端口进行的.其中,Master的端口地址为0x20, 0x21, Slave的端口地址位0xA0,0xA1.8259A具有两种命令,一种是ICW,其作用是用来初始化8259A芯片.另一个是OCW,其作用是用来向8259A发送命令.虽然在系统启动后BIOS会自动初始化8259A,但这并不是我们所需要的.因为在进入保护模式后,我们要设置IDT,因此我们必须根据所设置的IDT去初始化8259A.
对8259A的操作有两类命令,其中一类是ICW,另一类是OCW.ICW用来对8259A进行初始化,而OCW则用来在初始化后对8259A发布命令.有意思的是,8259A的两个端口对于这两类命令的发布是有固定安排的.对于0x20和0xA0端口,你可以向它们写入ICW1,OCW2,OCW3,读取IRR和ISR.对于0x21和0xA1端口,你可以向它们写入ICW2,ICW3,ICW4,并能够读写IMR寄存器.
下面我们分别来讨论这几个命令
ICW1:该命令作为初始化序列的第一条命令,一旦向端口送入该命令,8259A就认为初始化序列开始.
位 功能
7:5 MCS-80/85模式下的中断向量地址
4 必须设置为1
3 0:Edge Triggered Interrupts 1:Level Triggered Interrupts
2 0:Call Address Interval of 8 1:Call Address Interval of 4
1 0:Cascaded PICs 1:Single PIC
0 0:Don't need ICW4 1:Will be Sending ICW4
在设置时,对于80x86的CPU,其应设置为(00010001),也就是0x11.
ICW2:该命令用来指定所初始化的8259A中断请求的起始向量.其中ICW2的低3位必须为0,其这么做的原因在于当该8259A接收到一个中断请求时,低3位会自动填充为所接受到的向量号.因此这也就决定了我们设置的起始中断向量,必须为8的倍数.
ICW3:Master PIC和Slave PIC对于ICW3命令具有不同的格式
对于Master PIC,Slave PIC被接到了Master PIC的哪个IRQ上,则ICW3中相应的位就置1.在8259A中,由于SlavePIC是级连在Master PIC的IRQ2上的,因此ICW3的值应该为(00000100),也就是0x04.而对于SlavePIC其高5位必须设置为零,低3位为该PIC被级连到哪个Master PIC的IRQ号,在8259A中,其SlavePIC的值为(00000010),即0x02.
ICW4:
位 功能
7:5 保留,设置为0
4 0:Not Special Fully Nested Mode 1:Special Fully Nested Mode
3:2 0x:Non-Buffered Mode 10:Buffered Mode - Slave 11:Buffered Mode - Master
1 0:Normal EOI 1:Auto EOI
0 0:MCS-80/85 1:8086/8088 Mode
在80x86模式下,我们采用默认的Full Nested Mode,将ICW4设置为(000000001),即0x01.
而我们之所以我们要采用NormalEOI,其原因在于我们要允许中断请求的按优先级抢占.如果我们将EOI通知设定为自动模式,那么在CPU发出第二个INTA信号后,8259A中相应的ISR就会自动清零,而此时该中断服务程序还没有被调用.如果在该中断服务程序被调用的过程中,8259A收到了优先级比当前正在处理的中断优先级低的中断请求,由于正在处理的中断在ISR中相应的位已经清零,因此这个新的中断请求就完全可以抢占正在处理的优先级比它高的中断服务程序.
下面是初始化8259A的代码:
void init_8259A(byte master_vector,byte slave_vector){
outportb (PORT_8259A_M, 0x11); /* 开始对8259A进行初始化*/
outportb (PORT_8259A_S, 0x11);
outportb (PORT_8259A_M+1, master_vector); /* 起始中断向量号*/
outportb (PORT_8259A_S+1, slave_vector);
outportb (PORT_8259A_M+1, 1<<2); /*设置对IRQ2的掩码 */
outportb (PORT_8259A_S+1, 2); /* 设置对IRQ2的级连*/
outportb (PORT_8259A_M+1, 1); /* 完成对8259A的初始化*/
outportb (PORT_8259A_S+1, 1);}
在介绍完初始化这几个命令后,我们开始介绍如何通过OCW对8259A进行操作.
OCW1:该命令用来屏蔽所设定的中断请求.其操作方式是,向你要屏蔽的中断请求所在的8259A发送一个操作控制字.需要屏蔽哪个中断请求就将该字上相应的位置1即可.
实例代码如下:
#define PORT_INT_MASK_M 0x21
#definePORT_INT_MASK_S 0xA1
void mask_IRQ(byte IRQ){
byte mask;
if(IRQ > 15)
return;
if(IRQ < 8){
mask = inportb(PORT_INT_MASK_M);
mask |= 1 << IRQ;
outportb(PORT_INT_MASK_M, mask);}
else{
mask = inportb(PORT_INT_MASK_S);
mask |= 1 << (IRQ-8);
outportb(PORT_INT_MASK_S, mask);}
}
void unmask_IRQ(byte IRQ){
byte mask;
if(IRQ > 15)
return;
if(IRQ < 8){
mask = inportb(PORT_INT_MASK_M);
mask &= !(1 << IRQ);
outportb(PORT_INT_MASK_M, mask);}
else{
mask = inportb(PORT_INT_MASK_S);
mask &= !(1 << (IRQ-8));
outportb(PORT_INT_MASK_S, mask);}
}
OCW2:
位 功能
7:5
4:3 Must be set to 0
2:0
如果OCW2中的bit6被设置为0,那么该命令将对整个8259A有效.否则,将针对bit2:0这3位所代表的IRQ进行操作.由于我们前面已经将8259A设置为手动EOI模式,所以在这里我们要将bit7:5设置为(001)
OCW3:
位 功能
7 Must be set to 0
6:5 10:Reset Special Mask 11:Set Special Mask
4 Must be set to 0
3 Must be set to 1
2 0:No Poll Command 1:Poll Command
1:0 10:Next Read Returns IMR 11:Next Read Returns ISR