第一部分 IDT表的加载
作者:钱文祥(blast) 协议:CC(署名&相同形式散布&禁止修改) 原载于http://nul.pw/
第一部分 IDT表的加载
本书将从IDT的初始化开始介绍整个异常处理的流程,本章是第一部分的完整版本。
在处理异常时,操作系统不可避免地要知道哪一条指令出了问题。这个问题可以是硬件的,也可以是软件的(throw)。这里我们主要说明硬件部分。当CPU处理一条非法指令无法继续执行时,这时,会进入IDT(Interrupt Descriptor Table,“中断描述符表”。CPU用此表来处理中断和程序异常。)。
内核初始化开始时,ntoskrnl.exe的入口KiSystemStartup会执行初始化操作。在阶段0中,KiSystemStartup会初始化处理器状态。包括IDT、TSS、PCR。
首先是PCR(处理器控制结构)的初始化,可以了解到这块代码比较简单,观察其中调用GetMachineBootPointers的代码(因微软代码授权,不能贴完整代码,只贴出关键调用,并辅以讲解。下同):
INITIALIZE PCR:
stdCall GetMachineBootPointers
微软代码协议:有限免费使用协议,请参考微软WRK 1.1授权,下同
GetMachineBootPointers
然后让我们具体观察GetMachineBootPointers。GetMachineBootPointers的调用约定为:VOID GetMachineBootPointers( )。
此函数在系统启动时被调用。用于得到PCR和机器控制值。该函数仅仅对P0阶段有用。P0阶段boot loader在打开分页调用此函数前必须已经初始化完机器。
PCR的地址从KGDT_R0_PCR处得到、GDT和IDT从机器GDTR和IDTR得到、TSS由TSR和相关的描述符处计算而来。
(GDT->KGDT_R0_PCR === ring0 PCR, GDTR寄存器存储着GDT地址,IDTR寄存器存储着IDT的地址。)
GDTR(48-bit register)=保存=GDT的32位基地址和16位GDT的大小
IDTR(48-bit register)=保存=IDT的32位基地址和16位IDT的大小
LDTR(16-bit register)=保存=LDT段的选择子
TSR(16-bit register)=保存=TSS段的16位选择子
47-----------15|----------0|
|---------------|-----------| GDTR
|---------------|-----------| IDTR
|--SELECTOR-| TSR
|--SELECTOR-| LDTR
common segment descriptor
Id decimal hex
KGDT_NULL 0 0x00
KGDT_R0_CODE 8 0x08
KGDT_R0_DATA 16 0x10
KGDT_R3_CODE 24 0x18
KGDT_R3_DATA 32 0x20
KGDT_TSS 40 0x28
KGDT_R0_PCR 48 0x30
KGDT_R3_TEB 56 0x38
KGDT_VDM_TILE 64 0x40
KGDT_LDT 72 0x48
KGDT_DF_TSS 80 0x50
KGDT_NMI_TSS 88 0x58
也就是说,调用这个函数完成后,(edi) -> gdt、 (esi) -> pcr、 (edx) -> tss、 (eax) -> idt。
让我们分步查看。
1、调用sgdt指令(SGDT - Store Global Descriptor Table (286+privileged),因特尔手册)获取SGDT,并获得GDT地址。(仅后3字节有用,和许多描述符类似。)
sgdt fword ptr [ebp-8]
mov edi,[ebp-6] ; (edi) 为 gdt 地址
2、将fs移动到cx,并做CX & (~RPL_MASK),清高位,加上GDT地址,得到PCR描述符地址。下为伪代码。
mov cx,fs
and cx,(~ RPL_MASK)
movzx ecx,cx
add ecx,edi ; (ecx) 为 pcr descriptor
3、获取KGDT的基址的几部分,DX=MAKEWORD(HI, MID),提edx的低16位(DX)到高16位,将KGDT基址的低部分放入DX,这样就可以获得PCR地址,同时复制一份到ESI上。
mov dh,[ecx+KgdtBaseHi]
mov dl,[ecx+KgdtBaseMid]
shl edx,16
mov dx,[ecx+KgdtBaseLow] ; (edx)为pcr
mov esi,edx ; (esi)为pcr
4、STR(Store Task Register)把段选择子存储到CX(MOV CX, TR.SegmentSelector),然后清零高位,加上GDT获得TSS描述符地址。
str cx
movzx ecx,cx
add ecx,edi ; (ecx) 为 TSS descriptor
5、与之前类似的操作,从TSS描述符计算TSS地址。
mov dh,[ecx+KgdtBaseHi]
mov dl,[ecx+KgdtBaseMid]
shl edx,16
mov dx,[ecx+KgdtBaseLow] ; (edx) -> TSS
6、SIDT指令获取IDT地址。
sidt fword ptr [ebp-8]
mov eax,[ebp-6] ; (eax) -> Idt
7、函数返回。
**
SIDT operator
**
我们对加粗部分代码比较感兴趣,详细介绍一下。根据Intel手册,SGDT/SIDT(存储全局/中断描述符表格寄存器)指令的格式如下:
操作码 说明
SGDT m 将 GDTR 存储到 m
SIDT m 将 IDTR 存储到 m
具体说明为:
将全局描述符表格寄存器 (GDTR) 或中断描述符表格寄存器 (IDTR) 中的内容存储到目标操作数。目标操作数是指定 6 字节内存位置。如果操作数大小属性为 32 位,则寄存器的 16 位限制字段存储到内存位置的 2 个低位字节,32 位基址存储到 4 个高位字节。如果操作数大小属性为 16 位,则限制字段存储在 2 个低位字节,24 位基址存储在第三、四及五字节,第六字节使用 0 填充。
SGDT 与 SIDT 指令仅在操作系统软件中有用;不过它们也可以在应用程序中使用,而不会导致生成异常。
所以可以知道SIDT操作之后,EBP-8处的8个字节的高6字节为IDT。随后通过EBP-6取出IDT的值,置于EAX(Windows并不想要这6字节中的2字节限制字段部分)。之后函数返回。返回后,EDI为GDT,ESI为PCR,EDX为TSS,EAX为IDT。
上一层, KiSystemStartup
外层P0 KiSystemStartup的代码随后会把这几个寄存器的值保存起来,存放到本地的KissGdt、KissPcr、KissTss、KissIdt中。我们只关心IDT。
mov KissGdt, edi
mov KissPcr, esi
mov KissTss, edx
mov KissIdt, eax
之后,该过程初始化KiTrap08(双误)、KiTrap02(NMI/NPX),然后调用KiInitializePcr函数来初始化PCR。
KiInitializePcr
stdCall _KiInitializePcr, <KissPbNumber,KissPcr,KissIdt,KissGdt,KissTss,KissIdleThread,offset FLAT:_KiDoubleFaultStack>
1、在当前线程对象中设置当前程序指针
mov edx, KissIdleThread
mov ecx, offset FLAT:_KiIdleProcess ; (ecx)-> idle process obj
mov [edx]+ThApcState+AsProcess, ecx ; set addr of thread's process
2、设置PCR:TEB、PRCB指针。PRCB中PCR->InitialStack和其他一些东西在_KiInitialzeKernel中初始化。
mov dword ptr fs:PcTeb, 0 ; PCR->Teb = 0
3、初始化KernelDr7和KernelDr6为0。
mov dword ptr fs:PcPrcbData+PbProcessorState+PsSpecialRegisters+SrKernelDr6,0
mov dword ptr fs:PcPrcbData+PbProcessorState+PsSpecialRegisters+SrKernelDr7,0
KiSwapIDT
接下来,调用KiSwapIDT来把内核IDT项中错误顺序的Selector和Extended Offset域排成i386能识别的顺序。仅在启动处理器中调用。
stdCall _KiSwapIDT
观察KiSwapIDT函数,它的声明如下:
VOID KiSwapIDT ( )
主要可以调用这个函数编辑IDT。它会把周围的地址和存取域转换为实际可以使用的形式。
这个函数可以简易静态初始化IDT。
函数伪代码如下:
LONG Index;
USHORT Temp;
1、 重排IDT项以符合i386 中断门结构
for (Index = 0; Index <= MAXIMUM_IDTVECTOR; Index += 1) {
Temp = IDT[Index].Selector;
IDT[Index].Selector = IDT[Index].ExtendedOffset;
IDT[Index].ExtendedOffset = Temp;
}
}
2、函数返回后,系统切回到R3 flat选择符,这样可以允许系统的lazy段加载正常工作。
mov eax,KGDT_R3_DATA OR RPL_MASK ; Set RPL = ring 3
mov ds,ax
mov es,ax
3、从这里开始,系统拷贝自己的陷阱处理器来替代系统的调试处理器。 后续的rep movsd会把预设的处理器全部拷贝到IDT中。
mov eax, KissIdt ; (eax)-> Idt
push dword ptr [eax+40h] ; save double fault's descriptor
push dword ptr [eax+44h]
push dword ptr [eax+10h] ; save nmi fault's descriptor
push dword ptr [eax+14h]
mov edi,KissIdt
mov esi,offset FLAT:_IDT
mov ecx,offset FLAT:_IDTLEN ; _IDTLEN is really an abs, we use
shr ecx,2
rep movsd
pop dword ptr [eax+14h] ; restore nmi fault's descriptor
pop dword ptr [eax+10h]
pop dword ptr [eax+44h] ; restore double fault's descriptor
pop dword ptr [eax+40h]
**
IDT Entries
**
对于WRT而言,各IDT条目位于trap.asm,功能如下。其中的每个表项叫做一个“门描述符(gate descriptor)”,这里“门”的含义是当中断发生时必须先通过这些门,然后才能进入相应的处理程序。
IDTEntry _KiTrap00, D_INT032 ; 0: Divide Error
IDTEntry _KiTrap01, D_INT032 ; 1: DEBUG TRAP
IDTEntry _KiTrap02, D_INT032 ; 2: NMI/NPX Error
IDTEntry _KiTrap03, D_INT332 ; 3: Breakpoint
IDTEntry _KiTrap04, D_INT332 ; 4: INTO
IDTEntry _KiTrap05, D_INT032 ; 5: BOUND/Print Screen
IDTEntry _KiTrap06, D_INT032 ; 6: Invalid Opcode
IDTEntry _KiTrap07, D_INT032 ; 7: NPX Not Available
IDTEntry _KiTrap08, D_INT032 ; 8: Double Exception
IDTEntry _KiTrap09, D_INT032 ; 9: NPX Segment Overrun
IDTEntry _KiTrap0A, D_INT032 ; A: Invalid TSS
IDTEntry _KiTrap0B, D_INT032 ; B: Segment Not Present
IDTEntry _KiTrap0C, D_INT032 ; C: Stack Fault
IDTEntry _KiTrap0D, D_INT032 ; D: General Protection
IDTEntry _KiTrap0E, D_INT032 ; E: Page Fault
IDTEntry _KiTrap0F, D_INT032 ; F: Intel Reserved
IDTEntry _KiTrap10, D_INT032 ;10: 486 coprocessor error
IDTEntry _KiTrap11, D_INT032 ;11: 486 alignment
IDTEntry _KiTrap0F, D_INT032 ;12: Intel Reserved
IDTEntry _KiTrap0F, D_INT032 ;13: XMMI unmasked numeric exception
IDTEntry _KiTrap0F, D_INT032 ;14: Intel Reserved
IDTEntry _KiTrap0F, D_INT032 ;15: Intel Reserved
IDTEntry _KiTrap0F, D_INT032 ;16: Intel Reserved
IDTEntry _KiTrap0F, D_INT032 ;17: Intel Reserved
IDTEntry _KiTrap0F, D_INT032 ;18: Intel Reserved
IDTEntry _KiTrap0F, D_INT032 ;19: Intel Reserved
IDTEntry _KiTrap0F, D_INT032 ;1A: Intel Reserved
IDTEntry _KiTrap0F, D_INT032 ;1B: Intel Reserved
IDTEntry _KiTrap0F, D_INT032 ;1C: Intel Reserved
IDTEntry _KiTrap0F, D_INT032 ;1D: Intel Reserved
IDTEntry _KiTrap0F, D_INT032 ;1E: Intel Reserved
IDTEntry _KiTrap0F, D_INT032 ;1F: Reserved for APIC
初始化完成后,可以基本认为IDT已经初始化结束。下一节,我们将介绍与陷阱有关的内容,并实战调试陷阱的进入。
(第一部分 完)
https://blogs.msdn.microsoft.com/reiley/2011/11/20/x86-segment-addressing-revisited/