第一部分 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已经初始化结束。下一节,我们将介绍与陷阱有关的内容,并实战调试陷阱的进入。

(第一部分 完)

标签:none

仅有 1 条评论

  1. https://blogs.msdn.microsoft.com/reiley/2011/11/20/x86-segment-addressing-revisited/

添加新评论

captcha
请输入验证码