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

xiaoeryu Lv5

1. 中断和异常概述

协同式任务切换不一定要通过一个专门的接口例程进行,也可以用一般的例程进行。

用户任务通过调用门切换到内核的put_string例程执行,在内核任务返回用户任务之前可以进行任务切换,之后再通过retf返回用户任务执行。

  • 硬件中断信号,NMI是不可屏蔽中断、INTR是来自硬件中断引脚的可屏蔽中断。随机产生,与处理器是异步的。
  • 软件中断,INT N是在软件内部主动引发的中断。
  • 处理异常中断(Exceptions),是处理器内部产生的中断,表示处理器执行时产生了错误的状况。比如当处理器执行一条非法指令或者因条件不具备指令不能正常执行时将会引发这种类型的中断。如div指令除数是0的情况。 终端和异常 按照异常的产生原因分类:
    1. 指令执行异常:处理器在执行指令时检测到程序的错误并由此而引发的异常
    2. 程序调试异常:供调试器使用,由INTO、INT3主动发起。用来检查特定的机器状态是否出现。INTO检查标志寄存器的OF=1(溢出标志位)则执行指令引发异常中断。INT3指令由调试器单步执行使用。
    3. 机器检查异常:和处理器架构有关,如在奔腾4、P6处理器家族上就实现了机器检查架构,用这种异常检测与硬件有关的总线错误、奇偶校验错误、高速缓存错误等等。

根据异常的性质和严重性分类:

  1. 故障(Faults):通常可以纠正,如缺页异常。中断程序返回的是当前指令。
  2. 陷阱(Traps):通常是在执行了截获陷阱条件的指令之后立即产生,通常用于调试INT3、INTO。中断程序返回的是当前指令的下一条指令。
  3. 终止(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位,去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
其他不变,死循环打印cccccc

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表示一个新的初始化过程开始了。

0x20、0xA0接收到ICW1后,8259A期待从0x21、0xA0接收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中选取一个栈段指针加载到栈指针ESP
  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指令处。

再执行iretd从中断处理过程返回到内核任务中上一次0x70号中断的地方,即内核start里面的nop指令处:

添加nop指令的意图是,假定在执行这三条nop指令期间发生了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

程序加载:

虚拟机执行:

  • 标题: 中断和异常的处理与抢占式多任务
  • 作者: xiaoeryu
  • 创建于 : 2022-12-11 22:48:00
  • 更新于 : 2023-10-03 13:00:20
  • 链接: https://github.com/xiaoeryu/2022/12/11/30-中断和异常的处理与抢占式多任务/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论