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 处,这条指令的作用很大:
前面说过,此时内存中也不存在 BIOS,也就是说 IVT(中断向量表)也是不存在的,中断系统此时是不可用的,那么由 ROM BIOS 设置 IVT 。
IDTR.base 被初始化为 0,ROM BIOS 将不会对 IDTR.base 进行更改,因此如果实模式 OS 不更改 IDTR.base 的值,这意味着 IVT 在 0 的位置上,典型的如: DOS 操作系统。
在保护模式下 IDTR.base 将向不再是中断向量表,而是中断描述符表。不再称为 IVT 而是 IDT。那是因为:
在 x86/x64 体系中允许有 256 个中断存在,中断号从 0x00 - 0xff,共 256 个中断,如图:
上面这个图是实模式下的 IVT 表,每个向量占据 4 个字节,中断服务例程入口是以 segment:offset 形式提供的,offset 在低端,segment 在高端,整个 IVT 表从地址 0x0 - 0x3FF,占据了 1024 个字节,即 1K bytes
事实上,我们完全可以在实模式下更改 IVT 的地址,下面的代码作为示例:
1 ; **************************************************************** 2 ; * boot.asm for interrupt demo(real mode) on x86 * 3 ; * * 4 ; * Copyright (c) 2009-2011 * 5 ; * All rights reserved. * 6 ; * mik * 7 ; * visit web site : www.mouseos.com * 8 ; * bug send email : mik@mouseos.com * 9 ; * * 10 ; * * 11 ; * version 0.01 by mik * 12 ; *************************************************************** 13 14 BOOT_SEG equ 0x7c00 ; boot module load into BOOT_SEG 15 16 ;---------------------------------------------------------- 17 ; Now, the processor is real mode 18 ;---------------------------------------------------------- 19 bits 16 20 org BOOT_SEG ; for int 19 21 22 start: 23 mov ax, cs 24 mov ds, ax 25 mov es, ax 26 mov ss, ax 27 mov sp, BOOT_SEG 28 29 mov si, msg1 30 call printmsg 31 sidt [old_IVT] ; save old IVT 32 mov cx, [old_IVT] 33 mov [new_IVT], cx ; limit of new IVT 34 mov dword [new_IVT+2], 0x8000 ; base of new IVT 35 mov si, [old_IVT+2] 36 mov di, [new_IVT+2] 37 rep movsb 38 lidt [new_IVT] ; set new IVT 39 40 mov si, msg2 41 call printmsg 42 43 jmp $ 44 45 46 ;----------------------------------- 47 ; printmsg() - print message 48 ;----------------------------------- 49 printmsg: 50 mov ah, 0x0e 51 xor bh, bh 52 print_loop: 53 lodsb 54 test al,al 55 jz done 56 int 0x10 57 jmp print_loop 58 done: 59 ret 60 61 62 old_IVT dw 0 ; limit of IVT 63 dd 0 ; base of IVT 64 new_IVT dw 0 ; limit of IVT 65 dd 0 ; base of IVT 66 67 68 msg1 db 'Hi, print message with old IVT', 10,13, 0 69 msg2 db 'Now,pirnt message with new IVT', 13, 10, 0 70 71 72 times 510-($-$$) db 0 73 74 dw 0xaa55 75 76 ; end of boot.asm
在 vmware 上这段代码的执行结果如图:
这段代码在实模式下将 IVT 表复制到 0x8000 位置上,然后将 IVT 地址设为 0x8000 上,这样完全可以正常工作。正如代码上看到的,我做:
在中断向量表里还有许多空 vector 是未使用的,我们可以在这些空白的向量里设置自己的中断服务例程,典型的如: DOS 操作系统中使用了 0x21 号向量作为 DOS 提供给用户的系统调用!
在这里我将展示,使用 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
引入保护模式后,情形变得复杂多了,实施了权限控制机制,为了支持权限的控制增添了几个重要的数据结构,下面是与中断相关的结构:
在 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),在中断体系里分为两种引发方式:
然而软件上发起的中断调用还可分为:
硬件引发的中断请求还可分为:
无论何种方式,进入 interrupt handler 的途径都是一样的。
在发生中断时,processor 在 IDTR.base 里可以获取 IDT 的地址,根据中断号在 IDT 表里读取 descriptor,descriptor 的职责是给出 interrupt handler 的入口地址,processor 会检查中断号(vector)是否超越 IDT 的 limit 值。
上图是 interrupt handler 的定位查找图。在 32 位模式下的过程是:
它的逻辑用 C 描述,类似下面:
ong 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 的逻辑过程,为了清楚展示这个过程,这里省略了各种的检查机制!
processor 会对 IDT 表中的 descriptor 类型进行检查,这个检查发生在当读取 IDT 表中的 descriptor 时。
在 IDT 中的 descriptor 类型要属于:
非上述所说的类型,都将会产生 #GP 异常。当 descriptor 的 S 标志为 1 时,表示这是个 user 的 descriptor,它们是:code/data segment descriptor。可以看到在 32 位保护模式下 IDT 允许存在 interrupt/trap gate 以及 task gate
在 32 位保护模式下 interrupt handler 也能使用 16-bit gate descriptor,包括:
这是一个比较特别的现象,假如使用 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
在 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 */ }
我们看看下面这个图:
当我们设:
那么 IDT 表的有效地址范围是:0x10000 - 0x1001f,也就是:IDTR.base + IDTR.limit 这表示:
上面是这 4 个 vector 的有效范围,因此:当设 IDTR.limit = 0x1e 时,如果访问 vector 3 时(调用中断3)processor 检测到访问 IDT 越界而出错!
因此:访问的 vector 地址在 IDTR.base 到 IDTR.base + IDTR.limit(含)之外,将会产生 #GP 异常。
访问权限的检查是 x86/x64 体系中保护措施中非常重要的一环,它控制着访问者是否有权限进行访问,在访问 interrupt handler 过程权限控制中涉及 3 个权限类别:
CPL 权限级别代表着访问者的权限,也就是说当前正在执行代码的权限,要理解权限控制的逻辑,你需要明白下面两点:
在调用 interrupt handler 中并不使用 selector 来访问 gate 而是使用使用 vector 来访问 gate,因此中断权限控制中并不使用 RPL 权限类别,我们可以得知中断访问权限控制的要素:
需同时满足上面的两个式子,在比较表达式中数字高的权限低,数字低的权限高!用 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 */ }
对于 gate 的权设置,我们应考虑 interrupt handler 调用上的两个原则:
由这两个原则产生了 gate 权根设置的两个设计方案:
这是现代操作系统典型的 gate 权限设置思路,绝大部分的 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 级的。
前面说过:interrupt handler 的执行权限应该至少不低于调用者的权限,意味着 interrupt handler 需要在高权限级别下运行。无论是系统提供给用户的系统服务例程还是系统内部使用的 interrupt handler 我们都应该将 interrupt handler 设置为 0 级别的运行权限(最高权限),这样才能保证 interrupt handler 能访问系统的全部资源。
在权限检查方面,要求 DPLs 权限(interrupt handler 的执行权限)要高于或等于调用者的权限,也就是 CPL 权限,当数字上 DPLs 要小于等于 CPL(DPLs <= CPL)。
使用 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 的类型是:
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
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) { ... ... }
在使用 task gate 的情形下变得异常复杂,你需要为 new task 准备一个 task 信息的 TSS,然而你必须事先要设置好当前的 TSS 块,也就是说,系统中应该有两个 TSS 块:
当前的 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 块中应该设置新任务的入口信息,其中包括: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 标志位,需要代码中清除。
在本例中,我们来看看当进行任务切换时发生了什么,processor 会设置一些标志位:
设置 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 会清目标任务的 eflags.NT = 0,如前所述目标任务的 TSS descriptor 也会被置为 available
联系客服