来源:SoftRCE.net
本文基于Windows2003以上32位系统,因为XP处理NMI很弱,我们后面再说。
最近为点硬件写驱动,需要处理关于NMI的一些东西,索性把以前记录的一些资料和最新的体会写下来,希望对大家有用。
Nmi(Non Maskable Interrupt),一般在紧急的电源故障、总线超时或者存储器奇偶校验出错时被发出,对应的中断向量号为2,即int 2。CPU有两根引脚INTR和NMI接受外部中断请求信号:INTR接受可屏蔽中断请求,NMI接受不可屏蔽中断请求。在80386以上,标志寄存器 EFLAGS中的IF标志决定是否屏蔽可屏蔽中断请求。下面是调试器的结果:
kd> !idt -a
Dumping IDT:
00: 8082409e nt!KiTrap00
01: 8082421a nt!KiTrap01
02: Task Selector = 0x0058
03: 8082462a nt!KiTrap03
04: 808247a6 nt!KiTrap04
05: 80824904 nt!KiTrap05
06: 80824a86 nt!KiTrap06
07: 808250e6 nt!KiTrap07
08: Task Selector = 0x0050
09: 80825518 nt!KiTrap09
0a: 80825632 nt!KiTrap0A
//...
ff: 808231b0 nt!KiUnexpectedInterrupt207
相关的中断可以查阅详细资料[1],我们只关心下面两个:
IDTEntry _KiTrap02, D_INT032 ; 2: NMI/NPX Error
IDTEntry _KiTrap08, D_INT032 ; 8: Double Exception
即Windbg中显示的:
02: Task Selector = 0x0058
08: Task Selector = 0x0050
表 示的是系统为处理这两个int而设置的任务门,KGDT_DF_TSS(50h)和KGDT_NMI_TSS(58h)。那么是系统什么时候设置的呢?ntoskrnl加载的时候,KERNEL_ENTRY_POINT是KiSystemStartup()函数,反汇编不难看出它的流程,你也可以参考WangYu的一篇文章[2],或者可以参考ReactOs的代码,不过我们简单地反汇编一些:
1)首先是为NMI建立任务门,这样我们就能捕捉到NMI了:
INIT:006053CE mov eax, [ebp+var_10] ; 之前由GetMachineBootPointers得到的Idt
INIT:006053D1 lea ecx, [eax+10h] ; 16/8 -> 2号中断描述符
INIT:006053D4 mov byte ptr [ecx+5], 85h ; 10000101 -> dpl=0, present, taskgate
INIT:006053D8 mov word ptr [ecx+2], 58h ; KGDT_NMI_TSS(58h)
INIT:006053DE lea ecx, [edi+58h]
INIT:006053E1 mov byte ptr [ecx+5], 89h ; 10001001 -> 32bit, dpl=0, present, TSS32, not busy
INIT:006053E5 mov edx, offset _KiNMITSS
INIT:006053EA mov eax, edx
INIT:006053EC mov [ecx+2], ax ; KgdtBaseLow(2h)
INIT:006053F0 shr eax, 10h
INIT:006053F3 mov [ecx+7], ah ; KgdtBaseHi(7h)
INIT:006053F6 mov [ecx+4], al ; KgdtBaseMid(4h)
INIT:006053F9 mov eax, 68h ; MINIMUM_TSS_SIZE(68h)
INIT:006053FE mov [ecx], ax ; KgdtLimitLow(0h)
INIT:00605401 push edx
INIT:00605402 push edx
INIT:00605403 call KiInitializeTSS(x)
2)然后是建立堆栈:
INIT:00605408 pop edx
INIT:00605409 mov eax, cr3
INIT:0060540C mov [edx+1Ch], eax ; TssCr3(1ch)
INIT:0060540F mov eax, offset P0BootStack
INIT:00605414 mov eax, [eax+38h]
INIT:00605417 mov [edx+4], eax ; TssEsp0(4h) , NMI stack
INIT:0060541A mov [edx+38h], eax
INIT:0060541D mov dword ptr [edx+20h], offset _KiTrap02
INIT:00605424 mov dword ptr [edx+24h], 0 ; eflags
INIT:0060542B mov word ptr [edx+4Ch], 8 ; KGDT_R0_CODE(8h) -> CS
INIT:00605431 mov word ptr [edx+58h], 30h ; KGDT_R0_PCR(30h) -> FS
INIT:00605437 mov word ptr [edx+50h], ss
INIT:0060543A mov word ptr [edx+48h], 23h ; KGDT_R3_DATA(20h) + RPL_MASK(3h)
INIT:00605440 mov word ptr [edx+54h], 23h ; 初始化ES,DS
INIT:00605446 push offset _KiDoubleFaultStack
//...
INIT:0060545D call KiInitializePcr(x,x,x,x,x,x,x)
这里涉及一些结构,用Windbg可以清晰地看到:
kd> !pcr 0
kd> dt -r1 nt!_KPCR
kd> dt nt!_KIDTENTRY (_KGDTENTRY)
kd> dt -r1 nt!_KTHREAD
kd> dt -r1 nt!_KPROCESS
如果想在XP里设置Handler处理NMI,那么也该仿造以上过程,设置处理例程KiTrap02,把NMI-TSS里的EIP指向你的处理代码,然后建立堆栈,最后还要在iret后面添加jmp回到你的处理代码.原因简单地说,就是每当接受一个NMI中断,处理器会在内部屏蔽再次响应NMI,这一屏蔽过程直到执行中断返回指令IRET后才结束。还是直接看反汇编的KiTrap02代码吧:
.text:00484748 ; =============== S U B R O U T I N E =====================
.text:00484748
.text:00484748 _KiTrap02 proc near
.text:00484748
.text:00484748 var_8 = dword ptr -8
.text:00484748 var_4 = dword ptr -4
.text:00484748
.text:00484748 cli
.text:00484749 mov eax, ds:0FFDFF040h ; nt!_KPCR + 40 == _KTSS
.text:0048474E mov ecx, ds:0FFDFF124h ; nt!_KTHREAD
.text:00484754 mov edi, [ecx+38h] ; Edi= _KPROCESS
.text:00484757 mov ecx, [edi+18h] ; ULONG_PTR DirectoryTableBase[2]
.text:00484757 ; DirectoryTableBase[1] -> hyperspace
.text:00484757 ; DirectoryTableBase[0] -> CR3
.text:0048475A mov [eax+1Ch], ecx ; TSS.Edx= DirectoryTableBase[0]
.text:0048475D mov cx, [edi+30h]
.text:00484761 mov [eax+66h], cx ; TSS.IoMapBase= KPROCESS.IopmOffset
.text:00484765 mov ecx, [edi+20h] ; LdtDescriptor:_KGDTENTRY
.text:00484768 test ecx, ecx
.text:0048476A jz short loc_484770
.text:0048476A
.text:0048476C mov cx, 48h
.text:0048476C
.text:00484770 loc_484770:
.text:00484770 mov [eax+60h], cx ; set TSS.LDT
; if(_KPROCESS.LdtDescriptor==0) replaced by KGDT_LDT(0x48)
.text:00484770
.text:00484774 push dword ptr ds:0FFDFF040h ; push dword ptr PCR[PcTss]
.text:0048477A mov eax, ds:0FFDFF03Ch ; mov eax, PCR[PcGdt(3c)]
.text:0048477F mov ch, [eax+5Fh] ; mov ch, [eax+KGDT_NMI_TSS(58)+KgdtBaseHi(7)]
.text:00484782 mov cl, [eax+5Ch] ; mov cl, [eax+KGDT_NMI_TSS(58)+KgdtBaseMid(4)]
.text:00484785 shl ecx, 16
.text:00484788 mov cx, [eax+5Ah] ; mov cx, [eax+KGDT_NMI_TSS(58)+KgdtBaseLow(2)]
.text:0048478C mov ds:0FFDFF040h, ecx ; mov PCR[PcTss], ecx
.text:0048478C ; // PCR.TSS points to the NMI TSS
.text:0048478C
.text:00484792 pushf
.text:00484793 and [esp+8+var_8], 11111111111111111011111111111111b
.text:0048479A popf ; // Clear Nested Task bit in EFLAGS, no more NMI handler
.text:0048479A
.text:0048479B mov ecx, ds:0FFDFF03Ch ; PcGdt
.text:004847A1 lea eax, [ecx+58h] ; lea eax, [ecx] + KGDT_NMI_TSS
.text:004847A4 mov byte ptr [eax+5], 10001001b
; 32bit, dpl=0, present, TSS32, not busy
.text:004847A4 ; // Clear the busy bit in the TSS selector
.text:004847A4
.text:004847A8 mov eax, [esp+4+var_4] ; var_4 = KiSaveProcessorState(...)
.text:004847AB push 0
//... ...
.text:0048480B push ebp
.text:0048480C call KiSaveProcessorState(x,x)
.text:0048480C
.text:00484811 call KiHandleNmi() ; <---------------- Our CallBack Routines here..
.text:00484811
.text:00484816 or al, al ; if any CallBackRoutine handled nmi trap..
.text:00484818 jnz short Nmi_Magic_Handled ; Jump to restore and wait..
.text:00484818
.text:0048481A cmp ds:NmiUnknowFlag, 0 ; if(NmiFlagX!= 0)
.text:00484821 jnz short loc_484825 ; Setup Kernel debugger...
.text:00484821
.text:00484823 jmp short loc_484849 ; Pass nmi to Hal.dll,actually crash..
.text:00484823
.text:00484825 ; ---------------------------------------------------------------------------
.text:00484825
.text:00484825 loc_484825:
.text:00484825 cmp ds:NmiUnknowFlag, 8 ; NmiFlagX== 0 Nothing setuped,just pass to HAL
.text:00484825 ; 1-7 Nothing setuped,just pass to HAL and do crash
.text:00484825 ; 8 Get into KdDebugger
.text:00484825 ; >8 Dead Lock
.text:0048482C jb short loc_484849
.text:0048482C
.text:0048482E jnz short loc_484847
; if ( !_KdDebuggerNotPresent )
; {
; if ( _KdDebuggerEnabled )
; KeEnterKernelDebugger();
; }
; else
; while(1);
.text:00484830 cmp ds:_KdDebuggerNotPresent, 0
.text:00484837 jnz short loc_484847
.text:00484839 cmp ds:_KdDebuggerEnabled, 0
.text:00484840 jz short loc_484847
.text:00484842 call KeEnterKernelDebugger()
.text:00484847 loc_484847:
.text:00484847 jmp short loc_484847
.text:00484849 ; ---------------------------------------------------------------------------
.text:00484849 loc_484849:
.text:00484849 inc ds:NmiUnknowFlag
.text:0048484F push 0
.text:00484851 call ds:HalHandleNMI(x) ; <----------------
.text:00484851
.text:00484857 dec ds:NmiUnknowFlag ; --NmiFlagX != 0 then crash
.text:0048485D jnz short _Nmi_DoCrash ; But can we reach here? hmm..
.text:0048485D
.text:0048485F
.text:0048485F Nmi_Magic_Handled:
.text:0048485F mov eax, ds:0FFDFF040h ; eax= _KTSS
.text:00484864 cmp word ptr [eax], 58h ; if KGDT_NMI_TSS(58h), Tss error,just crash
.text:00484868 jz short _Nmi_DoCrash
.text:00484868
.text:0048486A add esp, 8Ch ; Nmi handled,so let's restore
.text:0048486A
.text:00484870 pop dword ptr ds:0FFDFF040h ; restore PcTss
.text:00484876 mov ecx, ds:0FFDFF03Ch ; mov ecx, PCR[PcGdt]
.text:0048487C lea eax, [ecx+28h] ; lea eax, [ecx] + KGDT_TSS
.text:0048487F mov byte ptr [eax+5], 8Bh ; 32bit, dpl=0, present, TSS32, *busy*
.text:00484883 pushf
.text:00484884 or [esp+4+var_4], 4000h ; Set Nested Task bit in EFLAGS
.text:0048488B popf ; then iretd will start a tast switch
.text:0048488C iret
.text:0048488C
.text:0048488D ; ---------------------------------------------------------------------------
.text:0048488D jmp _KiTrap02 ; <--------!!!!!!!!!!!!!!!!!!!!!
.text:00484892
.text:00484892 _Nmi_DoCrash:
.text:00484892 mov eax, 2
.text:00484897 jmp _KiSystemFatalException ; Crash!! KeBugCheck2(UNEXPECTED_KERNEL_MODE_TRAP)
.text:00484897
.text:00484897 _KiTrap02 endp
大 致的流程是更新TSS,屏蔽中断响应,保存CPU状态,调用用户注册的NMI处理例程;如果不能处理,则交给默认的Nmi Handler,也就是HalHandleNMI(x),但是它并不能做什么实质性的解决工作,只是准备BugCheck...在XP下,NMI一旦触发,都是交给HalHandleNMI,然后直接crash掉系统.Win2003下提供了一个很好的机制KeRegisterNmiCallback, 你可以注册Nmi的处理例程,这样发生Nmi中断的时候,你可以进行一些收尾工作,但是注意在回调函数里有很多的限制,不能有系统函数调用,不能去获取 SpinLock,必须使用Interlocked系列进行数据操作等等..
函数原型:
PVOID
KeRegisterNmiCallback(
PNMI_CALLBACK CallbackRoutine,
PVOID Context
);
还原出来的源代码如下:
static PVOID gKiNmiCallbackListHead= NULL;
KSPIN_LOCK KiNmiCallbackListLock;
typedef struct _KNMI_CALLBACK_RECORD
{
struct _KNMI_CALLBACK_RECORD *PreCallBackRecord;
PNMI_CALLBACK CallbackRoutine;
PVOID Context;
_KNMI_CALLBACK_RECORD *CurCallBackRecord;
} KNMI_CALLBACK_RECORD, *PKNMI_CALLBACK_RECORD;
以上是全局变量和结构体的声明.
PVOID _KeRegisterNmiCallback( IN PNMI_CALLBACK CallbackRoutine,
IN OPTIONAL PVOID Context )
{
PVOID result;
PKNMI_CALLBACK_RECORD pKNmiCallbackRecord;
KIRQL OldIrql;
result = ExAllocatePoolWithTag(0, 16, 'IMNK');
pKNmiCallbackRecord = (PKNMI_CALLBACK_RECORD)result;
if ( result )
{
pKNmiCallbackRecord->CallbackRoutine = CallbackRoutine;
pKNmiCallbackRecord->Context = Context;
pKNmiCallbackRecord->CurCallBackRecord = result;
OldIrql = KfAcquireSpinLock(&KiNmiCallbackListLock);
pKNmiCallbackRecord->PreCallBackRecord = gKiNmiCallbackListHead;
InterlockedCompareExchange( (PLONG)&gKiNmiCallbackListHead, (LONG)pKNmiCallbackRecord,(LONG)gKiNmiCallbackListHead );
KfReleaseSpinLock(&KiNmiCallbackListLock, OldIrql);
result = pKNmiCallbackRecord->CurCallBackRecord;
}
return result;
}
NTSTATUS _KeDeregisterNmiCallback(PVOID Handle)
{
KIRQL OldIrql;
PKNMI_CALLBACK_RECORD *tmpCallBackRecord;
PKNMI_CALLBACK_RECORD CurCallBackRecord;
NTSTATUS result;
CurCallBackRecord = (PKNMI_CALLBACK_RECORD)gKiNmiCallbackListHead;
OldIrql = KfAcquireSpinLock(&KiNmiCallbackListLock);
tmpCallBackRecord = &(PKNMI_CALLBACK_RECORD)gKiNmiCallbackListHead;
if ( !gKiNmiCallbackListHead )
{
KfReleaseSpinLock(&KiNmiCallbackListLock, OldIrql);
result = 0xC0000008; return result;
}
do
{
if ( (ULONG)(CurCallBackRecord->CurCallBackRecord) == (ULONG)Handle )
break;
tmpCallBackRecord = (PKNMI_CALLBACK_RECORD*)CurCallBackRecord;
CurCallBackRecord = *(PKNMI_CALLBACK_RECORD*)CurCallBackRecord;
}
while ( CurCallBackRecord );
if ( CurCallBackRecord && (ULONG)(CurCallBackRecord->CurCallBackRecord) == (ULONG)Handle )
{
*tmpCallBackRecord = *(PKNMI_CALLBACK_RECORD*)CurCallBackRecord;
KfReleaseSpinLock(&KiNmiCallbackListLock, OldIrql);
ExFreePoolWithTag(CurCallBackRecord, 'IMNK'); result = 0;
}
else
{
KfReleaseSpinLock(&KiNmiCallbackListLock, OldIrql); result = 0xC0000008;
}
return result;
}
其中,gKiNmiCallbackListHead是系统的一个全局变量,它指向最后注册的CallBackRecord,以反向的链表把所有的CallBack连接起来.正如MSDN中描述的:
When a nonmaskable interrupt occurs, the system calls each registered callback in reverse order from the order in which they were registered.
For the first callback, the system passes FALSE as the Handled parameter. For each subsequent callback, if any previous callback returned TRUE, the system passes TRUE as the Handled parameter, otherwise it passes FALSE.
If any callback returns a value of TRUE, the system considers the interrupt to have been handled.
Otherwise, the system calls the HAL's default handler for the interrupt, which normally causes the system to bug check.
正如前面的反汇编代码,NMI处理的流程大致为KiSystemStartup -> _KiTrap02 -> KiHandleNmi -> HalHandleNMI.把其他两个函数的代码也还原如下:
BOOLEAN _KiHandleNmi()
{
PKNMI_CALLBACK_RECORD CurCallBackRecord;
BOOLEAN nmiHandled;
CurCallBackRecord = gKiNmiCallbackListHead;
nmiHandled = FALSE;
while ( CurCallBackRecord )
{
nmiHandled = (CurCallBackRecord->CallbackRoutine)(CurCallBackRecord->Context, nmiHandled) | nmiHandled;
CurCallBackRecord = CurCallBackRecord->PreCallBackRecord;
}
return nmiHandled;
}
HalHandleNMI比较长,加了点注释:
void _HalHandleNMI(PVOID NmiInfo)
{
UCHAR EISAExNmiStatus;
UCHAR PortNmiStatus;
UCHAR EisaPort;
PUCHAR Port;
UCHAR EisaNMIMsgBuff[28];
ULONG i;
HalpNMIInProgress = 1;
HalpDoingCrashDump = 1;
EisaPort = READ_PORT_UCHAR((PUCHAR)0x61); // 0x61 -> SYSTEM_CONTROL_PORT_B
if ( (unsigned __int8)InbvIsBootDriverInstalled() )
{
InbvAcquireDisplayOwnership();
InbvResetDisplay();
InbvSolidColorFill(0, 0, 639, 479, 4);
InbvSetTextColor(15); InbvInstallDisplayStringFilter(0);
InbvEnableDisplayString(1); InbvSetScrollRegion(0, 0, 639, 479);
}
HalDisplayString("/n*** Hardware Malfunction/n/n");
HalDisplayString("Call your hardware vendor for support/n/n");
if ( EisaPort & 0x80 )
HalDisplayString("NMI: Parity Check / Memory Parity Error/n");
if ( EisaPort & 0x40 )
HalDisplayString("NMI: Channel Check / IOCHK/n");
if ( HalpBusType == 1 ) // #define MACHINE_TYPE_EISA 1
{
EISAExNmiStatus = READ_PORT_UCHAR((PUCHAR)0x461); // 0x461 -> EISA_EXTENDED_NMI_STATUS
EisaPort = EISAExNmiStatus;
if ( (char)EISAExNmiStatus < 0 )
HalDisplayString("NMI: Fail-safe timer/n");
if ( EisaPort & 0x40 )
HalDisplayString("NMI: Bus Timeout/n");
if ( EisaPort & 0x20 )
HalDisplayString("NMI: Software NMI generated/n");
EisaPort = 1;
port = (EisaPort << 12) + 0xC80;
do {
WRITE_PORT_UCHAR(Port, 0xFF);
if ( (char)READ_PORT_UCHAR(Port) >= 0 )
{
PortNmiStatus = READ_PORT_UCHAR(Port + 4);
if ( PortNmiStatus & 0x2 )
{
if ( PortNmiStatus != 0xFFFFFFFF )
{ // EisaNMIMsg[]: "NMI: Eisa IOCHKERR board %/n"
memcpy(EisaNMIMsgBuff, &EisaNMIMsg, sizeof(EisaNMIMsgBuff));
i = 0;
if ( EisaNMIMsgBuff )
{
while ( EisaNMIMsgBuff != '%' )
{
++i;
if ( !EisaNMIMsgBuff )
goto DisplayMsg;
}
EisaNMIMsgBuff = (EisaPort > 9 ? 'A'-10 : '0') + EisaPort;
}
DisplayMsg: HalDisplayString(EisaNMIMsgBuff);
}
}
}
++EisaPort; Port += 0x1000;
} while ( EisaPort <= 0xF );
}
HalDisplayString("/n*** The system has halted ***/n");
if ( HalpNMIDumpFlag ) //Registry/Machine/System/CurrentControlSet/Control/CrashControl
KeBugCheckEx(0x80, 'ODT', 0, 0, 0);
if ( !KdDebuggerNotPresent )
{
if ( KdDebuggerEnabled )
KeEnterKernelDebugger();
} while ( 1 );
}
有 意思的是HalpNMIDumpFlag这个全局标志,它决定了NMI触发而且要Crash系统的时候是否生成 dump[3].其实就是注册表的这个位置:HKEY_LOCAL_MACHINE/System/CurrentControlSet/Control /CrashControl,CrashDumpEnabled设置为2,NMICrashDump设置为1.这个可以为服务器生成dump以调试,比如 DELL PE2850,设置好BISO允许使用NMI Button后,你只要按下机器上的NMI按钮就可以了.
todo...
排版太难受,要详细写的也搞的不想写了,以后再更新:)
________________________________________
[1] http://en.wikipedia.org/wiki/Interrupt_descriptor_table
http://wiki.osdev.org/GDT_Tutorial
[2] http://advdbg.org/blogs/advdbg_system/articles/409.aspx
[3] http://www.microsoft.com/whdc/system/sysinternals/dmpsw.mspx
[4] Nice Links:
http://bbs.pediy.com/showthread.php?p=417908
本文基于Windows2003以上32位系统,因为XP处理NMI很弱,我们后面再说。
最近为点硬件写驱动,需要处理关于NMI的一些东西,索性把以前记录的一些资料和最新的体会写下来,希望对大家有用。
Nmi(Non Maskable Interrupt),一般在紧急的电源故障、总线超时或者存储器奇偶校验出错时被发出,对应的中断向量号为2,即int 2。CPU有两根引脚INTR和NMI接受外部中断请求信号:INTR接受可屏蔽中断请求,NMI接受不可屏蔽中断请求。在80386以上,标志寄存器 EFLAGS中的IF标志决定是否屏蔽可屏蔽中断请求。下面是调试器的结果:
kd> !idt -a
Dumping IDT:
00: 8082409e nt!KiTrap00
01: 8082421a nt!KiTrap01
02: Task Selector = 0x0058
03: 8082462a nt!KiTrap03
04: 808247a6 nt!KiTrap04
05: 80824904 nt!KiTrap05
06: 80824a86 nt!KiTrap06
07: 808250e6 nt!KiTrap07
08: Task Selector = 0x0050
09: 80825518 nt!KiTrap09
0a: 80825632 nt!KiTrap0A
//...
ff: 808231b0 nt!KiUnexpectedInterrupt207
相关的中断可以查阅详细资料[1],我们只关心下面两个:
IDTEntry _KiTrap02, D_INT032 ; 2: NMI/NPX Error
IDTEntry _KiTrap08, D_INT032 ; 8: Double Exception
即Windbg中显示的:
02: Task Selector = 0x0058
08: Task Selector = 0x0050
表 示的是系统为处理这两个int而设置的任务门,KGDT_DF_TSS(50h)和KGDT_NMI_TSS(58h)。那么是系统什么时候设置的呢?ntoskrnl加载的时候,KERNEL_ENTRY_POINT是KiSystemStartup()函数,反汇编不难看出它的流程,你也可以参考WangYu的一篇文章[2],或者可以参考ReactOs的代码,不过我们简单地反汇编一些:
1)首先是为NMI建立任务门,这样我们就能捕捉到NMI了:
INIT:006053CE mov eax, [ebp+var_10] ; 之前由GetMachineBootPointers得到的Idt
INIT:006053D1 lea ecx, [eax+10h] ; 16/8 -> 2号中断描述符
INIT:006053D4 mov byte ptr [ecx+5], 85h ; 10000101 -> dpl=0, present, taskgate
INIT:006053D8 mov word ptr [ecx+2], 58h ; KGDT_NMI_TSS(58h)
INIT:006053DE lea ecx, [edi+58h]
INIT:006053E1 mov byte ptr [ecx+5], 89h ; 10001001 -> 32bit, dpl=0, present, TSS32, not busy
INIT:006053E5 mov edx, offset _KiNMITSS
INIT:006053EA mov eax, edx
INIT:006053EC mov [ecx+2], ax ; KgdtBaseLow(2h)
INIT:006053F0 shr eax, 10h
INIT:006053F3 mov [ecx+7], ah ; KgdtBaseHi(7h)
INIT:006053F6 mov [ecx+4], al ; KgdtBaseMid(4h)
INIT:006053F9 mov eax, 68h ; MINIMUM_TSS_SIZE(68h)
INIT:006053FE mov [ecx], ax ; KgdtLimitLow(0h)
INIT:00605401 push edx
INIT:00605402 push edx
INIT:00605403 call KiInitializeTSS(x)
2)然后是建立堆栈:
INIT:00605408 pop edx
INIT:00605409 mov eax, cr3
INIT:0060540C mov [edx+1Ch], eax ; TssCr3(1ch)
INIT:0060540F mov eax, offset P0BootStack
INIT:00605414 mov eax, [eax+38h]
INIT:00605417 mov [edx+4], eax ; TssEsp0(4h) , NMI stack
INIT:0060541A mov [edx+38h], eax
INIT:0060541D mov dword ptr [edx+20h], offset _KiTrap02
INIT:00605424 mov dword ptr [edx+24h], 0 ; eflags
INIT:0060542B mov word ptr [edx+4Ch], 8 ; KGDT_R0_CODE(8h) -> CS
INIT:00605431 mov word ptr [edx+58h], 30h ; KGDT_R0_PCR(30h) -> FS
INIT:00605437 mov word ptr [edx+50h], ss
INIT:0060543A mov word ptr [edx+48h], 23h ; KGDT_R3_DATA(20h) + RPL_MASK(3h)
INIT:00605440 mov word ptr [edx+54h], 23h ; 初始化ES,DS
INIT:00605446 push offset _KiDoubleFaultStack
//...
INIT:0060545D call KiInitializePcr(x,x,x,x,x,x,x)
这里涉及一些结构,用Windbg可以清晰地看到:
kd> !pcr 0
kd> dt -r1 nt!_KPCR
kd> dt nt!_KIDTENTRY (_KGDTENTRY)
kd> dt -r1 nt!_KTHREAD
kd> dt -r1 nt!_KPROCESS
如果想在XP里设置Handler处理NMI,那么也该仿造以上过程,设置处理例程KiTrap02,把NMI-TSS里的EIP指向你的处理代码,然后建立堆栈,最后还要在iret后面添加jmp回到你的处理代码.原因简单地说,就是每当接受一个NMI中断,处理器会在内部屏蔽再次响应NMI,这一屏蔽过程直到执行中断返回指令IRET后才结束。还是直接看反汇编的KiTrap02代码吧:
.text:00484748 ; =============== S U B R O U T I N E =====================
.text:00484748
.text:00484748 _KiTrap02 proc near
.text:00484748
.text:00484748 var_8 = dword ptr -8
.text:00484748 var_4 = dword ptr -4
.text:00484748
.text:00484748 cli
.text:00484749 mov eax, ds:0FFDFF040h ; nt!_KPCR + 40 == _KTSS
.text:0048474E mov ecx, ds:0FFDFF124h ; nt!_KTHREAD
.text:00484754 mov edi, [ecx+38h] ; Edi= _KPROCESS
.text:00484757 mov ecx, [edi+18h] ; ULONG_PTR DirectoryTableBase[2]
.text:00484757 ; DirectoryTableBase[1] -> hyperspace
.text:00484757 ; DirectoryTableBase[0] -> CR3
.text:0048475A mov [eax+1Ch], ecx ; TSS.Edx= DirectoryTableBase[0]
.text:0048475D mov cx, [edi+30h]
.text:00484761 mov [eax+66h], cx ; TSS.IoMapBase= KPROCESS.IopmOffset
.text:00484765 mov ecx, [edi+20h] ; LdtDescriptor:_KGDTENTRY
.text:00484768 test ecx, ecx
.text:0048476A jz short loc_484770
.text:0048476A
.text:0048476C mov cx, 48h
.text:0048476C
.text:00484770 loc_484770:
.text:00484770 mov [eax+60h], cx ; set TSS.LDT
; if(_KPROCESS.LdtDescriptor==0) replaced by KGDT_LDT(0x48)
.text:00484770
.text:00484774 push dword ptr ds:0FFDFF040h ; push dword ptr PCR[PcTss]
.text:0048477A mov eax, ds:0FFDFF03Ch ; mov eax, PCR[PcGdt(3c)]
.text:0048477F mov ch, [eax+5Fh] ; mov ch, [eax+KGDT_NMI_TSS(58)+KgdtBaseHi(7)]
.text:00484782 mov cl, [eax+5Ch] ; mov cl, [eax+KGDT_NMI_TSS(58)+KgdtBaseMid(4)]
.text:00484785 shl ecx, 16
.text:00484788 mov cx, [eax+5Ah] ; mov cx, [eax+KGDT_NMI_TSS(58)+KgdtBaseLow(2)]
.text:0048478C mov ds:0FFDFF040h, ecx ; mov PCR[PcTss], ecx
.text:0048478C ; // PCR.TSS points to the NMI TSS
.text:0048478C
.text:00484792 pushf
.text:00484793 and [esp+8+var_8], 11111111111111111011111111111111b
.text:0048479A popf ; // Clear Nested Task bit in EFLAGS, no more NMI handler
.text:0048479A
.text:0048479B mov ecx, ds:0FFDFF03Ch ; PcGdt
.text:004847A1 lea eax, [ecx+58h] ; lea eax, [ecx] + KGDT_NMI_TSS
.text:004847A4 mov byte ptr [eax+5], 10001001b
; 32bit, dpl=0, present, TSS32, not busy
.text:004847A4 ; // Clear the busy bit in the TSS selector
.text:004847A4
.text:004847A8 mov eax, [esp+4+var_4] ; var_4 = KiSaveProcessorState(...)
.text:004847AB push 0
//... ...
.text:0048480B push ebp
.text:0048480C call KiSaveProcessorState(x,x)
.text:0048480C
.text:00484811 call KiHandleNmi() ; <---------------- Our CallBack Routines here..
.text:00484811
.text:00484816 or al, al ; if any CallBackRoutine handled nmi trap..
.text:00484818 jnz short Nmi_Magic_Handled ; Jump to restore and wait..
.text:00484818
.text:0048481A cmp ds:NmiUnknowFlag, 0 ; if(NmiFlagX!= 0)
.text:00484821 jnz short loc_484825 ; Setup Kernel debugger...
.text:00484821
.text:00484823 jmp short loc_484849 ; Pass nmi to Hal.dll,actually crash..
.text:00484823
.text:00484825 ; ---------------------------------------------------------------------------
.text:00484825
.text:00484825 loc_484825:
.text:00484825 cmp ds:NmiUnknowFlag, 8 ; NmiFlagX== 0 Nothing setuped,just pass to HAL
.text:00484825 ; 1-7 Nothing setuped,just pass to HAL and do crash
.text:00484825 ; 8 Get into KdDebugger
.text:00484825 ; >8 Dead Lock
.text:0048482C jb short loc_484849
.text:0048482C
.text:0048482E jnz short loc_484847
; if ( !_KdDebuggerNotPresent )
; {
; if ( _KdDebuggerEnabled )
; KeEnterKernelDebugger();
; }
; else
; while(1);
.text:00484830 cmp ds:_KdDebuggerNotPresent, 0
.text:00484837 jnz short loc_484847
.text:00484839 cmp ds:_KdDebuggerEnabled, 0
.text:00484840 jz short loc_484847
.text:00484842 call KeEnterKernelDebugger()
.text:00484847 loc_484847:
.text:00484847 jmp short loc_484847
.text:00484849 ; ---------------------------------------------------------------------------
.text:00484849 loc_484849:
.text:00484849 inc ds:NmiUnknowFlag
.text:0048484F push 0
.text:00484851 call ds:HalHandleNMI(x) ; <----------------
.text:00484851
.text:00484857 dec ds:NmiUnknowFlag ; --NmiFlagX != 0 then crash
.text:0048485D jnz short _Nmi_DoCrash ; But can we reach here? hmm..
.text:0048485D
.text:0048485F
.text:0048485F Nmi_Magic_Handled:
.text:0048485F mov eax, ds:0FFDFF040h ; eax= _KTSS
.text:00484864 cmp word ptr [eax], 58h ; if KGDT_NMI_TSS(58h), Tss error,just crash
.text:00484868 jz short _Nmi_DoCrash
.text:00484868
.text:0048486A add esp, 8Ch ; Nmi handled,so let's restore
.text:0048486A
.text:00484870 pop dword ptr ds:0FFDFF040h ; restore PcTss
.text:00484876 mov ecx, ds:0FFDFF03Ch ; mov ecx, PCR[PcGdt]
.text:0048487C lea eax, [ecx+28h] ; lea eax, [ecx] + KGDT_TSS
.text:0048487F mov byte ptr [eax+5], 8Bh ; 32bit, dpl=0, present, TSS32, *busy*
.text:00484883 pushf
.text:00484884 or [esp+4+var_4], 4000h ; Set Nested Task bit in EFLAGS
.text:0048488B popf ; then iretd will start a tast switch
.text:0048488C iret
.text:0048488C
.text:0048488D ; ---------------------------------------------------------------------------
.text:0048488D jmp _KiTrap02 ; <--------!!!!!!!!!!!!!!!!!!!!!
.text:00484892
.text:00484892 _Nmi_DoCrash:
.text:00484892 mov eax, 2
.text:00484897 jmp _KiSystemFatalException ; Crash!! KeBugCheck2(UNEXPECTED_KERNEL_MODE_TRAP)
.text:00484897
.text:00484897 _KiTrap02 endp
大 致的流程是更新TSS,屏蔽中断响应,保存CPU状态,调用用户注册的NMI处理例程;如果不能处理,则交给默认的Nmi Handler,也就是HalHandleNMI(x),但是它并不能做什么实质性的解决工作,只是准备BugCheck...在XP下,NMI一旦触发,都是交给HalHandleNMI,然后直接crash掉系统.Win2003下提供了一个很好的机制KeRegisterNmiCallback, 你可以注册Nmi的处理例程,这样发生Nmi中断的时候,你可以进行一些收尾工作,但是注意在回调函数里有很多的限制,不能有系统函数调用,不能去获取 SpinLock,必须使用Interlocked系列进行数据操作等等..
函数原型:
PVOID
KeRegisterNmiCallback(
PNMI_CALLBACK CallbackRoutine,
PVOID Context
);
还原出来的源代码如下:
static PVOID gKiNmiCallbackListHead= NULL;
KSPIN_LOCK KiNmiCallbackListLock;
typedef struct _KNMI_CALLBACK_RECORD
{
struct _KNMI_CALLBACK_RECORD *PreCallBackRecord;
PNMI_CALLBACK CallbackRoutine;
PVOID Context;
_KNMI_CALLBACK_RECORD *CurCallBackRecord;
} KNMI_CALLBACK_RECORD, *PKNMI_CALLBACK_RECORD;
以上是全局变量和结构体的声明.
PVOID _KeRegisterNmiCallback( IN PNMI_CALLBACK CallbackRoutine,
IN OPTIONAL PVOID Context )
{
PVOID result;
PKNMI_CALLBACK_RECORD pKNmiCallbackRecord;
KIRQL OldIrql;
result = ExAllocatePoolWithTag(0, 16, 'IMNK');
pKNmiCallbackRecord = (PKNMI_CALLBACK_RECORD)result;
if ( result )
{
pKNmiCallbackRecord->CallbackRoutine = CallbackRoutine;
pKNmiCallbackRecord->Context = Context;
pKNmiCallbackRecord->CurCallBackRecord = result;
OldIrql = KfAcquireSpinLock(&KiNmiCallbackListLock);
pKNmiCallbackRecord->PreCallBackRecord = gKiNmiCallbackListHead;
InterlockedCompareExchange( (PLONG)&gKiNmiCallbackListHead, (LONG)pKNmiCallbackRecord,(LONG)gKiNmiCallbackListHead );
KfReleaseSpinLock(&KiNmiCallbackListLock, OldIrql);
result = pKNmiCallbackRecord->CurCallBackRecord;
}
return result;
}
NTSTATUS _KeDeregisterNmiCallback(PVOID Handle)
{
KIRQL OldIrql;
PKNMI_CALLBACK_RECORD *tmpCallBackRecord;
PKNMI_CALLBACK_RECORD CurCallBackRecord;
NTSTATUS result;
CurCallBackRecord = (PKNMI_CALLBACK_RECORD)gKiNmiCallbackListHead;
OldIrql = KfAcquireSpinLock(&KiNmiCallbackListLock);
tmpCallBackRecord = &(PKNMI_CALLBACK_RECORD)gKiNmiCallbackListHead;
if ( !gKiNmiCallbackListHead )
{
KfReleaseSpinLock(&KiNmiCallbackListLock, OldIrql);
result = 0xC0000008; return result;
}
do
{
if ( (ULONG)(CurCallBackRecord->CurCallBackRecord) == (ULONG)Handle )
break;
tmpCallBackRecord = (PKNMI_CALLBACK_RECORD*)CurCallBackRecord;
CurCallBackRecord = *(PKNMI_CALLBACK_RECORD*)CurCallBackRecord;
}
while ( CurCallBackRecord );
if ( CurCallBackRecord && (ULONG)(CurCallBackRecord->CurCallBackRecord) == (ULONG)Handle )
{
*tmpCallBackRecord = *(PKNMI_CALLBACK_RECORD*)CurCallBackRecord;
KfReleaseSpinLock(&KiNmiCallbackListLock, OldIrql);
ExFreePoolWithTag(CurCallBackRecord, 'IMNK'); result = 0;
}
else
{
KfReleaseSpinLock(&KiNmiCallbackListLock, OldIrql); result = 0xC0000008;
}
return result;
}
其中,gKiNmiCallbackListHead是系统的一个全局变量,它指向最后注册的CallBackRecord,以反向的链表把所有的CallBack连接起来.正如MSDN中描述的:
When a nonmaskable interrupt occurs, the system calls each registered callback in reverse order from the order in which they were registered.
For the first callback, the system passes FALSE as the Handled parameter. For each subsequent callback, if any previous callback returned TRUE, the system passes TRUE as the Handled parameter, otherwise it passes FALSE.
If any callback returns a value of TRUE, the system considers the interrupt to have been handled.
Otherwise, the system calls the HAL's default handler for the interrupt, which normally causes the system to bug check.
正如前面的反汇编代码,NMI处理的流程大致为KiSystemStartup -> _KiTrap02 -> KiHandleNmi -> HalHandleNMI.把其他两个函数的代码也还原如下:
BOOLEAN _KiHandleNmi()
{
PKNMI_CALLBACK_RECORD CurCallBackRecord;
BOOLEAN nmiHandled;
CurCallBackRecord = gKiNmiCallbackListHead;
nmiHandled = FALSE;
while ( CurCallBackRecord )
{
nmiHandled = (CurCallBackRecord->CallbackRoutine)(CurCallBackRecord->Context, nmiHandled) | nmiHandled;
CurCallBackRecord = CurCallBackRecord->PreCallBackRecord;
}
return nmiHandled;
}
HalHandleNMI比较长,加了点注释:
void _HalHandleNMI(PVOID NmiInfo)
{
UCHAR EISAExNmiStatus;
UCHAR PortNmiStatus;
UCHAR EisaPort;
PUCHAR Port;
UCHAR EisaNMIMsgBuff[28];
ULONG i;
HalpNMIInProgress = 1;
HalpDoingCrashDump = 1;
EisaPort = READ_PORT_UCHAR((PUCHAR)0x61); // 0x61 -> SYSTEM_CONTROL_PORT_B
if ( (unsigned __int8)InbvIsBootDriverInstalled() )
{
InbvAcquireDisplayOwnership();
InbvResetDisplay();
InbvSolidColorFill(0, 0, 639, 479, 4);
InbvSetTextColor(15); InbvInstallDisplayStringFilter(0);
InbvEnableDisplayString(1); InbvSetScrollRegion(0, 0, 639, 479);
}
HalDisplayString("/n*** Hardware Malfunction/n/n");
HalDisplayString("Call your hardware vendor for support/n/n");
if ( EisaPort & 0x80 )
HalDisplayString("NMI: Parity Check / Memory Parity Error/n");
if ( EisaPort & 0x40 )
HalDisplayString("NMI: Channel Check / IOCHK/n");
if ( HalpBusType == 1 ) // #define MACHINE_TYPE_EISA 1
{
EISAExNmiStatus = READ_PORT_UCHAR((PUCHAR)0x461); // 0x461 -> EISA_EXTENDED_NMI_STATUS
EisaPort = EISAExNmiStatus;
if ( (char)EISAExNmiStatus < 0 )
HalDisplayString("NMI: Fail-safe timer/n");
if ( EisaPort & 0x40 )
HalDisplayString("NMI: Bus Timeout/n");
if ( EisaPort & 0x20 )
HalDisplayString("NMI: Software NMI generated/n");
EisaPort = 1;
port = (EisaPort << 12) + 0xC80;
do {
WRITE_PORT_UCHAR(Port, 0xFF);
if ( (char)READ_PORT_UCHAR(Port) >= 0 )
{
PortNmiStatus = READ_PORT_UCHAR(Port + 4);
if ( PortNmiStatus & 0x2 )
{
if ( PortNmiStatus != 0xFFFFFFFF )
{ // EisaNMIMsg[]: "NMI: Eisa IOCHKERR board %/n"
memcpy(EisaNMIMsgBuff, &EisaNMIMsg, sizeof(EisaNMIMsgBuff));
i = 0;
if ( EisaNMIMsgBuff )
{
while ( EisaNMIMsgBuff != '%' )
{
++i;
if ( !EisaNMIMsgBuff )
goto DisplayMsg;
}
EisaNMIMsgBuff = (EisaPort > 9 ? 'A'-10 : '0') + EisaPort;
}
DisplayMsg: HalDisplayString(EisaNMIMsgBuff);
}
}
}
++EisaPort; Port += 0x1000;
} while ( EisaPort <= 0xF );
}
HalDisplayString("/n*** The system has halted ***/n");
if ( HalpNMIDumpFlag ) //Registry/Machine/System/CurrentControlSet/Control/CrashControl
KeBugCheckEx(0x80, 'ODT', 0, 0, 0);
if ( !KdDebuggerNotPresent )
{
if ( KdDebuggerEnabled )
KeEnterKernelDebugger();
} while ( 1 );
}
有 意思的是HalpNMIDumpFlag这个全局标志,它决定了NMI触发而且要Crash系统的时候是否生成 dump[3].其实就是注册表的这个位置:HKEY_LOCAL_MACHINE/System/CurrentControlSet/Control /CrashControl,CrashDumpEnabled设置为2,NMICrashDump设置为1.这个可以为服务器生成dump以调试,比如 DELL PE2850,设置好BISO允许使用NMI Button后,你只要按下机器上的NMI按钮就可以了.
todo...
排版太难受,要详细写的也搞的不想写了,以后再更新:)
________________________________________
[1] http://en.wikipedia.org/wiki/Interrupt_descriptor_table
http://wiki.osdev.org/GDT_Tutorial
[2] http://advdbg.org/blogs/advdbg_system/articles/409.aspx
[3] http://www.microsoft.com/whdc/system/sysinternals/dmpsw.mspx
[4] Nice Links:
http://bbs.pediy.com/showthread.php?p=417908