10.12 随笔一则
下面的随笔是我昨天夜读源代码的笔记,算是一个自我小结。本随笔粗粒度跟踪了 Windows NT5.2 的启动过程(源代码级 / 及 Win32 兼容内核 / 本人初学者,水平有限,您多包涵),正好也应应 Raymond 老师将要出版的新书 —— 《软件调试》。Raymond 老师在新书的某一章节重点关注了 KdInitSystem() —— 内核调试器的初始化过程,我自然就没理由狗尾续貂了。所以,本随笔重点关注一下 KeInitSystem() —— 执行体的初始化过程,特别是 MSR 寄存器的初始化、进 SSDT 之前 Trap 的相关过程等 —— 我想对于刚接触内核的人来说,第一个吸引眼球的东西多半就是 SSDT (及相关)了。
为了不给 Advdbg.org 和自己带来不必要的麻烦,我最终还是将这里列出的代码选为 ReactOS 的代码,关于其许可和版权信息您可以参见 ReactOS 的 LICENSE。反正它们基本上是二进制兼容的,况且“市面上”的 2000 代码没有 SYSENTER 相关信息。 PS : 看见 MJ001 前辈说 XP SP3 已经到手,可怜的我咋什么都没有呢?
那么我们从什么说起呢?一直往前追踪可就没边了,虽然原来我读过一些 Award BIOS 的源代码,但是这的确是很痛苦的事情,咱以后再聊。这次就从 Ntldr 开始吧。
首先从 Ntldr 中提一个 PE 出来 —— osloader.exe。用IDA来分析osloader.exe,当IDA的分析停止时,你就可以看到代码的入口点了(上面这句话不是我说的,这是《NTLDR Reverse Engineering》文中的话) —— NtProcessStartup() [NtProcessStartup@..\ntoskrnl\ke\i386\boot.S]。
========================
[1] : NtProcessStartup()
========================
对于老版本的 ReactOS Alex Ionescu 是这么说的:
Remove all the remaining code in boot.S and make KiRosPrepareForSystemStartup fastcall. Now NtProcessStartup just does a jmp to KiRosPrepareForSystemStartup without any other code.
代码 / 代码片断如下: [NtProcessStartup@..\ntoskrnl\ke\i386\boot.S Old]
/*
* FILE: ntoskrnl/ke/i386/boot.S
* COPYRIGHT: See COPYING in the top level directory
* PURPOSE: Kernel Bootstrap Code
* PURPOSE: FreeLDR Wrapper Bootstrap Code
* PROGRAMMER: Alex Ionescu (alex at relsoft.net)
*/
@@ -30,16 +30,6 @@
.text
.func NtProcessStartup
_NtProcessStartup:
/* Load the initial kernel stack */
lea eax, _P0BootStack
sub eax, (NPX_FRAME_LENGTH + KTRAP_FRAME_LENGTH + KTRAP_FRAME_ALIGN)
mov esp, eax
/* Save initial CR0 state */
push CR0_EM + CR0_TS + CR0_MP
/* Call the main kernel initialization */
push edx
call _KiRosPrepareForSystemStartup@4
jmp @KiRosPrepareForSystemStartup@8
.endfunc
关于 NtProcessStartup() 最近的一次代码更新日志是这样的:
Add two more lines in boot.S which detect boot-by-NTLDR and jump into KiSystemService (I thought FreeLdr didn't use the entrypoint, but it looks like it does, so this hack is needed).
代码 / 代码片断如下: [NtProcessStartup@..\ntoskrnl\ke\i386\boot.S]
.text
.func NtProcessStartup
_NtProcessStartup:
/* NTLDR Boot: Call the main kernel initialization */
test dword ptr [esp+4], 0x80000000
jnz _KiSystemStartup@ 4
/* FREELDR Boot: Cal the FreeLDR wrapper */
jmp @KiRosPrepareForSystemStartup@8
.endfunc
其实在 ReactOS 里 NtProcessStartup() 说白了就是一个无条件的转移...
====================================
[2] : KiRosPrepareForSystemStartup()
====================================
嗯,看来新版的 NtProcessStartup() 还支持 Ntldr 启动了。不过我们还是追寻 ReactOS 自己的主线 —— KiRosPrepareForSystemStartup()。
代码 / 代码片断如下: [KiRosPrepareForSystemStartup@..\ntoskrnl\ke\freeldr.c]
VOID
FASTCALL
KiRosPrepareForSystemStartup(IN ULONG Dummy,
IN PROS_LOADER_PARAMETER_BLOCK LoaderBlock)
{
PLOADER_PARAMETER_BLOCK NtLoaderBlock;
.........
/* Load the GDT and IDT */
Ke386SetGlobalDescriptorTable(*(PKDESCRIPTOR)&KiGdtDescriptor.Limit);
Ke386SetInterruptDescriptorTable(*(PKDESCRIPTOR)&KiIdtDescriptor.Limit);
/* Initialize the boot TSS */
Tss = &KiBootTss;
TssEntry = &KiBootGdt[KGDT_TSS / sizeof(KGDTENTRY)];
TssEntry->HighWord.Bits.Type = I386_TSS;
TssEntry->HighWord.Bits.Pres = 1;
TssEntry->HighWord.Bits.Dpl = 0;
TssEntry->BaseLow = (USHORT)((ULONG_PTR)Tss & 0xFFFF);
TssEntry->HighWord.Bytes.BaseMid = (UCHAR)((ULONG_PTR)Tss >> 16);
TssEntry->HighWord.Bytes.BaseHi = (UCHAR)((ULONG_PTR)Tss >> 24);
.........
/* Do general System Startup */
KiSystemStartup(NtLoaderBlock);
}
简要解释一下,KiSystemStartup() 函数是内核镜像 Ntoskrnl.exe 的 KERNEL_ENTRY_POINT,这个分水岭也是算做 Win32 初始化的第二大阶段。另外,CPU(CPUs) 的 GDT、IDT 和 TSS 等数据结构也需要在此调用前后完成初始化。
=======================
[3] : KiSystemStartup()
=======================
下面开始 KiSystemStartup() 的“展示”,我认为如此核心的代码是非常值得一读的~
代码 / 代码片断如下: [KiSystemStartup@..\ntoskrnl\ke\i386\kiinit.c]
VOID
NTAPI
KiSystemStartup(IN PLOADER_PARAMETER_BLOCK LoaderBlock)
{
ULONG Cpu;
PKTHREAD InitialThread;
ULONG InitialStack;
PKGDTENTRY Gdt;
PKIDTENTRY Idt;
PKTSS Tss;
PKIPCR Pcr;
/* Save the loader block and get the current CPU */
KeLoaderBlock = LoaderBlock;
Cpu = KeNumberProcessors;
.........
/* Initialize the machine type */
KiInitializeMachineType();
.........
/* Get GDT, IDT, PCR and TSS pointers */
KiGetMachineBootPointers(&Gdt, &Idt, &Pcr, &Tss);
/* Setup the TSS descriptors and entries */
Ki386InitializeTss(Tss, Idt, Gdt);
/* Initialize the PCR */
RtlZeroMemory(Pcr, PAGE_SIZE);
KiInitializePcr(Cpu,
Pcr,
Idt,
Gdt,
Tss,
InitialThread,
KiDoubleFaultStack);
.........
/* Clear DR6/7 to cleanup bootloader debugging */
__writefsdword(KPCR_TEB, 0);
__writefsdword(KPCR_DR6, 0);
__writefsdword(KPCR_DR7, 0);
/* Setup the IDT */
KeInitExceptions();
/* Load Ring 3 selectors for DS/ES */
Ke386SetDs(KGDT_R3_DATA | RPL_MASK);
Ke386SetEs(KGDT_R3_DATA | RPL_MASK);
.........
/* Setup CPU-related fields */
__writefsdword(KPCR_NUMBER, Cpu);
__writefsdword(KPCR_SET_MEMBER, 1 << Cpu);
__writefsdword(KPCR_SET_MEMBER_COPY, 1 << Cpu);
__writefsdword(KPCR_PRCB_SET_MEMBER, 1 << Cpu);
/* Initialize the Processor with HAL */
HalInitializeProcessor(Cpu, KeLoaderBlock);
.........
/* Check if this is the boot CPU */
if (!Cpu)
{
/* Initialize debugging system */
KdInitSystem(0, KeLoaderBlock);
/* Check for break-in */
if (KdPollBreakIn()) DbgBreakPointWithStatus(1);
}
/* Raise to HIGH_LEVEL */
KfRaiseIrql(HIGH_LEVEL);
.........
/* Switch to new kernel stack and start kernel bootstrapping */
KiSetupStackAndInitializeKernel(&KiInitialProcess.Pcb,
InitialThread,
(PVOID)InitialStack,
(PKPRCB)__readfsdword(KPCR_PRCB),
(CCHAR)Cpu,
KeLoaderBlock);
}
这代码片断的份量够重,其实根本无需我做太多解释,细看看函数的名字就可以看出它们的意义 —— 初始化 Processors、TSS、Exceptions(IDT)、KdInitSystem (内核调试引擎 ^_^),提升IRQL、调用 KiInitializeKernel()、然后再降IRQL,直到 KiIdleLoop()执行完,KiSystemStartup() 函数最终退化为 Idle 进程,这大致就是它的生命周期了。
=======================================
[4] : KiSetupStackAndInitializeKernel()
=======================================
刚才提到的 KiInitializeKernel() 关键函数、以及 KiSystemStartup() 函数功成名就后退化为 Idle 进程的实现秘密全在函数 KiSetupStackAndInitializeKernel()里,这个不瞧瞧怎么行。 ; )
代码 / 代码片断如下: [KiSetupStackAndInitializeKernel@..\ntoskrnl\ke\i386\boot.S]
.globl _KiSetupStackAndInitializeKernel@ 24
.func KiSetupStackAndInitializeKernel@ 24
_KiSetupStackAndInitializeKernel@ 24:
...........
/* Copy all parameters to the new stack */
push [esi+24]
push [esi+20]
push [esi+16]
push [esi+12]
push [esi+8]
push [esi+4]
xor ebp, ebp
call _KiInitializeKernel@ 24
/* Set the priority of this thread to 0 */
mov ebx, PCR[KPCR_CURRENT_THREAD]
mov byte ptr [ebx+KTHREAD_PRIORITY], 0
/* Force interrupts enabled and lower IRQL back to DISPATCH_LEVEL */
sti
mov ecx, DISPATCH_LEVEL
call @KfLowerIrql@4
/* Set the right wait IRQL */
mov byte ptr [ebx+KTHREAD_WAIT_IRQL], DISPATCH_LEVEL;
/* Jump into the idle loop */
jmp @KiIdleLoop@0
.endfunc
很好,流程很清晰! 下面的继续~
========================
[5] KiInitializeKernel()
========================
从上面 KiSetupStackAndInitializeKernel() 汇编函数里的 6 个压栈,我们就能把 KiInitializeKernel() 函数参数猜个一二了。但是 KiInitializeKernel() 是一个非常庞大的大家伙,Idel 进程就是由这个函数创建的,同时它也调用了执行体的初始化函数入口 —— ExpInitializeExecutive()。此时,各个函数的重要性已经不分彼此了...
代码 / 代码片断如下: [KiInitializeKernel@..\ntoskrnl\ke\i386\kiinit.c]
VOID
NTAPI
KiInitializeKernel(IN PKPROCESS InitProcess,
IN PKTHREAD InitThread,
IN PVOID IdleStack,
IN PKPRCB Prcb,
IN CCHAR Number,
IN PLOADER_PARAMETER_BLOCK LoaderBlock)
{
/* Initialize portable parts of the OS */
KiInitSystem();
/* Initialize the Idle Process and the Process Listhead */
InitializeListHead(&KiProcessListHead);
PageDirectory.QuadPart = 0;
KeInitializeProcess(InitProcess,
0,
0xFFFFFFFF,
&PageDirectory,
FALSE);
..........
/* Setup the Idle Thread */
KeInitializeThread(InitProcess,
InitThread,
NULL,
NULL,
NULL,
NULL,
NULL,
IdleStack);
InitThread->NextProcessor = Number;
InitThread->Priority = HIGH_PRIORITY;
InitThread->State = Running;
InitThread->Affinity = 1 << Number;
InitThread->WaitIrql = DISPATCH_LEVEL;
InitProcess->ActiveProcessors = 1 << Number;
..........
/* Set basic CPU Features that user mode can read */
SharedUserData->ProcessorFeatures[PF_MMX_INSTRUCTIONS_AVAILABLE] =
(KeFeatureBits & KF_MMX) ? TRUE: FALSE;
SharedUserData->ProcessorFeatures[PF_COMPARE_EXCHANGE_DOUBLE] =
(KeFeatureBits & KF_CMPXCHG8B) ? TRUE: FALSE;
SharedUserData->ProcessorFeatures[PF_XMMI_INSTRUCTIONS_AVAILABLE] =
((KeFeatureBits & KF_FXSR) && (KeFeatureBits & KF_XMMI)) ? TRUE: FALSE;
SharedUserData->ProcessorFeatures[PF_XMMI64_INSTRUCTIONS_AVAILABLE] =
((KeFeatureBits & KF_FXSR) && (KeFeatureBits & KF_XMMI64)) ? TRUE: FALSE;
SharedUserData->ProcessorFeatures[PF_3DNOW_INSTRUCTIONS_AVAILABLE] =
(KeFeatureBits & KF_3DNOW) ? TRUE: FALSE;
SharedUserData->ProcessorFeatures[PF_RDTSC_INSTRUCTION_AVAILABLE] =
(KeFeatureBits & KF_RDTSC) ? TRUE: FALSE;
/* Set up the thread-related fields in the PRCB */
Prcb->CurrentThread = InitThread;
Prcb->NextThread = NULL;
Prcb->IdleThread = InitThread;
/* Initialize the Kernel Executive */
ExpInitializeExecutive(Number, LoaderBlock);
..........
}
简单的列了一些代码,类似于 Idle 进程、线程创建与初始化;SharedUserData 那个数据结构(后面 SYSENTER 要打下交道,所以这里列一下);还有就是关键的 ExpInitializeExecutive() 函数调用。
============================
[6] ExpInitializeExecutive()
============================
如果上面的 KiSetupStackAndInitializeKernel() 函数还是 Stub 的话,那么 ExpInitializeExecutive() 函数则是正式的执行体初始化过程,代码里可以很清晰的看到所谓的 phase 0 和 phase 1 两个步骤。而 phase 1 步骤就是 Windows XP 启动时有 logo 闪动,时间废的最长的那个。话不多言,接代码!
代码 / 代码片断如下: [ExpInitializeExecutive@..\ntoskrnl\ex\init.c]
VOID
NTAPI
ExpInitializeExecutive(IN ULONG Cpu,
IN PLOADER_PARAMETER_BLOCK LoaderBlock)
{
...........
/* Set phase to 0 */
ExpInitializationPhase = 0;
...........
/* Setup NT System Root Path */
sprintf(Buffer, "C:%s", LoaderBlock->NtBootPathName);
...........
/* Initialize the executive at phase 0 */
if (!ExInitSystem()) KEBUGCHECK(PHASE0_INITIALIZATION_FAILED);
/* Initialize the memory manager at phase 0 */
if (!MmInitSystem(0, LoaderBlock)) KeBugCheck(PHASE0_INITIALIZATION_FAILED);
...........
/* Create the Basic Object Manager Types to allow new Object Types */
if (!ObInit()) KEBUGCHECK(OBJECT_INITIALIZATION_FAILED);
/* Load basic Security for other Managers */
if (!SeInit()) KEBUGCHECK(SECURITY_INITIALIZATION_FAILED);
/* Initialize the Process Manager */
if (!PsInitSystem(LoaderBlock)) KEBUGCHECK(PROCESS_INITIALIZATION_FAILED);
/* Initialize the PnP Manager */
if (!PpInitSystem()) KEBUGCHECK(PP0_INITIALIZATION_FAILED);
/* Initialize the User-Mode Debugging Subsystem */
DbgkInitialize();
...........
}
.: 1 :. 随便挑了一些代码片断,这种代码要是不去读一读,我想睡觉都会睡不踏实的~ 我现在很感兴趣的是 Device Tree 的 PnP 视图模式,驱网上 Tiamo 前辈提出的“总线是如何枚举驱动的”的确是一件很有意思的事情 —— 我想答案可以在上面代码的这里找找 —— PpInitSystem()。 ^_^
.: 2 :. 然后看看代码这里:
#if DBG
/* On checked builds, allocate the system call count table */
KeServiceDescriptorTable[0].Count =
ExAllocatePoolWithTag(NonPagedPool,
KiServiceLimit * sizeof(ULONG),
TAG('C', 'a', 'l', 'l'));
/* Use it for the shadow table too */
KeServiceDescriptorTableShadow[0].Count = KeServiceDescriptorTable[0].Count;
/* Make sure allocation succeeded */
if (KeServiceDescriptorTable[0].Count)
{
/* Zero the call counts to 0 */
RtlZeroMemory(KeServiceDescriptorTable[0].Count,
KiServiceLimit * sizeof(ULONG));
}
#endif
它解释了一般的 Free Build 系统上 Count 域为 0 的原因 —— #if DBG
kd> dd KeServiceDescriptorTableShadow
8055a640 804e36a8 00000000 0000011c 80513eb8 ; KeServiceDescriptorTableShadow
8055a650 bf999280 00000000 0000029b bf999f90 ; W32pServiceTable
8055a660 00000000 00000000 00000000 00000000
8055a670 00000000 00000000 00000000 00000000
8055a680 804e36a8 00000000 0000011c 80513eb8 ; KeServiceDescriptorTable
8055a690 00000000 00000000 00000000 00000000
8055a6a0 00000000 00000000 00000000 00000000
8055a6b0 00000000 00000000 00000000 00000000
.: 3 :. 最后值得一说的是这里: -*> NOTE <*-
/* Set system ranges */
SharedUserData->Reserved1 = (ULONG_PTR)MmHighestUserAddress;
SharedUserData->Reserved3 = (ULONG_PTR)MmSystemRangeStart;
各位看到了吗? Alex Ionescu 连 _KUSER_SHARED_DATA 数据结构的保留位都知道怎么初始化,这实力强悍的直让我想说脏话... PS : Alex Ionescu 貌似比我小 2 岁,1986 年生人!差距太可怕...
==================
[7] PsInitSystem()
==================
还是先回到正道上来,看看 PsInitSystem() 这个函数。您可能会好奇,为什么接着往下追踪是 Ps 系列的函数,原因是 Ntoskrnl 在完成 Idle 进程之后,接下去创建了 system 进线程,phase 1 初始化过程就是在这里被调度的(当然,这里有降 IRQL 的问题)。
代码 / 代码片断如下: [PsInitSystem@..\ntoskrnl\ps\psmgr.c]
BOOLEAN
NTAPI
PsInitSystem(IN PLOADER_PARAMETER_BLOCK LoaderBlock)
{
/* Check the initialization phase */
switch (ExpInitializationPhase)
{
case 0:
/* Do Phase 0 */
return PspInitPhase0(LoaderBlock);
case 1:
/* Do Phase 1 */
return PspInitPhase1();
default:
/* Don't know any other phase! Bugcheck! */
KeBugCheckEx(UNEXPECTED_INITIALIZATION_CALL,
1,
ExpInitializationPhase,
0,
0);
return FALSE;
}
}
Good!非常清晰的两个阶段,请注意这里特指的是 PS 部分的初始化,其中 ExpInitializationPhase 常量是全局的。
===================
[7] PspInitPhase0()
===================
下面让我们细看看进程初始化的第 0 阶段 —— PspInitPhase0()。
代码 / 代码片断如下: [PspInitPhase0@..\ntoskrnl\ps\psmgr.c]
BOOLEAN
NTAPI
PspInitPhase0(IN PLOADER_PARAMETER_BLOCK LoaderBlock)
{
.........
/* Setup callbacks */
for (i = 0; i < PSP_MAX_CREATE_THREAD_NOTIFY; i++)
{
ExInitializeCallBack(&PspThreadNotifyRoutine[i]);
}
for (i = 0; i < PSP_MAX_CREATE_PROCESS_NOTIFY; i++)
{
ExInitializeCallBack(&PspProcessNotifyRoutine[i]);
}
for (i = 0; i < PSP_MAX_LOAD_IMAGE_NOTIFY; i++)
{
ExInitializeCallBack(&PspLoadImageNotifyRoutine[i]);
}
.........
/* Create the Initial System Process */
Status = PspCreateProcess(&PspInitialSystemProcessHandle,
PROCESS_ALL_ACCESS,
&ObjectAttributes,
0,
FALSE,
0,
0,
0,
FALSE);
if (!NT_SUCCESS(Status)) return FALSE;
.........
/* Copy the process names */
strcpy(PsIdleProcess->ImageFileName, "Idle");
strcpy(PsInitialSystemProcess->ImageFileName, "System");
.........
/* Setup the system initialization thread */
Status = PsCreateSystemThread(&SysThreadHandle,
THREAD_ALL_ACCESS,
&ObjectAttributes,
0,
NULL,
Phase1Initialization,
LoaderBlock);
if (!NT_SUCCESS(Status)) return FALSE;
.........
}
那些 NotifyRoutine 的回调的意义就不多说了 ; ) ,主要是 System Process 和 Thread 的创建是在这里完成的。真正的关键点在上面那个红色的 Phase1Initialization,这意味着函数 Phase1Initialization() 是线程 system 的入口。不过这个线程目前是得不到时间片的 —— 回忆一下前面的代码 —— KiSystemStartup() 函数是将中断级提升到 HIGH_LEVEL 后再调用 KiInitializeKernel() 函数的。想得到调度的前提必须是等 KiInitializeKernel() 执行完,IRQL 降到 DISPATCH_LEVEL 才行。
==========================
[8] Phase1Initialization()
==========================
【未完,1/2都不到,作者吃饭中】
【累死我了,写随笔居然比看代码、调试还累】