30、中断和异常的处理与抢占式多任务

01、中断和异常概述

协同式任务切换不一定要通过一个专门的接口例程进行,也可以用一般的例程进行。
在这里插入图片描述
用户任务通过调用门切换到内核的put_string例程执行,在内核任务返回用户任务之前可以进行任务切换,之后再通过retf返回用户任务执行。
在这里插入图片描述

  • 硬件中断信号,NMI是不可屏蔽中断、INTR是来自硬件中断引脚的可屏蔽中断。随机产生,与处理器是异步的。
  • 软件中断,INT n是在软件内部主动引发的中断。
  • 处理异常中断(Exceptions),是处理器内部产生的中断,表示处理器执行时产生了错误的状况。比如当处理器执行一条非法指令或者因条件不具备指令不能正常执行时将会引发这种类型的中断。如div指令除数是0的情况。

在这里插入图片描述
中断和异常。
在这里插入图片描述
按照异常的产生原因分类:

  • 指令执行异常:处理器在执行指令时检测到程序的错误并由此而引发的异常。
  • 程序调试异常:供调试器使用,由INTO、INT3主动发起。用来检查特定的机器状态是否出现。INTO检查标志寄存器的OF=1(溢出标志)则执行指令引发异常。INT3指令供调试器进行单步执行。
  • 机器检查异常:和处理器架构有关,如在奔腾4、P6处理器家族上就实现了机器检查架构,用这种异常检测与硬件有关的总线错误、奇偶校验错误、高速缓存错误等等。

根据异常的性质和严重性分类:
在这里插入图片描述

  • 故障(Faults):通常可以纠正,如缺页异常。中断程序返回的是当前指令。
  • 陷阱(Traps):通常是在执行了截获陷阱条件的指令之后立即产生,通常用于调试INT3、INTO。中断程序返回的是当前指令的下一条指令。
  • 终止(Aborts):通常标志最严重的错误,如硬件错误、系统表错误(如GDTLDT数据不一致、无效、错误),这类异常一般无法精确的报告引起错误的指令的位置。发生时程序和错误都不可能重新启动,双重异常(当处理器发生异常时,在转入异常执行时有发生了另外一个异常)如中断向量号18,INT 0x18

对于某些异常来说,处理器在转入异常处理程序之前,会在当前栈中压入一个称为错误代码的数值,这样可以帮助诊断异常产生的位置和原因。

02、保护模式下中断和异常的向量分配

中断和异常的编号叫做中断向量。
在这里插入图片描述
其中错误代码是在中断发生时,在进入中断处理程序之前压在栈中的错误代码。

03、中断描述符表、中断门和陷阱门

实模式下的中断向量表:
在这里插入图片描述
中断发生时,处理器要么自发产生一个中断向量、要么从软中断指令的操作数的到中断向量、或者从外部的中断控制器取得一个中断向量。将该向量作为索引访问中断向量表IVT,具体做法是将中断向量乘以4作为偏移量访问IVT,从中取得中断处理过程的段地址和偏移地址,并转到那里执行。

保护模式下:使用中断描述符表IDTInterrupt Descriptor Table),保存和中断处理过程相关的描述符,包括中断门、陷阱门、任务门,门是特殊的描述符。

中断门描述符用来描述中断处理过程。
在这里插入图片描述
陷阱门描述符用来描述陷阱中断的处理过程。
在这里插入图片描述
任务门,32位处理器中支持,若IDT中描述的是一个任务门,则执行的是一个任务切换。在64位处理器中既不支持硬件任务切换、也不支持任务门。

实模式下的中断向量表IVT只能位于内存的最低端。保护模式下的中断描述符表IDT可以位于内存的任何位置。
在这里插入图片描述
IDT的第一个描述符即0号槽位也是有效的。

在这里插入图片描述

  • 处理器用中断向量 乘以 8得到表内偏移量,联合IDTR内的IDT基地址去访问内存;
  • 从中取得中断门或陷阱门描述符;
  • 在描述符中有中断处理过程的代码段选择子和段内偏移量;
  • 取决与代码段选择子的TI位,去GDTLDT中取得目标代码段的描述符。
  • 从目标代码段的描述符中取得目标代码段的段基地址;
  • 将段基地址 和 偏移量相加得到中带处理过程的线性基地址,从而转移执行。

使用中断向量访问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,界限值被初始化为0xFFFFlidt指令不影响任何标志位。代码如下:

。。。
	;准备开放中断
	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,取决于ICW1ICW2的内容,可能ICW3ICW4不需要发送。
在这里插入图片描述
对于主片来说先向0x20号端口发送ICW1,对于从片来说要想0xA0号端口发送ICW1ICW1是一个标志,每次8259A芯片接收到ICW1表示一个新的初始化过程开始了。

0x200xA0接收ICW1之后,8259A期待从0x210xA1接收ICW2,后续是否期待ICW3ICW4要看ICW1的内容。ICW1发送给0x200xA0号端口作为标志,之后ICW2ICW3ICW4会发送给0x210xA1号端口。

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、接着将EFLAGSCSEIP压入新栈;
    6、对于有错误代码的异常,处理器还要讲错误代码压入新栈。
    7、当中断返回时,要将EFLAGSCSEIP恢复,还将恢复原先的栈状态,即恢复之前临时保存段寄存器SS和栈指针ESP
    8、恢复之后段寄存器依然指向旧栈段,栈指针寄存器ESP依然指向进入中断之前的位置。

中断门和陷阱门的区别不大,通过中断门进入中断处理过程时,处理器先将EFLAGS压栈,再将其IF位清零以禁止嵌套的中断,即进入中断处理程序后不允许再响应别的中断。从中断返回时,将从栈中恢复EFLAGS的原始状态。

陷阱门的优先级较低,通过陷阱门进入中断处理过程时,EFLAGS的IF位不变,以允许其他中断优先处理。EFLAGSIF位只影响硬件中断,不影响NMI、异常、INT形式的软件中断不起作用。

错误代码:
在这里插入图片描述

  • EXT位:为1表示有NMI、硬件中断等引发;
  • IDT位:为1表示段选择子索引指向中断描述符表IDT中的门描述符,为0表示指向GDTLDT中的描述符;
  • TI位:表指示器位,当IDT位为0才有意义。为0表示段选择子索引指向GDT,为1指向LDT中的段描述符或门描述符;
  • 段选择子索引:用于指示GDTLDT中的段描述符、或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虚拟机:
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

「已注销」

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值