文章系列:
重新认识Intel中断(一)
重新认识Intel中断(二)
重新认识Intel中断(三)
文章目录
- Intel中断原理
- 实验
Intel中断原理
- 一句话解释,Intel中断和调用门很像,都通过门描述符间接实现代码转移,把任务门、中断门、陷阱门放在IDT中实现中断
- 中断是统称,可以细分成系统产生的Fault/Trap/Abort和用户定义的Interrupt。Fault和Trap的区别是从处理例程返回时CPU动作不一样,Fault要纠正错误,重新执行产生错误的指令,Trap会跳过错误,继续执行错误指令之后的一条指令。注意,系统产生的Fault/Trap/Abort,也能通过INT指令触发,后面的实验会提到。
- 中断发生时,代码转移的权限检查规则和调用门一样,但中断没有RPL,所以RPL不参加检查。检查规则参见重新认识Intel权限检查(二) 。一句话解释,IDT表中的门描述符被看做数据段,CPL权限必须高于或者等于DPL_G(数字上小于),同时必须低于或者等于DPL_B时,才能触发中断代码转移
- 中断发生时,如果DPL_B的权限和CPL一样,不会切换堆栈,只把EFLAGS,CS,EIP入栈。如果是系统产生的中断(前32位向量)会把ERROR CODE信息入栈,然后加载中断例程指令地址,执行
- 中断发生时,如果DPL_B的权限比CPL高,会发生堆栈切换。切换过程一句话解释,从CPU当前TSS中取出对应特权级的堆栈指针(假如从ring3升高到ring0,就取ring0对应的栈指针),保存当前SS,ESP到临时内存,加载ring0对应的CS,EIP地址到寄存器,完成堆栈切换,把先前内存中的SS,ESP入栈,再把EFLAGS,CS,EIP入栈,如果是系统产生的中断(前32位向量)会把ERROR CODE信息入栈,然后加载中断例程指令地址,执行。
- 有堆栈切换的中断,执行中断例程时CPL会升高到目标代码段的DPL
实验
-
异常
对比 INT 0模拟除0异常和系统产生的除0异常
- INT 0模拟除0异常
1)实验代码
1)准备除0异常处理例程,向屏幕打印一个D字符
_DEHandler:
DEHandler equ _DEHandler - $$
mov bx, 11
mov cx, 76
mov dl, 'D'
call SelectorInitCode:PrintChar
jmp $
2)安装异常处理例程到IDT
[SECTION .gdt]
; GDT
; 段基址, 段界限 , 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_INITCODE: Descriptor 0, SegCode32Len - 1, DA_C + DA_32; 非一致代码段
; GDT 结束
GdtLen equ $ - LABEL_GDT ; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址
; GDT 选择子
SelectorInitCode equ (LABEL_DESC_INITCODE - LABEL_GDT)
[SECTION .idt]
LABEL_IDT:
; 选择子 偏移
.000h: Gate SelectorInitCode, DEHandler, 0, DA_386IGate + DA_DPL2
IdtLen equ $ - LABEL_IDT
IdtPtr dw IdtLen - 1
dd 0
; END of [SECTION .idt]
; DA_386IGate EQU 8Eh ; 386 中断门类型值 TYPE = E表示中断门类型,P = 1表示段在内存中存在
; 除0异常处理例程在SelectorInitCode:DEHandler处,将代码段地址信息写入选择子,DPL_B = 0
; 除0异常的门描述符在表的第一项,对应中断向量是0,并且向量表中只有一项,其它中断没有处理例程
; 注意这里的DPL设置的是2,实验结果会分析为什么
3)进入保护模式,执行INT 0指令
[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_INIT:
xor eax, eax
mov es, ax
mov ds, ax
str ax ; 读取TR
cmp ax, SelectorTSS ; 检查TR是否被加载,如果加载不再重复
je .1
mov ax, SelectorTSS ; 加载TR
ltr ax
.1:
mov ax, cs
and ax, 10b
cmp ax, 10b ; 判断CPL
je .2 ; 如果CPL!=2,retf到ring2,否则跳转到.2执行测试代码
mov ax, SelectorStack0
mov ss, ax
mov sp, TopOfStack0
push SelectorStack2
push TopOfStack2
push SelectorInitCodeD2
push 0
retf
.2: ; 测试代码,CPL=2
int 0 ; 模拟除0指令
mov ax, 06h ; 制造系统产生的除0异常(6 % 0)
mov bl, 00h
div bl
jmp $
2)结果分析:
int 0指令执行之前,从cs看出CPL=2,用到了stack2堆栈。
base=0x00090180,cs=0x0012,eip=0x00000037,eflags=0x00000046
IDT中的中断门
.000h: Gate SelectorInitCode, DEHandler, 0, DA_386IGate + DA_DPL2
DPL_G=2
SelectorInitCode指向的代码段
SelectorInitCode equ (LABEL_DESC_INITCODE - LABEL_GDT)
DPL_B=0
满足权限检查,CPL权限大于等于DPL_G并且小于等DPL_B,并且会发生堆栈切换。eflags,cs,eip会被压如stack0。
如果DPL_G大于等于CPL,是1或者0,检查不通过;小于等于CPL,是3或者2,检查通过.
int 0指令执行后,cs:0x0008 => CPL=0,ss变成了stack0的地址,查看其指向的堆栈顶端,向下依次是
eflags=0x00000046,cs=0x0012,eip=0x00000037。证明我们的结论正确。
-
制造除0异常
把上个实验的代码中int 0去掉。制造一个除0异常
.2: ; 测试代码,CPL=2
; int 0
mov ax, 06h ; 制造系统产生的除0异常(6 % 0)
mov bl, 00h
div bl
jmp $
div指令执行前,eflags 0x00000046;cs:0x0012;eip: 0x0000003d ;ss:0x002a;esp: 0x0000001f
div指令执行后,寄存器信息和int 0几乎一样
-
用户定义的中断
使用任务门实现中断处理例程
- 实验关键代码
1)中断服务例程代码,打印一个U字符到屏幕
_UsrHandler:
UsrHandler equ _UsrHandler - $$
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子(目的)
mov edi, (80 * 11 + 79) * 2 ; 屏幕第 11 行, 第 79 列。
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov al, 'U'
mov [gs:edi], ax
iretd
2)将服务例程的地址放到TSS数据结构中
; TSS
[SECTION .tss]
ALIGN 32
[BITS 32]
LABEL_TSS1:
DD 0 ; Back
DD TopOfStack0 ; 0 级堆栈
DD SelectorStack0 ;
DD 0 ; 1 级堆栈
DD 0 ;
DD TopOfStack2 ; 2 级堆栈
DD SelectorStack2 ;
DD 0 ; CR3
DD UsrHandler ; EIP ;中断服务例程段内偏移
DD 0 ; EFLAGS
DD 0 ; EAX
DD 0 ; ECX
DD 0 ; EDX
DD 0 ; EBX
DD TopOfTask ; ESP
DD 0 ; EBP
DD 0 ; ESI
DD 0 ; EDI
DD 0 ; ES
DD SelectorInitCode; CS;中断服务例程代码段
DD SelectorTask; SS
DD 0 ; DS
DD 0 ; FS
DD 0 ; GS
DD 0 ; LDT
DW 0 ; 调试陷阱标志
DW $ - LABEL_TSS1 + 2 ; I/O位图基址
DB 0ffh ; I/O位图结束标志
TSS1Len equ $ - LABEL_TSS1
3)往IDT的80h项写入任务门描述符
[SECTION .idt]
LABEL_IDT:
; 选择子 偏移
.000h: Gate SelectorInitCode, DEHandler, 0, DA_386IGate + DA_DPL2
%rep 127
Gate SelectorInitCode, DummyHandler, 0, DA_386IGate
%endrep
.080h: Gate SelectorTSS1, 0, 0, DA_TaskGate + DA_DPL3;任务门
IdtLen equ $ - LABEL_IDT
IdtPtr dw IdtLen - 1
dd 0
; END of [SECTION .idt]
4)GDT表准备
[SECTION .gdt]
; GDT
; 段基址, 段界限 , 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_INITCODE: Descriptor 0, SegCode32Len - 1, DA_C + DA_32; 非一致代码段
LABEL_DESC_INITCODED2: Descriptor 0, SegCode32Len - 1, DA_C + DA_32 + DA_DPL2; 非一致代码段
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW + DA_DPL3; 显存首地址
LABEL_DESC_STACK0: Descriptor 0, TopOfStack0, DA_DRW; 堆栈段
LABEL_DESC_STACK2: Descriptor 0, TopOfStack2, DA_DRW + DA_DPL2 ; 堆栈段
LABEL_DESC_TASK: Descriptor 0, TopOfTask, DA_DRW ; 堆栈段
LABEL_DESC_TSS: Descriptor 0, TSSLen -1, DA_386TSS; TSS段
LABEL_DESC_TSS1: Descriptor 0, TSS1Len -1, DA_386TSS; TSS段
; GDT 结束
GdtLen equ $ - LABEL_GDT ; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址
; GDT 选择子
SelectorInitCode equ (LABEL_DESC_INITCODE - LABEL_GDT)
SelectorInitCodeD2 equ (LABEL_DESC_INITCODED2 - LABEL_GDT) + SA_RPL2
SelectorVideo equ (LABEL_DESC_VIDEO - LABEL_GDT) + SA_RPL3
SelectorStack0 equ (LABEL_DESC_STACK0 - LABEL_GDT)
SelectorStack2 equ (LABEL_DESC_STACK2 - LABEL_GDT) + SA_RPL2
SelectorTask equ (LABEL_DESC_TASK - LABEL_GDT)
SelectorTSS equ (LABEL_DESC_TSS - LABEL_GDT)
SelectorTSS1 equ (LABEL_DESC_TSS1 - LABEL_GDT)
; END of [SECTION .gdt]
5)主程序发起中断
.2: ; 测试代码,CPL=2
;int 0 ; 模拟除0指令
;mov ax, 06h ; 制造系统产生的除0异常(6 % 0)
;mov bl, 00h
;div bl
int 80h
jmp $
-
结果分析
执行int 80h指令前;CPL=2,堆栈用的GDT[0x0028],当前任务是GDT[0x0038]。
执行指令后,CPL变成0,当前堆栈切换成GDT[0x0030],GDT[0x0040]=32-Bit TSS (Busy) 变成Busy,由于不是显式的调用CALL指令,所以前一个任务的Busy标记不会被清空,表示嵌套的任务。查看堆栈信息,没有CS,EIP,EFLAGS信息,为什么?由于时任务切换,所以返回信息都放在GDT[0x0038]=32-Bit TSS 中了。 附: 实验完整源码见my github intel中断