NetRoc/cc682
前段时间和yuewang和一块三毛钱商量着写写Windows分析的文章,我来开个头吧,哈哈。既然是开头,所以就选择了内核入口点开始,我向来不怎么会写文章,也就当流水账记记吧,看能不能引出他们更好的分析出来J
Ntoskrnl的入口点函数名是KiSystemStartup,这是bootloader执行了一些基本的初始化之后跳转到的内核入口函数,用汇编语言实现。
一、KiSystemStartup功能介绍
KiSystemStartup第一次运行于processor 0,主要是初始化一些系统硬件状态,调用一些系统初始化过程,然后就进入调度程序,开始系统调度过程。而对于其他processor,初始化的时候也是进入KiSystemStartup,但是做的工作有所区别而已。
二、Processor 0(以后简称P0)开始执行KiSystemStartup时的系统环境
这个运行环境是由bootloader准备好的:
1、 一个精简版的IDT环境,从0到0x1F号中断已经被准备好
2、 一个完整的GDT被初始化出来并且Load。
3、 完整的TSS被初始化并且Load。
4、 页面映射经过了基本的初始化,并且设置好了初始化所需的最少的页面。虚拟内存的最低4M被直接映射到物理内存中。
5、 ntoskrnl.exe被装载到它内存描述符中的地址。也即编译时确定的基地址。
6、 DS=ES=SS,ESP指向一个可用的栈中。
7、 中断被关闭。
三、其他Processor开始执行KiSystemStartup的环境
IDT, GDT, TSS, stack, selectors, PCR全部初始化完成并可用,页表设置为当前运行的页表(这一点偶也不太明白,可能还需要看看以后的代码才能理解),具备一个LoaderBlock,作为在该处理器上执行KiSystemStartup的参数。
四、大致流程
1、 KiSystemStartup将参数KissLoaderBlock放到全局变量_KeLoaderBlock中
2、 取出_KeNumberProcessors,并判断是否是0。_KeNumberProcessors保存了系统中的处理器数目,这个变量被初始化为0,所以当Ntoskrnl开始执行时,这个变量还没有被填充。因此判断_KeNumberProcessors是否是0,就可以知道当前是不是第一次执行KiSystemStartup。
3、 如果是P0,会将_KiInitialThread和P0BootStack的地址分别保存 到_KeLoaderBlock中的对应字段中。_KiInitialThread是系统启动之后的初始线程,而P0BootStack应该是初始化时临时使用的内核堆栈,定义为db KERNEL_STACK_SIZE dup (?)。KERNEL_STACK_SIZE在i386中是0x3000,在AMD64中是0x6000。然后会设置fs为0x30,这是内核_KPCR结构的在GDT中序号。最后,会将处理器序号,也就是0,保存到_KPCR中对应位置,这个位置在i386和AMD64中也是不同的。
4、 下面又是所有处理器都会执行的代码了,设置初始线程的_ETHREAD:: Tcb:: ApcState:: ApcListHead[0],将_LIST_ENTRY的Flink和Blink都设置为自身。
5、 调用_KiInitializeMachineType过程,会设置一下机器类型。不过这里做得很简单,这个函数可能在未来也会有较大更改。主要的机器类型信息可能包含了总线类型、CPU大致的系列等简单信息。
6、 然后又判断是否是P0,如果不是,会跳过一大段初始化代码。
7、 在P0的情况下,调用GetMachineBootPointers函数,获取由bootloader初始化过的一些信息。从这个函数返回后,edi中保存gdt基地址,esi中保存pcr基地址,edx保存tss基地址,eax保存idt基地址。KiSystemStartup接下来会将这些值保存到自己的局部变量中使用。
8、 Bootloader初始化的TSS是16位的,KiSystemStartup在这里会将它的标志改为32位,然后连续调用_KiInitializeTSS2和_KiInitializeTSS初始化TSS。KiInitializeTSS2初始化了内核TSS结构在GDT中描述符的界限大小,以及初始化IOPM的相关结构。_KiInitializeTSS在TSS中首先设置不使用IOPM,然后设置Tss->Flags = 0,将EFLAGS清空。最后将LDT和ss0都设置为0。设置完成后,重新装载TR寄存器。
9、 接下来设置了double fault task gate。这里会设置IDT中的08号中断,设置成了一个任务门,并填充相应的TSS结构,是用于#DF异常时的。
10、 设置用于NMI fault 的task gate,设置IDT的02号中断。这是用于不可屏蔽中断的中断号。同样也调用_KiInitializeTSS填充了另外一个TSS结构。上面两条详细的原理参考Intel手册关于IDT和task gate的描述。
11、 调用_KiInitializePcr初始化了当前的pcr。
12、 将初始进程的_EPROCESS地址,即_KiInitialProcess的地址设置到了初始线程的_ETHREAD:: Tcb:: ApcState:: Process中。
13、 设置PCR->Teb = 0。
14、 设置PCR->PrcbData.ProcessorState.SpecialRegisters.KernelDr6和PCR->PrcbData.ProcessorState.SpecialRegisters.KernelDr7为0。这里是为了初始化内核调试器相关的东西,具体作用可能只有分析到相关代码才能知道了。
15、 调用_KiSwapIDT转换IDT描述符的格式。IDTENTRY定义如下:
typedef struct tagIDTENTRY
{
unsigned short OffsetLow;
unsigned short Selector;
unsigned char Reserved;
unsigned char Type:4;
unsigned char Always0:1;
unsigned char Dpl:2;
unsigned char Present:1;
unsigned short OffsetHigh;
} IDTENTRY, *PIDTENTRY;
这个函数将ntoskrnl定义的IDT表项数组中,选择子的Selector和OffsetHigh字段对换。这里估计是在初始化这些表项的时候,为了方便直接将处理代码的地址填到了&OffsetLow中,所以Selector保存了高位的地址,然后到后面来统一替换。详细的原理参见Intel手册。
16、 将ds和es的值设置为0x23,也就是Ring3下的ds和es值。
17、 将ntoskrnl中_IDT数组的内容复制到当前的IDT表中。前面设置的double fault和 nmi fault的表项不会被覆盖掉,而是使用新设置的内容。
18、 接下来又是所有处理器都会执行的操作了。调用_KiProcessorStart初始化处理器。这个函数会根据KiProcessorStartControl的不同值进行不同的操作,例如获取一些处理器信息、启动或者停止处理器等等。由于P0已经不需要初始化了,所以在P0阶段这个函数直接返回。
19、 获取_KiFreezeExecutionLock这个锁,用于修改一些和处理器相关的全局资源。主要是_KPCR里面的处理器相关的信息。然后调用了_HalInitializeProcessor函数,初始化该处理器的IDT。估计这个函数会继续为每个处理器调用KiSystemStartup函数。不过没能确认。
20、 将IRQL的信息保存下来。这是hal由参数传过来的。
21、 在_KeActiveProcessors中设置初始化完成的处理器MASK。
22、 调用_KiInitializeAbios初始化ABIOS结构。这里的详细原理就不太清楚了,因为没能分析过相关部分。
23、 将_KeNumberProcessors加1,增加已初始化完成的处理器数量。然后就会释放掉_KiFreezeExecutionLock锁了。
24、 接下来调用_KdInitSystem函数。这里应该会初始化内核调试器。只在P0上调用。
25、 后面将会初始化内核了,首先会将IRQL提升到HIGH_LEVEL,并初始化调用内核初始化函数使用的寄存器,包括传递参数的eax,ebx,edx,以及用于堆栈访问的esp和ebp。然后就调用_KiInitializeKernel进行内核初始化。这个函数相当复杂,也够一篇文章,这里就不写了。呵呵
26、 出来之后设置idle thread的优先级为0,开中断,降低IRQL到DISPATCH_LEVEL。然后检查并等待_KiBarrierWait这个锁。对P0来说,由于_KiBarrierWait初始化为0,所以直接就跳到idle线程了,其他处理器会一直等待_KiBarrierWait,直到允许他们运行。
27、 最后通过一个长跳转到KiIdleLoop函数,开始系统的处理和调度,整个系统初始化过程就完成了。
五、后记
唉,真的写起来才发现文章不好写啊。自己再看的时候都感觉不清不楚的,呵呵。不过暂时就这样吧J