深入理解 x86/x64 的中断体系

个人博客
  1. 实模式下的中断机制
  2. 保护模式下的中断机制

 

1. 实模式下的中断机制

x86 processor 在加电后被初始化为 real mode 也称为 real-address mode,关于实模式请详见文章:http://www.mouseos.com/arch/001.html

processor 执行的第一条指针在 0xFFFFFFF0 处,这个地址经过 North Bridge(北桥)和 South ridge(南桥)芯片配合解码,最终会访问到固化的 ROM 块,同时,经过别名机制映射在地址空间低端,实际上等于 ROM 被映射到地址空间最高端低端位置。

此时在系统的内存里其实并不存在 BIOS 代码,ROM BIOS 的一部分职责是负责安装 BIOS 代码进入系统内存。

jmp far f000:e05b

典型是这条指令就是 0xFFFFFFF0 处的 ROM BIOS 指令,执行后它将跳到 0x000FE05B 处,这条指令的作用很大:

  • 更新 CS.base 使 processor 变成纯正的 real mode
  • 跳转到低端内存,使之进入 1M 低端区域

前面说过,此时内存中也不存在 BIOS,也就是说 IVT(中断向量表)也是不存在的,中断系统此时是不可用的,那么由 ROM BIOS 设置 IVT 。

1.1 中断向量表(IVT)

IDTR.base 被初始化为 0,ROM BIOS 将不会对 IDTR.base 进行更改,因此如果实模式 OS 不更改 IDTR.base 的值,这意味着 IVT 在 0 的位置上,典型的如: DOS 操作系统。

保护模式下 IDTR.base 将向不再是中断向量表,而是中断描述符表。不再称为 IVT 而是 IDT。那是因为:

  • 在实模式下,DITR.base 指向的表格项直接给出中断服务例程(Interrupt Service Routine)的入口地址。
  • 在保护模式下,并不直接给出入口地址,而是门描述符(Interrupt/Trap/Task gate),从这些门描述符间接取得中断服务例程入口。

在 x86/x64 体系中允许有 256 个中断存在,中断号从 0x00 - 0xff,共 256 个中断,如图:

上面这个图是实模式下的 IVT 表,每个向量占据 4 个字节,中断服务例程入口是以 segment:offset 形式提供的,offset 在低端,segment 在高端,整个 IVT 表从地址 0x0 - 0x3FF,占据了 1024 个字节,即 1K bytes

1.2 改变中断向量表地址

事实上,我们完全可以在实模式下更改 IVT 的地址,下面的代码作为示例:


; ****************************************************************
; * boot.asm for interrupt demo(real mode) on x86                *
; *                                                              * 
; * Copyright (c) 2009-2011                                      *
; * All rights reserved.                                         *
; * mik                                                          *
; * visit web site : www.mouseos.com                             *
; * bug send email : mik@mouseos.com                             *
; *                                                              *
; *                                                              *
; * version 0.01 by mik                                          *  
; ***************************************************************


BOOT_SEG        equ 0x7c00        ; boot module load into BOOT_SEG


;----------------------------------------------------------
; Now, the processor is real mode 
;----------------------------------------------------------

        bits 16

        org BOOT_SEG                   ; for int 19
        
start:
        mov ax, cs
        mov ds, ax
        mov es, ax
        mov ss, ax
        mov sp, BOOT_SEG
        
        mov si, msg1
        call printmsg

        sidt [old_IVT]                        ; save old IVT

        mov cx, [old_IVT]
        mov [new_IVT], cx                ; limit of new IVT

        mov dword [new_IVT+2], 0x8000        ; base of new IVT

        mov si, [old_IVT+2]
        mov di, [new_IVT+2]

        rep movsb                       

        lidt [new_IVT]                        ; set new IVT
        
        mov si, msg2
        call printmsg


        jmp $


       

;-----------------------------------
; printmsg() - print message
;-----------------------------------
printmsg:
        mov ah, 0x0e
        xor bh, bh

print_loop:
        lodsb
        test al,al
        jz done
        int 0x10
        jmp print_loop

done:        
        ret


       

old_IVT         dw 0                        ; limit of IVT
                dd 0                        ; base of IVT

new_IVT         dw 0                        ; limit of IVT
                dd 0                        ; base of IVT


        
msg1            db 'Hi, print message with old IVT', 10,13, 0
msg2            db 'Now,pirnt message with new IVT', 13, 10, 0


        
times 510-($-$$) db 0
        
        dw 0xaa55
        
; end of boot.asm

在 vmware 上这段代码的执行结果如图:

这段代码在实模式下将 IVT 表复制到 0x8000 位置上,然后将 IVT 地址设为 0x8000 上,这样完全可以正常工作。正如代码上看到的,我做:

  1. 使用 sidt 指令取得 IDTR 寄存器的值,即 IVT 的 limit 和 base 值,保存在 old_IVT 里
  2. 设置 new_IVT 值,limit 等于 old_IVT 的 limit,base 设为 0x8000
  3. 将 IVT 表复制到 0x8000 处
  4. 使用 lidt 指令加载 IDTR 寄存器,即设 IVT 表在 0x8000 

1.3 设置自己的中断服务例程

在中断向量表里还有许多空 vector 是未使用的,我们可以在这些空白的向量里设置自己的中断服务例程,典型的如: DOS 操作系统中使用了 0x21 号向量作为 DOS 提供给用户的系统调用!

在这里我将展示,使用 0x40 向量作为自己的中断服务例程向量,我所需要做的是:

  1. 写一个自己的中断服务例程,在本例中的 my_isr
  2. 设置向量 0x40 的 segment 和 offset 值
  3. 调用 int 0x40 进行测试

中断服务例程 my_isr 很简单,仅仅是打印信息:

;------------------------------------------------
; our Interrupt Service Routine: vector 0x40
;-------------------------------------------------
my_isr:
        mov si, msg3
        call printmsg
        iret

接下来设置 vector 0x40 的 segment 和 offset:

        mov ax, cs
        mov bx, [new_IVT+2]  ; base of IVT
        mov dword [bx+0x40*4], my_isr ; set offset 0f my_isr
        mov [bx+0x40*4+2], ax  ; set segmet of my_isr

记住 segment 在高位,offset 在低位,这个 segment 是我们当前的 CS,offset 是我们的 ISR 地址,直接写入 IVT 表中就可以了

现在我们可以测试了:

        int 0x40

结果如下:

我们的 ISR 能正常工作了,我提供了完整的示例源码和磁盘映像下载:interrupt_demo


2. 保护模式下的中断机制

引入保护模式后,情形变得复杂多了,实施了权限控制机制,为了支持权限的控制增添了几个重要的数据结构,下面是与中断相关的结构:

  • gate descriptor(门描述符):用于描述和控制 Interrupt Service Routine 的访问,中断可使用的 gate 包括:
    • Interrupt-gate descriptor(中断门描述符)
    • Trap-gate descriptor(陷井门描述符)
    • Task-gate descriptor(任务门描述符)-- 在 64 位模式下无效
  • interrupt descriptor table(中断描述符表):用于存在 gate descriptor 的表格

在 32 位保护模式下,每个 gate descriptor 是 8 bytes 宽,在 64 位模式下 gate descriptor 被扩充为 16 bytes, 同时 64 位模式下不存在 task gate descriptor,因此在 64 位下的 IDT 只允许存放 interrupt/trap gate descriptor。

当我们执行调用中断时,processor 会在 IDT 表中寻找相应的 descriptor,并通过了权限检查转入相应的 interrupt service routine(大多数时候被称为 interrupt handler),在中断体系里分为两种引发方式:

  • 由硬件引发请求
  • 由软件执行调用

然而软件上发起的中断调用还可分为:

  • 引发 exception(异常) 执行 interrupt handler
  • 软件请求执行 system service(系统服务),而执行 interrupt handler

硬件引发的中断请求还可分为:

  • maskable interrupt(可屏蔽中断)
  • non-maskable interrupt(不可屏蔽中断)

无论何种方式,进入 interrupt handler 的途径都是一样的。

2.1 查找 interrupt handler 入口

