This document is for Windows NT v3.1 to Server 2003, x86 architecture. Alpha, Mips, and PowerPC is not documented in this article :).
NT Startup begins with the bootcode itself, we’ll take a look at the FAT version.
During initial system startup, the BIOS scans for devices to boot from, now, the Hard Disk is one of them (if it is installed), if it is installed, we call the bootcode for it by loading the first 512 bytes into RAM, this is the bootsector. The bootsector or bootcode scans for NTLDR in the filesystem file table. NTLDR loads itself with the following register contents when one uses FAT16/FAT12:
BX => Starting cluster number of Nt loader DL => Int 13h drive number DS:SI => Boot media BPB BS:DI => argument structure 1000:0000 => entire FAT table
Then, we read NTLDR and execute a far jump to 2000:0003, where NTLDR starts. NTLDR is formally called with prototype:
[c]VOID
NtProcessStartup(
IN PBOOT_CONTEXT BootContextRecord
);[/c]
Then, we have a hacky subroutine known as BlStartup, which fools the ARC NT loader into thinking that we’re actually running on a ARC system. We call it with parameters:
load osloader=\System32\NTLDR systempartition=%s osloadfilename=%s osloadpartition=%s osloadoptions=%s consoleinputname=multi(0)key(0)keyboard(0) consoleout=multi(0)video(0)monitor(0) x86systempartition=%s
NTLDR then loads its own configuration subsystem, and memory manager and creates the initial kernel loader block structure.
[c]typedef struct _LOADER_PARAMETER_BLOCK
{
LIST_ENTRY LoadOrderListHead;
LIST_ENTRY MemoryDescriptorListHead;
LIST_ENTRY BootDriverListHead;
ULONG KernelStack;
ULONG Prcb;
ULONG Process;
ULONG Thread;
ULONG RegistryLength;
PVOID RegistryBase;
PCONFIGURATION_COMPONENT_DATA ConfigurationRoot;
CHAR * ArcBootDeviceName;
CHAR * ArcHalDeviceName;
CHAR * NtBootPathName;
CHAR * NtHalPathName;
CHAR * LoadOptions;
PNLS_DATA_BLOCK NlsData;
PARC_DISK_INFORMATION ArcDiskInformation;
PVOID OemFontFile;
_SETUP_LOADER_BLOCK * SetupLoaderBlock;
PLOADER_PARAMETER_EXTENSION Extension;
BYTE u[12];
FIRMWARE_INFORMATION_LOADER_BLOCK FirmwareInformation;
} LOADER_PARAMETER_BLOCK, *PLOADER_PARAMETER_BLOCK;[/c]
This structure gets passed to the kernel after NTLDR loads all device driver PE objects into RAM, then we call SystemInitRoutine, which has prototype:
[c]typedef
VOID
(*PTRANSFER_ROUTINE) (
PLOADER_PARAMETER_BLOCK LoaderBlock
);[/c]
Which, in reality is _KiSystemStartup@24 in ntoskrnl. Ntoskrnl then reserves its own memory space and gets processor number and checks if the current processor is the boot processor. Next, we initialize the PCR (processor control region), another large structure:
[c]typedef struct _KPCR
{
union
{
NT_TIB NtTib;
struct
{
PEXCEPTION_REGISTRATION_RECORD Used_ExceptionList;
PVOID Used_StackBase;
PVOID Spare2;
PVOID TssCopy;
ULONG ContextSwitches;
ULONG SetMemberCopy;
PVOID Used_Self;
};
};
PKPCR SelfPcr;
PKPRCB Prcb;
UCHAR Irql;
ULONG IRR;
ULONG IrrActive;
ULONG IDR;
PVOID KdVersionBlock;
PKIDTENTRY IDT;
PKGDTENTRY GDT;
PKTSS TSS;
WORD MajorVersion;
WORD MinorVersion;
ULONG SetMember;
ULONG StallScaleFactor;
UCHAR SpareUnused;
UCHAR Number;
UCHAR Spare0;
UCHAR SecondLevelCacheAssociativity;
ULONG VdmAlert;
ULONG KernelReserved[14];
ULONG SecondLevelCacheSize;
ULONG HalReserved[16];
ULONG InterruptMode;
UCHAR Spare1;
ULONG KernelReserved2[17];
KPRCB PrcbData;
} KPCR, *PKPCR;[/c]
After this, Ntoskrnl initializes TSS structure, and then setup the NMI handler to handle NMI faults if raised by the hardware. After this, Ntoskrnl initializes the interprocessor interrupt vector and the processor count value to enable the kernel debugger subsystem. Then ntoskrnl calls KiInitializeKernel with prototype:
[c]VOID
KiInitializeKernel (
IN PKPROCESS Process,
IN PKTHREAD Thread,
IN PVOID IdleStack,
IN PKPRCB Prcb,
IN CCHAR Number,
PLOADER_PARAMETER_BLOCK LoaderBlock
);[/c]
This function gains control of the system after the bootstrap code was called, this function also initializes more core system structures, idle thread and process objects and the PCRB structure. After this, we call ExpInitializeExecutive to perform phase 0 initialization of the kernel subsystems, it has prototype:
[c]VOID
ExpInitializeExecutive(
IN ULONG Number,
IN PLOADER_PARAMETER_BLOCK LoaderBlock
);[/c]
The kernel then enables interrupts for x86, and initializes the translation tables by using the images that Ntldr passed to the kernel. ExpInitializeExecutive then initializes the crypto key exponent. Internal kernels can be built with a different key exponent, but it is always set to 0 when the kernels leave Microsoft. Then the kernel reinitializes hal.dll, and loads symbols for each driver (symbol loading in this stage was deprecated in Windows 2000). Then we retrieve the system control values (CmNtGlobalFlag) out of the registry and we do a logical OR on NtGlobalFlag with CmNtGlobalFlag’s values. Mm or Memory manager is then initialized along with page burning (/BURNMEMORY or /MAXMEM). Now, we initialize bootvid/Inbv in Windows 2000 or later, and we display system version on the console. After this, we initialize the Object Manager (ObInitSystem), Security subsystem (SeInitSystem) and process manager (PsInitSystem). These subsystems form the core of the NT kernel’s base functionality, along with Rtl or runtime library. Then, we initialize the plug and play subsystem (PpInitSystem). PsInitSystem creates a new thread using PsCreateSystemThread:
[c] //
// Phase 1 System initialization
//
if ( !NT_SUCCESS(PsCreateSystemThread(
&ThreadHandle,
THREAD_ALL_ACCESS,
&ObjectAttributes,
0L,
NULL,
Phase1Initialization,
(PVOID)LoaderBlock
)) ) {
return FALSE;
}[/c]
This thread controls phase 1 initialization. Phase 1 reinitializes Hal, Po, Ob, Se subsystems along with Executive (Ex), and kernel phase 1 (Ke) subsystems. We also start all non-boot processors in the SMP kernel (Ntkrnlmp) by calling KeStartAllProcessors();, processor licensing is also enforced.
[c] if ( KeLicensedProcessors ) {
if ( KeRegisteredProcessors > KeLicensedProcessors ) {
KeRegisteredProcessors = KeLicensedProcessors;
}
}[/c]
Ntoskrnl then creates the symbolic link to \SystemRoot and maps the System DLL (ntdll.dll) into userspace and loads it after Lpc subsystem and I/O subsystem are initialized. We then null out the loaderblock so that drivers can’t get ahold of it anymore ;). Then we execute smss.exe from %SystemRoot%\System32\smss.exe to start the rest of the user-mode subsystems such as Win32.
We leave kernel mode once the thread has started, and you see the windows desktop/windowstation in a matter of seconds after win32k/csrss is loaded. 🙂