购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.1 初始化调试系统

系统内核和外围设备是通过端口进行通信的。在系统和外围设备通信之前,需要对通信的端口进行初始化,这些工作在调试系统初始化过程中完成。这个过程也包括设置中断描述符表和初始化调试进程、线程结构。内核在两处初始化调试系统,分为内核初始化阶段0和内核初始化阶段1。现在我们把两处初始化阶段前移,把原属于内核初始化阶段0的移到Loader模块,把原属于内核初始化阶段1的移到原属于内核初始化阶段0的地方。KdpInitSystem函数的第一个参数BootPhase有两个可选值(0或1),表明当前处在哪一初始化阶段。全局变量KdpBootPhase保存了当前调试系统是在哪个阶段(0或1)。为了不修改太多代码,我们可以使用LoaderBlock来判断在哪一个阶段,如下列代码所示。

Loader模块入口函数NtProcessStartup第一行代码就调用KdpInitSystem(0,NULL)来初始化调试系统。第二个参数LoaderBlock在Loader模块设为NULL。第二阶段的调试系统初始化在内核下进行,第二个参数LoaderBlock指向一个LOADER_PARAMETER_BLOCK结构,这个结构的主要作用是取得内核映像基址,通过CONTAINING_RECORD宏把内核映像基址保存在PsNtosImageBase全局变量中,以便远程调试器使用其值。

KdpInitSystem函数的大部分代码主要是为了填充调试器版本块(KdVersionBlock)和调试器数据块(KdDebuggerDataBlock)两个结构,它们是全局变量。这两个数据结构将来要被远程调试器(WinDbg)使用。结构的很多字段可以在定义全局变量时设置,没必要放在函数内,否则会占用比较多的代码空间。笔者之所以特意把这些字段放在函数里设置,是因为这些字段比较重要,远程调试器将依赖这些结构字段来解析调试命令。调试器数据块KdDebuggerDataBlock使用的是32位版本,不存在成员NtBuildLab,NtBuildLab是在NT 5.1版本以后才增加的。WRK使用的是64版本KdDebuggerDataBlock,所以存在NtBuildLab,在WRK第一次中断到调试器时,命令窗口显示:

这里的3800.WRKP1.2(daveprobert)就是从NtBuildLab字段而来的。

KdDebuggerDataBlock结构的成员MmPageSize也非常重要,它表明系统使用的页面内存大小。一般按4KB或4MB(大页面)来划分一页的内存大小,我们默认使用4KB,所以设置此值等于PAGE_SIZE。如果不设置会有什么情况发生呢?

KdpInitSystem函数最后调用CpInitialize初始化通信端口,并设置KdDebuggerEnabled为TRUE,这样就完成了KD调试系统的初始化工作。

在Loader模块执行阶段,像0xffdff000这样的地址不可能访问。如下所示重要结构KIPCR是系统进入内核初始化阶段1之后列出的,调试系统需要使用这个结构存储数据。kd>dt nt!_KIPCR 0xffdff000

KIPCR有时也用KPCR表示,是内核处理器控制域(kernel processor control region)的缩写。KIPCR结构下来是PrcbData,它们紧靠在一起。KIPCR.Prcb指针指向KIPCR.PrcbData,KPCR结构成员的设置主要是在函数BlSetupFotNt(详见第4章),所以在未初始化PCR时不可能访问ffdff000。但在调试系统中需要用到PrcbData结构,所以定义了KdpKeGetCur-rentPrcb内联函数,它是从KeGetCurrentPrcb修改而来的,用来获取当前处理器域控制块(PrcbData)。在进入内核后KdpKeGetCurrentPrcb向KeGetCurrentPrcb兼容,这是由全局变量KdpBootPhase的值来决定的。

__readfsdword(FIELD_OFFSET(KPCR,Prcb))对应汇编mov eax,dword ptr fs:[00000020h],段寄存器fs选择子0x30,fs:[120]指向PrcbData,fs:[0]指向虚拟地址0xffdff000。KdpKeGetCur-rentPrcb只能用于调试系统内部使用,内核下使用KeGetCurrentPrcb,用途其实无差别,只是多一个对变量KdpBootPhase的判断。

内核虚拟地址0xffdff000和用户虚拟地址0x7ffe0000共享同一个物理页,在内核态KIPCR.Used_ExceptionList指向内核异常注册记录链表;在用户态KIPCR.NtTib.ExceptionList指向用户线程异常注册记录链表。

查看当前线程结构:

查看当前进程结构:

EPROCESS.Pcb首个成员是KPROCESS结构,如果找到进程的Pcb也就找到了进程的EPROCESS。

每个处理器用一个KPRCB结构存储有关信息。Windows定义处理器个数最多是32。用一个全局变量数组保存每一个处理器的结构。

我们为什么要定义这些全局结构呢?PCR.Prcb不是指向0xffdff120的吗?要知道,我们在Loader中首次初始化调试系统(BootPhase=0)时,还没有开启分页机制,这个高端内核地址0xffdff000(应该叫虚拟内存地址)并没有和物理页面建立映射,是不能访问的。但调试系统又要访问Prcb,如KiDispatchException函数调用KdpKeGetCurrentPrcb()来获得Prcb,怎么办呢?

首先要理清楚,访问的目的是什么,就是为了存储数据。为了存储数据,开辟全局内存空间KiProcessorBlock来代替Prcb也是可以的。进程结构KdDebugProcess是用来保存进程相关信息的,而线程结构KdDebugThread是用来保存线程相关信息的。对单核处理器来说,计算机开机运行后,就跟随着指令流程执行,这时还没有进程的概念。Loader还没有划分进程和线程,但是调试系统的设计就确定了进程和线程的存在。为了保证调试系统在阶段0也可以使用进程和线程来保存或设置相关的值,kd模块不想重复编写且又要向后兼容,所以引入了全局结构变量来表示进程、线程。

在内核初始化阶段0中,Idle进程、线程也是以全局结构的形式初始化。如果BootPhase等于1,KdpBootPhase也等于1,说明PCR已经建立,kd模块代码会根据KdpBootPhase的值找到当前进程、线程结构,这个时侯便是Idle进程、Idle线程了。在Loader阶段,调试系统进程是kd模块定义的全局结构KdDebugProcess。进入内核后,在内核初始化阶段0,调试系统进程是Idle。在内核初始化阶段1,调试系统进程是System。 4N8MMdkMdf/w5xtAGk8qxnEt2MlufoLGoiXSx8/6JaHWgm7/YHs6Is89dMFHtQfu

点击中间区域
呼出菜单
上一章
目录
下一章
×