在发生中断时,processor 在 IDTR.base 里可以获取 IDT 的地址,根据中断号在 IDT 表里读取 descriptor,descriptor 的职责是给出 interrupt handler 的入口地址,processor 会检查中断号(vector)是否超越 IDT 的 limit 值。

上图是 interrupt handler 的定位查找图。在 32 位模式下的过程是:

  1. 从 IDTR.base 得到 IDT 表地址
  2. 从 IDTR.base + vector * 8(每个 descriptor 为 8 bytes)处读取 8 bytes 宽的 escriptor
  3. 对 descriptor 进行分析检查,包括:
    • descriptor 类型的检查
    • IDT limit 的检查
    • 访问权限的检查
  4. 从 gate descriptor 里读取 selector
  5. 判断 code segment descriptor 是存放在 GDT 表还是 LDT 表
  6. 使用 selector 从 descriptor table 读取 code segment descriptor,当然这里也要经过对 code segment descriptor 的检查
    • descriptor 类型检查
    • GDT limit 检查
    • 访问权限的检查
  7. 从 code segment descriptor 中读取 code segment base 值
  8. 从 gate descriptor 里读取 interrupt handler 的 offset 值
  9. 得取 interrupt handler 的入口地址:base + offset,转入执行 interrupt handler

它的逻辑用 C 描述,类似下面:

long IDT_address;                             /* address of IDT */
long DT_address;                              /* GDT or LDT */
DESCRIPTOR gate_descriptor;                   /* gate descriptor */
DESCRIPTOR code_descriptor;                   /* code segment descriptor */
short selector;                               /* code segment selector */


IDT_address = IDTR.base;                      /* get address of IDT */
gate_descriptor = IDT_address + vector * 8;   /* get descriptor */

selector = gate_descriptor.selector;
DT_address = selector.TI ? LDTR.base : GDTR.base;                      /* address of GDT or LDT */
code_descriptor = GDT_address + selector * 8;                          /* get code segment descriptor */
interrupt_handler = code_descriptor.base + gate_descripotr.offset;     /* interrupt handler entry */

((*(void))interrupt_handler)();                  /* do interrupt_handler() */

上面的 C 代码显示了 processor 定位 interrupt handler 的逻辑过程,为了清楚展示这个过程,这里省略了各种的检查机制

2.2 IDT 表中 descriptor 类型的检查

processor 会对 IDT 表中的 descriptor 类型进行检查,这个检查发生在:

当读取 IDT 表中的 descriptor 时

在 IDT 中的 descriptor 类型要属于:

  • S = 0:属于 system 类 descriptor
  • descriptor 的 type 域应属于:
    • 1110:32-bit interrupt gate
    • 1111:32-bit trap gate
    • 0101:task gate
    • 0110:16-bit interrupt gate
    • 0111:16-bit trap gate

非上述所说的类型,都将会产生 #GP 异常。当 descriptor 的 S 标志为 1 时,表示这是个 user 的 descriptor,它们是:code/data segment descriptor。可以看到在 32 位保护模式下 IDT 允许存在 interrupt/trap gate 以及 task gate

2.3 使用 16-bit gate descriptor

在 32 位保护模式下 interrupt handler 也能使用 16-bit gate descriptor,包括:

  • 16-bit interrupt gate
  • 16-bit trap gate

这是一个比较特别的现象,假如使用 16-bit gate 来构建中断调用机制,实际上等于 interrupt handler 会从 32-bit 模式切换到 16-bit 模式执行。只要构建环境要素正确这样切换执行当然是没问题的。

这个执行环境要素需要注意的是:当使用 16-bit gate 时,也要相应使用 16-bit code segment descriptor。也就是在 gate descriptor 中的 selector 要使用 16-bit code segment selector。下面我写了个使用 16-bit gate 构建 interrupt 调用的例子:

; set IDT vector
        mov eax, BP_handler
        mov [IDT+3*8], ax                       ; set offset 15-0
        mov word [IDT+3*8+2], code16_sel        ; 16-bit code selector
        mov word [IDT+3*8+4], 0xc600            ; DPL=3, 16-bit interrupt gate
        shr eax, 16
        mov [IDT+3*8+8], ax                     ; offset 31-16

上面这段代码将 vector 3 设置为使用 16-bit interrupt gate,并且使用了 16-bit selector

下面是我的 interrupt handler 代码:

        bits 16

;-----------------------------------------------------
; INT3 BreakPoint handler for 16-bit interrupt gate
;-----------------------------------------------------
BP_handler:
       jmp do_BP_handler
BP_msg     db 'I am a 16-bit breakpoint handler on 32-bit proected mode',0

do_BP_handler:
       mov edi, 10
       mov esi, BP_msg
       call printmsg16

       iret

这个 interrupt handler 很简单,只是打印一条信息而已,值得注意的是,这里需要使用 bits 16 来指示编译为 16 位代码。

那么这样我们就可以使用 int3 指令来调用这个 16-bit 的 interrupt handler,执行结果如图:

完整的源代码和软盘映像下载:interrupt_demo1.rar

2.4 IDT 表的 limit 检查

在 IDT 表中查找索引 gate descriptor 时,processor 也会对 IDT 表的 limit 进行检查,这个检查的逻辑是:

gate_descriptor = IDTR.base + vector * 8;                  /* get gate descriptor */

if ((gate_descriptor + sizeof(DESCRIPTOR) - 1) > (IDTR.base + IDTR.limit))
{
        /* failure: #GP exception */
}

我们看看下面这个图:

当我们设:

  • IDTR.base = 0x10000
  • IDTR.limit = 0x1f

那么 IDT 表的有效地址范围是:0x10000 - 0x1001f,也就是:IDTR.base + IDTR.limit 这表示:

  • vector 0:0x10000 - 0x10007
  • vector 1:0x10008 - 0x1000f
  • vector 2: 0x10010 - 0x10017
  • vector 3: 0x10018 - 0x1001f

上面是这 4 个 vector 的有效范围,因此:当设 IDTR.limit = 0x1e 时,如果访问 vector 3 时(调用中断3)processor 检测到访问 IDT 越界而出错!

因此:访问的 vector 地址在 IDTR.base 到 IDTR.base + IDTR.limit(含)之外,将会产生 #GP 异常。

2.5 请求访问 interrupt handler 时的权限检查

访问权限的检查是 x86/x64 体系中保护措施中非常重要的一环,它控制着访问者是否有权限进行访问,在访问 interrupt handler 过程权限控制中涉及 3 个权限类别

  • CPL:当前 processor 所处的权限级别
  • DPLg:代表 DPL of gate,也就是 IDT 中 gate descriptor 所要求的访问权限级别
  • DPLs:代表 DPL of code segment,也就是 interrupt handler 的目标 code segment 所要求的访问权限级别

CPL 权限级别代表着访问者的权限,也就是说当前正在执行代码的权限,要理解权限控制的逻辑,你需要明白下面两点:

  1. 要调用 interrupt handler 那么首先你必须要有权限去访问 IDT 表中的 gate,这表示:CPL 的权限必须不低于 DPLg (gate 所要求的权限),这样你才有权限去访问 gate
  2. interrupt handler 会在高权限级别里执行,也就是说 interrupt handler 会在特权级别里运行。这表示:interrupt handler 里的权限至少不低于访问者的权限

在调用 interrupt handler 中并不使用 selector 来访问 gate 而是使用使用 vector 来访问 gate,因此中断权限控制中并不使用 RPL 权限类别,我们可以得知中断访问权限控制的要素:

  • CPL <= DPLg
  • CPL >= DPLs

需同时满足上面的两个式子,在比较表达式中数字高的权限低,数字低的权限高!用 C 描述为:

DPLg = gate_descriptor.DPL;               /* DPL of gate */
DPLs = code_descriptor.DPL;               /* DPL of code segment */

if ((CPL <= DPLg) && (CPL >= CPLs)) 
{
     /* pass */
} else {
     /* failure: #GP exception */
}

2.5.1 gate 的权限设置

对于 gate 的权设置,我们应考虑 interrupt handler 调用上的两个原则:

  • interrupt handler 开放给用户调用
  • interrupt handler 在系统内部使用

由这两个原则产生了 gate 权根设置的两个设计方案:

  • gate 的权限设置为 3 级:这样可以给用户代码有足够的权限去访问 gate
  • gate 的权限设置为 0 级:只允许内核代码访问,用户无权通过这个 gate 去访问 interrupt handler

这是现代操作系统典型的 gate 权限设置思路,绝大部分的 gate 都设置为高权限,仅有小部分允许用户访问。很明显:系统服务例程的调用入口应该设置为 3 级,以供用户调用

下面是很典型的设计:

  • #BP 异常:BreakPoint(断点)异常的 gate 应该设置为 3 级,使得用户程序能够使用断点调试程序。
  • 系统调用:系统调用是 OS 提供给用户访问 OS 服务的接口,因此 gate 必须设置为 3 级。

系统调用在每个 OS 实现上可能是不同的,#BP 异常必定是 vector 3,因此对于 vector 3 所使用的 gate 必须使用 3 级权限。

下面是在 windows 7 x64 操作系统上的 IDT 表的设置:

<bochs:2> info idt
Interrupt Descriptor Table (base=0xfffff80004fea080, limit=4095):
IDT[0x00]=64-Bit Interrupt Gate target=0x0010:fffff80003abac40, DPL=0
IDT[0x01]=64-Bit Interrupt Gate target=0x0010:fffff80003abad40, DPL=0
IDT[0x02]=64-Bit Interrupt Gate target=0x0010:fffff80003abaf00, DPL=0
IDT[0x03]=64-Bit Interrupt Gate target=0x0010:fffff80003abb280, DPL=3
IDT[0x04]=64-Bit Interrupt Gate target=0x0010:fffff80003abb380, DPL=3
IDT[0x05]=64-Bit Interrupt Gate target=0x0010:fffff80003abb480, DPL=0

... ...

IDT[0x29]=64-Bit Interrupt Gate target=0x0010:fffff80003bf2290, DPL=0
IDT[0x2a]=64-Bit Interrupt Gate target=0x0010:fffff80003bf22a0, DPL=0
IDT[0x2b]=64-Bit Interrupt Gate target=0x0010:fffff80003bf22b0, DPL=0
IDT[0x2c]=64-Bit Interrupt Gate target=0x0010:fffff80003abca00, DPL=3
IDT[0x2d]=64-Bit Interrupt Gate target=0x0010:fffff80003abcb00, DPL=3

IDT[0x2e]=64-Bit Interrupt Gate target=0x0010:fffff80003bf22e0, DPL=0
IDT[0x2f]=64-Bit Interrupt Gate target=0x0010:fffff80003b09590, DPL=0
IDT[0x30]=64-Bit Interrupt Gate target=0x0010:fffff80003bf2300, DPL=0

上面的粗体显示 interrupt gate 被设置为 3 级,在 windows 7 x64 下 vector 0x2c 和 0x2d 被设置为系统调用接口。实际上这两个 vector 的入口虽然不同,但是代码是一样的。你可以通过int 0x2c 和 int 0x2d 请求系统调用。

那么对于系统内部使用的 gate 我们应该保持与用户的隔离,绝大部分 interrupt handler 的 gate 权限都是设置为 0 级的。

2.5.2 interrupt handler 的 code segment 权限设置

前面说过:interrupt handler 的执行权限应该至少不低于调用者的权限,意味着 interrupt handler 需要在高权限级别下运行。无论是系统提供给用户的系统服务例程还是系统内部使用的 interrupt handler 我们都应该将 interrupt handler 设置为 0 级别的运行权限(最高权限),这样才能保证 interrupt handler 能访问系统的全部资源。

在权限检查方面,要求 DPLs 权限(interrupt handler 的执行权限)要高于或等于调用者的权限,也就是 CPL 权限,当数字上 DPLs 要小于等于 CPL(DPLs <= CPL)。

2.6 使用 interrupt gate

使用 interrupt gate 来构造中断调用机制的,当 processor 进入 interrupt handler 执行前,processor 会将 eflags 值压入栈中保存并且会清 eflags.IF 标志位,这意味着进入中断后不允许响应 makeable 中断(可屏蔽中断)。它的逻辑 C 描述为:

*(--esp) = eflags;                           /* push eflags */

if (gate_descriptor.type == INTERRUPT_GATE)
        eflags.IF = 0;                       /* clear eflags.IF */

interrupt handler 使用 iret 指令返回时,会将栈中 eflags 值出栈以恢复原来的 eflags 值。

下面是 interrupt gate 的结构图:

可以看到 interrupt gate 和 trap gate 的结构是完全一样的,除了以 type 来区分 gate 外,interrupt gate 的类型是:

  • 1110:32-bit interrupt gate
  • 0110:16-bit interrupt gate

32 位的 offset 值提供了 interrupt handler 的入口偏移地址,这个偏移量是基于 code segment 的 base 值,selector 域提供了目标 code segment 的 selector,用来在 GDT 或 LDT 进行查找 code segment descriptor。这些域的使用描述为:

if (gate_descriptor.selector.TI == 0)
        code_descriptor = GDTR.base + gate_descriptor.selector * 8;        /* GDT */
else
        code_descriptor = LDTR.base + gate_descriptor.selector * 8;        /* LDT */


interrupt_handler = code_descriptor.base + gate_descriptor.offset;         /* interrupt handler entry */

注得注意的是:在 interrupt gate 和 trap gate 中的 selector 它的 RPL 是不起作用的,这个 selector.RPL 将被忽略

在 OS 的实现中大部分的 interrupt handler 都是使用 interrupt gate 进行构建的。在 windows 7 x64 系统上全部都使用 interrupt gate 并没有使用 trap gate

2.7 使用 trap gate

trap gate 在结构上与 interrupt gate 是完全一样的,参见节 2.6 的那图,trap gate 与 interrupt gate 不同的一点是:使用 trap gate 的,processor 进入 interrupt handler 前并不改变 eflags.IF 标志,这意味着在 interrupt handler 里将允许可屏蔽中断的响应。

*(--esp) = eflags;                                               /* push eflags */

if (gate_descriptor.type == TRAP_GATE) {
                                                                /* skip: do nothing */
} else if (gate_descriptor.type == INTERRUPT_GATE){
         eflags.IF = 0;                                         /* clear eflags.IF */       
} else if (gate_descriptor.type == TASK_GATE) {
         ... ...
}

2.8 使用 task gate

在使用 task gate 的情形下变得异常复杂,你需要为 new task 准备一个 task 信息的 TSS,然而你必须事先要设置好当前的 TSS 块,也就是说,系统中应该有两个 TSS 块:

  • current TSS
  • TSS of new task

当前的 TSS 是系统初始化设置好的,这个 TSS 的作用是:当发生 task 切换时保存当前 processor 的状态信(当前进程的 context 环境),新任务的 TSS 是通过 task gete 进行切换时使用的 TSS 块,这个 TSS 是存放新任务的入口信息。

tss_desc        dw 0x67                ; seletor.SI = 3
                dw TSS
                dd 0x00008900

tss_gate_desc   dw 0x67                ; selector.SI = 4
                dw TSS_TASKGATE         
                dd 0x00008900

在上面的示例代码中,设置了两个 TSS descriptor,一个供系统初始化使用(tss_desc),另一个是为新任务而设置(tss_task_gate),代码中必须设置两个 TSS 块:

  • TSS
  • TSS_TASKGATE

TSS 块的内容是什么在这个示例中无关紧要,然而 TSS_TASKGATE 块中应该设置新任务的入口信息,其中包括:eip 和 cs 值,以后必要的 DS 与 SS 寄存器值,还有 eflags 和 GPRs 值,下面的代码正是做这项工作:

; set TSS for task-gate
         mov dword [TSS_TASKGATE+0x20], BP_handler32                ; tss.EIP
         mov dword [TSS_TASKGATE+0x4C], code32_sel                  ; cs
         mov dword [TSS_TASKGATE+0x50], data32_sel                  ; ss
         mov dword [TSS_TASKGATE+0x54], data32_sel                  ; ds
         mov dword [TSS_TASKGATE+0x38], esp                         ; esp
         pushf
         pop eax
         mov dword [TSS_TASKGATE+0x24], eax                         ; eflags

我将新任务的入口点设为 BP_handler32(),这个是 #BP 断点异常处理程序,保存当前的 eflags 值作为新任务的 eflags 值。

我们必须为 task gate 设置相应的 IDT 表项,正如下面的示例代码:

; set IDT vector: It's a  #BP handler

         mov word [IDT+3*8+2], tss_taskgate_sel                  ; tss selector
         mov dword [IDT+3*8+4], 0xe500                           ; type = task gate

示例代码中,我为 vector 3(#BP handler)设置为 task-gate descirptor,当发生 #BP 异常时,就会通过 task-gate 进行任务切换到我们的新任务(BP_handler32)。

; load IDT into IDTR
         lidt [IDT_POINTER]


; load TSS
         mov ax, tss_sel
         ltr ax

当然我们应该先设置好 IDT 表和加载当前的 TSS 块,这个 TSS 块就是我们所定义的第1个 TSS descirptor (tss_desc),这个 TSS 块里什么内容都没有,设置它的目的是为切换到新任务时,保存当前任务的 context 环境,以便执行完新任务后切换回到原来的任务。

        db 0xcc                         ; throw BreakPoint

现在我们就可以测试我们的 BP_handler32(),通过 INT3 指令引发 #BP 异常,这个异常通过 task-gate 进行切换。

我们的 BP_handler32 代码是这样的:

;-----------------------------------------------------
; INT3 BreakPoint handler for 32-bit interrupt gate
;-----------------------------------------------------
BP_handler32:
       jmp do_BP_handler32
BP_msg32    db 'I am a 32-bit breakpoint handler with task-gate on 32-bit proected mode',0

do_BP_handler32:

       mov edi, 10
       mov esi, BP_msg32
       call printmsg

       clts                   ; clear CR0.TS flag

       iret

它只是简单的显示一条信息,在这个 BP_handler32 中,我们应该要清 CR0.TS 标志位,这个标志位是通过 TSS 进行任务切换时,processor 自动设置的,然而 processsor 不会清 CR0.TS 标志位,需要代码中清除。

2.8.1 任务切换的情形

在本例中,我们来看看当进行任务切换时发生了什么,processor 会设置一些标志位:

  • 置 CR0.TS = 1
  • 置 eflags.NT = 1

设置 CR0.TS 标志位表示当前发生过任务切换,processor 只会置位,而不会清位,事实上,你应该使用 clts 指令进行清位工作。设置 eflags.NT 标志位表示当前任务是在嵌套层内,它指示当进行中断返回时,需切换回原来的任务,因此,请注意:

当执行 iret 指令时,processor 会检查 eflags.NT 标志是否置位

当 eflags.NT 被置位时,processor 执行另一个任务切换工作,从 TSS 块的 link 域中取出原来的 TSS selector 从而切换回原来的任务。这不像 ret 指令,它不会检查 eflags.NT 标志位。

processor 也会对 TSS descriptor 做一些设置标志,当进入新任务时,processor 会设置 new task 的 TSS descriptor 为 busy,当切换回原任务时,会置回这个任务的 TSS descriptor 为available,同时 processor 会检查 TSS 中的 link 域的 TSS selector(原任务的 TSS)是否为 busy,如果不为 busy 则会抛出 #TS 异常。

当然发生切换时 processor 会保存当前的 context 到 current TSS 块中,因此:

  • 切换到目标任务时,processor 会保存当前的任务 context 到 TSS,读取目标任务的 TSS,加载相应的信息,然后进入目标任务
  • 目标任务执行完后,切换回原任务时,processor 会保存目标任务的 context 到目标任务的 TSS 中,从目标任务的 TSS 块读取 link(原任务的 TSS selector),加载相应的信息,返回到原任务

当从目标任务返回时,processor 会清目标任务的 eflags.NT = 0,如前所述目标任务的 TSS descriptor 也会被置为 available。

x86 x64 中断 体系 深入

分享到:
评论加载中,请稍后...
创APP如搭积木 - 创意无限,梦想即时!
回到顶部