还在苦苦敲代码开发APP?你out啦! 试试积木搭建APP吧~

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

来源:个人博客     2015-08-18 16:25:18    人气:     我有话说( 0 人参与)

实模式下的中断机制中断向量表(IVT)改变中断向量表地址设置自己的中断服务例程保护模式下的中断机制查找 interrupt handler 入口IDT ...

  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 中断 体系 深入

本文源自互联网,采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可,
版权归原作者,如有问题请联系service@tsingfun.com (编辑:admin)
分享到: