文章目录
01、中断和异常概述
协同式任务切换不一定要通过一个专门的接口例程进行,也可以用一般的例程进行。
用户任务通过调用门切换到内核的put_string
例程执行,在内核任务返回用户任务之前可以进行任务切换,之后再通过retf
返回用户任务执行。
- 硬件中断信号,
NMI
是不可屏蔽中断、INTR
是来自硬件中断引脚的可屏蔽中断。随机产生,与处理器是异步的。 - 软件中断,
INT n
是在软件内部主动引发的中断。 - 处理异常中断(
Exceptions
),是处理器内部产生的中断,表示处理器执行时产生了错误的状况。比如当处理器执行一条非法指令或者因条件不具备指令不能正常执行时将会引发这种类型的中断。如div指令
中除数是0
的情况。
中断和异常。
按照异常的产生原因分类:
- 指令执行异常:处理器在执行指令时检测到程序的错误并由此而引发的异常。
- 程序调试异常:供调试器使用,由
INTO、INT3
主动发起。用来检查特定的机器状态是否出现。INTO
检查标志寄存器的OF=1
(溢出标志)则执行指令引发异常。INT3
指令供调试器进行单步执行。 - 机器检查异常:和处理器架构有关,如在奔腾4、P6处理器家族上就实现了机器检查架构,用这种异常检测与硬件有关的总线错误、奇偶校验错误、高速缓存错误等等。
根据异常的性质和严重性分类:
- 故障(
Faults
):通常可以纠正,如缺页异常。中断程序返回的是当前指令。 - 陷阱(
Traps
):通常是在执行了截获陷阱条件的指令之后立即产生,通常用于调试INT3、INTO
。中断程序返回的是当前指令的下一条指令。 - 终止(
Aborts
):通常标志最严重的错误,如硬件错误、系统表错误(如GDT
、LDT
数据不一致、无效、错误),这类异常一般无法精确的报告引起错误的指令的位置。发生时程序和错误都不可能重新启动,双重异常(当处理器发生异常时,在转入异常执行时有发生了另外一个异常)如中断向量号18,INT 0x18
。
对于某些异常来说,处理器在转入异常处理程序之前,会在当前栈中压入一个称为错误代码的数值,这样可以帮助诊断异常产生的位置和原因。
02、保护模式下中断和异常的向量分配
中断和异常的编号叫做中断向量。
其中错误代码是在中断发生时,在进入中断处理程序之前压在栈中的错误代码。
03、中断描述符表、中断门和陷阱门
实模式下的中断向量表:
中断发生时,处理器要么自发产生一个中断向量、要么从软中断指令的操作数的到中断向量、或者从外部的中断控制器取得一个中断向量。将该向量作为索引访问中断向量表IVT
,具体做法是将中断向量乘以4作为偏移量访问IVT,从中取得中断处理过程的段地址和偏移地址,并转到那里执行。
保护模式下:使用中断描述符表IDT
(Interrupt Descriptor Table
),保存和中断处理过程相关的描述符,包括中断门、陷阱门、任务门,门是特殊的描述符。
中断门
描述符用来描述中断处理过程。
陷阱门
描述符用来描述陷阱中断的处理过程。
任务门,32位处理器中支持,若IDT
中描述的是一个任务门,则执行的是一个任务切换。在64位处理器中既不支持硬件任务切换、也不支持任务门。
实模式下的中断向量表IVT
只能位于内存的最低端。保护模式下的中断描述符表IDT
可以位于内存的任何位置。
IDT
的第一个描述符即0号槽位
也是有效的。
- 处理器用中断向量 乘以 8得到表内偏移量,联合
IDTR
内的IDT
基地址去访问内存; - 从中取得中断门或陷阱门描述符;
- 在描述符中有中断处理过程的代码段选择子和段内偏移量;
- 取决与代码段选择子的
TI位
,去GDT
或LDT
中取得目标代码段的描述符。 - 从目标代码段的描述符中取得目标代码段的段基地址;
- 将段基地址 和 偏移量相加得到中带处理过程的线性基地址,从而转移执行。
使用中断向量访问IDT
时,中断向量超过IDT
界限值时,就会产生常规保护异常#GP
。
04、本章程序介绍
引导程序:c13_mbr0.asm
- 1、取出
GDT
所在线性基地址 - 2、创建本程序相关描述符,接着使用
cli
指令关闭中断响应。 - 3、进入保护模式
- 4、加载内核代码到内存中
- 5、创建内核相关描述符
- 6、跳转执行内核
内核程序:c30_core0.asm
- 1、创建各个段的选择子常量和
IDT
线性地址; - 2、内核头部段;
- 3、内核公共历例程段,除了之前创建的相关历程,本章增加了几个和中断相关的例程;
- 4、内核核心数据段,各种数据。
- 5、内核核心代码段,改变了内核入口点
start
的程序。
用户程序0:c30_app0.asm
- 其他不变,死循环打印字符
,,,,,
。
用户程序1:c30_app1.asm
- 其他不变,死循环打印字符
cccccccccc
。
05、创建并安装全部的256个中断门
在进入内核start
之后,准备创建内核任务、用户任务并进行任务切换,在此之前需要主备好保护模式下的中断系统。
中断或异常发生时,并不是直接调用中断或异常处理程序,而是用中断向量先到中断描述符表中寻找对应的中断描述符,即中断门或陷阱门,之后从中断门或陷阱门中间接找到中断处理过程。意味着必须为这个通用的中断处理过程创建中断门或陷阱门,并安装在中断描述符表IDT
中。
创建中断门代码如下:
。。。
;前20个向量是处理器异常使用的
mov eax,general_exception_handler ;门代码在段内偏移地址
mov bx,sys_routine_seg_sel ;门代码所在段的选择子
mov cx,0x8e00 ;32位中断门,0特权级
call sys_routine_seg_sel:make_gate_descriptor
。。。
中断门属性值8E00
:
创建好之后需要安装在中断描述符表IDT
中,IDT
现在还没有创建,创建IDT
就是指定表的其实线性基地址,并从这个地址安装中断门和陷阱门就可以了。
目前系统内存布局:
依次安装中断门或陷阱门:前20个中断门指向通用处理过程general_exception_handler
。
。。。
mov ebx,idt_linear_address ;中断描述符表的线性地址
xor esi,esi
.idt0:
mov [es:ebx+esi*8],eax ;基址变址寻址
mov [es:ebx+esi*8+4],edx
inc esi
cmp esi,19 ;安装前20个异常中断处理过程
jle .idt0
。。。
之后安装通过的中断门:后236个中断门都指向同一个中断处理程序general_interrupt_handler
。
。。。
;其余为保留或硬件使用的中断向量
mov eax,general_interrupt_handler ;门代码在段内偏移地址
mov bx,sys_routine_seg_sel ;门代码所在段的选择子
mov cx,0x8e00 ;32位中断门,0特权级
call sys_routine_seg_sel:make_gate_descriptor
mov ebx,idt_linear_address ;中断描述符表的线性地址
.idt1:
mov [es:ebx+esi*8],eax
mov [es:ebx+esi*8+4],edx
inc esi
cmp esi,255 ;安装普通的中断处理过程
jle .idt1
。。。
06、为实时时钟中断创建和安装中断门
使用实时时钟中断,默认中断号0x70
,当发生0x70
号中断时并不是执行一个通用的中断过程,而是执行它自己的中断处理过程rtm_0x70_interrupt_handle
。
现在需要创建0x70
号中断的中断门,并安装在中断描述符表中,以替换原先的通用中断门。
。。。
;设置实时时钟中断处理过程
mov eax,rtm_0x70_interrupt_handle ;门代码在段内偏移地址
mov bx,sys_routine_seg_sel ;门代码所在段的选择子
mov cx,0x8e00 ;32位中断门,0特权级
call sys_routine_seg_sel:make_gate_descriptor
mov ebx,idt_linear_address ;中断描述符表的线性地址
mov [es:ebx+0x70*8],eax
mov [es:ebx+0x70*8+4],edx
。。。
07、加载中断描述符表寄存器IDTR
接上一节,现在已经在中断描述符表中安装了256个中断门,除了0x70
号中断,其他都指向默认中断或异常处理过程。
当中断发生时,处理器如何找到中断描述符表呢?处理器中有一个中断描述符表寄存去IDTR
,保存着中断描述符表IDT
的线性基地址以及长度。现在应该将IDT
的基地址和界限值加载到IDTR
中。
偏移为m
的地方开辟出6个字节的空间。前2字节保存IDT
的界限值、后4字节保存这IDT
的线性基地址。执行此条指令时,处理器用段寄存器中的线性基地址 加上 指令中的偏移m
构成物理地址访问内存取出这6个字节。然后传送到处理器内部的IDTR
寄存器中。该指令在实模式下也能执行。
开机时,IDTR
中基地址被初始化为0x00000000
,界限值被初始化为0xFFFF
。lidt
指令不影响任何标志位。代码如下:
。。。
;准备开放中断
mov word [pidt],256*8-1 ;IDT的界限
mov dword [pidt+2],idt_linear_address
lidt [pidt] ;加载中断描述符表寄存器IDTR
。。。
08、重新设置8259A主片的中断向量
接上一节,理论上此时就可以开放中断,对到来的中断进行处理。但是还有一个问题,若中断控制器芯片还是8259A
,就需要对其重新初始化。
BIOS
会将8259A
主片中断号设置为如下,基本输入输出系统会将从片中段号设置为如下:
由于主片的中断向量和异常的中断向量冲突,所以需要重新初始化中断向量。
修改为后面的中断向量:
对8259A
编程需要使用初始化命令字ICW
。共4个,都是单字节命令,不是单独发送的,而是按顺序全部发送一遍,ICW1~ICW4
,取决于ICW1
、ICW2
的内容,可能ICW3
、ICW4
不需要发送。
对于主片来说先向0x20
号端口发送ICW1
,对于从片来说要想0xA0
号端口发送ICW1
。ICW1
是一个标志,每次8259A
芯片接收到ICW1
表示一个新的初始化过程开始了。
从0x20
、0xA0
接收ICW1
之后,8259A
期待从0x21
、0xA1
接收ICW2
,后续是否期待ICW3
、ICW4
要看ICW1
的内容。ICW1
发送给0x20
、0xA0
号端口作为标志,之后ICW2
、ICW3
、ICW4
会发送给0x21
、0xA1
号端口。
ICW1:
ICW2:
ICW3:
ICW4:
代码如下:
。。。
;设置8259A中断控制器
mov al,0x11
out 0x20,al ;ICW1:边沿触发/级联方式
mov al,0x20
out 0x21,al ;ICW2:起始中断向量
mov al,0x04
out 0x21,al ;ICW3:从片级联到IR2
mov al,0x01
out 0x21,al ;ICW4:非总线缓冲,全嵌套,正常EOI
mov al,0x11
out 0xa0,al ;ICW1:边沿触发/级联方式
mov al,0x70
out 0xa1,al ;ICW2:起始中断向量
mov al,0x04
out 0xa1,al ;ICW3:从片级联到IR2
mov al,0x01
out 0xa1,al ;ICW4:非总线缓冲,全嵌套,正常EOI
。。。
之后设置和时钟中断相关的硬件:
。。。
;设置和时钟中断相关的硬件
mov al,0x0b ;RTC寄存器B
or al,0x80 ;阻断NMI
out 0x70,al
mov al,0x12 ;设置寄存器B,禁止周期性中断,开放更
out 0x71,al ;新结束后中断,BCD码,24小时制
in al,0xa1 ;读8259从片的IMR寄存器
and al,0xfe ;清除bit 0(此位连接RTC)
out 0xa1,al ;写回此寄存器
mov al,0x0c
out 0x70,al
in al,0x71 ;读RTC寄存器C,复位未决的中断状态
。。。
09、中断和异常发生时的特权级检查
接上一节,目前中断描述符表已经创建,在这个表中,与所有中断、异常有关的描述符已经安装完毕。包括0x70
号中断,其中断门已经安装完毕,指向其自己的中断处理过程。
接下来开中断,将标志寄存器的IF位
置1,那么中断就可以随时进来。
当中断发生时,处理器从软中断指令、或中断控制器芯片取得中断向量。用这个向量从中断描述符表IDT
中取出中断门、陷阱门、任务门,但是中断向量只是一个代表中断号码的数字,没有表指示器、RPL
字段,所以中断和异常发生时不检查RPL
字段;
10、中断和异常发生时的栈切换过程
当中断发生时,处理器使用中断向量乘以8,到IDTR
指定的中断描述符表IDT
中取出一个描述符;
去除的描述符可能时中断门、陷阱门和任务门,中断门、陷阱门会转去执行中断处理程序,任务门会进行一个任务切换,本章并不是使用这种方式进行任务切换。
1、若目标代码段的特权级 等于 当前代码段的特权级,则使用当前代码段的栈,即中断和异常发生前正在使用的栈。
2、若目标代码段的特权级 大于 当前代码段的特权级,则处理器会切换到目标代码段的栈,那么这个新栈来自于当前任务的TSS
中。从中选取一个和目标代码段相同特权级的栈。
当中断或异常发生时,若当前特权级CPL
和目标代码段特权级DPL
不同,则系统中必须至上存在一个任务。
- 当前任务如上,若当前正在
代码段2
执行,则发生中断或异常,则切换到代码段1
执行,且不需要切换栈。 - 若当前正在
代码段3
执行,则发生中断或异常,则切换到代码段1
执行,此时需要切换栈
1、首先临时保存段寄存器SS
和栈指针ESP
;
2、根据目标代码段的特权级,从当前任务的TSS
中选取一个栈段选择子和栈指针;
3、将选择的栈段选择子加载到段寄存器SS
,将选择的栈指针加载到栈指针ESP
;
4、切换到新栈,将刚才临时保存的段寄存器SS
和栈指针ESP
压入新栈;
5、接着将EFLAGS
、CS
、EIP
压入新栈;
6、对于有错误代码的异常,处理器还要讲错误代码压入新栈。
7、当中断返回时,要将EFLAGS
、CS
、EIP
恢复,还将恢复原先的栈状态,即恢复之前临时保存段寄存器SS
和栈指针ESP
。
8、恢复之后段寄存器依然指向旧栈段,栈指针寄存器ESP
依然指向进入中断之前的位置。
中断门和陷阱门的区别不大,通过中断门进入中断处理过程时,处理器先将EFLAGS
压栈,再将其IF位清零以禁止嵌套的中断,即进入中断处理程序后不允许再响应别的中断。从中断返回时,将从栈中恢复EFLAGS
的原始状态。
陷阱门的优先级较低,通过陷阱门进入中断处理过程时,EFLAGS
的IF位不变,以允许其他中断优先处理。EFLAGS
的IF
位只影响硬件中断,不影响NMI
、异常、INT形式的软件中断不起作用。
错误代码:
EXT位
:为1表示有NMI
、硬件中断等引发;IDT位
:为1表示段选择子索引指向中断描述符表IDT
中的门描述符,为0表示指向GDT
、LDT
中的描述符;TI位
:表指示器位,当IDT
位为0才有意义。为0表示段选择子索引指向GDT
,为1指向LDT
中的段描述符或门描述符;段选择子索引
:用于指示GDT
或LDT
中的段描述符、或IDT
内的门描述符。此为就是平时使用的段选择子的高13位。
11、在中断处理过程中实施任务切换(含NOP指令的介绍)
接上一节,接着开放中断:
。。。
sti ;开放硬件中断
。。。
假定在执行指令mov ebx, message_0
时发生0x70
号中断,这条指令完成后立即响应中断,用中断号0x70乘以8到中断描述符表中取出中断门进行特权级检查,然后进入0x70
号中断的处理过程执行。
此时还没有创建内核任务,那么TR中的内容是无效的,因为0x70
号中断的处理过程在内核公共例程段,特权级为0,当前特权级也是0,所以不需要切换栈,自然也不需要访问任务状态段TSS
。
0x70
号中断的处理过程:
。。。
;-------------------------------------------------------------------------------
rtm_0x70_interrupt_handle: ;实时时钟中断处理过程
pushad
mov al,0x20 ;中断结束命令EOI
out 0xa0,al ;向8259A从片发送
out 0x20,al ;向8259A主片发送
mov al,0x0c ;寄存器C的索引。且开放NMI
out 0x70,al
in al,0x71 ;读一下RTC的寄存器C,否则只发生一次中断
;此处不考虑闹钟和周期性中断的情况
;请求任务调度
call sys_routine_seg_sel:initiate_task_switch
popad
iretd
。。。
之后的内容为:
- 显示处理器品牌信息;
- 安装调用门,对门进行测试;
- 创建内核任务相关,在创建之前使用
cli
指令清中断,之后sti
指令开放中断; - 创建第一个用户任务,在创建之前使用
cli
指令清中断,之后sti
指令开放中断; - 1、在开中断之后,若立即发生了
0x70
号中断,将执行任务切换,首先执行rtm_0x70_interrupt_handle
,在里面先保存当前内核任务的状态到内核任务的TSS
中,接着将用户任务的状态从其TSS
中恢复到处理器中,TR就指向用户任务,用户任务就成了当前任务。
2、第一次执行用户任务从入口点执行,先切换栈,再死循环打印,,,,,,,
,在执行jmp .do_prn
指令之前若发生了0x70
号中断,又转到rtm_0x70_interrupt_handle
执行;
3、这一次用户任务是当前任务,找到就绪的内核任务,先保存当前任务即用户任务的状态到其TSS
,然后将内核任务的状态从其TSS
中恢复到处理器。此时TR指向内核任务,内核任务成为当前任务。内核任务从下面返回:
因为之前内核任务是从这里切换出去的,热然后retf
返回到中断处理过程rtm_0x70_interrupt_handle
,如下:返回到popad
指令处。
再从这里从中断处理过程返回到内核任务中上一次0x70
号中断的地方,即内核start
里的如下指令处:
添加nop指令的意图是,假定在执行这3条指令期间发生了0x70
号中断,于是处理器又一次在内核任务中执行中断处理过程。 - 之后顺序执行代码即可。
其中nop
指令:
在调试期中,可能需要动态修改一个正在执行的程序,如想把08 C9
这条指令去掉,此时最好的办法就是将其改为90 90
。
12、抢占式多任务的执行效果演示
接上一节,指令nop
指令之后继续创建第二个用户任务:
。。。
;为说明任务切换而特意添加的无操作指令
nop
nop
nop
;可以创建更多的任务,例如:
cli
mov ecx,0x46
call sys_routine_seg_sel:allocate_memory
mov word [es:ecx+0x04],0 ;任务状态:空闲
call append_to_tcb_link ;将此TCB添加到TCB链中
push dword 100 ;用户程序位于逻辑100扇区
push ecx ;压入任务控制块起始线性地址
call load_relocate_program
sti
。。。
之后内核任务就是无限循环:
。。。
.do_switch:
mov ebx,core_msg2
call sys_routine_seg_sel:put_string
;清理已经终止的任务,并回收它们占用的资源
call sys_routine_seg_sel:do_task_clean
hlt
jmp .do_switch
。。。
程序加载:
Bochs
虚拟机:
Virtual Box
虚拟机: