Windows 内核技术详解笔记

说明

本文结合《Windows 内核情景分析》(毛德操著)、《软件调试》(张银奎著)、《Windows 核心编程》、《寒江独钓-Windows 内核安全编程》、《Windows PE 权威指南》、《C++反汇编与逆向分析揭秘》以及 ReactOS 操作系 统 (V0.3.12)源码,以《Windows 内核情景分析》为蓝本,对 Windows 内核重要框架、函数、结构体进行 解析

由于工程庞大,我能理解到的只是冰山一角,但本文力求做到让每个读者都能从整体上理解 Windows 内核 的架构,并大量解释一些关键细节。

本文适合读者:熟悉 C 语言、汇编,PE 文件格式,有一定驱动/内核程序开发经历的读者 本文阅读顺序:基础较弱的读者请遵循篇章序号,否则可能会吃力。 本文解读方式:

  • 1、源码、伪码结合,展示主流程,很多时候忽略权限、错误检查,多线程互斥等旁枝末节
  • 2、函数的参数没有严格排序,很多不重要的参数也省略了,要注意
  • 3、结构体内的成员没有严格排序,成员名称也不严格对应,并只列出一些重要成员
  • 4、一些清理工作,如关闭句柄、释放内存、释放互斥对象等工作省略
  • 5、很多时候,函数体开头声明的那些没有初始值的局部变量我都略去了

(我所做的修改基本不影响从代码层次理解 Windows 内核的原理)

  • 写作初衷 1: 我一直对 Rootkit 感兴趣,但是以前在不熟悉内核的情况下,总是不知道要在哪个位置挂钩, 要 Hook 哪些函数才能达到我的目的。
  • 写作初衷 2:以前在写文件系统过滤驱动、Ndis 过滤驱动以及其他驱动时遇到的种种疑惑,因此,总想看 一下 ddk 提供的内核函数到底是怎么实现的。

于是翻看了毛老师的大作,受益匪浅,在基本理清了原理与细节后,特此做了一番总结
希望这篇文章 能够为安全界的朋友尽一点绵薄之力。
由于工作原因我接触到的知识面有限,不可能逐一摸透 Windows 的方方面面,再说ReactOS 本来就与 Windows 有一些小差别
因此希望各位朋友带着批判的态度去阅读本文。(当然我是尽我所能,认真写完逐篇的)


本文术语约定

  • 描述符:指用来描述一件事物的“结构体”。如缓冲描述符,描述了一个缓冲的基址、长度等信息。
  • 中断描述符,描述了那个中断向量对应的分配状态、isr 等信息
  • Entry:指表中的表项、条目,有时也指函数入口
  • SSDT:基本系统服务表(其实全称应叫系统服务派遣表)
  • Shadow - SSDT:GUI/GDI 系统服务函数表,这是第二张 SSDT
  • SSDTDT:系统服务表描述符表,表中每个元素是一个 SSDT 描述符(注意内核中有两张 SSDT 和两张 SSDTDT)
  • IDT:中断描述符表,每个 cpu 一个。(每个表项是一个描述符,可以简单视为 isr)
  • ISR:中断服务例程,IDT 表中的中断描述符所描述的中断处理函数
  • EPR:异常处理例程,IDT 表中的异常描述符所描述的异常处理函数
  • VA:虚拟地址,
  • PA:物理地址,
  • LA:线性地址,
  • RVA:相对虚拟地址
  • foa:文件偏移
  • PDE:页目录中的表项,保存着对应二级页表的物理地址,又叫“二级页表描述符”
  • PTE:二级页表中的表项,真正记录着每个虚拟页面的映射情况以及其他信息,又叫“映射描述符” 页目录:(又叫一级页表、总页表),一个 PDE 数组,这个数组的大小刚好占据一个页面
  • 二级页表:一个 PTE 数组,这个数组的大小也刚好占据一个页面(进程有一个总页表+1024 个二级页表)
  • AREA:地址空间中的一块连续的区段,VirtualAlloc 分配内存都是以区段为单位
  • 内存分配:表示从地址空间中用 VirtualAlloc 预定或者提交映射一块内存,不是指 malloc、new、HeapAlloc
  • PID:进程 ID、进程号。(其实也是个句柄)
  • TID:线程 ID、线程号。(其实也是个句柄)
  • PDO:物理设备对象,相对于 fdo 而言。Pdo 并不一定是最底层的那个硬件 pdo
  • FDO:功能设备对象,相对于 pdo 而言。Fdo 也可能直接访问硬件芯片。fdo 与 pdo 只是一种相对概念。
  • 栈底 pdo:又叫”基石 pdo’,”硬件 pdo’,指用作堆栈基石的那个 pdo,它是由相应的总线驱动内部创建的 。
  • 端口设备对象:端口驱动或者小端口驱动中创建的设备对象(他下面是硬件 pdo)
  • 总线驱动:用来驱动总线的驱动(总线本身也是一种特殊的设备),如 pci.sys 总线驱动
  • 端口驱动:由厂家提供的真正用来直接访问硬件芯片的驱动,位于总线驱动上层
  • 功能驱动:指类驱动。如鼠标类驱动 mouseclass.sys,磁盘类驱动 disk.sys
  • 上层过滤驱动:位于功能类驱动上面的驱动
  • 下层过滤驱动:位于功能驱动下面,端口驱动上面的驱动
  • 顶层驱动:指位于栈顶的驱动
  • 中间驱动:intermediate drivers,凡是夹在顶层驱动与端口驱动之间的那些驱动都叫中间驱动
  • 设备树:由 PnP 管理器构造的一颗用来反映物理总线布局的”硬件设备树’。
  • 设备节点:设备树中的节点。每个节点都表示一个真正的”硬件 pdo”
  • 老式驱动:即 NT 式驱动,指不提供 AddDevice 或通过 NtLoadDriver 加载的驱动
  • WDM 驱动:指提供了 AddDevice 并且不是通过 NtLoadDriver 加载的驱动
  • IRP 派遣例程:又叫分发例程、派遣函数。驱动程序中用来响应处理 irp 的函数。(Dispatch)
  • 设备绑定:指将设备”堆栈’到原栈顶设备上面,成为新的栈顶设备。
  • 文件:指物理介质上的文件(磁盘、光盘、U 盘)
  • 文件对象:每次打开设备时生成一个文件对象(文件对象不是文件,仅仅表示对设备的一次打开上下文,因此文件对象又叫打开者)
  • 套接字驱动:afd.sys 套接字设备:\Device\Afd\Endpoint
  • 套接字文件对象:每打开一次套接字设备生成一个套接字文件对象
  • 套接字FCB:每个套接字文件对象关联的FCB,用来描述套接字的其他信息
  • 地址文件对象:每次打开传输层的tdi设备时生成的一个文件对象,用于套接字绑定
  • 地址对象:传输层中为每个地址文件对象创建一个地址对象,用来描述一个地址(IP、端口号、协议等)
  • Socket irp:发往 afd 套接字设备(即\Device\Afd\Endpoint)的 irp
  • Tdi irp:发往传输层设备(即\Device\Tcp,\Device\Udp,\Device\RawIp)的 irp
  • 物理卷设备:指磁盘卷、光盘卷、磁带卷等物理卷设备,由相应类型的硬件驱动创建 磁盘卷设备:指磁盘分区,设备对象名为\Device\HarddiskN\PartitionN 形式(N 从 0 开始)
  • 文件卷设备:由文件系统内部创建的挂载(即绑定)在物理卷上的匿名设备
  • Cdo:控制设备对象。一个驱动通常创建有一个 cdo,用来与外界通信。
  • FSD:文件系统驱动,File System Driver 缩写。
  • 簇:文件以簇为分配单位。一个文件包含 N 个簇,簇之间不必物理连续,一个簇一般为 4KB
  • 扇区:系统以扇区为单位进行磁盘 IO。一个簇包含 N 个扇区,一个扇区一般为 512B
  • 文件块:磁盘文件中的文件块,对应于内核中的文件缓冲段
  • 缓冲段:文件块在内核中的缓冲
  • ACL:访问控制表。每个 Ntfs 文件、内核对象都有一份 ACL,记录了各用户、组的访问权限
  • Token:访问令牌。每个线程、进程都有一个 Token,记录了包含的特权、用户、组等信息
  • SID:指用户 ID、组 ID、机器 ID,用来唯一标识。
  • 主令牌:进程自己的令牌
  • 客户令牌:也即模拟令牌。每个线程默认使用进程的令牌,但也可模式使用其他进程的令牌




系统调用

Windows 的地址空间分用户模式与内核模式,低2GB 的部分叫用户模式,高2G 的部分叫内核模式,位于用户空间的代码不能访问内核空间,位于内核空间的代码却可以访问用户空间
一个线程的运行状态分内核态与用户态,当指令位于用户空间时,就表示当前处于内核态,当指令位于内核空间时,就处于内核态.
一个线程由用户态进入内核态的途径有3 种典型的方式:

  • 1、 主动通过int 2e(软中断自陷方式)或sysenter 指令(快速系统调用方式)调用系统服务函数,主动进入内核
  • 2、 发生异常,被迫进入内核
  • 3、 发生硬件中断,被迫进入内核

主动进入内核

现在讨论第一种进入内核的方式:(又分为两种方式)

int2e

通过老式的int 2e 指令方式调用系统服务(因为老式cpu 没提供sysenter 指令)

如ReadFile 函数调用系统服务函数NtReadFile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Kernel32.ReadFile() //点号前面表示该函数的所在模块
{
//所有Win32 API 通过NTDLL 中的系统服务存根函数调用系统服务进入内核
NTDLL.NtReadFile();
}
NTDLL.NtReadFile()
{
Mov eax,152 //我们要调用的系统服务函数号,也即SSDT 表中的索引,记录在eax 中
If(cpu 不支持sysenter 指令)
{
Lea edx,[esp+4] //用户空间中的参数区基地址,记录在edx 中
Int 2e //通过该自陷指令方式进入KiSystemService,"调用’对应的系统服务
}
Else
{
Lea edx,[esp +4] //用户空间中的参数区基地址,记录在edx 中
Sysenter //通过sysenter 方式进入KiFastCallEntry,"调用’对应的系统服务
}
Ret 36 //不管是从int 2e 方式还是sysenter 方式,系统调用都会返回到此条指令处
}

Int 2e 的内部实现原理

该指令是一条自陷指令,执行该条指令后,cpu 会自动将当前线程的当前栈切换为本线程的内核栈(栈分
用户栈、内核栈),保存中断现场,也即那5 个寄存器。
然后从该cpu 的中断描述符表(简称IDT)中找到这个2e 中断号对应的函数(也即中断服务例程,简称ISR),jmp 到对应的isr 处继续执行,此时这个ISR
本身就处于内核空间了,当前线程就进入内核空间了

Int 2e 指令可以把它理解为intel 提供的一个内部函数,它内部所做的工作如下

1
2
3
4
5
6
7
8
9
10
11
Int 2e
{
Cli //cpu 一中断,立马自动关中断
Mov esp, TSS.内核栈地址 //切换为内核栈,TSS 中记录了当前线程的内核栈地址
Push SS
Push esp
Push eflags
Push cs
Push eip //这5 项工作保存了中断现场【标志、ip、esp】
Jmp IDT[中断号] //跳转到对应本中断号的isr
}

IDT整体布局

IDT 的整体布局:【异常->空白->5 系->硬】(推荐采用7 字口诀的方式重点记忆)

异常

前20 个表项存放着各个异常的描述符(IDT 表不仅可以放中断描述符,还放置了所有异常的异常处理描述符,0x00-0x13)保留:0x14-0x1F,忽略这块号段

空白

接下来存放一组空闲的保留项(0x20-0x29),供系统和程序员自己分配注册使用

5 系

然后是系统自己注册的5 个预定义的软中断向量(软中断指手动的INT 指令)

(0x2A-0x2E 5 个系统预注册的中断向量

  • 0x2A:KiGetTickCount
  • 0x2B:KiCallbaclReturn
  • 0x2C:KiRaiseAssertion
  • 0x2D:KiDebugService
  • 0x2E:KiSystemService

最后的表项供驱动程序注册硬件中断使用和自定义注册其他软中断使用(0x30-0xFF)

下面是中断号的具体的分配情况
0x00-0x13 固定分配给异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0x00: Divide error(故障)
0x01: Debug (故障或陷阱)
0x02: 保留未用(为非屏蔽中断保留的,NMI)
0x03: breakpoint(陷阱)
0x04: Overflow(陷阱)
0x05: Bounds check(故障)
0x06: Invalid Opcode(故障)
0x07: Device not available(故障)
0x08: Double fault(异常中止)
0x09: Coprocessor segment overrun(异常中止)
0x0A: Invalid TSS(故障)
0x0B: Segment not present(故障)
0x0C: Stack segment(故障)
0x0D: General protection(故障)
0x0E: Page fault(故障)
0x0F: Intel 保留
0x10: Floating point error(故障)
0x11: Alignment check(故障)
0x12: Machine check(异常中止)
0x13: SIMD floating point(故障)

0x14-0x1f:Intel 保留给他公司将来自己使用(OS 和用户都不要试图去使用这个号段,不安全)
———————-以下的号段可用于自由分配给OS、硬件、用户使用———————–

linux 等其他系统是怎么划分这块号段的,不管,我们只看Windows 的情况

1
2
3
0x20-0x29:Windows 没占用,因此这块号段我们也可以自由使用
0x2A-0x2E:Windows 自己本身使用的5 个中断号
0x30-0xFF:Windows 决定把这块剩余的号段让给硬件和用户使用

参见《寒江独钓》一书P93 页注册键盘中断时,搜索空闲未用表项是从0x20 开始,到0x29 结束的
就知道为什么寒江独钓是在这段范围内搜索空白表项了(其实我们也完全可以从0x14 开始搜索)

Windows 系统中,0x30-0xFF 这块号段让给了硬件和用户自己使用。
事实上,这块号段的开头部分默认都是让给硬件IRQ 使用的,也即是分配给硬件IRQ 的。
IRQ N 默认映射到中断号0x30+N,如IRQ0 用于系统时钟,
系统时钟中断号默认对应就是0x30。当然程序员也可以修改APIC(可编程中断控制器)将IRQ 映射到自定义的中断号。

IRQ 对外部设备分配,但IRQ0,IRQ2,IRQ13 必须如下分配:

  • IRQ0 —->间隔定时设备
  • IRQ2 —->8259A 芯片
  • IRQ13 —->外部数学协处理器
    其余的IRQ 可以任意分配给外部设备。
    虽然一个IRQ 只对应一个中断号,但是由于IRQ 数量有限,而设备种类成千上万,因此多个设备可以使用
    同一个IRQ,进而,多个设备可以分配同一个中断号。因此,一个中断号可以共享给多个设备同时使用。

明白了IDT,就可以看到0x2e 号中断的isr 为KiSystemService,顾名思义这个中断号专用于提供系统服务。


KiSystemService

在正式分析KiSystemService,前,先看下几个辅助函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
SaveTrap() //这个函数用来保存寄存器现场和其他状态信息
{
Push 0 //LastError
Push ebp
Push ebx
Push esi
Push edi
Push fs //此时的fs 若是从用户空间自陷进来的就指着TEB,反之指着kpcr
Push kpcr.ExceptionList
Push kthread.PreviousMode
Sub esp,0x48 //腾给调式寄存器保存用
-----------至此,上面的这些语句连同int 2e 中的语句在栈上构造了一个trap 帧-----------------
Mov CurTrapFrame,esp //当前Trap 帧的地址
Mov CurTrapFrame.edx, kthread.TrapFrame //将上次的trap 帧地址记录到edx 成员中
Mov kthread.TrapFrame, CurTrapFrame, //修改本线程当前trap 帧的地址
Mov kthread.PreviousMode,GetMode(进入内核前的CS) //根据CS 自动确定上次模式
Mov kpcr.ExceptionList,-1 //表示刚进入内核时,尚未安装seh
Mov fs,kpcr //一进入内核就让fs 改指向当前cpu 的描述符kpcr,不再指向TEB
If(当前线程处于调试状态)
保存 DR0-DR7 到 trap 帧中
}
FindTableCall() //这个函数用来查表,拷贝参数,调用系统服务
{
Mov edi,eax //系统函数号,低 12 位为索引,第 13 为表示是哪张系统服务表中的索引
Mov eax, edi.低 12//eax=真正的服务号
If(edi.第 13 位=1) //if 这是 shadow SSDT 中的系统函数号
{
If(当前线程.服务描述符表!=shadow)
当前线程.服务描述符表=shadow //换用另外一张描述符表
}
服务表描述符=当前线程.服务描述符表[edi.第 13 位] Mod edi=服务表描述符.base //这个系统服务表的地址 Mov ebx,[edi+eax*4] //查表获得这个函数的地址
Mov ecx=服务表描述符.Number[eax] //查表获得的这个系统函数的参数大小

Mov esi,edx //esi=用户空间中的参数地址 Mov edi,esp //esp 已经为内核栈的栈顶地址
Rep movsb //将所有参数从用户空间复制到内核空间,相当于 N 个连续 push 压参
Call ebx //调用对应的系统服务函数
}
KiSystemService()//int 2e 的 isr,内核服务函数总入口,注意这个函数可以嵌套、递归!!!
{
SaveTrap();
Sti //开中断
//---------------上面保存完寄存器等现场后,开始查 SSDT 表调用系统服务------------------
FindTableCall();
//---------------------------------调用完系统服务函数后------------------------------
Move esp,kthread.TrapFrame; //将栈顶回到 trap 帧结构体处
Cli //关中断
if(上次模式==UserMode)
{
Call KiDeliverApc //遍历执行本线程的内核 APC 和用户 APC 队列中的所有 APC 函数 清理 Trap 帧,恢复寄存器现场
Iret //返回用户空间
}
Else
{
返回到原 call 处后面的那条指令处
}

}

上面所说的 Trap 帧(TrapFrame)是指一个结构体,用来保存系统调用、中断、异常发生时的寄存器现场, 方便以后回到用户空间/回到中断处时,恢复那些寄存器的值,继续执行

Trap帧中除了保存了所有寄存器现场外,还附带保存了一些其他信息,如 seh 链表的地址等

Trap帧

必须说一下 trap 帧的结构体布局定义: typedef struct _KTRAP_FRAME //Trap现场帧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
typedef struct _KTRAP_FRAME //Trap现场帧 
{
// ------------------这些是KiSystemService保存的---------------------------
ULONG DbgEbp;
ULONG DbgEip;
ULONG DbgArgMark;
ULONG DbgArgPointer;
ULONG TempSegCs;
ULONG TempEsp;
ULONG Dr0;
ULONG Dr1;
ULONG Dr2;
ULONG Dr3;
ULONG Dr6;
ULONG Dr7;
ULONG SegGs;
ULONG SegEs;
ULONG SegDs;
ULONG Edx;//xy 这个位置不是用来保存edx的,而是用来保存上个Trap帧,因为Trap帧是可以嵌套的
ULONG Ecx; //中断和异常引起的自陷要保存eax,系统调用则不需保存ecx
ULONG Eax;//中断和异常引起的自陷要保存eax,系统调用则不需保存eax
ULONG PreviousPreviousMode;
struct _EXCEPTION_REGISTRATION_RECORD FAR *ExceptionList;//上次seh链表的开头地址
ULONG SegFs;
ULONG Edi;
ULONG Esi;
ULONG Ebx; ULONG Ebp;
//----------------------------------------------------------------------------------------
ULONG ErrCode;//发生的不是中断,而是异常时,cpu还会自动在栈中压入对应的具体异常码在这儿
//-----------下面5个寄存器是由int 2e内部本身保存的或KiFastCallEntry模拟保存的现场---------
ULONG Eip;
ULONG SegCs;
ULONG EFlags;
ULONG HardwareEsp; ULONG HardwareSegSs;
//---------------以下用于用于保存V86模式的4个寄存器也是cpu自动压入的-------------------
ULONG V86Es;

ULONG V86Ds;
ULONG V86Fs; ULONG V86Gs;
} KTRAP_FRAME, *PKTRAP_FRAME;
KPCR&&KPRCB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Struct KPCR 
{
KPCR_TIB Tib;//类似于 TEB.TIB,内部第一个字段都是 ExceptionList
KPCR* self;//自身结构体的地址,方便直接寻址
KPRCB* kprcb;//处理器控制块的地址、
KIRQL irql;//当前 cpu 的 irql
USHORT* IDT;//本 cpu 的 IDT 地址,一有中断/异常就去这个表找 isr、epr
USHORT* GDT;//全局描述符表地址
KTSS* TSS;//记录了本 cpu 上当前运行线程的状态信息,重要字段有内核栈地址,IO 权限位图
„„
}
Struct KPRCB
{
KTHREAD* CurrentThread;//本 cpu 上当前运行的线程
KTHREAD* NextThread;//本 cpu 上将抢占当前线程的下个线程(抢占式调度核心)
BYTE CpuID;//不多说
ULONG KernelTime,UserTime;//本 cpu 的累计运行时间统计信息
„„
}
SSDT&Shadow SSDT

系统中有两张“系统服务表”,即 SSDT 和 shadow SSDT。
同样系统中也有两张“系统服务表描述符表”, 每个表都包含两个描述符。
两张表中第一个描述符都是 SSDT 的描述符,第二个描述符都是 shadow SSDT 的 描述符。
但是第一个表的第二个描述符是空白的,因此第一张表实际上只能描述 SSDT 表,第二张表可以 描述 SSDT 表和 shadow SSDT 表。

所以一旦调用的是 shadow SSDT 表中系统服务函数,
这个线程就会自动换 用第二张服务表描述符表,具体为: Mov kthread.ServiceTable, 第二张服务表描述符表
这样这个线程就变为一个 GUI 线程,以后都使用 第二张“系统服务表描述符表”了

“系统服务表描述符”是一个结构体,用来描述一张系统服务表的各种信息,如下定义:

1
2
3
4
5
6
7
Struct KSERVICE_TABLE_DESCRIPTOR 
{
ULONG* base;//系统服务表的地址
ULONG* CountTable;//该系统服务表中每个函数的历史调用次数统计表
ULONG limit;//该系统服务表的大小,也即容量
BYTE* ArgSizeTable;//记录该系统服务表中每个函数参数大小的表
}




通过快速调用指令

通过快速调用指令(Intel 的是 sysenter,AMD 的是 syscall)调用系统服务

老式的 cpu 不支持、不提供 sysenter 指令,只能由 int 2e 模拟中断方式进入内核调用系统服务
但是那种方式有一个明显的缺点,就是速度慢!(如 int 2e 内部本身要保存 5 个寄存器的现场,然后还 要去 IDT 中查找 isr,这个过程消耗的时间太多)
因此 x86 系列从奔腾 2 代开始为系统调用专门增设了 一条 sysenter 指令以及相应的寄存器 msr。
同样sysenter 指令也可看做 intel 提供的一个内部函数

1
2
3
4
5
6
7
Sysenter() 
{
Mov ss,msr_ss
Mov esp,msr_esp //关键
Mov cs,msr_cs
Mov eip,msr_eip //关键
}

系统在启动初始化过程中,会将上面四个 msr 寄存器设为固定的值,其中 msr_esp 为 DPC 函数专用堆栈, Msr_eip 则固定为KiFastCallEntry


KiFastCallEntry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
KiFastCallEntry() //快速系统调用总入口 
{
Mov fs,kpcr //一进入内核,就将 fs 改指向处理器描述符 kpcr
Mov esp,TSS.ESP //一进入内核,就换用内核栈(每个线程的内核栈地址保存在 TSS 中)
Push ds
Push edx //edx 为用户空间栈的栈顶地址,保存在这儿,方便以后回到用户空间时恢复
Push eflags
Push cs
Push sysenter 指令的后面那条指令的地址 //将用户空间中的返回地址保存在这儿
//--------上面的 5 条 push 指令模拟中断、异常发生时 cpu 自动保存的那 5 个寄存器的现场------------
Cli //关中断,构造 Trap 现场帧的过程中需要暂时关中断
Mov eflags,0x2 SaveTrap();
Sti //开中断
// ---------------上面保存完寄存器等现场后,查 SSDT 表调用对应系统服务----------------------
FindTableCall();
// ------------------------------------调用完系统服务函数后--------------------------------
Move esp,kthread.TrapFrame; //将栈顶回到 trap 帧结构体处 Cli //关中断

Call KiDeliverApc //遍历执行本线程的内核 APC 和用户 APC 队列中的所有 APC 函数

// 清理 Trap 帧,恢复寄存器现场
Sti //开中断
// -----------------------------------下面返回用户空间-------------------------------------
Mov ecx,保存的用户空间栈顶地址
Mov edx,保存的返回地址,也即 sysenter 指令的后面那条指令的地址
sysexit //可以把这条指令理解为一个 fastcall 调用约定函数
}

Sysexit

Sysexit 指令也可理解为一个函数,它做的工作如下:

1
2
3
4
5
6
7
8
Sysexit 
{
Mov cs,msr_cs
Mov ss,msr_ss
Mov esp,ecx //换用用户空间中的栈
Mov eip,edx //这样就返回用户空间中了,所有系统调用总是先返回到 NTDLL.dll 中的某个固定位置
// 最后一路返回到 NTDLL 中发起系统调用的那个存根函数体内
}


PreviousMode

前面讲过,线程的内核结构 KTHREAD 中,有一个字段记录了 PreviousMode
这个“上一模式”指的就是,进入本次系统调用前的模式,也即指进入 SSDT 表中的服务函数前的模式是在用户空间还是内核空间。
Windows 不仅支持由用户空间发起系统调用,也支持由内核空间发起系统调用,为此Windows 专门配备了 Zw 系列的内核服务封装函数,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Ntoskrnl.ZwCreateFile() //模拟构造一个 Trap 现场,然后调用系统服务 
{
Mov eax,系统服务号
Lea edx,[esp+4]
Push eflags
Push cs //关键。根据 cs 的值设置 KTHREAD.PreviousMode 字段
//注意在调用本函数前,此处不再模拟中断、异常时自动保存的 ss、esp、eip 寄存器
Call KiSystemService
Ret //这样,调用完系统服务后,就返回到这儿了,不再返回到 NTDLL 中的 sysenter 指令后面了
}

NTSTATUS NtReadFile(„)
{

KPROCESSOR_MODE PreviousMode = KeGetPreviousMode();

}

KeGetPreviosMode()
{
Return kthread.PreviousMode;
}

这样内核 API KeGetPreviosMode 的返回值就是内核模式了

上面这个 NtReadFile 系统服务函数需要获得上次模式,而这个上次模式是在构造 TrapFrame 中的 过程中根据 cs 的值设置的。
因此凡是需要读取上次模式的系统服务函数,都必须有一个“正确的 TrapFrame”。
因此 ZwXXX 系列的系统服务封装函数会在内部Push eflags,Push cs,Call KiSystemService
这三条指令就恰好伪造了一个“正确的 TrapFrame”,使得系统服务能够正确运行。
换言之:凡是需要读取“正确TrapFrame”的系统服务函数都不能直接手工调用,必须调用他们的 ZwXXX 封装函数。反之就可以直接调用。


cs,ds,es,fs,gs,ss段寄存器介绍

fs 在用户态间接指向 TEB,在内核态间接指向 kpcr
其他 5 个段寄存器都可以理解为一个描述符 如cs段寄存器描述符

1
2
3
4
5
struct cs{ 
BOOL bInGDT;//指示下面的 idx 是在 GDT 表中还是 LDT 表中的索引,一般为 TRUE
Int idx;// GDT/LDT 描述符表中,本 cs 段描述符的索引位置
Int rpl://本段的特权级别:0 或者 3
}

简单的讲可以将他们视为 GDT 或 LDT 中的段描述符索引 更多基础信息参考:张银奎 -《软件调试》




内存管理篇

32 位系统中有 4GB 的虚拟地址空间
每个进程有一个地址空间,共 4GB,(具体分为低 2GB 的用户地址空间+高 2GB 的内核地址空间) 各个进程的用户地址空间不同,属于各进程专有,内核地址空间部分则几乎完全相同
虚拟地址如0x11111111, 看似这 8 个数字是一个整体,其实是由三部分组成的,是一个三维地址
将这 个 32 位的值拆开,高 10 位表示二级页表号,中间 10 位表示二级页表中的页号,最后 12 位表示页内偏移(2^12=4kb)
因此一个虚拟地址实际上是一个三维地址,指明了本虚拟地址在哪个二级页表,又在哪个 页以及页内偏移是多少 这三样信息!
即10-10-12的分页模式, 当然也有2-9-9-12的分页模式, 理论上这样的PAE分页模式可以突破32g的内存, 达到64g的物理地址空间

【虚拟地址 = 二级页表号.页号.页内偏移】:口诀【页表、页号、页偏移】

Cpu 访问物理内存的原理介绍

Cpu访问内存途径

如高级语言

1
2
DWORD g_var; //假设这个全局变量被编译器编译为 0x00000004 
g_var=100;

那么这条赋值语句编译后对应的汇编语句为:mov DWORD PTR[0x00000004],100

这里0x00000004就是一个虚拟地址,简称 VA

那么这条 mov 指令究竟是如何寻址的呢?
寻址过程为:CPU 中的虚拟地址转换器也即 MMU,将虚拟地址 0x00000004 转换为物理地址 具体转换过程为:

根据 CR3 寄存器中记录的当前进程页表的物理地址,找到总页表也即页目录,再根据虚拟地址中的页表号,以页表号为索引,找到总页表中对应的 PDE
再根据 PDE 找到对应的二级页表, 再以虚拟地址中的页号部x分为索引,找到二级页表中的对应 PTE
再根据这个 PTE 记录的映射关系,找到这个虚拟页面对应的物理页面
最后加上虚拟地址中的页内偏移部分,加上这个偏移值,就得出最后的物理地址。

具体用下面的函数可以形象表达寻址转换过程

mov DWORD PTR[0x00000004],100 //这条指令的内部原理(没考虑二级缓冲情况)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{ 
va=0x00000004;//页表号=0,页号=0,页内偏移=4

总页表=CR3; //本进程的总页表的物理地址固定保存在 cr3 寄存器中
PDE=总页表[va.页表号]; //PDE 为对应的二级页表描述符

二级页表=PDE.PageAddr; //得出本二级页表的地址
PTE=二级页表[va.页号]; //得出到该虚拟地址所在页面的 PTE 映射描述符

If(PTE 空白) //PTE 为空表示该虚拟页面尚未建立映射
触发 0x0e 号页面访问异常(具体为缺页异常)
Else

If(PTE.bPresent==false) //PTE 的这个字段表示该虚拟页面当前是否映射到了物理内存
触发 0x0e 号页面访问异常(具体为缺页异常)
Else

If(CR0.wp==1 && PTE.Writable==false) //已开启页面写保护功能,就检查这个页面是否可写
触发 0x0e 号页面访问异常(具体为页面访问保护越权异常)
Else
物理地址 pa =cs.base + PTE.PageAddr + va.页内偏移 //得出对应的物理地址


将得到的 pa 放到地址总线上,100 放在数据总线上,经由 FSB->北桥->内存总线->内存条 写入内存
}

简单的讲,可以将他们视为 GDT 或 LDT 中的段描述符索引 更多基础信息参考:张银奎 -《软件调试》

注:在做 SSDT hook、IDT hook 时,由于 SSDT 与 IDT 这两张表各自所在的页面都是只读的,也即他们的 PTE 中标志位标示了该页面不可写。
因此一修改 SSDT、IDT 就会报异常,一个简单的处理方法是是关闭 CRO 中的 wp 即写保护位,这样就可以修改了




虚拟页面结构

进程,地址空间,区段,区块,页面的逻辑层次关系
一个虚拟页面实际上有五级限定:
【进程.地址空间.区段.区块.虚拟页面】 意为:哪个进程的哪个地址空间中的哪个区段中的哪个区块中的哪个虚拟页面

地址空间

前文说了,每个进程有两个地址空间,一个用户地址空间,一个内核地址空间,该地址空间的内核结构体定义为:

1
2
3
4
5
6
7
8
9
Struct  MADDRESS_SPACE //地址空间描述符 
{
MEMORY_AREA* MemoryRoot;//本地址空间的已分配区段表(一个 AVL 树的根)
VOID* LowestAddress;//本地址空间的最低地址(用户空间是 0,内核空间是 0x80000000)
EPROCESS* Process;//本地址空间的所属进程
/*一个表,表中每个元素记录了本地址空间中各个二级页表中的 PTE 个数,一旦某个二级页表中的 PTE 个数减到了 0,就自动释放该二级页面表本身,体现为稀疏数组特征*/
USHORT* PageTableRefCountTable;
ULONG PageTableRefCountTableSize;//上面那个表的大小
}

地址空间中所有已分配的区段都记录在一张表中,这个表不是简单的数组,而是一个 AVL 树,用来提高查 找效率。
每个区段的基址都对齐 64KB 或 4KB(指 64KB 整倍数),各个区段之间可以有空隙, 区段的分布是很零散的!各个区段之间,夹杂的空隙就是尚未分配的虚拟内存。


区段

注:所谓已分配区段,是指已经过 VirtualAlloc 预订(reserve)或提交(commit)后的虚拟内存 区段的描述符如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Struct  MEMORY_AREA   //区段描述符 
{
Void* StartingAddress; //开始地址,普通区段对齐 64KB,其它类型区段对齐 4KB
Void* EndAddress;//结尾地址,EndAddress – StartingAddress 就是该区段的大小
MEMORY_AREA* Parent;//AVL 树中的父节点
MEMORY_AREA* LeftChild;//左边的子节点
MEMORY_AREA* RightChild;//右边的子节点
//常见的区段类型有:普通型区段、视图型区段、缓冲型区段(后面文件系统中会讲到)等
ULONG type;//本区段的类型
ULONG protect;//本区段的保护权限,可读、可写、可执行的组合
ULONG flags;//当初分配本区段时的分配标志
BOOLEAN DeleteInProgress;//本区段是否标记为了"已删除"

ULONG PageOpCount;

Union{
Struct //这个 Struct 专用于视图型区段
{
//凡是含有 ROS 字样的函数与结构体都表示是 ReactOS 与 Windows 中不同的实现细节
ROS_SECTION_OBJECT* section;
ULONG ViewOffest;//指本视图型区段在所在 Segment 内部的偏移
MM_SECTION_SEGMENT* Segment;//所属 Segment
BOOLEAN WriteCopyView;//本视图区段是不是一个写复制区段
}SectionData;
LIST_ENTRY RegionListHead;//本区段内部的所有 Region 区块,放在一个链表中
}Data;
}

浅谈区段类型:

  • MEMORY_AREA_VIRTUAL_MEMORY://普通型区段,由 VirtuAlloc 应用层用户分配的区段都是普通区段
  • MEMORY_AREA_SECTION_VIEW://视图型区段,用于文件映射、共享内存
  • MEMORY_AREA_CACHE_SEGMENT://用于文件缓冲的区段(一个簇大小)
  • MEMORY_AREA_PAGED_POOL://内核分页池中的区段 MEMORY_AREA_KERNEL_STACK://用于内核栈中的区段
  • MEMORY_AREA_PEB_OR_TEB://用于 PEB、TEB 的区段
  • MEMORY_AREA_MDL_MAPPING://内核中专用于建立 MDL 映射的区段
  • MEMORY_AREA_CONTINUOUS_MEMORY://对应的物理页面也连续的区段
  • MEMORY_AREA_IO_MAPPING://内核空间中用于映射外设内存(如显存)的区段
  • MEMORY_AREA_SHARED_DATA://内核空间中用于与用户空间共享的区段

区块

1
2
3
4
5
6
7
Struct  MM_REGION //区块描述符 
{
ULONG type;//指本区块的分配类型(预定型分配、提交型分配),又叫映射状态(已映射、尚未映射)
ULONG protect;//本区块的访问保护权限,可读、可写、可执行的组合
ULONG length;//区块长度,对齐页面大小(4KB)
LIST_ENTRY RegionListEntry;//用来挂入所在区段的区块链表
}

内存以区段为分配单位,一个区段内部,又按分配类型、保护属性划分区块。
一个区块包含一到多个内存页面,分配类型相同并且保护权限相同的区域组成一个个的区块,因此,称为“同属性区块”。
一个区段内部,相邻区块之间的属性肯定是不相同的(分配类型或保护权限不同)
若两个相邻区块的属性相同了,会自动合并成一个新的区块。


内存各个Mm函数

MEMORY_AREA* MmLocateMemoryAreaByAddress(MADDRESS_SPACE* as, void* addr);
这个内核函数用于在指定地址空间中查找指定地址所属的已分配区段,如果返回 NULL,表示该地址尚不处 于任何已分配区段中,也即表示该地址尚未分配。

Void* MmFindGap(MADDRESS_SPACE* as, ULONG len, ULONG AlignGranularity, BOOL TopDown)
这个函数在指定地址空间中 查找一块符合 len 长度的空闲(也即未分配)区域,返回找到的空闲区的地址,
AlignGranularity 表示该空白区必须的对齐粒度,TopDown 表示是否从高地址端向低地址端搜索

MEMORY_AREA* MmLocateMemoryAreaByRegion(MADDRESS_SPACE* as, void* addr, ULONG len)
这个函数从指定地址空间的低地址端向高地址段搜索,返回第一个与给点区间(addr,len)有交集的已分 配区段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
NTSTATUS MmCreateMemoryArea(MADDRESS_SPACE* as, type, void** BaseAddr, Len, protect, bFixedAddr, AllocFlags,   MEMORY_AREA** Result) 
{
Len=Align(Len,4kb);//区段长度都要对齐 4kb
UINT BaseAlign;//区段的基址对齐粒度

If(type==普通区段)
BaseAlign=64KB;
Else
BaseAlign =4KB;

If(*BaseAddr ==NULL && !bFixedAddr)//if 用户不要求从固定地址处开始分配
{
*BaseAddr=MmFindGap(as,Len, BaseAlign, AllocFlags 要求 TopDown?);
}
Else//else 只要用户给定了基址,就必须从那儿开始分配
{
*BaseAddr=Align(*BaseAddr, BaseAlign);
}

If(要分配的区域没有完全落在指定地址空间内部)
Return fail;

If(MmLocateMemoryAreaByRegion(as,*BaseAddr,Len)!=0)//if 这段范围已分配过
Return fail;
}
//找到了一个空闲区域后/指定的地址满足分配要求,就把这块区域分配出去

Memory_Area* Area=ExAllocatePool(NonPagePool, sizeof(*Area),tag);
ZeroMemory(Area);
Area.type=type;//本区段的初始分配类型(初始时,一个区段内部就一个区块)
Area.StartAddr=*BaseAddr;
Area.EndAddr=*BaseAddr+Len;
Area.protect=protect;//本区段的初始保护属性
Area.flags=Allocflags;

MmInsertMemoryArea(as,Area);//分配后插入地址空间中的已分配区段表中(AVL 树)
*Result=Area;
Return succ;
}

上面这个函数用来从指定地址或者让系统自动寻找一块空闲的区域,分配一块指定长度、类型的区段。

所谓分配:包含 reserve 型分配(即预定型分配)和 commit 型分配(即提交型分配)
预定:只占用分配一块区段,不建立映射
提交:分配一块区段并建立映射(映射到磁盘页文件/物理内存页面/普通文件)

MM_REGION* MmFindRegion(void* AreaBaseAddr, LIST_ENTRY* RegionListHead, void* TgtAddr, Void** RegionBaseAddr)
这个函数从指定区段的区块链表中,查找给定目标地址 TgtAddr 落在哪一个区块内 第一个参数表示区段的基址。函数返回找到的区段并顺便将该区段的基址也存入最后一个参数中返回给调 用者

MM_REGION* MmSplitRegion(MM_REGION* rgn, BaseAddr, StartAddr,Len, NewType,NewProtect AlterFunc)
这个函数将指定区块内部的指定区域(StartAddr,Len)修改为新的分配类型、保护属性,使原区块分裂, 一分为三(特殊情况一分为二)
然后调用 AlterFunc 跟着修改二级页表中,新区块的那些 PTE,最后再跟 着修改物理页面分配情况。函数返回新分出来的那个中间区块。这是一个内部辅助函数。

NTSTATUS MmAlterRegion(AreaBaseAddr, RegionListHead, TgtAddr,Len, NewType,NewProtect, AlterFunc)
这个函数是个通用函数,用来修改指定区段内部的指定区域的分配类型、保护属性,然后调用
调用 AlterFunc 跟着修改二级页表中,目标区域对应的那些 PTE,最后再跟着修改物理 页面的分配情况。


物理页面

内核中有一个全局的物理页面数组,和7个物理页面链表。
分别是:

  • PHYSICAL_PAGE MmPageArray[];//物理内存有多大,该数组就有多大
  • LIST_ENTRY FreeZeroedPageListHead;//空闲物理页面链表(且物理页面已清 0)
  • LIST_ENTRY FreeUnzeroedPageListHead;//空闲物理页面链表(但物理页面尚未清 0)
  • LIST_ENTRY UsedPageListHeads[4];//细分为 4 大消费用途的忙碌物理页面链表,各链表中按 LRU 顺序
  • LIST_ENTRY BiosPageListHead;//用于 Bios 的物理页面链表

物理页面数组是一个物理页面描述符数组,每个元素描述对应的物理页面(数组索引号即 物理页号,又叫 pfn)
每个描述符是一个PHYSICAL_PAGE结构体

1
2
3
4
5
6
7
8
9
10
11
Struct  PHYSICAL_PAGE //物理页面描述 
{
Type ;//该物理页面的空闲占用状态(1 表示空闲,2 表示已占用,3 表示分给了 BIOS)
Consumer;//该物理页面的消费用途(用户/内核分页池/内核非分页池/文件缓冲 四种)
Zero;//标志本页面是否已清 0
ListEntry;//用来挂入那 7 个链表之一
ReferenceCount;//引用计数,一旦减到 0,页面就变为空闲状态,进入空闲链表
SWAPENTRY SavedSwapEntry;//对应的来源页文件,用于置换,一般为空
LockCount;//本物理页面的锁定计数(物理页面可锁定在内存中,不许置换到外存) MapCount;//同一个物理页面可以映射到 N 个进程的 N 个虚拟页面
MM_RMAP_ENTRY* RmapListHead;//本物理页面映射给的那些虚拟页面,组成的链表
}


物理页面的状态转换

一个物理页面的典型状态转换过程为

  • 起初处于空闲并清 0 的状态,然后应内存分配要求分配给 4 个消费者之一, 同时将该物理页面记录到对应消费者的 UsedPageListHead 链表中
  • 最后用户用完后主动释放,或者因为物理内存紧张,被迫释放换到外存,而重新进入空闲状态,但此时尚未清0,将进入 FreeUnzeroedPageList 链表。
  • 然后内核中有一个守护线程会定时、周期扫描这个空闲链表,将物理内存清0,转入 FreeZeroedPageList 链表,等候下次被分配。

如此周而复返

这段函数为指定消费者分配一个物理页面, 并第一时间将物理页面清 0, 然后返回分得的物理页号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
PFN_NUMBER  MmAllocPage(ULONG ConsumerType) 
{
PFN_NUMBER Pfn;//物理页号
PPHYSICAL_PAGE PageDescriptor;
BOOLEAN NeedClear = FALSE;//是否需要清零
if (FreeZeroedPageList链表 为空)
{
if (FreeUnzeroedPageList 为空)
return 0;//如果两个空闲链表都为空就失败
PageDescriptor = MiRemoveHeadList(&MmFreePageListHead);
NeedClear = TRUE;
}
else
PageDescriptor = MiRemoveHeadList(&MmZeroedPageListHead);
//从空闲链表中摘下来一个空闲页面后,初始化

MmAvailablePages--;//总的可用物理页数--

PageDescriptor->ReferenceCount = 1;//刚分配的物理页面的引用计数为1
PageDescriptor->LockCount=0;//表示可被置换到外存
PageDescriptor->MapCount=0;//表示刚分配的物理页面尚未映射到任何虚拟页面

//记录到分配链表中
InserTailList(&UsedPageListHeads[ConsumerType], PageDescriptor);
if (NeedClear)
MiZeroPage(PfnOffset);//清0
Pfn = PageDescriptor-MmPageArray;//pfn=数组的索引,就是物理页号
return Pfn;
}

这个函数先检查配额,再检查空闲页面阀值,做好准备工作后,再才分配物理页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
NTSTATUS  MmRequestPageMemoryConsumer(consumer, PFN* pfn) 
{
//先检查物理页面配额,超出配额,就自我修剪
If(本消费者的分得的物理页面数量 = 本消费者的最大配额)
{
//换出那个消费者的某个物理页面到外存,腾出一个物理页面出来
Call 对应消费者的自我页面修剪函数
}

If(当前系统总的空闲页面总量 < 储备阀值)
{
If(consumer==非分页池消费者)
{
*pfn = MmAllocPage(consumer);
//分完后唤醒系统中的平衡线程去平衡物理页面,填补空白
KeSetEvent(&MiBalancerEvent);
Return succ;
}
Else
{
*pfn = 请求平衡线程赶紧从其他消费者手中退出一个物理页面;
Return succ;
}
}
Else
*pfn = MmAllocPage(consumer);
}

这个函数释放指定消费者占用的指定物理页面,实际上是递减引用计数,引用计数减到 0 后就挂入系统空闲链表

1
2
3
4
5
6
7
8
9
10
11
12
NTSTATUS  MmReleasePageMemory(consumer, pfn) 
{
Consumer.UsedPageCount--;//递减本消费者持有的页面计数;
pfn.ReferenceCount--;//递减本页面的引用计数
If(pfn.ReferenceCount==0)
{
If(有其他分配请求正在等待退让物理页面)
将这个 pfn 分给那个 Pending 中的分配请求
Else
将这个页面挂入系统 FreeUnzeroedPageList 链表;
}
}


漫谈页目录、二级页表

前面讲到每个虚拟地址看似是一个整形值,实际上由三部分组成:页表号.页号.页内偏移;

为什么不是直接的页号.页内偏移呢,直接采用一个简单的一维数组,记录所有虚拟页面的这样多直观!

原因是:一个进 程的虚拟地址空间太大,如果为每个虚拟页面都分配一个 PTE,那么将占用大量内存
不信我们算一下:一个进程中总共有4GB/4KB=2^20个虚拟页面,也即 1MB 个虚拟页面
如果直接采用一维数组, 描述这个 1MB 页面的映射情况,那么整个数组大小=1MB*sizeof(PTE)=4MB
这样光页表部分就占据了 4MB 的内存。
注意页表部分本身占用的内存是非分页内存,也即真金白银地占据着 4MB 物理内存,这 4MB 在现在的机器看来并不算多
但在早期只有 256MB 物理内存的老爷机上(最多只能同时支持 256MB/4MB 个=64 个进程),已经算够多了!

相反如果采用页目录+二级页表的方式就会节省很多内存!
一个二级页表本身有一个页面大小, 可以容纳4KB/sizeof(PTE)=1024个 PTE
换句话说一个二级页表可以描述 1024 个页面的映射情况(换算成字节数,一个二级页面能描述1024*4kb 的地址空间)
一个进程总共有 4GB 地址空间,那么整个地址空间就有4GB/(1024*4kb)=1024个二级页表
那些暂时未映射的一大片虚拟地址,一般是高端的地址,就对应这 1024个二级页表中靠后的那些二级页表,就可以暂时不用为他们分配物理内存了
只有在确实要访问那些虚拟页面时,才分配他们对应的二级页表,这样按需分配就节省了物理内存。

另外, 32 位系统中每个进程有 1024 个二级页表外加一个页目录。 咋一看似乎系统中有 1025 个页表维持着映射关系
其实不然, 因为页目录本身是一个特殊的二级页表,也是那 1024 个二级页表中的一个。
概念上,我们把第一个二级页表理解为页目录。这样系统中实际上共有 1024 个二级页表

包括页目录本身在内,但要注意页目录并不在二级页表区的中的第一个位置,而是在中间的某个位置,后面我会推算页目录本身的虚拟地址在什么地方。
明白了这个道理, 就可以由任意一个虚拟地址推算出他所在的二级页表在页目录中的索引位置

1
2
#define ADDR_TO_PDE_OFFSET(addr)(  v/(1024*4kb) ) 
#define ADDR_TO_PAGE_TABLE(addr) ADDR_TO_PDE_OFFSET(addr)

这样每个进程的页表不再是个简单的数组,而变成了一个稀疏数组。
页目录中的每个 PDE 描述了每个二级页表本身的物理地址。如果 PDE=0,就表示那个二级页表尚未分配,体现为稀疏数组特征。
实际上一个进程很少使用到整个 4GB 地址空间, 因此页目录中的绝大多数 PDE 都是空的, 实际的二级页面个数往往很少。

PTE

每个虚拟页面的映射描述符(即 PTE)的位置是固定的,根据虚拟页号可以自然算出那个虚拟页面的映射 描述符位置,找到映射描述符的位置后,就可以获得该虚拟页面的当前映射情况(是否已映射,若已映射, 是映射到了物理内存还是页文件,又具体映射到了哪个具体的物理页面,这些信息都一一获悉)
因此 PTE 映射描述符是页表的核心,现在看一下 PTE 它的结构。

PTE 的结构,PTE 是二级页表中的表项,用来描述一个虚拟页面的映射情况以及其他信息
注意 PTE 本身长度为 4B,但我们可以把它当做一个描述符结构体,并不妨碍理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Struct PTE 
{
Union
{
Struct
{
Bool bPresent;//重点字段,表示该虚拟页面是否映射到了物理内存
Bool bWritable;//表示这个虚拟页面是否可写
Bool bUser;//表示是否是用户地址空间中的虚拟页面

Bool bReaded;//表示本虚拟页面自从上次置换到内存后是否曾被读过
Bool bDirty;//表示本虚拟页面自从上次置换到内存后是否曾被写过
Bool bGlobal;//表示本 PTE 表项是全局页面的映射描述符,切换进程时不用刷新本 PTE
UINT pfn;//关键字段,表示本虚拟页面对应的物理页号
}Mem;
Struct
{
文件中的页面号;
页文件号;//系统中可以支持多个 Pagefile.sys 页文件
}File;
}
}

这样,这个 PTE 如果映射到了内存,就解释为 Mem 结构体,如果映射到了页文件,就解释为 File 结构体。

MmCreateVirtualMapping

以下这个函数用来为指定的一段连续虚拟页面,批量创建 PTE,建立到各个物理页面的映射。

注意虚拟页面一定是连续的, 物理页面数组中的物理页面是可以零散分布的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
NTSTATUS  MmCreateVirtualMapping(process,  FirstVirtualPageAddr, VirtualPageCount, PfnArray,
PfnCount, PteFlags)
{
If(VirtualPageCount != PfnCount )
Return fail;

DWORD NewPTE=ConstructPte(PteFlags);//拷贝 PTE 中的那些 Bool 标志位
Void* CurPageAddr = FirstVirtualPageAddr;//当前虚拟页面的地址
PTE* Pt;//当前虚拟页面的 PTE 在二级页表中对应的位置

For(int i=0; i< VirtualPageCount;i++)//遍历每个要创建映射的虚拟页面
{
//这个函数下文有解析
Pt = MmGetPageTableForProcess(process, CurPageAddr);//找到这个页面的 pte 位置
OldPte = *Pt;//记录这个虚拟页面原来的 PTE
If(OldPte 映射到了页文件)
return fail;

If(OldPte 映射到了物理内存)
// 撤销原来的映射;
NewPTE.pfn = PfnArray[i];//关键,将这个虚拟页面映射到指定的物理页面

*pt = NewPTE;//修改原 PTE 表项
//递增对应二级页表中的 PTE 个数,这个函数其实是创建 PTE,不是修改 PTE

Process.地址空间.PageTableRefCountTable[ ADDR_TO_PAGE_TABLE(CurPageAddr) ]++;

If(OldPte 映射到了某物理内存页面)
MiFlushTlb(pt, CurPageAddr);//同步更新 cpu 二级缓冲中的 PTE

CurPageAddr+=4KB;//下一个虚拟页面
}
}

MmDeleteVirtualMapping

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
NTSTATUS MmDeleteVirtualMapping(process, PageAddr,  bFreePhysicalPage,  BOOL* bDirty, PFN* pfn) 
{
PTE* pt= MmGetPageTableForProcess(process, CurPageAddr);//找到这个页面的 pte 位置
PTE OldPte=*pt;
*pt=0;//全 0 就表示删除 PTE

//*注意,一个物理页面可能还可能被其他虚拟页面映射着,应该等到该物理页面的 MapCount 减到 0 时 才释放这个物理页面*/
If(bFreePhysicalPage)
MmReleasePageMemoryConsumer(pfn);

//递减对应二级页表中的 PTE 个数
Process.地址空间.PageTableRefCountTable[ ADDR_TO_PAGE_TABLE(CurPageAddr) ] --;

If(Process.地址空间.PageTableRefCountTable[ ADDR_TO_PAGE_TABLE(CurPageAddr) ] = 0)
MmReleasePageTable(process,PageAddr);//释放对应的整个二级页表,体现稀疏数组特征

*bDirty=OldPte.bDirty;
*pfn=OldPte.pfn;//返回原来映射的物理页面号
}


物理页面的临时映射(Hyperspace)

Windows 中,不管是应用程序还是内核程序,都不能直接访问物理内存

Mov eax,DWORD PTR[物理地址]是不允许的,不支持的。

所有非 IO 指令都只能访问虚拟内存地址

Mov eax, DWORD PTR[虚拟地址]形式

但是有时候我们明明已经知道了某个东西固定在物理内存条某处
假如系统时间的值永远固定存放在物理内存条的物理地址0x80000000

我们已经知道了物理地址,如何访问获得系统时间值呢?这是个问题!
Windows 为了解决这样的直接访问物理内存操作提供了手段!
其中之一便是:为物理页面建立临时映射, 也即可以将某个物理页面映射到系统地址空间中的那段专用于临时页面映射的保留区域。
具体的系统地址空间中专用于临时映射的那段保留区的起始虚拟地址为#define HYPERSPACE 0xC0400000

保留区的大小为:1024 个虚拟页面,也即1024*4KB=4MB大小
下面这个函数用来将指定物理页面临时映射到保留区中的某个虚拟页面,返回得到的虚拟页面地址

MmCreateHyperspaceMapping

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Void* MmCreateHyperspaceMapping(pfn) 
{
PTE* Pte=临时映射保留区的映射描述符们所在的二级页表;//也即第一个临时页面的映射描述符
Pte+=pfn%1024;//从这个虚拟页面的映射描述符开始,向后搜索第一个尚未映射的虚拟页面
For(i=pfn%1024; i<1024; i++,Pte++)//先遍历后面的那些 PTE
{
If(*pte == 空白)
{
*pte.pfn=pfn;
Break;
}
}
If(i==1024)//如果后面部分未找到一个空闲 PTE,又从前面部分开始查找
{

PTE* Pte=临时映射保留区的映射描述符们所在的二级页表;//回到开头
For(i=0; i<pfn%1024;i++,Pte++)
{
If(*pte == 空白)
{
*pte.pfn=pfn;
Break;
}
}//end for
}//end if(i==1024)
//上面是一个简单的闭式 hash 表的查找过程,找到一个尚未映射的临时保留虚拟页面后,就返回
Return HYPERSPACE + i*4kb;
}

既然叫临时映射,那用完后就得撤销映射

MmDeleteHyperspaceMapping

MmDeleteHyperspaceMapping(pfn);//这个函数就是用来删除以前建立的临时映射,省略


虚拟页面与物理页面之间的映射

一个物理页面可以映射到 N 个进程的 N 个虚拟页面中,但一个虚拟页面同一时刻只能映射到一个物理页面。
可以这么理解:"一个物理页面当前可能被 N 个虚拟页面映射着", "本虚拟页面当前映射着一个物理页面"

每个虚拟页面又分四种映射状态:

  • 1、 映射着某个物理页面(已分配且已映射)
  • 2、 映射着某个磁盘页文件中的某个页面(已分配且已映射)
  • 3、 没映射到任何物理存储介质(对应的 PTE=0),但是可能被预定了(已分配,但尚未映射)
  • 4、 裸奔(尚未分配,以上情况都不满足)

一个进程的用户地址空间高达 2GB,分成很多虚拟页面,如果时时刻刻都让这些虚拟页面映射着物理内存, 那么物理内存恐怕很快就分完了。
所以同一时刻,只有最频繁访问的那些虚拟页面映射着物理页面(最 频繁访问的那些虚拟页面就构成了一个进程的工作集)
工作集中的所有虚拟页面都映射着物理页面,一旦访问工作集外面的虚拟页面,势必引发缺页异常
系统的缺页异常处理函数会自动帮我们处理这种异常(自动分配一个物理页面,将那个引发缺页异常的虚拟页面映射着的外存页面以分页读 irp 的形式读入到新 分配的物理页面中,然后修改那个虚拟页面的映射关系,指向那个新分配的物理页面)
这就是系统的缺页异常处理函数的工作原理,应用程序毫不知情。

寻找PTE

要想查询一个虚拟页面的映射情况(有没有映射,有的话,又映射到了什么地方 这些信息)
唯一的办法就是要找到这个虚拟页面的 PTE 映射描述符,那么如何查找呢?

#define PAGETABLE_MAP 0xC0000000
如前文所述每个进程的页表区都真枪实弹的占据着对应的物理内存

系统为了方便,把每个进程的页表区都事先固定映射到了虚拟地址0xC0000000处,长度为1024个页表 * 每个页表本身的大小(即 4KB)=4MB。
因此各个进程的页表区也都是被系统映射到了同一段虚拟空间(0xC0000000---0xC0000000+4MB)处。
这段区域用来映射二级页表们

#define PAGEDIR_MAP (PAGETABLE_MAP + PAGETABLE_MAP/1024)

PAGEDIR_MAP表示页目录本身所在的虚拟页面的地址, 这是怎么得来的呢?

是经过下面这样推算出来的PAGEDIR_MAP= PAGETABLE_MAP + idx*每个二级页面的大小
= PAGETABLE_MAP + idx*4kb
= PAGETABLE_MAP + (PAGETABLE_MAP 偏移/每个二级页表描述的长度范围) * 4kb
= PAGETABLE_MAP + (PAGETABLE_MAP/(1024*4kb)) * 4kb
= PAGETABLE_MAP + PAGETABLE_MAP/1024
因此只要知道了页表区中第一个二级页面的虚拟地址,就可以推算出页目录本身的虚拟地址

进一步:

#define ADDR_TO_PDE(PageAddr) PAGEDIR_MAP + PageAddr/(1024*1024) //直接推算 PDE 的地址
#define ADDR_TO_PTE(PageAddr) PAGETABLE_MAP + PageAddr/1024 //直接推算 PTE 的地址

这两个宏我就不想多说了
下面这个函数用来找到指定进程中的指定虚拟页面的映射描述符位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
PTE* MmGetPageTableForProcess(process, PageAddr) 
{
ULONG PDE_IDX = ADDR_TO_PDE_OFFSET(PageAddr);//计算该虚拟页面的映射描述符在哪个二级页表中 PDE* PageDir;//页目录的虚拟地址
If(process!=当前进程 && PageAddr<2GB)//if PageAddr 是其他进程的用户空间中的某个虚拟页面
{
PFN pfn=process.pcb.DirectoryTableBase;//获得那个进程的页目录所在的物理页面号
PageDir=MmCreateHyperspaceMapping(pfn);//临时映射那个物理页面,以便访问它的页表

If(PageDir[PDE_IDX]==空白)
Return NULL;//若整个二级页面尚未分配,返回 NULL

Pfn= PageDir[PDE_IDX].pfn;//获得二级页表所在的物理页号
MmDeleteHyperspaceMapping(PageDir);//不用在访问页目录了,撤销临时映射
PTE* pte= MmCreateHyperspaceMapping(Pfn);//再临时映射二级页表本身,以便访问它
Return pte+ADDR_TO_PTE_OFFSET(PageAddr);//OK,返回那个页面的映射描述符
}

Else//反之,若那个虚拟页面就在本进程的用户地址空间或公共的系统地址空间中,就直接推算页目 录的虚拟地址,免得为其创建临时映射,以提高效率
{
PageDir=ADDR_TO_PDE(PageAddr);//直接推算的页目录的虚拟地址
If(PageDir[PDE_IDX]==空白)
Return NULL;//若整个二级页面尚未分配,返回 NULL
Return ADDR_TO_PTE(PageAddr);//直接推算得到这个虚拟页面的映射描述符地址
}
}

前面说过各个进程的用户地址空间是私有的,各不相同的; 内核地址空间部分则几乎完全相同

为什么是几乎呢, 而不是全部呢?
那就是因为内核地址空间中,每个进程的二级页表区和临时映射区,没有映射到相同的物理页面。

MmUpdatePageDir(process, KernePagelAddr,PageCount)

每当内核地址空间中的某组页面的映射发生变化,系统就会调用这个函数将内核地址空间中从KernePagelAddr 开始的一组内核虚拟页面
从系统的公共内核页表中同步复制这些页面的 PTE到各个进程的对应页表中
这样就使得每个进程的内核页面映射都相同,落到同一个物理页面或者文件页面中。

但是系统绝不会同步修改各个进程的二级页表区和临时映射区中那些虚拟页面的映射描述符
因为那部分虚拟页面由每个进程自己单独维护映射,各不相同。
也即每个进程的内核页表部分都 copy 自系统,用户页表部分各不相同。

综上:【各个进程的用户地址空间各不相同,内核地址空间相同,但页表区、临时映射区除外】


内存分配

下面看一下普通的内存分配流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Void*  Kernel32.VirtualAlloc(void* BaseAddr, Len, AllocType, protect) 
{
Void* addr=BaseAddr;
NTDLL.NtVirtualAlloc(&addr, Len, AllocType, protect)
{
Mov eax,服务号
Lea edx,[esp+4] //记录用户区参数地址
Sysenter

//--------------------用户模式与内核模式分界线-----------------------
KiFastCallEntry()
{

NtAllocateVirtualMemory(hCurProcess,&BaseAddr, &Len, AllocType, protect);

Sysexit
}
Return status;
}
Return addr;
}

如上,应用层的这个 API 调用内核服务函数,从指定进程的用户空间中分配一块指定特征的区段,最后返回区段的地址。

内核中的服务函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
NTSTATUS  NtAllocateVirtualMemory(hProcess, void** BaseAddr, int* Len, AllocType, protect) 
{
If(参数不合法)//一般 SST 中的内核服务函数入口处都会对用户空间传下来的参数进行合法性检查
Return fail;

*BaseAddr=Align(*BaseAddr,64kb);
*Len=Align(*Len,4kb);

EPROCESS* process;//该进程对象的内核结构
ObReferenceObjectByHandle(hProcess,PROCESS_VM_OPERATION,UserMode,&process,„)//获得对象

Type=(AllocType & MEM_COMMIT)?MEM_COMMIT:MEM_RESERVE;//提交型分配或预定型分配
MADDRESS_SPACE* As = process->VadRoot;//VadRoot 表示该进程的用户地址空间

If(*BaseAddr!=NULL)//if 用户给定了分配的起始地址,必须从那儿分配
{
MEMORY_AREA* Area=MmLocateMemoryAreaByAddress(As,*BaseAddr);

If(Area!=NULL)//如果该地址落在事先已经分配的某个区段中
{
AreaLen=Area->EndAddress – Area->StartingAddress;
//如果用户要求分配的这块区域完全落在那个已分配区段中,就修改分配类型、保护属性
//然后调用 AlterFunc 执行合并、拆分、修改页面映射等相关工作
If(AreaLen >= *Len)
{
MmAlterRegion(As,Area->StratingAddr, Area->区块链表, *BaseAddr,*Len,Type,protect AlterFunc=MmModifyAttributes);
Return succ;
}
Else
Return fail;
}//end If(Area!=NULL)
}//end if(*BaseAddr!=NULL)

//若用户没指定地址,或者即使指定了地址,但那个地址尚未落入任何已分配区段中,就分配区段
MmCreateMemoryArea(As,普通型区段,BaseAddr,Len,protect,„);
MmInitializeRegion(Area);//每个区段初始分配时,内部就初始化为:包含一个区块。
Return succ;
}

注意上面函数分配的区段尚未建立映射,既没有映射到物理内存,也没有映射到页文件
但是该区段已经分配,会被记录到地址空间的已分配区段表中(AVL树)
由于尚未映射,此时该区段中各个页面的 PTE 映射描述符是空的,cpu一访问这个页面就会引发缺页异常


页面访问异常

当 cpu 访问一个虚拟页面时,如果

  • 1、 该虚拟页面尚未映射到物理页面,触发典型的0x0e号缺页异常
  • 2、 该虚拟页面映射着了某个物理页面,但是读写访问权限不匹配,触发0x0e越权异常 不管是缺页异常还是越权异常,都叫页面访问异常。

一旦发生异常,cpu 自动从当前 cpu 的IDT[异常号]位置找到对应的异常处理函数(简称 epr
epr 最终将调用MmAccessFault函数处理该异常
注意发生异常时,cpu还会自动把具体的异常原因号(非异常号)压入内核栈帧中,然后跳转到对应的epr
该epr是_KiTrap14函数,该epr在内部构造好异常Trap帧后(也即保存寄存器现场)
Jmp到KiTrap0EHandler异常处理函数,这个函数从CR2寄存器读取发生异常的内存单元地址,然后调用下面的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{ 

//异常码的最低位表示是因缺页引起的异常还是写保护引起的异常
Status = MmAccessFault(TrapFrame->ErrCode & 1, Cr2,TrapFrame->SegCs & MODE_MASK, TrapFrame);

}


NTSTATUS MmAccessFault(bool bProtect, MemoryAddr, Mode, void* TrapInfo)
{
If(bProtect)
Return MmpAccessFault(Mode, MemoryAddr, TrapInfo?TRUE:FALSE);
Else
Return MmNotPresentFault(Mode, MemoryAddr, TrapInfo?TRUE:FALSE);
}

bProtect表示是越权引起的异常还是缺页引起的异常, MemoryAddr表示访问的内存单元地址, Mode表示该指令位于哪个模式空间

缺页异常处理

看缺页异常是怎么处理的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
NTSTATUS MmNotPresentFault(Mode, Address) 
{
MADDRESS_SPACE AddressSpace;
If(Mode==KernelMode)
AddressSpace =MmGetKernelAddressSpace();
Else
AddressSpace =当前进程的用户地址空间;
do
{
MemoryArea = MmLocateMemoryAreaByAddress(AddressSpace, Address);
//如果一个页面尚未映射,那么它的PTE==0,这种情况引发缺页异常
// 如果该地址落在了一个已经分配的区段中,那么MemoryArea不会为NULL,否则MemoryArea为NULL。
// 如果既未建立映射也未分配, 就不会进行缺页置换处理,而是直接返回失败,抛出Win32异常,通知上层应用程序去处理 。
// 相反如果已经分配过了,并且分配类型是commit,就由系统自己进行缺页处理,调入页面。
if (MemoryArea == NULL || MemoryArea->DeleteInProgress)
return (STATUS_ACCESS_VIOLATION);


switch (MemoryArea->Type)
{
case MEMORY_AREA_PAGED_POOL://分页池中的区段
Status = MmCommitPagedPoolAddress(Address);
break;

case MEMORY_AREA_SECTION_VIEW://视图型区段
Status = MmNotPresentFaultSectionView(AddressSpace,MemoryArea,Address);
break;

case MEMORY_AREA_VIRTUAL_MEMORY://普通型区段
Status = MmNotPresentFaultVirtualMemory(AddressSpace,MemoryArea,Address);
break;
}
}while (Status == STATUS_MM_RESTART_OPERATION);
}

如上只有这几种区段中的页面才有可能被置换到外存去,各种类型的区段的缺页处理都不同,我们看典型的普通型区段的缺页处理;

缺页-页换入

以下是普通型区段的换入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
   NTSTATUS  
MmNotPresentFaultVirtualMemory(AddressSpace,MemoryArea,Address)
{
NTSTATUS win32ExcepCode;//由 cpu 异常码转换后的 win32 异常码
Region=MmFindRegion(MemoryArea->StratinngAddress, MemoryArea->区块链表,Address);
If(Region->Type==MEM_RESERVE || Region->Protect == PAGE_NO_ACCESS)
{
win32ExcepCode==STATUS_ACCESS_VIOLATION;
return win32ExcepCode;
}

If(当前正有其他线程在处理这个页面异常,正在做置换工作)
等待那个线程处理完缺页异常,return succ;
MmRequestPageMemoryConsumer(USER,&pfn);//分配一个空闲物理页面
If(MmIsPageSwapEntry(Address))//if 这个虚拟地址所在的虚拟页面映射到了外存
{
MmDeletePageFileMapping(Address,&SwapEntry);//返回原映射的那个外存页面,然后删除原映射
MmReadFromSwapPage();//将外存页面读入新分配的物理页面中
Pfn.SavedSwapEntry=SwapEntry;//记录本物理页面,当初是从这个页文件调入的
}
//创建映射,将该虚拟页面改映射到新分配的物理页面
MmCreateVirtualMapping(AddressSpace->process, Address, Region->Protect, &pfn 数组,1 个元素)
//将这个虚拟页面插入那个物理页面的映射链表中(多个虚拟页面可映射到同一物理页面)
MmInsertRmap(pfn, AddressSpace->process,Align(Address,4kb)); Return succ;
}



NTSTATUS MmReadFromSwapEntry(SwapEntry,pfn)
{
MDL mdl;

MmBuildMdlFromPages(mdl,pfn);//将物理页面 pfn 映射到系统的 mdl 映射区中
FileNo=SwapEntry.FileNo;
FileOffset=SwapEntry.PageNo * 4kb;
//这个函数内部会构造一个分页读 irp 发往文件系统,最后发给磁盘驱动,读入页文件中对应的页面
Status=IoPageRead(PagingFileList[FileNo]->FileObject, FileOffset,mdl,…);//读入到物理页面
if (Status == STATUS_PENDING)
{
KeWaitForSingleObject(&Event, Executive, KernelMode, FALSE,
NULL //看到没,Timeout参数=NULL,表示一直等到磁盘页面读入完成
);
Status = Iosb.Status;
}

Return status;
}

由于涉及到磁盘 I/O,因此置换过程有点耗时!
频繁的缺页异常往往会造成系统性能瓶颈,这就是时间换空间带来的副作用。

另外:由于MmReadFromSwapEntry这个函数会在内部调用KeWaitForSingleObject一直等到页面读入到内存后才返回原处继续执行。
但是KeWaitForSingleObject这个函数,如果是要等待的话,只能运行在DISPATCH_LEVEL irql以下,否则蓝屏。

这就是为什么在DISPATCH_LEVEL及其以上 irql 时,千万不能访问分页内存。
因为分页内存可能在磁盘中,这样一触发缺页中断
在这个 irql 尝试去读取磁盘页面时,就会因为 KeWaitForSingleObject 的问题而崩溃。

【换言之根源是 DISPATCH 中断级的代码不能调用 KeWaitForSingleObject 等待任意对象】

下面引自 DDK 原话:“Callers of KeWaitForSingleObject must be running at IRQL <= DISPATCH_LEVEL. However, if Timeout = NULL or *Timeout != 0, the caller must be running at IRQL <= APC_LEVEL and in a nonarbitrary thread context.”
看到没只有在Timeout != NULL && *Timeout==0的情况下,才可以在DISPATCH_LEVEL等待


缺页-页换出

每当一个消费者持有的物理页面数量超过自身配额,消费者会主动自我修剪一部分物理页面,置换到外存。

每当系统总体空闲物理内存紧张时(即小于最低空闲页数阀值也即 64 个页面时)
内核中的那个平衡线程也会强制修剪某些物理页面,置换到外存,以腾出一个物理页面出来。
注意并不是物理内存完全耗尽后才开始发生置换操作,而是物理内存快要用完(小于 64 个页面)时,系统就开始着手置换操作了。

下面是置换函数原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
NTSTATUS  MmPageOutVirtualMemory(MADDRESS_SPACE* as,  MEMORY_AREA* Area, PageAddr) 
{
PTE* pt= MmGetPageTableForProcess(process, CurPageAddr);//找到这个页面的 pte 位置
PTE pte=*pt;

PFN pfn=pte.pfn;
SavedSwapEntry = pfn.SavedSwapEntry;
If(pte.bDirty == false)//如果该页面未脏,那好办
{
MmDeleteVirtualMapping(as.process, PageAddr, „);//删除该虚拟页面对应的原 PTE
If(SavedSwapEntry != 0 )//if 该物理页面是从页文件调入的,就直接使用那个页文件
{
//将该虚拟页面对应的 PTE 重定向映射到原先的页文件中的那个页面
MmCreatePageFileMapping(as.process, PageAddr, SavedSwapEntry);
Pfn.SavedSwapEntry = 0;
}

MmReleasePageMemoryConsumer(USER, pfn);//既然换到外存了,那就释放物理页面变成空闲状态
Return succ;
}
Else//如果已经脏了,工作有点多
{
If(SavedSwapEntry == 0 )//if 该物理页面是从页文件调入的,就直接使用那个页文件
NewSwapEntry= MmAllocSwapPage();//从磁盘上的页文件中分配一个文件页面
Else
NewSwapEntry= SavedSwapEntry;//沿用原来的页文件页面

MmWriteToSwapPage(pfn ---> NewSwapEntry);//以分页写 irp 的形式将物理页面内容写入外存页面
MmDeleteVirtualMapping(as.process, PageAddr, „);//删除该虚拟页面对应的原 PTE
MmCreatePageFileMapping(as.process, PageAddr, NewSwapEntry);//重定向
Pfn.SavedSwapEntry = 0;
MmReleasePageMemoryConsumer(USER, pfn);//既然换到外存了,那就释放物理页面变成空闲状态
}

}

平衡线程进行的换出操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
NTSTATUS MiBalancerThread() 
{
WaitObjects[0]=&MiBalanceEvent;
WaitObjects[1]=&MiBalancerTimer;
Whilr(true)
{
Status=KeWaitForMultipleObjects(2,WaitObjects,WaitAny,Executive,KernelMode,„);
If(status==STATUS_SUCCESS)//如果收到了内核发来的一个平衡请求
{
While(系统总空闲页数 < 阀值+5)
调用各个消费者的修剪函数;
}
Else//定时醒来
{

For(遍历每个消费者)
{
If(该消费者占有的物理页数是否超过了自己的配额 || 系统空闲物理页数小于了阀值) 调用它的修剪函数;
}
}
}

}

系统中整个分四大消费者:文件缓冲,用户空间,内核分页池,内核非分页池

看下典型的 User 消费者是如何修剪自己的物理页面的

1
2
3
4
5
6
7
8
9
10
11
12
13
NTSTATUS  MmTrimUserMemory(ToTrimPageCount, ULONG* ActualTrimPageCount) 
{
*ActualTrimPageCount=0;
Pfn=MmGetLRUFirstUserPage();//根据 LRU 算法找到要换出去的物理页面

While(pfn!=0 && ToTrimPageCount>0)
{
MmPageOutPhysicalAddress(pfn);//换出去
*ActualTrimPageCount++;
Pfn=MmGetLRUNextUserPage(pfn);//获得下一个要换出去的物理页面
}
Return succ;
}

置换算法是 LRU,最近以来最少被访问到的物理页面优先换出去。讲述操作系统原理的书籍一般都有介绍,在此不解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
NTSTATUS MmPageOutPhysicalAddress(pfn) 
{
//获得第一个映射到本物理页面的虚拟页面
FirstEntry=MmGetRmapListHeadPage(pfn);
Process=FirstEntry->process;
PageAddr=FirstEntry->address;

If(PageAddr>2GB)
AddressSpace=内核地址空间;
Else
AddressSpace=process->VadRoot;//目标进程的用户空间

MemoryArea = MmLocateMemoryAreaByAddress(AddressSpace, PageAddr);
If(MemoryArea->Type == 视图型区段)//表示 if 这个物理页面是一个共享页面,被多个进程映射共享
{
遍历 pfn.映射链表,一一处理;//特别处理
Return succ;
}
Else if(MemoryArea->Type == 普通型区段)
{
MmPageOutVirtualMemory(„);
}

}


内存映射文件与共享物理内存

相信编写过应用程序的朋友都知道“内存映射文件”一说。
简单地讲内存映射文件就是把磁盘上的文件当做物理内存使用。

这样要读写文件时, 不用再原始地调用ReadFile,WriteFile函数读写文件。
可以直接把文件映射到虚拟内存,然后直接读写虚拟内存即可对文件进行读写。

当一个文件映射到虚拟内存后,一读写对应的虚拟内存,势必引发缺页异常
系统的缺页异常处理函数自动处理,把文件页面调入读入物理内存。
这样就间接地对文件进行了 IO。

除了普通的纯数据文件可以映射到内存外,exe、dll 等可执行文件和磁盘中的页文件也是以内存映射文件的方式进行访问的。
应用层的CreateFileMapping这个 API 就是专用来创建文件映射用的。

除此之外,两个进程也可以共享物理内存,只要把同一个物理页面映射到这两个进程的地址空间即可
物理内存共享也是靠内存映射文件机制实现的,只不过映射的不是普通磁盘文件,而是页文件。

相关结构

内核相关结构定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Struct ROS_SECTION_OBJECT
{
CSHORT type;//本结构体的类型
CSHORT size;//本结构体的实际长度(结构体后面经常可以衔接其他数据,size 包含了那部分的长度)
ULONG protect;//本 section 的保护权限
ULONGLONG MaxSize;//本 section 的最大长度
ULONG AllocationAttributes;//包含了本 section 的文件类型
FILE_OBJECT* FileObject;//创建本 section 的那个文件对象(文件句柄)
Union
{
MM_SECTION_SEGMENT* Segment;//数据文件 section 中的唯一 segment
MM_IMAGE_SECTION_OBJECT* ImageSegments;//镜像文件中的 Segment 数组
} section;
};

如上普通数据文件section内部就包含一个segment
可执行镜像文件(统称 PE 文件)section中一般包含多个segment
对应 PE 文件中的每个“节”, 如.TEXT节, .DATA节,.RSRC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct MM_IMAGE_SECTION_OBJECT 
{
ULONG_PTR ImageBase;
ULONG_PTR StackReserve;
ULONG_PTR StackCommit;
ULONG_PTR EntryPoint;
USHORT Subsystem;
USHORT ImageCharacteristics;
USHORT MinorSubsystemVersion;
USHORT MajorSubsystemVersion;
USHORT Machine;

BOOLEAN Executable;
ULONG NrSegments;// 本ImageSection中的segment个数,也即"节’个数
ULONG ImageSize;
PMM_SECTION_SEGMENT Segments;//本ImageSection中的segment数组
};

参考《Windows PE 权威指南》一书
PE 文件头的节表区中每个节的格式定义为

1
2
3
4
5
6
7
8
9
10
11
12
13
Struct    PE_SEGMENT
{
BYTE Name[IMAGE_SIZEOF_SHORT_NAME=8]; //8 个字节的节名 如".text" ".rdata" ".data"
DWORD VirtualSize;//该节未对齐前的原始数据大小
DWORD VirtualAddress; //该节的 RVA DWORD
SizeOfRawData; //该节的 FAS 也即文件对齐大小,一般指对齐 512B 后的大小
DWORD PointerToRawData; //该节的 FOA,即文件偏移 DWORD
PointerToRelocations; //专用于 obj 文件 DWORD
PointerToLinenumbers; //用于调试
WORD NumberOfRelocations; //专用于 obj 文件
WORD NumberOfLinenumbers; //用于调试
DWORD Characteristics; //该节的属性(可读、可写、可执行、可共享、可丢弃、可分页等属性)
} IMAGE_SECTION_HEADER;

每个节的 Characteristics 的特征属性包括下面几个:

  • IMAGE_SCN_CNT_CODE 该节中包含有代码 如.text
  • IMAGE_SCN_CNT_INITIALIZED_DATA 该节中包含有已初始化的数据 如.data
  • IMAGE_SCN_CNT_UNINITIALIZED_DATA 该节中包含有尚未初始化的数据,如.bss .data?
  • IMAGE_SCN_MEM_DISCARDABLE 该节加载到内存后是可抛弃的,如 dll 中的.reloc 重定位节就是可以抛弃的
  • IMAGE_SCN_MEM_NOT_CACHED 节中数据不会经过缓冲
  • IMAGE_SCN_MEM_NOT_PAGED 该节不准交换到页文件中,sys 文件中的节(除.page)都不可换出
  • IMAGE_SCN_MEM_SHARED 这个节可以被多个进程共享,如 dll 中的共享节。也即表示本节是否允许写复 制。(默认允许)
  • IMAGE_SCN_MEM_EXECUTE 本节可执行 IMAGE_SCN_MEM_READ 本节可读 IMAGE_SCN_MEM_WRITE 本节可写

在内核中,每个节的结构体定义则如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Struct    MM_SECTION_SEGMENT
{
LONG FileOffset;//foa
ULONG VirtualAddress;//其实是相对虚拟地址偏移 rva,不是 va
ULONG RawLength;//本节在文件中的原始实际长度
ULONG Length;//本节对齐后的长度(一般指对齐 4KB)
ULONG protect;//可读、可写、可执行这些保护属性
ULONG ReferenceCount;
SECTION_PAGE_DIRECTORY PageDir;//本节内部的页表,后文有详说 ULONG Flags;

ULONG Characteristics;
BOOL WriteCopy;//本节是否写复制,pe 文件中一般为 TRUE,数据文件中一般为 FALSE
}

Api流程

创建section

要使用内存映射文件, 首先需要创建一个公共的“文件 section”(section 是一种内核对象)
以后谁要访问这个文件,映射这个文件 section 就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

NTSTATUS NtCreateSection(hFile, HANDLE* hSection, DesiredAccess, ObjectAttribute, MaxSize, protect, AllocAttr,)
{
If(ExGetPreviousMode() == UserMode)
基本参数检查;

ROS_SECTION_OBJEC* SectionObject;
//ReactOS 中 SECTION 对象结构体的定义,与 Windows 中有差别
//为指定文件创建一个 section 对象

MmCreateSection(hFile, &SectionObject , DesiredAccess, ObjectAttribute, MaxSize, protect, AllocAttr);
ObInsertObject(SectionObject, …, hSection);//将对象插入对象目录和句柄表,返回它的句柄在 hSection 中
}

NTSTATUS MmCreateSection(hFile, ROS_SECTION_OBJEC** SectionObject , DesiredAccess, ObjectAttribute, MaxSize, protect, AllocAttr,)
{
If(AllocAttr & SEC_IMAGE)
//if 用户给定的 hFile 是一个可执行镜像文件
Return MmCreateImageSection(hFile,SectionObject, DesiredAccess, ObjectAttribute, MaxSize, … )

If(hFile!=NULL)//创建普通数据文件 section
Return MmCreateDataFileSection(hFile,SectionObject, DesiredAccess, ObjectAttribute, MaxSize, … );
Else//用户没给给定文件,说明是要创建一段多进程共享的物理内存
Return MmCreatePageFileSection(hFile,SectionObject, DesiredAccess, ObjectAttribute, MaxSize, … );
}

如上这个函数可以创建三种文件 section
其中若是要创建共享物理内存,就创建页文件 section, 共享物理内存起初是在页文件中的。

可执行文件的 section 创建过程比较繁琐,涉及逐个逐个字段解析文件头,虽然繁杂但是过程简单
在此不详述,更多内容参考《Windows PE 权威指南》
看一下普通数据文件 section 的创建过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
NTSTATUS MmCreateDataFileSection(hFile,SectionObject, DesiredAccess, ObjectAttribute, MaxSize, protect, AllocAttr );

{
FILE_OBJECT* FileObject; MM_SECTION_SEGMENT * Segment; ROS_SECTION_OBJECT* Section;
//创建一个 section 内核对象,返回指针在 Section 中
ObCreateObject(MmSectionObjectType, ObjectAttribute, sizeof(ROS_SECTION_OBJECT), &Section, …);

*SectionObject = Section; Section->protect=protect;
Section->AllocateAttribute=AllocAttr;
ObreferenceObjectByHandle(hFile, IoFileObjecType, &FileObject);//从文件句柄得到对应的文件对象

If(MaxSize==0)
MaxSize=GeFileLen(FileObject);//0 就表示采用默认的文件大小,使用整个文件

If(MaxSize> GeFileLen(FileObject))
//增长磁盘文件的大小
Segment = ExAllocatePool(NonPagePool, sizeof(MM_SECTION_SEGMENT));
Segment->ReferenceCount=1;
Segment->FileOffset=0;
Segment->protect=protect;
Segment->Flags=MM_DATAFILE_SEGMENT;//标志这是普通数据文件中的一个 segment
Segment->WriteCopy=FALSE;//数据文件的 segment 默认不是写复制的,这点与 pe 文件不同
Segment->RawLength=MaxSize;
Segment->Length=Align4kb(MaxSize);
Segment->VirtualAddress=0;//指 rva=0
Section->segment=segment;//普通数据文件就一个 segment,而且整个文件就是一个 segment
Section->MaxSize=MaxSize;//记录本 section 对象的长度
Section->Fileobject=Fileobject;//记录本 section 是由哪个文件对象创建的
FileObject->SectionObjectPointer->DataSectionObject = segment;
Return succ;
}

映射section

创建好了 section 对象后,就可以让任意进程拿去映射了,不过映射是以视图为单位进行的

【section, segment, 视图, 页面】,这是这四者之间的层级关系请牢记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
NTSTATUS NtMapViewOfSection(hSection, ViewOffset, ViewSize,	AllocType, protect,    hProcess, void** BaseAddr )
{
PreviousMode=ExGetPreviousMode();
If(PreviousMode == UserMode)
参数检查;
ViewOffset=Align4kb(ViewOffset);
ViewSize=Align4kb(ViewSize);
ObReferenceObjectByHandle(hSection---> Section);//获得对应的对象
MmMapViewOfSection(Section, ViewOffset,ViewSize, AllocType, protect, hProcess, void** BaseAddr );
}




NTSTATUS NtMapViewOfSection(Section, ViewOffset, ViewSize, AllocType, protect, hProcess, void** BaseAddr )
{

AddressSpace=process->VadRoot;//那个进程的用户地址空间
// 若是 PE 文件的 section,则加载映射文件中的每个 segment
// 注意此时的 ViewOffset 和 ViewSize 参数不 起作用,将自动把每个完整 segment 当做一个视图来映射。
If(Section->AllocationAttribute & SEC_IMAGE)
{
ULONG i;
ULONG NrSegments;
ULONG_PTR ImageBase;
ULONG ImageSize;
PMM_IMAGE_SECTION_OBJECT ImageSectionObject;
PMM_SECTION_SEGMENT SectionSegments;

ImageSectionObject = Section->ImageSection;
SectionSegments = ImageSectionObject->Segments;//节数组
NrSegments = ImageSectionObject->NrSegments;//该pe文件中的节数
ImageBase = (ULONG_PTR)*BaseAddress;
if (ImageBase == 0)
ImageBase = ImageSectionObject->ImageBase;
ImageSize = 0;

//下面的循环遍历该pe文件中所有需要加载的节,计算所有节的大小总和
for (i = 0; i < NrSegments; i++)
{
if (!(SectionSegments[i].Characteristics & IMAGE_SCN_TYPE_NOLOAD))//所需要加载这个节
{
ULONG_PTR MaxExtent;
//该节的rva+该节的对齐4KB长度
MaxExtent=SectionSegments[i].VirtualAddress + SectionSegments[i].Length;
ImageSize = max(ImageSize, MaxExtent);
}
}
ImageSectionObject->ImageSize = ImageSize;

//如果该pe文件期望加载的区域中有任何一个地方被占用了,重定位,dll文件一般都会重定位
if (MmLocateMemoryAreaByRegion(AddressSpace, ImageBase,PAGE_ROUND_UP(ImageSize)))
{
if ((*BaseAddress) != NULL)//如果用户的要求是必须加载到预期地址处,返回失败!
return(STATUS_UNSUCCESSFUL);
ImageBase = MmFindGap(AddressSpace, ImageSize, PAGE_SIZE, FALSE);//重定位,找空闲区
}


//一次性加载映射该pe文件中的所有节
for (i = 0; i < NrSegments; i++)
{
//注意pe文件中有的节是不用加载的
if (!(SectionSegments[i].Characteristics & IMAGE_SCN_TYPE_NOLOAD))
{
PVOID SBaseAddress = ((char*)ImageBase + (SectionSegments[i].VirtualAddress);
//把该节整体当做一个view进行映射。由此可见,pe文件中的每个节也是一个视图型区段
MmMapViewOfSegment(AddressSpace,
Section,
&SectionSegments[i],//该视图所在的第一个节
&SBaseAddress,//该节的预期映射地址
SectionSegments[i].Length,//ViewSize=整个节的长度
SectionSegments[i].Protection,
0,//ViewOffset=0
0);
}
}
*BaseAddress = (PVOID)ImageBase;//返回该 PE 文件实际加载映射的地址
}
Else//普通数据文件和页文件的 section,都只有一个 segment
{
MmMapViewOfSegment(AddressSpace, section, section->segmen, ViewOffSet, ViewSize ,
AllocType & MEM_TOPDOWN, protect, hProcess, void** BaseAddr);
}

}





NTSTATUS MmMapViewOfSegment(AddressSpace, section , segment, ViewOffset, ViewSize, AllocType, protect, hProcess,void** BaseAddr)
{
MEMORY_AREA* Area;
MmCreateMemoryArea(AddressSpace, 视图型区段, BaseAddr,ViewSize, protect, AllocType, &Area);

//记录本视图区段映射的是哪个 section 的哪个 segment 中的哪个位置
Area->Data.SectionData.Section=Section;
Area->Data.SectionData.Segment=segment;
Area->Data.SectionData.ViewOffset=ViewOffset;
Area->Data.SectionData..WriteCopyView=FALSE;//视图型区段默认是不"写复制’的 初始化 Area 区段中的区块链表;
//初始时,整个区段中就一个区块
}

如上将文件中的的某个 segment 中的某部分视图映射到虚拟内存后
视图中的这些虚拟页面的初始时的 PTE 尚是空白的,Cpu 一访问视图区段中的虚拟页面,立马引发缺页异常。
系统的缺页异常处理函数此时就会自动将对应的那些文件页面读入内存中。

之前我们看过了普通型区段的缺页异常处理流程,现在是时候看一下视图型区段的缺页处理流程了


缺页异常处理-视图型区段

回顾一下

当发生缺页异常时,将进入缺页异常处理函数,再进入MmAccessFault()函数,再进入MmNotPresentFault 函数
这个函数根据发生缺页的虚拟页面所在的区段类型进入现在的“视图区段页面异常处理函数”

即下面的函数, 看看是怎么处理这种异常的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
NTSTATUS MmNotPresentFaultSectionView(AddressSpace,    MemoryArea,    Addr, …)
{
PageAddr=Align4kb(Addr);//缺页内存单元所在的虚拟页面

//计算这个页面在所属 segment 内的偏移
Offset = PageAddr - MemoryArea->StartingAddress + MemoryArea->Data.SectionData.ViewOffset;
Segment=MemoryArea->Data.SectionData.Segment;//该页面所在 segment


If(该页面的 PTE 映射到了 页文件)//还记得有一种 section 是页文件 section 吧?
按普通页文件异常处理方式处理;//前文已讲,不再赘述
Else
{
//在这个 segment 内部维护的那个页面映射表中找到这个虚拟页面的映射描述符
Entry = MmGetPageEntrySectionSegment(Segment, Offset);
PFN Pfn;
If(*Entry==0)//空白,表示既未映射到物理页面,也未映射到文件中的页面
{
FileOffset=ConvertTo(Segment, Offset);//将 segment 内的偏移转换成文件中的偏移
Pfn = 分配一个空闲物理页面;
MiReadPage(MemoryArea, FileOffset, pfn);//读入文件中对应的页面到内存中
*Entry=Pfn;//将分得的物理页面保存到这个映射描述符中,方便其他线程使用
}
Else if( Entry 是一个文件页面) //最典型的情况
{
Entry=ConvertToSwapEntry(Entry);//转换格式
Pfn=分配一个空闲物理页面;
MmReadFromSwapEntry(Entry, Pfn);//读入到内存
*Entry=Pfn;//将分得的物理页面保存到这个映射描述符中,方便其他线程使用
}
Else
//刚好是一个物理页面
Pfn=*Entry;
MmCreateVirtualMapping(PageAddr <------>Pfn);//建立映射
MmInsertRmap(pfn, CurProcess,PageAddr);//将这个虚拟页面添加到那个物理页面的映射链表中
}

}

每个segment内部也有一个页面映射表,描述了本segment内部各个虚拟页面的映射情况。 表中的每个映射描述符,要么映射到物理页面,要么映射到普通文件页面(注意不是页文件),要么为空
其工作原理与进程的页表是相同的。

为什么多出来一个 segment 页表呢?
根本原因就是普通页表中的 PTE, 无法映射到普通数据文件, 映射描述符的格式不一样。


驱动程序分配内存

最后看一个非常常见的内核函数(供驱动程序用来分配内存的日常函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Void*   ExAllocatePool(PoolType,  NumberOfBytes)
{
Return ExAllocatePoolWithTag(PoolType, NumberOfBytes, 'NONE');
}

Void* ExAllocatePoolWithTag(PoolType, NumberOfByte, tag)
{
if (NumberOfBytes > PageSize-BlockHeadSize)//超出一个页面
{
//大于一个页面大小的分配特殊处理;
Retun MiAllocatePoolPages(PoolType, NumberOfBytes);;
}
For(遍历空闲块表)
{
If(找到了一个合乎大小的空闲块)
{
从空闲块链表中摘下一个合乎大小的块;
前后合并相邻块;//是在一个页面内分隔、合并
Return 找到的块地址;
}
}
//如果已有的空闲链表找不到这样一个大小的块
在池中分配一个新的页面;
在新页面中把前面的部分割出来,后面剩余的部分挂入池中的空闲块表中;
Return 分得的块地址
}

内核中池的分配原理同用户空间中的堆一样,都是先用VirtuAllocate去分配一个页面,然后在这个页面中寻找空闲块,分给用户。

每个池块的块头含有一些附加信息,如这个池块的大小,池类型,该池块的 tag 标记等信息。
用户空间中的malloc,new堆块分配函数,都是调用HeapAlloc API函数从堆管理器维护的 N 个 虚拟页面中分出一些零散的块出来, 每个堆块的块头、块尾也含有一些附加信息
如堆块大小,防止堆块溢出的cookie等信息。

堆管理器则在底层调用VirtualAllocAPI分配,增长虚拟页面,提供底层服务。

详细的用户空间中的堆分配原理请参考: 张银奎-《软件调试》一书




内核对象

写过Windows 应用程序的朋友都常常听说“内核对象”、“句柄”等术语却无从得知他们的内核实现到底是怎样的, 本篇文章就揭开这些技术的神秘面纱。

常见的内核对象

常见的内核对象有

  • Job、Directory(对象目录中的目录)
  • SymbolLink(符号链接)
  • Section(内存映射文件)
  • Port(LPC 端口)、
  • IoCompletion(Io 完成端口)
  • File(并非专指磁盘文件)
  • 同步对象(Mutex、Event、Semaphore、Timer)、
  • Key(注册表中的键)
  • Token(用户/组令牌)
    = Process、Thread、Pipe、Mailslot、Debug(调试端口)等

内核对象就是一个数据结构,就是一个struct 结构体
各种不同类型的对象有不同的定义,本片文章不专门介绍各个具体对象类型的结构体定义,只讲述一些公共的对象管理机制。

所有内核对象都遵循统一的使用模式

  • 第一步:先创建对象;
  • 第二步:打开对象,得到句柄(可与第一步合并在一起,表示创建时就打开)
  • 第三步:通过 API 访问对象;
  • 第四步:关闭句柄,递减引用计数;
  • 第五步:句柄全部关完并且引用计数降到 0 后,销毁对象。

句柄就是用来维系对象的把柄,就好比 N 名纤夫各拿一条绳,同拉一艘船。
每打开一次对象就可拿到一个句柄,表示拿到该对象的一次访问权。
内核对象是全局的,各个进程都可以访问

比如两个进程想要共享某块内存来进行通信,就可以约定一个对象名
例如一个进程可以用CreatFileMapping("SectionName")创建一个 section, 而另一个进程可以用 OpenFileMapping("SectionName")打开这个 section,这样这个 section 就被两个进程共享了。

(注意:本篇说的都是内核对象的句柄。像什么 hWnd、hDC、hFont、hModule、hHeap、hHook 等等其他 句柄,并不是指内核对象,因为这些句柄值不是指向进程句柄表中的索引,而是另外一种机制)




对象头

各个对象的结构体虽然不同,但有一些通用信息记录在对象头中,看下面的结构体定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef struct _OBJECT_HEADER 
{
LONG PointerCount;//引用计数
union
{
LONG HandleCount;//本对象的打开句柄计数(每个句柄本身也占用一个对象引用计数)
volatile VOID* NextToFree;//下一个要延迟删除的对象
};

OBJECT_TYPE* Type;//本对象的类型,类型本身也是一种内核对象,因此我习惯叫"类型对象"
UCHAR NameInfoOffset;//对象名的偏移(无名对象没有Name)
UCHAR HandleInfoOffset;//各进程的打开句柄统计信息数组
UCHAR QuotaInfoOffset;//对象本身实际占用内存配额(当不等于该类对象的默认大小时要用到这个)
UCHAR Flags;//对象的一些属性标志

union
{
OBJECT_CREATE_INFORMATION* ObjectCreateInfo;//来源于创建对象时的OBJECT_ATTRIBUTES
PVOID QuotaBlockCharged;
};

PSECURITY_DESCRIPTOR SecurityDescriptor;//安全描述符(对象的拥有者、ACL等信息)
QUAD Body;//通用对象头后面紧跟着真正的结构体(这个字段是后面真正结构体中的第一个成员)
} OBJECT_HEADER, *POBJECT_HEADER;

如上Body 就是对象体中的第一个字段,头部后面紧跟具体对象类型的结构体定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct _OBJECT_HEADER_NAME_INFO 
{
POBJECT_DIRECTORY Directory;//对象目录中的父目录(不一定是文件系统中的目录)
UNICODE_STRING Name;//相对于Directory的路径或者全路径
ULONG QueryReferences;//对象名查询操作计数

} OBJECT_HEADER_NAME_INFO, *POBJECT_HEADER_NAME_INFO;

typedef struct _OBJECT_HEADER_CREATOR_INFO
{
LIST_ENTRY TypeList;//用来挂入所属"对象类型’中的链表(也即类型对象内部的对象链表)
PVOID CreatorUniqueProcess;//表示本对象是由哪个进程创建的

} OBJECT_HEADER_CREATOR_INFO, *POBJECT_HEADER_CREATOR_INFO;

对象头中记录了NameInfoHandleInfoQuotaInfoCreatorInfo 这 4 种可选信息。
如果这 4 种可选信息全部都有的话,整个对象的布局从低地址到高地址的内存布局为:

QuotaInfo-> HandleInfo->NameInfo->CreatorInfo->对象头->对象体

这 4 种可选信息的相对位置倒不重要,但是必须记住,他们都是在对象头中的上方(也即对象头上面的低地址端)。
以下为了方便不妨叫做“对象头中的可选信息”、“头部中的可选信息”。

于是有宏定义:

1
2
3
4
5
6
7
8
9
10
//由对象体的地址得到对象头的地址 
#define OBJECT_TO_OBJECT_HEADER(pBody) CONTAINING(pBody,OBJECT_HEADER,Body)

//得到对象的名字
#define OBJECT_HEADER_TO_NAME_INFO(h)
h->NameInfoOffset?(h - h->NameInfoOffset):NULL

//得到对象的创建者信息
#define OBJECT_HEADER_TO_CREATOR_INFO(h)
h->Flags & OB_FLAG_CREATOR_INFO?h-sizeof(OBJECT_HEADER_CREATOR_INFO):NULL

所有有名字的对象都会进入内核中的对象目录中,对象目录就是一棵树。
内核中有一个全局指针变量ObpRootDirectoryObject,就指向对象目录树的根节点,根节点是一个根目录。

对象目录的作用就是用来将对象路径解析为对象地址。
给定一个对象路径,就可以直接在对象目录中找到对应的对象。
就好比给定一个文件的全路径,一定能从磁盘的根目录中向下一直搜索找到对应的文件。

1
2
3
如某个设备对象的对象名(全路径)是`"\Device\MyCdo"`
那么从根目录到这个对象的路径中:`Device 是根目录中的子目录,MyDevice 则是 Device 目录中的子节点。`
对象有了名字,应用程序就可以直接调用 CreateFile 打开这个对象,获得句柄,没有名字的对象无法记录到

对象目录中,应用层看不到,只能由内核自己使用。




对象目录

内核中各种类型的对象在对象目录中的位置:

  • 目录对象: 最常见就是对象目录中的目录节点(可以作为叶节点)
  • 普通对象: 只能作为叶节点
  • 符号链接对象: 只能作为叶节点

注意文件对象和注册表中的键对象看似有文件名、键名,但此名非对象名。因此文件对象与键对象是无名的,无法进入对象目录中

根目录也是一种目录对象,符号链接对象可以链接到对象目录中的任何节点,包括又链向另一个符号链接对象。
对象目录中,每个目录节点下面的子节点可以是

  • 1、 普通对象节点
  • 2、 子目录
  • 3、 符号链接

该目录中的所有子节点对象都保存在该目录内部的目录项列表中。不过这个列表不是一个简单的数组,而是一个开式hash 表用来方便查找。
根据该目录中各个子对象名的hash值,将对应的子对象挂入对应的hash链表中,用hash方式存储这些子对象以提高查找效率
目录本身也是一种内核对象,其类型就叫"目录类型"
现在就可以看一下这种对象的结构体定义

1
2
3
4
5
6
7
typedef struct _OBJECT_DIRECTORY 
{
struct _OBJECT_DIRECTORY_ENTRY* HashBuckets[37];//37条hash链
EX_PUSH_LOCK Lock;
struct _DEVICE_MAP *DeviceMap;

} OBJECT_DIRECTORY, *POBJECT_DIRECTORY;

如上目录对象中的所有子对象按hash值分门别类的安放在该目录内部不同的hash链中 其中每个目录项的结构体定义为:

1
2
3
4
5
6
typedef struct _OBJECT_DIRECTORY_ENTRY 
{
struct _OBJECT_DIRECTORY_ENTRY * ChainLink;//下一个目录项(即下一个子节点)
PVOID Object;//对象体的地址
ULONG HashValue;//所在hash链
} OBJECT_DIRECTORY_ENTRY, *POBJECT_DIRECTORY_ENTRY;

看到没每个目录项记录了指向的对象的地址,同时间接记录了对象名信息
下面这个函数用来在指定的目录中查找指定名称的子对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
VOID* ObpLookupEntryDirectory(IN POBJECT_DIRECTORY Directory, 
IN PUNICODE_STRING Name,
IN ULONG Attributes,
IN POBP_LOOKUP_CONTEXT Context)
{

BOOLEAN CaseInsensitive = FALSE;
PVOID FoundObject = NULL;

//表示对象名是否严格大小写匹配查找
if (Attributes & OBJ_CASE_INSENSITIVE) CaseInsensitive = TRUE;

HashValue=CalcHash(Name->Buffer);//计算对象名的hash值
HashIndex = HashValue % 37;//获得对应的hash链索引

//记录本次是在那条hash中查找
Context->HashValue = HashValue;
Context->HashIndex = (USHORT)HashIndex;
if (!Context->DirectoryLocked)
ObpAcquireDirectoryLockShared(Directory, Context);//锁定目录,以便在其中进行查找操作

//遍历对应hash链中的所有对象
AllocatedEntry = &Directory->HashBuckets[HashIndex];
LookupBucket = AllocatedEntry;
while ((CurrentEntry = *AllocatedEntry))
{
if (CurrentEntry->HashValue == HashValue)
{
ObjectHeader = OBJECT_TO_OBJECT_HEADER(CurrentEntry->Object);
HeaderNameInfo = OBJECT_HEADER_TO_NAME_INFO(ObjectHeader);

if ((Name->Length == HeaderNameInfo->Name.Length) &&
(RtlEqualUnicodeString(Name, &HeaderNameInfo->Name, CaseInsensitive)))
{
break;//找到对应的子对象
}
}
AllocatedEntry = &CurrentEntry->ChainLink;
}

if (CurrentEntry)//如果找到了子对象
{
if (AllocatedEntry != LookupBucket)
// 将找到的子对象挂入链表的开头,方便下次再次查找同一对象时直接找到;
FoundObject = CurrentEntry->Object;
}

if (FoundObject) //如果找到了子对象
{
ObjectHeader = OBJECT_TO_OBJECT_HEADER(FoundObject);
ObpReferenceNameInfo(ObjectHeader);//递增对象名字的引用计数
ObReferenceObject(FoundObject);//注意递增了对象本身的引用计数

if (!Context->DirectoryLocked)
ObpReleaseDirectoryLock(Directory, Context);
}

//检查本次函数调用前,查找上下文中是否已有一个先前的中间节点对象,若有就释放
if (Context->Object)
{
ObjectHeader = OBJECT_TO_OBJECT_HEADER(Context->Object);
HeaderNameInfo = OBJECT_HEADER_TO_NAME_INFO(ObjectHeader);
ObpDereferenceNameInfo(HeaderNameInfo);
ObDereferenceObject(Context->Object);
}
Context->Object = FoundObject;
return FoundObject;//返回找到的子对象
}

如上hash查找子对象,找不到就返回 NULL。
注意由于这个函数是在遍历路径的过程中逐节逐节的调用的,所以会临时查找中间的目录节点,记录到Context中。


对象类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
typedef struct _OBJECT_TYPE 
{
ERESOURCE Mutex;
LIST_ENTRY TypeList;//本类对象的链表,记录所有同类对象
UNICODE_STRING Name;//类型名
PVOID DefaultObject;//指本类对象默认使用的同步事件对象
ULONG Index;//本类型的索引,也即表示这是系统中第几个注册的对象类型
ULONG TotalNumberOfObjects;//对象链表中总的对象个数
ULONG TotalNumberOfHandles;//所有同类对象的打开句柄总数
ULONG HighWaterNumberOfObjects;//历史本类对象个数峰值
ULONG HighWaterNumberOfHandles; //历史本类对象的句柄个数峰值

//关键字段。创建类型对象时,会将类型信息拷贝到下面这个字段中
OBJECT_TYPE_INITIALIZER TypeInfo;
ULONG Key;//事实上用作内存分配的tag,同类对象占用的内存块都标记为同一个tag
ERESOURCE ObjectLocks[4];
} OBJECT_TYPE;

typedef struct _OBJECT_TYPE_INITIALIZER
{
USHORT Length;//本结构体本身的长度
BOOLEAN UseDefaultObject;//是否使用全局默认的同步事件对象
BOOLEAN CaseInsensitive;//指本类对象的对象名是否大小写不敏感

ULONG InvalidAttributes;//本类对象不支持的属性集合
GENERIC_MAPPING GenericMapping;//一直懒得去分析这个字段
ULONG ValidAccessMask;// 本类对象支持的属性集合
BOOLEAN SecurityRequired;//本类对象是否需要安全控制(另外:凡是有名字的对象都需要安全控制)
BOOLEAN MaintainHandleCount;//对象头中是否维护句柄统计信息
BOOLEAN MaintainTypeList;//是否维护创建者信息(也即是否需要挂入到所属对象类型的链表中)

POOL_TYPE PoolType;//本类对象位于分页池还是非分页池(一般内核对象都分配在非分页池中)
ULONG DefaultPagedPoolCharge; //对象占用的分页池总体大小
ULONG DefaultNonPagedPoolCharge;//对象占用的非分页池总体大小

OB_DUMP_METHOD DumpProcedure;//?
OB_OPEN_METHOD OpenProcedure;//打开对象时调用,非常重要
OB_CLOSE_METHOD CloseProcedure;//关闭句柄时调用,非常重要
OB_DELETE_METHOD DeleteProcedure;//销毁对象时调用,非常重要
OB_PARSE _METHOD ParseProcedure;//自定义的路径解析函数(设备、文件、键都提供了此函数)
OB_SECURITY_METHOD SecurityProcedure;//查询、设置对象安全描述符的函数
OB_QUERYNAME_METHOD QueryNameProcedure;//文件对象提供了自定义的QueryNameString函数
OB_OKAYTOCLOSE_METHOD OkayToCloseProcedure;//每次关闭句柄前都会调用这个函数检查可否关闭
} OBJECT_TYPE_INITIALIZER, *POBJECT_TYPE_INITIALIZER;

Windows 内核中有许多预定义的对象类型,程序员也可以自己注册一些自定义的对象类型,就像自注册”窗口类”一样。
下面这个函数用来注册一种对象类型(注意对象类型本身也是一种内核对象,因此”对象类型”即是”类型对象”, “类型对象”即是”对象类型”)

自注册对象类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
NTSTATUS ObCreateObjectType(IN PUNICODE_STRING TypeName, 
IN POBJECT_TYPE_INITIALIZER ObjectTypeInitializer,
OUT POBJECT_TYPE *ObjectType)
{
ObpInitializeLookupContext(&Context);

//若 \ObjectTypes 目录下已经创建过了这种对象类型。返回失败
ObpAcquireDirectoryLockExclusive(ObpTypeDirectoryObject, &Context);
if (ObpLookupEntryDirectory(ObpTypeDirectoryObject,
TypeName,
OBJ_CASE_INSENSITIVE,
FALSE,
&Context))
{
ObpReleaseLookupContext(&Context);
return STATUS_OBJECT_NAME_COLLISION;//不能重复创建同一种对象类型
}


ObjectName.Buffer = ExAllocatePoolWithTag(PagedPool,TypeName->MaximumLength,tag);
ObjectName.MaximumLength = TypeName->MaximumLength;
RtlCopyUnicodeString(&ObjectName, TypeName);

//分配一块内存,创建类型对象
Status = ObpAllocateObject(NULL, //CreateInfo=NULL
&ObjectName,//对象的名字
ObpTypeObjectType,//类型对象本身的类型
sizeof(OBJECT_TYPE),//对象的大小
KernelMode,
(POBJECT_HEADER*)&Header);
LocalObjectType = (POBJECT_TYPE)&Header->Body;
LocalObjectType->Name = ObjectName;//类型对象的自身的名称
Header->Flags |= OB_FLAG_KERNEL_MODE | OB_FLAG_PERMANENT;//类型对象全由内核创建并有永久 性

LocalObjectType->TotalNumberOfObjects =0;
LocalObjectType->TotalNumberOfHandles =0; //本类对象的个数与句柄个数=0
//拷贝类型信息(这个TypeInfo就是类型描述符)
LocalObjectType->TypeInfo = *ObjectTypeInitializer;
LocalObjectType->TypeInfo.PoolType = ObjectTypeInitializer->PoolType;

//类型对象的对象体上面的所有头部大小
HeaderSize = sizeof(OBJECT_HEADER) +
sizeof(OBJECT_HEADER_NAME_INFO)+(ObjectTypeInitializer->MaintainHandleCount
?sizeof(OBJECT_HEADER_HANDLE_INFO) : 0);
if (ObjectTypeInitializer->PoolType == NonPagedPool)
LocalObjectType->TypeInfo.DefaultNonPagedPoolCharge += HeaderSize;
else
LocalObjectType->TypeInfo.DefaultPagedPoolCharge += HeaderSize;

//查询、设置对象安全描述符的函数
if (!ObjectTypeInitializer->SecurityProcedure)
LocalObjectType->TypeInfo.SecurityProcedure = SeDefaultObjectMethod;

if (LocalObjectType->TypeInfo.UseDefaultObject)
{
LocalObjectType->TypeInfo.ValidAccessMask |= SYNCHRONIZE;//本对象可用于同步操作
LocalObjectType->DefaultObject = &ObpDefaultObject;//其实是个全局的Event对象
}
//文件对象的结构体中可自带一个事件对象,WaitForSingleObject(FileObject)等待的就是那个事件
else if ((TypeName->Length == 8) && !(wcscmp(TypeName->Buffer, L"File")))
LocalObjectType->DefaultObject =FIELD_OFFSET(FILE_OBJECT,Event);//偏移
else if ((TypeName->Length == 24) && !(wcscmp(TypeName->Buffer, L"WaitablePort")))
LocalObjectType->DefaultObject = FIELD_OFFSET(LPCP_PORT_OBJECT,WaitEvent);//偏移
else
LocalObjectType->DefaultObject = NULL;

InitializeListHead(&LocalObjectType->TypeList);
CreatorInfo = OBJECT_HEADER_TO_CREATOR_INFO(Header);

if (CreatorInfo) //将这个类型对象注册、加入全局链表中,注意这两个TypeList的含义是不一样的
InsertTailList(&ObpTypeObjectType->TypeList,&CreatorInfo->TypeList); LocalObjectType->Index = ObpTypeObjectType->TotalNumberOfObjects;
//将这个类型对象加入全局数组中
if (LocalObjectType->Index < 32)//对象类型较少,一般够用
ObpObjectTypes[LocalObjectType->Index - 1] = LocalObjectType;

//将类型对象插入 \ObjectTypes 目录中(目录内部的指定hash链中)
bSucc=ObpInsertEntryDirectory(ObpTypeDirectoryObject, &Context, Header);
if (bSucc)
{
ObpReleaseLookupContext(&Context);
*ObjectType = LocalObjectType;
return STATUS_SUCCESS;
} Else
{
ObpReleaseLookupContext(&Context);
return STATUS_INSUFFICIENT_RESOURCES;
}
}

如上大致的流程就是创建一个对象类型,然后加入对象目录中的\ObjectTypes目录中。

内核中的对象管理器在初始化的时候

会初始化对象目录。先注册创建名为"Directory""SymbolicLink"的对象类型, 然后在对象目录中创建根目录"\","\ObjectTypes"目录,"\DosDevices"目录等预定义目录。

内核中的IO管理器在初始化的时候

先会注册创建名为"Device""File""Driver"等对象类型
由于对象类型本身也是一种有名字的对象,所以也会挂入对象目录中
位置分别为:"\ObjectTypes\Device", “\ObjectTypes\File","\ObjectTypes\Driver"于是,我们的驱动就可以创建对应类型的对象了。

下面我们具体看几个重点对象类型的创建过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
OBJECT_TYPE_INITIALIZER  Oti = {0}; 
Oti.Length=sizeof(OBJECT_TYPE_INITIALIZER);
Oti.UseDefaultObject=TRUE;
Oti.MaintainTypeList=TRUE;//一般都会维持类型信息,加入到所属类型的链表中
Oti.PoolType=NonPagePool;//所有内核对象,都默认分配在非分页池中
Oti.InvalidAttributes=OBJ_OPENLINK;//一般都不许打开符号链接
Oti.DefaultNonPagePoolCharge=sizeof(OBJECT_DIRECTORY);
Oti.UseDefaultObject=FALSE;
… ObCreateObjectType("Directory", &oti, &ObpDirectoryType);//创建普通的目录对象类型


Oti.DefaultNonPagePoolCharge=sizeof(OBJECT_SYMBOLIC_LINK);
Oti.ValidAccessMask=SYMBOLIC_LINK_ALL_ACCESS;
Oti.ParseProcedure=ObpParseSymbolicLink;//关键字段。自定义解析后面的路径名
Oti.DeleteProcedure=ObpDeleteSymbolicLink;
… ObCreateObjectType("SymbolicLink",&oti,&ObSymbolicLinkType);//创建符号链接对象类型


Oti.DefaultNonPagePoolCharge=sizeof(DEVICE_OBJECT);
Oti.ParseProcedure=IopParseDevice; //关键字段。自定义解析后面的路径名
Oti.SecurityProcedure=IopSecurityFile;
… ObCreateObjectType("Device", &oti, &IoDeviceObjectType);//创建设备对象类型



Oti.DefaultNonPagePoolCharge=sizeof(FILE_OBJECT);
Oti.UseDefaultObject=FALSE;//每个文件对象内部有一个自己的事件对象
Oti.ParseProcedure=IopParseFile; //关键字段。自定义解析后面的路径名
Oti.SecurityProcedure=IopSecurityFile;
Oti.QueryNameProcedure=IopQueryNameFile;//文件对象自己负责
ObQueryNameString Oti.CloseProcedure=IopCloseFile;//关闭文件句柄时调用的函数(句柄关完后生成 MJ_Cleanup irp) Oti.DeleteProcedure=IopDeleteFile;//销毁文件对象时调用的函数(对象销毁前生成 MJ_Close irp)
… ObCreateObjectType("File", &oti, &IoFileObjectType);//创建文件对象类型

我们看到,符号链接设备文件这三类对象都提供了自定义的路径解析函数。
(后文中,这册表键对象也会提供一个自定义解析函数)因为这几种对象,对象后面的剩余路径并不在对象目录中,对象目录中的 叶节点到这几种对象就是终点了。

比如物理磁盘卷设备对象上的某一文件路径"\Device\Harddisk0\Partition0\Dir1\Dir2\File.txt"的解析过程是

  • 先顺着对象目录中的根目录,按\Device\Harddisk0\Partition0这个路径解析到这一层,找到对应的卷设备对象
  • 再后面剩余的路径Dir1\Dir2\File.txt就由具体的文件系统去解析了,最终找到对应的文件对象

另外注意一下,文件对象在句柄关完后,将产生一个IRP_MJ_CLEANUP; 文件对象在引用减到 0 后,销毁前将产生IRP_MJ_CLOSE
这就是这两个 irp 的产生时机。

简单记忆【柄完清理,引完关闭】


句柄

任意进程,只要每打开一个对象就会获得一个句柄,这个句柄用来标志对某个对象的一次打开,通过句 柄,可以直接找到对应的内核对象。
句柄本身是进程的句柄表中的一个结构体,用来描述一次打开操作。
句柄值则可以简单看做句柄表中的索引,并不影响理解。
HANDLE的值可以简单的看做一个整形索引值。 每个进程都有一个句柄表,用来记录本进程打开的所有内核对象。
句柄表可以简单看做为一个一维数组, 每个表项就是一个句柄,一个结构体,一个句柄描述符

其结构体定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct _HANDLE_TABLE_ENTRY //句柄描述符 
{
union
{
PVOID Object;//关键字段。该句柄指向的内核对象(注意是其头部)
ULONG_PTR ObAttributes;//关键字段。该句柄的属性
PHANDLE_TABLE_ENTRY_INFO InfoTable;
ULONG_PTR Value;//值(可见值本身是一个复合体),最低3位表示该句柄的属性(Value= Obje ct | ObAttributes)
};
union
{
ULONG GrantedAccess;//关键字段。该句柄的访问权限
struct
{
USHORT GrantedAccessIndex;
USHORT CreatorBackTraceIndex;
};
LONG NextFreeTableEntry;//当本句柄是一个空闲表项时,用来链接到句柄表中下一个空闲表项
};
} HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;

句柄表则定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typedef struct _HANDLE_TABLE   //句柄表描述符 
{
ULONG TableCode; //表的地址|表的层数(该字段的最后两位表示表的层数)
PHANDLE_TABLE_ENTRY **Table;
PEPROCESS QuotaProcess;//所属进程
PVOID UniqueProcessId; //所属进程的PID
EX_PUSH_LOCK HandleTableLock[4];
LIST_ENTRY HandleTableList;//用来挂入全局的句柄表链表(间接给出了系统中的进程列表)
EX_PUSH_LOCK HandleContentionEvent;
ERESOURCE HandleLock;
LIST_ENTRY HandleTableList;
KEVENT HandleContentionEvent;
PHANDLE_TRACE_DEBUG_INFO DebugInfo;
LONG ExtraInfoPages;
ULONG FirstFree;//第一个空闲表项的索引位置
ULONG LastFree;//最后一个空闲表项的索引位置
ULONG NextHandleNeedingPool;//本句柄表本身占用的内存页数
LONG HandleCount;//表中的有效句柄总数

union
{
ULONG Flags;
UCHAR StrictFIFO:1;

};
} HANDLE_TABLE, *PHANDLE_TABLE;

进程的EPROCESS结构体中有一个字段HANDLE_TABLE* ObjectTable;指的就是该进程的句柄表

1
2
3
4
5
6
7
HANDLE ExCreateHandle(PHANDLE_TABLE HandleTable,   PHANDLE_TABLE_ENTRY HandleTableEntry) 
{
EXHANDLE Handle;
NewEntry = ExpAllocateHandleTableEntry(HandleTable,&Handle);//在句柄表中找到一个空闲表项
*NewEntry = *HandleTableEntry;//复制句柄表项
return Handle.GenericHandleOverlay;//返回句柄值(也即空闲表项的索引位置)
}

上面这个函数与其说是创建一个句柄,不如说是插入一个句柄。
在指定句柄表中找到一个空闲未用的表项,然后将句柄插入到那个位置,最后返回句柄的"索引"

下面这个函数用来打开对象,获得句柄

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
NTSTATUS ObpCreateHandle(IN OB_OPEN_REASON OpenReason,//4种打开时机 
IN PVOID Object, //要打开的对象
IN PACCESS_STATE AccessState, //句柄的访问权限
IN ULONG HandleAttributes, //句柄的属性
IN KPROCESSOR_MODE AccessMode,
OUT PHANDLE ReturnedHandle) //返回的句柄值
{
BOOLEAN AttachedToProcess = FALSE, KernelHandle = FALSE;
NewEntry.Object = ObjectHeader;//关键。将该句柄指向对应的对象头

if (HandleAttributes & OBJ_KERNEL_HANDLE)//如果用户要求创建一个全局型的内核句柄
{
HandleTable = ObpKernelHandleTable;//改用内核句柄表
KernelHandle = TRUE;

//将当前线程挂靠到system进程,也即修改当前的CR3,将页表换成system进程的页表
if (PsGetCurrentProcess() != PsInitialSystemProcess)
{
KeStackAttachProcess(&PsInitialSystemProcess->Pcb, &ApcState);
AttachedToProcess = TRUE;
}
}
else
HandleTable = PsGetCurrentProcess()->ObjectTable;//使用当前进程的句柄表


//检查是否可以独占打开,检查权限,若各项检查通过才打开对象,递增句柄计数,调用对象的OpenP rocedure等等工作
Status = ObpIncrementHandleCount(Object,
AccessState,
AccessMode,
HandleAttributes,
PsGetCurrentProcess(),
OpenReason);
if (!NT_SUCCESS(Status))
return Status;

NewEntry.ObAttributes |= (HandleAttributes & OBJ_HANDLE_ATTRIBUTES);//填上句柄的属性

DesiredAccess =AccessState->RemainingDesiredAccess|AccessState->PreviouslyGrantedAccess;
GrantedAccess = DesiredAccess &(ObjectType->TypeInfo.ValidAccessMask);
NewEntry.GrantedAccess = GrantedAccess;//填上句柄的属性
Handle = ExCreateHandle(HandleTable, &NewEntry);//将句柄插入到句柄表中

if (Handle)//if 插入成功
{
if (KernelHandle)
Handle = ObMarkHandleAsKernelHandle(Handle);//将句柄值的最高位设为1,标记为内核句柄

*ReturnedHandle = Handle;
if (AttachedToProcess)
KeUnstackDetachProcess(&ApcState);//撤销挂靠
return STATUS_SUCCESS;
} Else
{

return STATUS_INSUFFICIENT_RESOURCES;
}
}

打开对象,以得到一个访问句柄。有四种打开时机:

  • 1、 创建对象时就打开,如 CreateFile 在创建一个新文件时,就同时打开了那个文件对象
  • 2、 显式打开,如 OpenFile,OpenMutex,OpenProcess 显式打开某个对象
  • 3、 DuplicateHandle 这个 API 间接打开对象,获得句柄
  • 4、 子进程继承父进程句柄表中的句柄,也可看做是一种打开
    在这四种情况下,都会调用这个函数来打开对象,得到一个句柄。
    OpenReason 参数就是指打开原因、时机 注意句柄值的最高位为 1,就表示这是一个内核全局句柄,可以在各个进程中通用。
    否则一般的句柄只能在对应的进程中有意义。

另外有两个特殊的伪句柄,他们并不表示索引而是一个简单的代号值

  • GetCurrentProcessHandle 返回的句柄值是-1
  • GetCurrentThreadHandle 返回的句柄值是-2
    对这两个句柄要特殊处理。《Windows 核心编程》一书专门强调了这两个句柄的使用误区

句柄结构

句柄不光含有指向对象的指针,每个句柄都还有自己的访问权限与属性,这也是非常重要的。
访问权限表示本次打开操作要求的、申请的并且最终得到的权限。
句柄属性则表示本句柄是否可以继承,是否是独占打开的,是否是一个内核句柄等属性。

在驱动程序开发中,经常遇到的下面这个结构

1
2
3
4
5
6
7
8
9
10
11
typedef struct _OBJECT_ATTRIBUTES 
{
ULONG Length;//本结构体的长度
HANDLE RootDirectory;//相对目录(不一定是父目录)
PUNICODE_STRING ObjectName;//相对RootDirectory这个目录的剩余路径 或者 全路径
//上面两个字段一起间接构成对象的全路径
ULONG Attributes;//对象属性与句柄属性的混合
PVOID SecurityDescriptor;// SD安全描述符
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;
typedef CONST OBJECT_ATTRIBUTES *PCOBJECT_ATTRIBUTES;

创建对象、打开对象时都会用到这个结构。

下面这个函数用来创建一个指定类型的内核对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
NTSTATUS ObCreateObject(IN POBJECT_TYPE Type, 
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN KPROCESSOR_MODE AccessMode,
IN ULONG ObjectSize,
IN ULONG PagedPoolCharge OPTIONAL,
IN ULONG NonPagedPoolCharge OPTIONAL,
OUT PVOID *Object)
{
ObjectCreateInfo = ObpAllocateObjectCreateInfoBuffer(LookasideCreateInfoList);
Status = ObpCaptureObjectCreateInformation(ObjectAttributes,FALSE,ObjectCreateInfo, &ObjectName);//提取ObjectAttributes中的字段

if (!PagedPoolCharge)
PagedPoolCharge = Type->TypeInfo.DefaultPagedPoolCharge;
if (!NonPagedPoolCharge)
NonPagedPoolCharge = Type->TypeInfo.DefaultNonPagedPoolCharge;

ObjectCreateInfo->PagedPoolCharge = PagedPoolCharge;
ObjectCreateInfo->NonPagedPoolCharge = NonPagedPoolCharge;

//从对应池中分配内存,创建对应的对象
Status = ObpAllocateObject(ObjectCreateInfo,&ObjectName,Type,ObjectSize,AccessMode, &Header);

return Status;
}

其实真正的工作函数是ObpAllocateObject
它内部调用ExAllocatePoolWithTag(ObjectType->PoolType, 可选头总大小+ ObjectSize, Tag)分配对象内存
然后初始化设置头部中的Flags等其他工作。(绝大多数内核对象都分配在非分页池中)

OBJECT_ATTRIBUTES结构体中的Attributes字段是个混合成员,由句柄属性、对象属性、打开属性复合而成
可以取下面的组合

  • OBJ_INHERIT://句柄属性,表示句柄是否可继承给子进程
  • OBJ_PERMANENT://指该对象是否永久存在于对象目录中直到对象销毁.(目录\符号链接\设备\文件 都是)
  • OBJ_EXLUSIVE://对象属性,指该对象同一时刻只能被一个进程独占打开
  • OBJ_CASE_INSENSITIVE://打开属性,表示本次打开操作查找比较对象名时大小写不敏感
  • OBJ_OPENIF://打开属性,表示 if 对象存在就打开
  • OBJ_OPENLINK://打开属性,表示本次打开是否可以直接打开符号链接
  • OBJ_KERNEL_HANDLE://句柄属性,表示要求得到一个内核句柄

而对象头中的 Flags 字段则完全表示对象的一些属性标志

  • OB_FLAG_CREATE_INFO;//表示头部中含有创建时的属性信息
  • OB_FLAG_CREATOR_INFO;//表示含有创建者进程信息
  • OB_FLAG_KERNEL_MODE://表示 PreviousMode 是内核模式的代码创建的本对象
  • OB_FLAG_EXCLUSIVE://表示同一时刻只能被一个进程独占打开
  • OB_FLAG_PERMANET://永久性对象,直到对象完全销毁时才脱离对象目录
  • OB_FLAG_SINGLE_PROCESS://表示含有每进程的句柄统计信息
  • OB_FLAG_DEFER_DELETE;//标记本对象被延迟删除了

创建的对象,如果有名字就需要插入到对象目录和句柄表中。即使没有名字也需要插入到句柄表中
这样才能让应用程序得以找到该对象以进行访问。
下面这个函数就是做这个的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
NTSTATUS ObInsertObject(IN PVOID Object, 
IN PACCESS_STATE AccessState OPTIONAL,
IN ACCESS_MASK DesiredAccess,
OUT PHANDLE Handle)//返回得到的句柄
{
ObjectHeader = OBJECT_TO_OBJECT_HEADER(Object);
ObjectCreateInfo = ObjectHeader->ObjectCreateInfo;
ObjectNameInfo = ObpReferenceNameInfo(ObjectHeader);
ObjectType = ObjectHeader->Type;
ObjectName = NULL;
if ((ObjectNameInfo) && (ObjectNameInfo->Name.Buffer))
ObjectName = &ObjectNameInfo->Name; PreviousMode = KeGetPreviousMode();

//无名对象(并且不需要安全控制)就不挂入对象目录,仅仅插入句柄表中
if ( (ObjectName==NULL) && !(ObjectType->TypeInfo.SecurityRequired))
{
ObjectHeader->ObjectCreateInfo = NULL;
Status = ObpCreateUnnamedHandle(Object,DesiredAccess,ObjectCreateInfo->Attributes,
PreviousMode,Handle);
return Status;
}

InsertObject = Object;
if (ObjectName)//若是一个有名对象
{
//这个函数有两种用途。
//当Object不为NULL表示将Object插入到对象目录中的指定位置
//当Object为NULL表示查找指定的对象目录位置处的对象
//两种用途都将指定目录位置处的对象返回到InsertObject中
Status = ObpLookupObjectName(ObjectCreateInfo->RootDirectory,
ObjectName,
ObjectCreateInfo->Attributes,
ObjectType,
ObjectCreateInfo->ParseContext,
Object,//要插入的对象
&InsertObject);//返回最终那个位置处的对象
//如果原位置处已有同名对象,插入失败
if ((NT_SUCCESS(Status)) && (InsertObject) && (Object != InsertObject))
{
OpenReason = ObOpenHandle;//既然插入失败了,那就是要打开对象,获得句柄
if (ObjectCreateInfo->Attributes & OBJ_OPENIF)//检查本次打开操作的要求
{
if (ObjectType != OBJECT_TO_OBJECT_HEADER(InsertObject)->Type)
Status = STATUS_OBJECT_TYPE_MISMATCH;
else
Status = STATUS_OBJECT_NAME_EXISTS;//看到没,应用层经常返回这个出错值
}
else
{
Status = STATUS_OBJECT_NAME_COLLISION;
}
return Status;
}
}
if (InsertObject == Object)//if 插入成功
OpenReason = ObCreateHandle;//只有第一次创建对象的时候才会插入对象目录中

ObjectHeader->ObjectCreateInfo = NULL;//不再需要了
if (Handle)//如果用户要求插入句柄表,就插入句柄表,得到一个句柄
{
Status = ObpCreateHandle(OpenReason,InsertObject,AccessState,
ObjectCreateInfo->Attributes, PreviousMode,Handle);
}
return Status;
}

常用内核函数

ObReferenceObjectByHandle

下面看一下驱动程序经常调用的那些内核函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
NTSTATUS ObReferenceObjectByHandle(IN HANDLE Handle,IN ACCESS_MASK DesiredAccess, 
IN POBJECT_TYPE ObjectType,IN KPROCESSOR_MODE AccessMode,
OUT PVOID* Object,OUT POBJECT_HANDLE_INFORMATION HandleInformation)
{
*Object = NULL;
//若句柄是一个内核句柄或当前进程、线程的句柄
if (HandleToLong(Handle) < 0)
{
if (Handle == NtCurrentProcess())//若句柄值是当前进程的句柄(-1),特殊处理
{
if ((ObjectType == PsProcessType) || !(ObjectType))
{
CurrentProcess = PsGetCurrentProcess();
GrantedAccess = CurrentProcess->GrantedAccess;
//if内核模式/要求的权限<=进程对象支持的权限(权限检查)
if ((AccessMode == KernelMode) ||!(~GrantedAccess & DesiredAccess))
{
if (HandleInformation)
{
HandleInformation->HandleAttributes = 0;
HandleInformation->GrantedAccess = GrantedAccess;
}

ObjectHeader = OBJECT_TO_OBJECT_HEADER(CurrentProcess);
InterlockedExchangeAdd(&ObjectHeader->PointerCount, 1);//递增引用计数
*Object = CurrentProcess;//返回得到的对象指针
Status = STATUS_SUCCESS;
}
Else //权限检查不通过
Status = STATUS_ACCESS_DENIED;
}
else
Status = STATUS_OBJECT_TYPE_MISMATCH;
return Status;
}
else if (Handle == NtCurrentThread())//若句柄值是当前线程的句柄(-2),特殊处理
{
if ((ObjectType == PsThreadType) || !(ObjectType))
{
CurrentThread = PsGetCurrentThread();
GrantedAccess = CurrentThread->GrantedAccess;
if ((AccessMode == KernelMode) ||!(~GrantedAccess & DesiredAccess))
{
if (HandleInformation)
{
HandleInformation->HandleAttributes = 0;
HandleInformation->GrantedAccess = GrantedAccess;
}

ObjectHeader = OBJECT_TO_OBJECT_HEADER(CurrentThread);
InterlockedExchangeAdd(&ObjectHeader->PointerCount, 1);
*Object = CurrentThread;
Status = STATUS_SUCCESS;
}
else
Status = STATUS_ACCESS_DENIED;
}
else
Status = STATUS_OBJECT_TYPE_MISMATCH;
return Status;
}
else if (AccessMode == KernelMode)//若句柄是一个内核句柄
{
Handle = ObKernelHandleToHandle(Handle);//去掉最高位的1,转为普通句柄
HandleTable = ObpKernelHandleTable;//采用内核句柄表
}
}
Else //最典型的情况,普通句柄,就使用当前进程的句柄表
HandleTable = PsGetCurrentProcess()->ObjectTable;

//以该句柄的值为“索引”,找到句柄表中对应的句柄表项
HandleEntry = ExMapHandleToPointer(HandleTable, Handle)
if (HandleEntry)//如果找到了,这就是一个有效句柄
{
ObjectHeader = ObpGetHandleObject(HandleEntry);//关键。获得该句柄指向的对应对象
if (!(ObjectType) || (ObjectType == ObjectHeader->Type))
{
GrantedAccess = HandleEntry->GrantedAccess;
if ((AccessMode == KernelMode) ||!(~GrantedAccess & DesiredAccess))//通过权限检查
{
InterlockedIncrement(&ObjectHeader->PointerCount);
Attributes = HandleEntry->ObAttributes & OBJ_HANDLE_ATTRIBUTES;

if (HandleInformation)
{
HandleInformation->HandleAttributes = Attributes;
HandleInformation->GrantedAccess = GrantedAccess;
}
*Object = &ObjectHeader->Body;//返回的是对象体的地址
return STATUS_SUCCESS;
}
Else //权限检查没通过
Status = STATUS_ACCESS_DENIED;
}
else
Status = STATUS_OBJECT_TYPE_MISMATCH;
}
Else //有可能用户给定的句柄值是一个无效句柄,在句柄表中找不到
Status = STATUS_INVALID_HANDLE;

*Object = NULL;
return Status;
}

如上这个函数从句柄得到对应的内核对象,并递增其引用计数。

两个特殊情况
#define NtCurrentProcess() (HANDLE)-1
#define NtCurrentThread() (HANDLE)-2
这是两个伪句柄值,永远获得的是当前进程、线程的内核对象。

另外若句柄值的最高位是 1,则是一个内核句柄各进程通用。
内核型句柄是"System"进程的句柄表中的句柄。
因此要获得内核句柄对应的对象,系统会挂靠到"System"进程的地址空间中,去查询句柄表。

根据句柄值在句柄表中找到对应的表项是靠ExMamHandleToPointer这个函数实现的,这个函数又在内部调用ExpLookupHandleTableEntry 来真正查找。
句柄表组织为一个稀疏数组(目的用来节省内存),但可以简单的看做一个一维数组,不影响理解,句柄值本身也可简单理解为一个索引。

ObReferenceObjectByPointer

1
2
3
4
5
6
7
8
9
10
11
NTSTATUS ObReferenceObjectByPointer(IN PVOID Object,IN ACCESS_MASK DesiredAccess, 
IN POBJECT_TYPE ObjectType,IN KPROCESSOR_MODE AccessMode)
{
POBJECT_HEADER Header;
Header = OBJECT_TO_OBJECT_HEADER(Object);
if ((Header->Type != ObjectType) && ((AccessMode != KernelMode) || (ObjectType == ObSymbolicLinkType)))
return STATUS_OBJECT_TYPE_MISMATCH;
InterlockedIncrement(&Header->PointerCount);//递增对象的引用计数

return STATUS_SUCCESS;
}

上面这个函数其实是递增对象的引用计数而已

手握一个引用计数后,就可以防止对象被析构释放,因为对象只有在引用计数减到 0 后才会释放,从而防止因对象析构引起的莫名其妙的崩溃


对象目录中的查找过程

给定一个对象名,如"\Device\Harddisk0\Partition0\Dir1\Dir2\File.txt",如何查找到对应的对象呢?

这个路径先在对象目录中一路找到\Device\Harddisk0\Partition0 表示的磁盘卷设备对象,然后再沿着剩余路径"Dir1\Dir2\File.txt"找到对应的文件对象,不过后半部的查找过程是文件系统的事了,后面我将详细讲解。

这里看前半部的查找,是如何找到对应的卷设备的。
前文我们讲过了一个函数:ObpLookupEntryDirectory,那个函数用来在指定的目录中找到指定名称的子对 象,现在就需要沿着路径,反复调用这个函数找到我们的卷设备。

下面的函数就是用来这个目的的。可以给定任意一个起点目录,以及相对那个起点目录的任意长的路径,找到指定的对象。
这个函数的代码有点 长…(请做好心理准备),
原函数差不多有 800 行长,我做了粗略压缩,在我的详尽解释下,相信您可以看明白的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
NTSTATUS ObpLookupObjectName(IN HANDLE RootHandle OPTIONAL,//搜索的起点对象,如果为NULL,则表示根节点 
IN PUNICODE_STRING ObjectName,//相对RootHandle的路径或者全路径
IN ULONG Attributes,//本次查找操作的属性(如是否忽略大小写)
IN POBJECT_TYPE ObjectType,//要查的目标对象的类型
IN OUT PVOID ParseContext,//其它
IN PVOID InsertObject OPTIONAL,//如果不为NULL,则表示找不到就插入这个对象
IN OUT PACCESS_STATE AccessState,//传入当前线程的令牌以及申请的访问权限
OUT POBP_LOOKUP_CONTEXT LookupContext,//用来返回查找结果细节信息
OUT PVOID *FoundObject)//返回最终找到的对象
{
*FoundObject = NULL;//初始为未找到 PVOID Object=NULL;
//分别指相对当前起点对象的剩余路径、剩余路径中的第一个节点名
UNICODE_STRING RemainingName, ComponentName;
BOOLEAN Reparse = FALSE, SymLink = FALSE;//分别表示是否需要重新解析、是否为符号链接的标志
//表示搜索路径中当前的目录,父目录,以及起点目录
POBJECT_DIRECTORY Directory = NULL, ParentDirectory = NULL, RootDirectory;
//表示最终要插入到那个位置的叶目录及父目录
POBJECT_DIRECTORY ReferencedDirectory = NULL, ReferencedParentDirectory = NULL;
OB_PARSE_METHOD ParseRoutine;//对象自带的解析函数
ULONG MaxReparse = 30;//符号链接等,总的重新解析次数最大不能超过30次

NTSTATUS Status = STATUS_SUCCESS;//预期找到成功
ObpInitializeLookupContext(LookupContext);

if (!(ObjectType) || (ObjectType->TypeInfo.CaseInsensitive))
Attributes |= OBJ_CASE_INSENSITIVE;//检查本类对象是否支持对象名可以忽略大小写


if (RootHandle)//如果给定了搜索起点
{
Status = ObReferenceObjectByHandle(RootHandle,AccessMode, &RootDirectory,…);
ObjectHeader = OBJECT_TO_OBJECT_HEADER(RootDirectory);//得到起点对象
if (ObjectHeader->Type != ObDirectoryType)//if给定的起点对象不是目录
{
ParseRoutine = ObjectHeader->Type->TypeInfo.ParseProcedure;
if (!ParseRoutine) return STATUS_INVALID_HANDLE;//非目录对象必须自带解析函数
MaxReparse = 30;
while (TRUE)
{
RemainingName = *ObjectName;//当前的剩余路径=初始路径
Status = ParseRoutine(RootDirectory,//起点
ObjectType,
AccessState,
AccessCheckMode,
Attributes,
IN、OUT ObjectName,//传入初始全路径,可能传出新的初始路径
IN、OUT &RemainingName,//传入当前剩余路径,返回最终的剩余路径
ParseContext,
&Object);//返回找到的对象

// if自带解析函数解析完毕了,不要求重新解析
if ((Status != STATUS_REPARSE) && (Status != STATUS_REPARSE_OBJECT))
{
if (!NT_SUCCESS(Status))//如果未能解析
Object = NULL;
else if (!Object)//如果未能找到目标对象
Status = STATUS_OBJECT_NAME_NOT_FOUND;

*FoundObject = Object;//有可能找到的不是最终的目标对象
return Status;//不管查找成功还是失败都从这儿返回了
}
//if自带解析函数要求从根目录开始按新的初始路径重新解析
else if ( (ObjectName->Buffer[0] == L”\\”) )
{
RootDirectory = ObpRootDirectoryObject;
RootHandle = NULL;
goto ParseFromRoot;//从根目录开始解析
}
//if自带解析函数要求从指定对象开始重新解析剩余路径(很少见)
else if (--MaxReparse)//如果未能超出最大重解析次数,就重定向解析
continue;
else//如果超出了最大重解析次数,失败返回
{

*FoundObject = Object;
if (!Object) Status = STATUS_OBJECT_NAME_NOT_FOUND;
return Status;
}
}
}
}
Else //如果没有给定搜索起点,就从根目录开始搜索,把ObjectName参数当绝对路径使
{
RootDirectory = ObpRootDirectoryObject;//搜素起点=根目录
if ( (ObjectName->Buffer[0] != L”\\”))//绝对路径必须以“\”开头
return STATUS_OBJECT_PATH_SYNTAX_BAD;
}

ParseFromRoot:
if (!SymLink)
{
Reparse = TRUE;//表示是否需要继续重新解析的标志,初始为真
MaxReparse = 30;//最多30次
}
while (Reparse)//这层循环表示处理每一轮从根目录开始的重新解析操作
{
RemainingName = *ObjectName;//该轮重解析的新的初始剩余路径
Reparse = FALSE;//预期本次解析操作一次解析成功,不需要重定向解析
while (TRUE)//遍历该轮重解析的路径中的各个节点
{
Object = NULL;
if ((RemainingName.Length) && (RemainingName.Buffer[0] == L”\\”))
{
RemainingName.Buffer++;
RemainingName.Length -=2;
}
//上面的操作跳过剩余路径中开头的\字符
ComponentName = RemainingName;
while (RemainingName.Length)
{
if (RemainingName.Buffer[0] == L”\\”) break;
RemainingName.Buffer++;
RemainingName.Length -=2;
}
ComponentName.Length -= RemainingName.Length;
//上面的操作获得当前剩余路径中的第一个节点名,即ComponentName

if (!Directory) Directory = RootDirectory;//当前目录


//检查当前线程的令牌是否含有遍历当前目录的权限(先检查令牌特权,再检查用户/组权)
if (!(AccessState->Flags & TOKEN_HAS_TRAVERSE_PRIVILEGE))
{
ReferencedDirectory = Directory;
if (ParentDirectory)
{
if (!ObpCheckTraverseAccess(ParentDirectory,
DIRECTORY_TRAVERSE,//检查目录遍历权限
AccessState,AccessCheckMode,))
break;//权限检查失败,退出整个函数
}
}
if (RemainingName.Length==0)//if ComponentName是路径中的最后一个节点了
{
if (!ReferencedDirectory)
ReferencedDirectory = Directory;//记录叶目录是哪一个
if (InsertObject)//要在叶目录中插入对象,必须锁定整个目录
ObpAcquireDirectoryLockExclusive(Directory, LookupContext);
}
//关键。查找剩余路径中的第一个节点(这个函数前面已分析过)
Object = ObpLookupEntryDirectory(Directory,&ComponentName,Attributes,
InsertObject ? FALSE : TRUE,LookupContext);
if (!Object)
{
if (RemainingName.Length>0)
{
Status = STATUS_OBJECT_PATH_NOT_FOUND;
break;//如果路径中间的某个目录不存在,失败退出整个函数
}
else if (InsertObject==NULL)//if遍历完整个路径了
{
Status = STATUS_OBJECT_NAME_NOT_FOUND;
break;//如果找不到目标对象,又不要求插入新对象,失败返回整个函数(很常见)
}


//if用户要求:找不到目标对象就插入一个(这就体现了该函数的另一功能)
//先检查当前线程的令牌是否具有在这个目录中插入对象的权限
if (!ObCheckCreateObjectAccess(Directory,
ObjectType == ObDirectoryType ?
DIRECTORY_CREATE_SUBDIRECTORY :
DIRECTORY_CREATE_OBJECT,
AccessState,
&ComponentName)) //子对象名
{
break;//没有插入权限,失败返回整个函数

}
ObpInsertEntryDirectory(Directory,LookupContext,ObjectHeader);//插入目录中 ObjectHeader = OBJECT_TO_OBJECT_HEADER(InsertObject);

NewName = ExAllocatePoolWithTag(PagedPool,ComponentName.Length,tag);
ObjectNameInfo = OBJECT_HEADER_TO_NAME_INFO(ObjectHeader);
RtlCopyMemory(NewName,ComponentName.Buffer,ComponentName.Length);

if (ObjectNameInfo->Name.Buffer)//释放该对象原来的名字
ExFreePoolWithTag(ObjectNameInfo->Name.Buffer, OB_NAME_TAG );
ObjectNameInfo->Name.Buffer = NewName;//设置新插入对象的名字
ObjectNameInfo->Name.Length = ComponentName.Length;
ObjectNameInfo->Name.MaximumLength = ComponentName.Length;

Status = STATUS_SUCCESS;
Object = InsertObject;//成功返回
break;
}

ReparseObject://如果找到了剩余路径中的第一个子节点(那个节点在对象目录中存在)
ObjectHeader = OBJECT_TO_OBJECT_HEADER(Object);
ParseRoutine = ObjectHeader->Type->TypeInfo.ParseProcedure;
//if这个节点自带有一个解析函数(那肯定不是目录)
if ((ParseRoutine) && (!(InsertObject) || (ParseRoutine == ObpParseSymbolicLink)))
{
Directory = NULL;//不再有当前目录
InterlockedExchangeAdd(&ObjectHeader->PointerCount, 1);

if (ReferencedDirectory)
ReferencedDirectory = NULL;

if (ReferencedParentDirectory)
ReferencedParentDirectory = NULL;
//调用这个对象自带的解析函数
Status = ParseRoutine(Object,ObjectType,AccessState,AccessCheckMode,
Attributes,
IN、OUT ObjectName,//初始的全路径,可能传出新的初始路径
IN、OUT &RemainingName,//传入初始的剩余路径,传出最终的剩余路径
ParseContext,
&Object);
if ((Status == STATUS_REPARSE) || //要求从重根目录开始重新按新的初始路径解析
(Status == STATUS_REPARSE_OBJECT))//要求从指定对象开始重新解析剩余路径
{
if ((Status == STATUS_REPARSE_OBJECT) || (ObjectName->Buffer[0] == L”\\”))
{
RootHandle = NULL;
ParentDirectory = NULL;
RootDirectory = ObpRootDirectoryObject;
if (Status == STATUS_REPARSE_OBJECT)
{
Reparse = FALSE;//不用回到最外层的循环,直接从内层位置重新解析
goto ReparseObject;
}
else
{
Reparse = TRUE;//重新回到外层循环从根目录开始解析
SymLink = TRUE;
goto ParseFromRoot;
}
}
}
else if (!NT_SUCCESS(Status))
Object = NULL;
else if (Object==NULL)
Status = STATUS_OBJECT_NAME_NOT_FOUND;
break;//失败返回整个函数
}
Else //剩余路径中的第一个节点是个普通的目录或者是路径中的最后一个节点
{
if (RemainingName.Length==0)//若找到的是最后一个节点
{
Status = ObReferenceObjectByPointer(Object,ObjectType,AccessMode);
break;//哈哈,终于舒口气了。成功返回找到的这个对象(最典型的情形)
}
else
{
//若找到的是中间的某个普通目录,修改状态后,回到内层循环继续向后查找
if (ObjectHeader->Type == ObDirectoryType)
{
ReferencedParentDirectory = ReferencedDirectory;
ParentDirectory = Directory;
Directory = Object;//修改当前目录为新的目录
ReferencedDirectory = NULL;
}
Else//很少见
{

}
}
}
}
}

//下面返回找到的对象

*FoundObject = Object;
if (Object==NULL)
{
if ((Status == STATUS_REPARSE) || (NT_SUCCESS(Status)))
Status = STATUS_OBJECT_NAME_NOT_FOUND;//强制转为查找失败
}
return Status;
}

至此对象目录的查找过程也详尽分析结束了。
这个函数根据路径在对象目录中查找对象。

  • 如果找到了,就返回找到点额对象;
  • 如果找不到,并且用户不要求插入新对象在那儿,就返回失败,否则插入用户指定的对象。

一个函数两种用途,后一个用途用于在创建对象时,把新创的有名对象插入对象目录
注意: 物理的磁盘卷设备对象、文件对象、注册表键对象以及符号链接对象都提供了自定义的名字解析函 数。关于具体对象类型的解析,以后的篇章会逐渐讲到。

ObReferenceObjectByName

下面这个函数经常使用,但有诀窍DDK文档中并未公开这个函数,但是这个函数实际上是导出的,声明一下 就可以使用了
这个函数可以直接根据对象名,找到对应的对象(是不是很方便?)
其内部原理就是调用上面的函数在对象目录中查找对象(这个函数实际上是导出的,虽然 DDK 中并未有这个函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
NTSTATUS ObReferenceObjectByName(IN PUNICODE_STRING ObjectPath,//对象的全路径(以\开头) 
IN ULONG Attributes,//查找属性
IN PACCESS_STATE PassedAccessState,//传入的令牌和申请的权限,可选
IN ACCESS_MASK DesiredAccess,//申请的访问权限,与上面那个参数任选其一
IN POBJECT_TYPE ObjectType,//当AccessMode传KernelMode时,可忽略此参数
IN KPROCESSOR_MODE AccessMode,//驱动程序中一般传KernelMode
IN OUT PVOID ParseContext,//其它
OUT PVOID* ObjectPtr)//返回找到的对象
{
PVOID Object = NULL;
Status = ObpCaptureObjectName(&ObjectName, ObjectPath, AccessMode, TRUE);
if (!PassedAccessState)//如果用户没传这个参数,就创建
{
PassedAccessState = &AccessState;
//将当前线程的令牌以及申请的访问权限记录到AccessState中
Status = SeCreateAccessState(&AccessState,&AuxData,DesiredAccess,
&ObjectType->TypeInfo.GenericMapping);
}
*ObjectPtr = NULL;
Status = ObpLookupObjectName(NULL, //看到没,NULL表示根目录
&ObjectName,//绝对路径
Attributes,//查找属性
ObjectType,
AccessMode,

ParseContext,
PassedAccessState,//传入当前线程的令牌
&Context,
&Object);

if (NT_SUCCESS(Status))
{
if (ObpCheckObjectReference(Object,
PassedAccessState, //传入当前线程的令牌
AccessMode,&Status))
*ObjectPtr = Object;
}
return Status;
}

假如要找到名为即DeviceName等于"\Device\MyCdo"的设备对象这个函数典型的调用方法示例为

1
2
3
4
5
ObReferenceObjectByName( 
&DeviceName,
OBJ_CASE_INSENSITIVE,//大小写木敏感 NULL, //PassedAccessState FILE_ALL_ACCESS, //DesiredAccess IoDeviceObjectType,//也可以直接传NULL
KernelMode,
NULL, //ParseContext (PVOID*)&DeviceObject);//返回找到的设备对象

ObOpenObjectByPointer

调用上面的那个函数后,可以根据对象名直接得到它的指针,得到指针后我们还可以趁此打开该对象以得到一个访问句柄

因为有的场合我们不能使用对象指针,只能使用句柄。

下面的函数就是这个用途

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
NTSTATUS ObOpenObjectByPointer(IN PVOID Object, 
IN ULONG HandleAttributes,
IN PACCESS_STATE PassedAccessState,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_TYPE ObjectType,
IN KPROCESSOR_MODE AccessMode,
OUT PHANDLE Handle)
{
*Handle = NULL;
Status = ObReferenceObjectByPointer(Object,ObjectType,AccessMode);//递增引用计数
Header = OBJECT_TO_OBJECT_HEADER(Object);

if (!PassedAccessState)//如果用户没传入一个令牌,就创建一个
{
PassedAccessState = &AccessState;
Status = SeCreateAccessState(&AccessState,&AuxData,DesiredAccess, &Header->Type->TypeInfo.GenericMapping);
}

if (Header->Type->TypeInfo.InvalidAttributes & HandleAttributes)//检查句柄属性是否支持
return STATUS_INVALID_PARAMETER;

//在句柄表中创建一个相应对象的句柄
Status = ObpCreateHandle(ObOpenHandle,//OpenReason
Object,
ObjectType,
PassedAccessState,
0,
HandleAttributes,
NULL,
AccessMode,
NULL,
Handle);//返回得到的句柄
return Status;
}

ObOpenObjectByName

事实上更多的场合是,我们有一个对象的名称,想要打开那个对象,一步得到它的访问句柄(比如OpenMutex)。
因此下面的函数就是用于这个常用目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
NTSTATUS ObOpenObjectByName(IN POBJECT_ATTRIBUTES ObjectAttributes,//对象的路径 
IN POBJECT_TYPE ObjectType,
IN KPROCESSOR_MODE AccessMode,
IN PACCESS_STATE PassedAccessState,//传入的令牌
IN ACCESS_MASK DesiredAccess,//要求的访问权限
IN OUT PVOID ParseContext,
OUT PHANDLE Handle)//返回得到的句柄
{
PVOID Object = NULL;
*Handle = NULL;
TempBuffer = ExAllocatePoolWithTag(NonPagedPool,sizeof(OB_TEMP_BUFFER),tag);
Status = ObpCaptureObjectCreateInformation(ObjectAttributes,
AccessMode,
&TempBuffer->ObjectCreateInfo,
&ObjectName);
if (!PassedAccessState)
{
if (ObjectType) GenericMapping = &ObjectType->TypeInfo.GenericMapping;
PassedAccessState = &TempBuffer->LocalAccessState;
Status = SeCreateAccessState(&TempBuffer->LocalAccessState,&TempBuffer->AuxData,
DesiredAccess,GenericMapping);
}

//调用这个函数完成真正的查找工作
Status = ObpLookupObjectName(TempBuffer->ObjectCreateInfo.RootDirectory,
&ObjectName,
TempBuffer->ObjectCreateInfo.Attributes,
ObjectType,
AccessMode,
ParseContext,
NULL,
PassedAccessState,
&TempBuffer->LookupContext,
&Object);
ObjectHeader = OBJECT_TO_OBJECT_HEADER(Object);
if (ObjectHeader->Flags & OB_FLAG_CREATE_INFO)
{
OpenReason = ObCreateHandle;//说明对象正在创建过程中(也即创建对象时就打开一个句柄)
if (ObjectHeader->ObjectCreateInfo)
{
ObpFreeObjectCreateInformation(ObjectHeader->ObjectCreateInfo);
ObjectHeader->ObjectCreateInfo = NULL; //对象打开得到句柄后就不再需要保留创建信息
}
}
Else //典型情况:说明是对象创建完后的普通显式打开操作(非创建时打开)
OpenReason = ObOpenHandle;

if (ObjectHeader->Type->TypeInfo.InvalidAttributes & TempBuffer->ObjectCreateInfo.Attributes)
{
Status = STATUS_INVALID_PARAMETER;
}
else
{
//在句柄表中创建句柄
Status = ObpCreateHandle(OpenReason,
Object,
ObjectType,
PassedAccessState,
0,
TempBuffer->ObjectCreateInfo.Attributes,
&TempBuffer->LookupContext,
AccessMode,
NULL,
Handle);
}

return Status;
}

ObDereferenceObject

下面是递减对象引用计数的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
LONG  ObDereferenceObject(IN PVOID Object) 
{
Header = OBJECT_TO_OBJECT_HEADER(Object);
if (Header->PointerCount < Header->HandleCount)
return Header->PointerCount;//引用计数必须>=句柄计数(因为句柄也是一种引用)
NewCount = InterlockedDecrement(&Header->PointerCount);//递减

if (NewCount==0)//引用计数递减到0后才删除对象
{
if (!KeAreAllApcsDisabled())
ObpDeleteObject(Object, FALSE);//立即删除该对象
else
ObpDeferObjectDeletion(Header);//让后台的系统工作线程延迟删除这个对象
}
return NewCount;//返回递减后的值
}

VOID ObpDeleteObject(IN PVOID Object, IN BOOLEAN CalledFromWorkerThread)
{
Header = OBJECT_TO_OBJECT_HEADER(Object);
ObjectType = Header->Type;
NameInfo = OBJECT_HEADER_TO_NAME_INFO(Header);
CreatorInfo = OBJECT_HEADER_TO_CREATOR_INFO(Header);
if ((CreatorInfo) && !(IsListEmpty(&CreatorInfo->TypeList)))
RemoveEntryList(&CreatorInfo->TypeList);//从对象类型的对象链表中移除该对象
if ((NameInfo) && (NameInfo->Name.Buffer))
ExFreePool(NameInfo->Name.Buffer); if (Header->SecurityDescriptor) 。。。
if (ObjectType->TypeInfo.DeleteProcedure)//调用对象类型自身提供的删除函数
{
if (!CalledFromWorkerThread)
Header->Flags |= OB_FLAG_DEFER_DELETE;
ObjectType->TypeInfo.DeleteProcedure(Object);
}
ObpDeallocateObject(Object);//内部调用ExFreePool最终释放对象结构体本身
}

以上两个函数说明:对象的引用计数一旦减到0,就释放对象及其相关其它结构从系统中消失


句柄的遗传和复制

说到句柄请自觉建立起这样一个完整概念:"具有指定权限和属性的访问句柄"
一个句柄代表一次对对象的打开操作(句柄最终的目的是用来访问对象的,因此叫"访问句柄")
句柄的权限指本次打开指定对象时,当初申请得到的访问权限,以后凡是使用基于这个句柄的操作都不能超出当初打开时申请得到的访问权限(如打开文件时申请的权限是读)
那么使用那个hFile调用WriteFile,将拒绝访问,即使当前用户拥有对那个文件的写权限。

句柄的属性则有重要的一条:是否可继承给子进程。(句柄在打开时通过OBJ_INHERIT标志指示可否继承)

CreateProcess

Win32 API

1
2
3
4
5
6
7
8
BOOL CreateProcess( 
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles, //指是否继承父进程的句柄表中那些可继承的句柄
......
);

父进程句柄表中的每个可继承句柄继承到子进程句柄表中的对应索引位置
这样父子进程就可以使用同一个句柄值,找到同一个内核对象。

这就是"继承"一词的由来。
我们知道各个句柄(除开内核型句柄)在各个进程中不能通用,但一旦继承给子进程后,就可以通用了。

ExDupHandleTable

下面这个函数用来给子进程创建一个句柄表,并从父进程中复制句柄表项达到继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
PHANDLE_TABLE ExDupHandleTable(IN PEPROCESS Process,//子进程 
IN PHANDLE_TABLE HandleTable,//父进程的句柄表
IN PEX_DUPLICATE_HANDLE_CALLBACK DupHandleProcedure)//辅助回调函数
{
BOOLEAN Failed = FALSE;
NewTable = ExpAllocateHandleTable(Process, FALSE);//句柄表描述符结构体
while (NewTable->NextHandleNeedingPool < HandleTable->NextHandleNeedingPool)
ExpAllocateHandleTableEntrySlow(NewTable, FALSE);//分配与父句柄表对应大小的内存

NewTable->HandleCount = 0;//有效句柄个数
NewTable->FirstFree = 0;//第一个空闲表项的位置

Handle.Value = SizeOfHandle(1);//索引位置
//遍历父句柄表中的每个表项,复制到子句柄表
while ((NewEntry = ExpLookupHandleTableEntry(NewTable, Handle)))
{
HandleTableEntry = ExpLookupHandleTableEntry(HandleTable, Handle);//得到父表项
do
{
if (!(HandleTableEntry->Value & OBJ_INHERIT))//if 该父句柄不可继承
Failed = TRUE;
else//if 该父句柄可继承
{
if (!ExpLockHandleTableEntry(HandleTable, HandleTableEntry))
Failed = TRUE;//句柄表锁定失败,不管他
else
{
//检查是否可复制,若可以,就递增对象的句柄计数、引用计数,然后复制句柄项
if (DupHandleProcedure(Process,HandleTable,HandleTableEntry,NewEntry))
{
*NewEntry = *HandleTableEntry;//关键。完完全全复制句柄表项
Failed = FALSE;
NewEntry->Value |= EXHANDLE_TABLE_ENTRY_LOCK_BIT;
NewTable->HandleCount++;
}
else
Failed = TRUE;
}
}
if (Failed)//若不可复制
{
NewEntry->Object = NULL;//标记为一个无效的句柄
NewEntry->NextFreeTableEntry = NewTable->FirstFree;
NewTable->FirstFree = Handle.Value;
}
Handle.Value += SizeOfHandle(1);
NewEntry++;//下一个句柄
HandleTableEntry++;//下一个句柄
} while (Handle.Value % SizeOfHandle(LOW_LEVEL_ENTRIES));
Handle.Value += SizeOfHandle(1);
}

//将句柄表插入到全局链表中(这也是一个检测隐藏的方法)
InsertTailList(&HandleTableListHead, &NewTable->HandleTableList);
return NewTable;//返回给子进程创建的句柄表
}

句柄的关闭

Win32 API CloseHandleclosesocket会在内部调用NtClose,最终会调用到下面的内核函数来关闭句柄。

ObpCloseHandle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
NTSTATUS  ObpCloseHandle(IN HANDLE Handle,IN KPROCESSOR_MODE AccessMode) 
{
BOOLEAN AttachedToProcess = FALSE;
PEPROCESS Process = PsGetCurrentProcess();

if (ObIsKernelHandle(Handle, AccessMode))//内核句柄(即“system”进程中的句柄)
{
HandleTable = ObpKernelHandleTable;
Handle = ObKernelHandleToHandle(Handle);

if (Process != PsInitialSystemProcess)
{
KeStackAttachProcess(&PsInitialSystemProcess->Pcb, &ApcState);//挂靠到system进程
AttachedToProcess = TRUE;
}
}
else
HandleTable = Process->ObjectTable;//典型,使用当前进程的句柄

HandleTableEntry = ExMapHandleToPointer(HandleTable, Handle);//得到对应的句柄表项
if (HandleTableEntry)
{
//调用下面的这个函数真正完成句柄关闭工作
Status = ObpCloseHandleTableEntry(HandleTable,HandleTableEntry,Handle,AccessMode);
if (AttachedToProcess) KeUnstackDetachProcess(&ApcState);
Status = STATUS_SUCCESS;
}
else
{
if (AttachedToProcess) KeUnstackDetachProcess(&ApcState);
Status = STATUS_INVALID_HANDLE;//返回无效句柄错误
}
return Status;
}

ObpCloseHandleTableEntry

实际的句柄关闭工作是下面这个函数ObpCloseHandleTableEntry,请详细看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
NTSTATUS ObpCloseHandleTableEntry(IN PHANDLE_TABLE HandleTable, 
IN PHANDLE_TABLE_ENTRY HandleEntry,
IN HANDLE Handle,
IN KPROCESSOR_MODE AccessMode)
{
ObjectHeader = ObpGetHandleObject(HandleEntry);
ObjectType = ObjectHeader->Type;
Body = &ObjectHeader->Body;
GrantedAccess = HandleEntry->GrantedAccess;
//关闭句柄前,检查对象的当前状态是否可以关闭一个句柄
if (ObjectType->TypeInfo.OkayToCloseProcedure)
{
if (!ObjectType->TypeInfo.OkayToCloseProcedure(CurProcess,Body,Handle,AccessMode))
return STATUS_HANDLE_NOT_CLOSABLE;
}

//再检查句柄本身是否可以关闭
if ( HandleEntry->ObAttributes & OBJ_PROTECT_CLOSE )
return STATUS_HANDLE_NOT_CLOSABLE;
ExDestroyHandle(HandleTable, Handle, HandleEntry);//关键,将对应位置处的句柄标记为空闲
ObpDecrementHandleCount(Body,CurProcess,GrantedAccess,ObjectType);//递减总的句柄计数
ObDereferenceObject(Body);//同时递减一个引用计数(句柄本身也占据一个引用)
return STATUS_SUCCESS;
}


VOID ObpDecrementHandleCount(IN PVOID ObjectBody,IN PEPROCESS Process,
IN ACCESS_MASK GrantedAccess,IN POBJECT_TYPE ObjectType)
{
ObjectHeader = OBJECT_TO_OBJECT_HEADER(ObjectBody);
OldHandleCount = ObjectHeader->HandleCount;

NewCount = InterlockedDecrement(&ObjectHeader->HandleCount);//递减句柄计数 ProcessHandleCount = 当前进程对该对象持有的句柄计数;
if (ObjectType->TypeInfo.MaintainHandleCount) { 递减对应进程对该对象的句柄计数; }

//每次关闭句柄时都会调用对象类型注册的句柄关闭时函数,如文件对象类提供了IopCloseFile
if (ObjectType->TypeInfo.CloseProcedure)
ObjectType->TypeInfo.CloseProcedure(Process,ObjectBody,GrantedAccess,
ProcessHandleCount,OldHandleCount);
ObpDeleteNameCheck(ObjectBody);//这个函数在非永久对象的句柄一关完后,就把对象移出对象目录
InterlockedDecrement((PLONG)&ObjectType->TotalNumberOfHandles);
}

如上每次关闭前都会检查句柄是否可以关闭,然后关闭句柄,调用注册的句柄关闭时函数。

IopCloseFile

文件对象类自己注册了一个句柄关闭函数IopCloseFile文件句柄在关完后的工作如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
VOID IopCloseFile(IN PEPROCESS Process OPTIONAL,IN PVOID ObjectBody,IN ACCESS_MASK GrantedAccess, 
IN ULONG HandleCount,//关闭句柄前,当前进程对该对象持有的句柄计数 IN ULONG SystemHandleCount)// 关闭句柄前,该对象总的句柄计数
{
PFILE_OBJECT FileObject = (PFILE_OBJECT)ObjectBody;
if (HandleCount != 1) return;
if (SystemHandleCount != 1) return;
//上面两条语句说明,文件对象只有在所有句柄都关完后才做下面的附加工作

if (FileObject->Flags & FO_DIRECT_DEVICE_OPEN)
DeviceObject = IoGetAttachedDevice(FileObject->DeviceObject);//物理磁盘卷设备
else
DeviceObject = IoGetRelatedDeviceObject(FileObject);//文件系统中的卷设备
FileObject->Flags |= FO_HANDLE_CREATED;

if (FileObject->Flags & FO_SYNCHRONOUS_IO) IopLockFileObject(FileObject);
KeClearEvent(&FileObject->Event);
KeInitializeEvent(&Event, SynchronizationEvent, FALSE);

Irp = IoAllocateIrp(DeviceObject->StackSize, FALSE);
Irp->UserEvent = &Event;
Irp->UserIosb = &Irp->IoStatus;
Irp->Tail.Overlay.Thread = PsGetCurrentThread();
Irp->Tail.Overlay.OriginalFileObject = FileObject;
Irp->Overlay.AsynchronousParameters.UserApcRoutine = NULL;
Irp->Flags = IRP_CLOSE_OPERATION | IRP_SYNCHRONOUS_API;

StackPtr = IoGetNextIrpStackLocation(Irp);
StackPtr->MajorFunction = IRP_MJ_CLEANUP;//关键
StackPtr->FileObject = FileObject;
IopQueueIrpToThread(Irp);//将该irp挂入线程的irp队列

Status = IoCallDriver(DeviceObject, Irp);//发出irp
if (Status == STATUS_PENDING)
KeWaitForSingleObject(&Event, UserRequest, KernelMode, FALSE, NULL);

KeRaiseIrql(APC_LEVEL, &OldIrql);
IopUnQueueIrpFromThread(Irp);
KeLowerIrql(OldIrql);
IoFreeIrp(Irp);

if (FileObject->Flags & FO_SYNCHRONOUS_IO) IopUnlockFileObject(FileObject);
}

如上在文件对象的所有句柄关闭了时,系统会生成一个IRP_MJ_CLEANUPirp。
另外文件对象在引用计数递减到0后,在最后销毁前,会生成IRP_MJ_CLOSE irp。

简单一句话【柄完清理、引完关闭】,这两个irp 的产生时机是非常重要的。




进程线程

本篇主要讲述进程的启动过程、线程的调度与切换、进程挂靠

进程的启动过程

1
2
3
4
5
6
7
8
9
10
11
12
BOOL CreateProcess ( 
LPCTSTR lpApplicationName, //
LPTSTR lpCommandLine, // command line string
LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD
LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD
BOOL bInheritHandles, //
DWORD dwCreationFlags, // creation flags
LPVOID lpEnvironment, // new environment block
LPCTSTR lpCurrentDirectory, // current directory name
LPSTARTUPINFO lpStartupInfo, // startup information
LPPROCESS_INFORMATION lpProcessInformation // process information
);

这个 Win32API 在内部最终调用如下:

1
2
3
4
5
6
7
BOOL CreateProcess(„) 
{

NtCreateProcess(„);//间接调用这个系统服务,先创建进程
NtCreateThread(„);//间接调用这个系统服务,再创建该进程的第一个线程(也即主线程)

}

进程的4GB地址空间分两部分,内核空间+用户空间看下面几个定义:

  • #define MmSystemRangeStart 0x80000000 //系统空间的起点
  • #define MM_USER_PROB_ADDRESS MmSystemRangeStart-64kb //除去高端的 64kb 隔离区
  • #define MM_HIGHEST_USER_ADDRESS MmUserProbAddress-1 //实际的用户空间中最高可访问地址
  • #define MM_LOWEST_USER_ADDRESS 64kb //实际的用户空间中最低可访问地址
  • #define KI_USER_SHARED_DATA 0xffdf0000 //内核空间与用户空间共享的一块区域 由此可见,用户地址

空间的范围实际上是从64kb---->0x80000000-64kb这块区域。

访问NULL指针报异常的原因就是NULL(0)落在了最前面的64kb保留区中

内核中提供了一个全局结构变量,该结构的类型是KUSER_SHARED_DATA
内核中的那个结构体变量所在的虚拟页面起始地址为:0xffdf0000大小为一个页面大小。

这个内核页面对应的物理内存页面也映射到了每个进程的用户地址空间中,而且是固定映在同一处:0x7ffe0000
这样用户空间的程序直接访问用户空间中的这个虚拟地址,就相当于直接访问了内核空间中的那个公共页面。

所以那个内核页面称之为内核空间提供给各个进程的一块共享之地。

事实上这个公共页面非常有用,可以在这个页面中放置代码,应用程序直接在 r3 层运行这些代码 如在内核中进行IAT hook

如上【用户空间的范围就是低2GB的空间除去前后64kb后的那块区域】

用户空间

圈定了用户空间的地皮后,现在就到了划分用户空间的时候了。
用户空间的布局:(以区段(Area)为单位进行划分)

MmInitializeProcessAddressSpace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
NTSTATUS MmInitializeProcessAddressSpace(IN PEPROCESS Process,IN PVOID Section,IN OUT PULONG Flags) 
{
NTSTATUS Status = STATUS_SUCCESS;
SIZE_T ViewSize = 0;
PVOID ImageBase = 0;
PROS_SECTION_OBJECT SectionObject = Section;
USHORT Length = 0;

KeAttachProcess(&Process->Pcb);//必须将当前线程挂靠到子进程的地址空间 Process->AddressSpaceInitialized = 2;
if (SectionObject)
{
FileName = SectionObject->FileObject->FileName;
Source = (PWCHAR)((PCHAR)FileName.Buffer + FileName.Length);
if (FileName.Buffer)
{
while (Source > FileName.Buffer)
{
if (*--Source ==L”\\”)
{
Source++;
break;
}
else
Length++;
}
}
Destination = Process->ImageFileName;//任务管理器显示的进程名就是这个(大小写相同)
Length = min(Length, sizeof(Process->ImageFileName) - 1);
while (Length--) *Destination++ = (UCHAR)*Source++;
*Destination = ’\0’;

//将进程的exe文件映射到地址空间中
Status = MmMapViewOfSection(Section,Process,&ImageBase,0,0,NULL,&ViewSize,0,
MEM_COMMIT,…);
Process->SectionBaseAddress = ImageBase;//记录实际映射到的地址(一般为0x00400000)
}
KeDetachProcess();//撤销挂靠
return Status;
}

上面的函数将进程的 exe 文件映射到用户地址空间中(注意 exe 文件内部是分开按节映射)

PspMapSystemDll

映射(即加载)exe文件后,再映射ntdll.dll到用户空间(事实上固定映到某处)

1
2
3
4
5
6
7
8
9
10
11
12
13
NTSTATUS  PspMapSystemDll(PEPROCESS Process,PVOID *DllBase,BOOLEAN UseLargePages) 
{
LARGE_INTEGER Offset = {{0, 0}};
SIZE_T ViewSize = 0;
PVOID ImageBase = 0;

//将NTDLL.dll文件映射到地址空间(每个NTDLL.dll事实上都映射到所有进程地址空间的同一处)
Status = MmMapViewOfSection(PspSystemDllSection,Process,&ImageBase,0,0,&Offset,&ViewSiz e, ViewShare,0,…);


if (DllBase) *DllBase = ImageBase;
return Status;
}

上面这个函数将ntdll.dll映射到地址空间下面这个函数创建该进程的PEB

MmCreatePeb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
NTSTATUS  MmCreatePeb(PEPROCESS Process,PINITIAL_PEB InitialPeb,OUT PPEB *BasePeb) 
{
PPEB Peb = NULL;
SIZE_T ViewSize = 0;
PVOID TableBase = NULL;
KAFFINITY ProcessAffinityMask = 0;
SectionOffset.QuadPart = (ULONGLONG)0;
*BasePeb = NULL;
KeAttachProcess(&Process->Pcb);//因为PEB指针是子进程中的地址,所以要挂靠
//创建一个PEB
Status = MiCreatePebOrTeb(Process, sizeof(PEB), (PULONG_PTR)&Peb);
RtlZeroMemory(Peb, sizeof(PEB));
//根据传入的InitialPeb参数初始化新建的peb
Peb->InheritedAddressSpace = InitialPeb->InheritedAddressSpace;
Peb->Mutant = InitialPeb->Mutant;
Peb->ImageUsesLargePages = InitialPeb->ImageUsesLargePages;

Peb->ImageBaseAddress = Process->SectionBaseAddress;//
Peb->OSMajorVersion = NtMajorVersion; Peb->OSMinorVersion = NtMinorVersion;
Peb->OSBuildNumber = (USHORT)(NtBuildNumber & 0x3FFF);
Peb->OSPlatformId = 2; /* VER_PLATFORM_WIN32_NT */
Peb->OSCSDVersion = (USHORT)CmNtCSDVersion;

Peb->NumberOfProcessors = KeNumberProcessors;

//经典的两个调试检测标志
Peb->BeingDebugged = (BOOLEAN)(Process->DebugPort != NULL ? TRUE : FALSE);
Peb->NtGlobalFlag = NtGlobalFlag;


Peb->MaximumNumberOfHeaps = (PAGE_SIZE - sizeof(PEB)) / sizeof(PVOID);
Peb->ProcessHeaps = (PVOID*)(Peb + 1);//PEB结构体后面是一个堆数组

NtHeaders = RtlImageNtHeader(Peb->ImageBaseAddress);//获取文件头中的NT头
Characteristics = NtHeaders->FileHeader.Characteristics;
if (NtHeaders)
{
_SEH2_TRY
{
ImageConfigData = RtlImageDirectoryEntryToData(Peb->ImageBaseAddress,TRUE,
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG,&ViewSize);
Peb->ImageSubsystem = NtHeaders->OptionalHeader.Subsystem;
Peb->ImageSubsystemMajorVersion = NtHeaders->OptionalHeader.MajorSubsystemVersio n;
Peb->ImageSubsystemMinorVersion = NtHeaders->OptionalHeader.MinorSubsystemVersio n;
if (NtHeaders->OptionalHeader.Win32VersionValue)
{
Peb->OSMajorVersion = NtHeaders->OptionalHeader.Win32VersionValue & 0xFF;
Peb->OSMinorVersion = (NtHeaders->OptionalHeader.Win32VersionValue >> 8) & 0xFF;
Peb->OSBuildNumber = (NtHeaders->OptionalHeader.Win32VersionValue >> 16) & 0x3FFF;
Peb->OSPlatformId = (NtHeaders->OptionalHeader.Win32VersionValue >> 30) ^ 2;
}

if (ImageConfigData != NULL)
{
ProbeForRead(ImageConfigData,sizeof(IMAGE_LOAD_CONFIG_DIRECTORY),
sizeof(ULONG));//读取pe文件中的加载配置信息
if (ImageConfigData->CSDVersion)
Peb->OSCSDVersion = ImageConfigData->CSDVersion;
if (ImageConfigData->ProcessAffinityMask)
ProcessAffinityMask = ImageConfigData->ProcessAffinityMask;
}
if (Characteristics & IMAGE_FILE_UP_SYSTEM_ONLY)
Peb->ImageProcessAffinityMask = 0;
else
Peb->ImageProcessAffinityMask = ProcessAffinityMask;
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
KeDetachProcess();

_SEH2_YIELD(return STATUS_INVALID_IMAGE_PROTECT);
}
_SEH2_END;
}
KeDetachProcess();
*BasePeb = Peb;
return STATUS_SUCCESS;
}

如上上面这个函数为进程创建一个PEB并根据exe文件头中的某些信息初始化里面的某些字段事实上
这个PEB结构体的地址固定安排在0x7FFDF000处,占据一个页面大小。
该页中这个PEB结构体后面就是一个堆数组,存放该进程中创建的所有堆。

BasepInitializeEnvironment

下面的函数为子进程分配一个参数块(即创建参数)和环境变量块(即环境变量字符串)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
NTSTATUS BasepInitializeEnvironment(HANDLE ProcessHandle,PPEB Peb,//子进程的Peb 
LPWSTR ApplicationPathName,LPWSTR lpCurrentDirectory,
LPWSTR lpCommandLine,
LPVOID lpEnvironment,//传给子进程的环境变量块
SIZE_T EnvSize,//环境变量块的大小
LPSTARTUPINFOW StartupInfo,DWORD CreationFlags,
BOOL InheritHandles)
{
PRTL_USER_PROCESS_PARAMETERS RemoteParameters = NULL;
PPEB OurPeb = NtCurrentPeb();//当前进程(即父进程)的Peb
LPVOID Environment = lpEnvironment;
RetVal = GetFullPathNameW(ApplicationPathName, MAX_PATH,FullPath,&Remaining);
RtlInitUnicodeString(&ImageName, FullPath);
RtlInitUnicodeString(&CommandLine, lpCommandLine);
RtlInitUnicodeString(&CurrentDirectory, lpCurrentDirectory);
if (StartupInfo->lpTitle)
RtlInitUnicodeString(&Title, StartupInfo->lpTitle);
else
RtlInitUnicodeString(&Title, L"");
Status = RtlCreateProcessParameters(&ProcessParameters,&ImageName,
lpCurrentDirectory ?&CurrentDirectory : NULL,
&CommandLine,Environment,&Title,..);
if (Environment)
Environment = ScanChar = ProcessParameters->Environment;
else
Environment = ScanChar = OurPeb->ProcessParameters->Environment;
if (ScanChar)
{
EnviroSize =CalcEnvSize(ScanChar);//计算环境变量块的长度

Size = EnviroSize;
//为子进程分配一个环境变量块(跨进程远程分配内存)
Status = ZwAllocateVirtualMemory(ProcessHandle,
(PVOID*)&ProcessParameters->Environment,
0,&Size,MEM_COMMIT,PAGE_READWRITE);
//将环境变量块复制到子进程的空间中
ZwWriteVirtualMemory(ProcessHandle,ProcessParameters->Environment,
Environment,
EnviroSize,
NULL);
}
ProcessParameters->StartingX = StartupInfo->dwX;
ProcessParameters->StartingY = StartupInfo->dwY;
ProcessParameters->CountX = StartupInfo->dwXSize;
ProcessParameters->CountY = StartupInfo->dwYSize;
ProcessParameters->CountCharsX = StartupInfo->dwXCountChars;
ProcessParameters->CountCharsY = StartupInfo->dwYCountChars;
ProcessParameters->FillAttribute = StartupInfo->dwFillAttribute;
ProcessParameters->WindowFlags = StartupInfo->dwFlags;
ProcessParameters->ShowWindowFlags = StartupInfo->wShowWindow;
if (StartupInfo->dwFlags & STARTF_USESTDHANDLES)//让子进程使用自定义的三个标准IO句柄
{ //经常用于匿名管道重定向
ProcessParameters->StandardInput = StartupInfo->hStdInput;
ProcessParameters->StandardOutput = StartupInfo->hStdOutput;
ProcessParameters->StandardError = StartupInfo->hStdError;
}
if (CreationFlags & DETACHED_PROCESS)
ProcessParameters->ConsoleHandle = HANDLE_DETACHED_PROCESS;
else if (CreationFlags & CREATE_NO_WINDOW)
ProcessParameters->ConsoleHandle = HANDLE_CREATE_NO_WINDOW;
else if (CreationFlags & CREATE_NEW_CONSOLE)
ProcessParameters->ConsoleHandle = HANDLE_CREATE_NEW_CONSOLE;
else
{
//让子进程继承父进程的控制台句柄
ProcessParameters->ConsoleHandle = OurPeb->ProcessParameters->ConsoleHandle;
//让子进程继承父进程的三个标准句柄
if (!(StartupInfo->dwFlags &
(STARTF_USESTDHANDLES | STARTF_USEHOTKEY | STARTF_SHELLPRIVATE)))
{
BasepCopyHandles(ProcessParameters,OurPeb->ProcessParameters,InheritHandles);
}
}
Size = ProcessParameters->Length;//参数块本身的长度

//在子进程中分配一个参数块
Status = NtAllocateVirtualMemory(ProcessHandle,&RemoteParameters,0,&Size,
MEM_COMMIT,PAGE_READWRITE); ProcessParameters->MaximumLength = Size;
//在子进程中分配一个参数块
Status = NtWriteVirtualMemory(ProcessHandle,RemoteParameters,ProcessParameters,
ProcessParameters->Length,NULL);
//将参数块复制到子进程的地址空间中
Status = NtWriteVirtualMemory(ProcessHandle,
&Peb->ProcessParameters,
&RemoteParameters,
sizeof(PVOID),
NULL);
RtlDestroyProcessParameters(ProcessParameters);
return STATUS_SUCCESS;
}

BasepCreateFirstThread

下面的函数创建第一个线程的(用户栈、内核栈、初始内核栈帧)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
HANDLE BasepCreateFirstThread(HANDLE ProcessHandle,LPSECURITY_ATTRIBUTES lpThreadAttributes, 
PSECTION_IMAGE_INFORMATION SectionImageInfo,PCLIENT_ID ClientId)
{
BasepCreateStack(ProcessHandle,
SectionImageInfo->MaximumStackSize,//默认为1MB
SectionImageInfo->CommittedStackSize,//默认为4kb
&InitialTeb);//创建(即分配)该线程的用户栈


BasepInitializeContext(&Context,
NtCurrentPeb(),//赋给context.ebx
SectionImageInfo->TransferAddress,//赋给context.eax(也即oep)
InitialTeb.StackBase,// 赋给context.esp
0);//0表示是主线程的用户空间总入口

ObjectAttributes = BasepConvertObjectAttributes(&LocalObjectAttributes,
lpThreadAttributes,NULL);

Status = NtCreateThread(&hThread,THREAD_ALL_ACCESS,ObjectAttributes,ProcessHandle,
ClientId,&Context,&InitialTeb,TRUE);

Status = BasepNotifyCsrOfThread(hThread, ClientId);//通知csrss进程线程创建通知
return hThread;
}

BasepCreateStack

下面的函数用来分配一个用户栈(每个线程都要分配一个)【栈底、栈顶、提交界】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
NTSTATUS BasepCreateStack(HANDLE hProcess, 
SIZE_T StackReserve,//栈的保留大小。默认为1MB
SIZE_T StackCommit,//初始提交大小。默认为4KB,一个页面
OUT PINITIAL_TEB InitialTeb)//用来构造初始teb
{
ULONG_PTR Stack = NULL;
BOOLEAN UseGuard = FALSE;
Status = NtQuerySystemInformation(SystemBasicInformation,&SystemBasicInfo,
sizeof(SYSTEM_BASIC_INFORMATION),NULL);
if (hProcess == NtCurrentProcess())
{
Headers = RtlImageNtHeader(NtCurrentPeb()->ImageBaseAddress);
StackReserve = (StackReserve) ?
StackReserve : Headers->OptionalHeader.SizeOfStackReserve;
StackCommit = (StackCommit) ?
StackCommit : Headers->OptionalHeader.SizeOfStackCommit;
}
else
{
StackReserve = (StackReserve) ? StackReserve :SystemBasicInfo.AllocationGranularity;
StackCommit = (StackCommit) ? StackCommit : SystemBasicInfo.PageSize;
}

//栈的区段长度对齐64kb
StackReserve = ROUND_UP(StackReserve, SystemBasicInfo.AllocationGranularity);
StackCommit = ROUND_UP(StackCommit, SystemBasicInfo.PageSize);

//预定这么大小的栈(默认1MB)
Status = ZwAllocateVirtualMemory(hProcess, (PVOID*)&Stack,0,&StackReserve,MEM_RESERVE,
PAGE_READWRITE);
InitialTeb->AllocatedStackBase = (PVOID)Stack;//栈区段的分配基址
InitialTeb->StackBase = (PVOID)(Stack + StackReserve);//栈底

Stack += StackReserve - StackCommit;
if (StackReserve > StackCommit)
{
UseGuard = TRUE;
Stack -= SystemBasicInfo.PageSize;
StackCommit += SystemBasicInfo.PageSize; //多提交一个保护页
}
//初始提交这么大小的页面(也就是最常见的一个页外加一个保护页的大小)
Status = ZwAllocateVirtualMemory(hProcess, (PVOID*)&Stack,0,&StackCommit,MEM_COMMIT,
PAGE_READWRITE);

InitialTeb->StackLimit = (PVOID)Stack;// StackLimit表示第一个尚未提交页的边界
if (UseGuard)
{
SIZE_T GuardPageSize = SystemBasicInfo.PageSize;

Status = ZwProtectVirtualMemory(hProcess, (PVOID*)&Stack,&GuardPageSize,
PAGE_GUARD | PAGE_READWRITE);//改为PAGE_GUARD属性
InitialTeb->StackLimit = (PVOID)((ULONG_PTR)InitialTeb->StackLimit - GuardPageSize);
}
return STATUS_SUCCESS;
}

BasepInitializeContext

下面这个函数构造该线程的初始寄存器上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
VOID BasepInitializeContext(IN PCONTEXT Context,IN PVOID Parameter,IN PVOID StartAddress, 
IN PVOID StackAddress,IN ULONG ContextType)
{
Context->Eax = (ULONG)StartAddress;//oep或用户指定的线程入口函数
Context->Ebx = (ULONG)Parameter;//peb
Context->Esp = (ULONG)StackAddress;//栈底就是初始栈顶
Context->SegFs = KGDT_R3_TEB | RPL_MASK;//fs指向TEB
Context->SegEs = KGDT_R3_DATA | RPL_MASK;
Context->SegDs = KGDT_R3_DATA | RPL_MASK;
Context->SegCs = KGDT_R3_CODE | RPL_MASK;
Context->SegSs = KGDT_R3_DATA | RPL_MASK;
Context->SegGs = 0;
Context->EFlags = 0x3000; // IOPL 3
if (ContextType == 1)
Context->Eip = (ULONG)BaseThreadStartupThunk; //普通线程的用户空间总入口
else if (ContextType == 2) //纤程
Context->Eip = (ULONG)BaseFiberStartup;
else
Context->Eip = (ULONG)BaseProcessStartThunk; //主线程的用户空间总入口
Context->ContextFlags = CONTEXT_FULL;//所有字段全部有效
Context->Esp -= sizeof(PVOID);//腾出参数空间
}

MmCreateTeb

当线程创建起来后,会紧跟着创建它的teb。现在暂时不看NtCreateThread是怎样实现的,看一下teb的创建过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
NTSTATUS MmCreateTeb(IN PEPROCESS Process, 
IN PCLIENT_ID ClientId,//线程的客户id即【进程id.线程id】
IN PINITIAL_TEB InitialTeb,
OUT PTEB *BaseTeb)//返回teb的地址
{
NTSTATUS Status = STATUS_SUCCESS;
*BaseTeb = NULL;
KeAttachProcess(&Process->Pcb);//挂靠到子进程地址空间
Status = MiCreatePebOrTeb(Process, sizeof(TEB), (PULONG_PTR)&Teb);//从peb处往低地址端搜索

_SEH2_TRY
{
RtlZeroMemory(Teb, sizeof(TEB));
Teb->NtTib.ExceptionList = -1;//初始是没有seh
Teb->NtTib.Self = (PNT_TIB)Teb;//将self指向指针结构的地址,方便寻址
Teb->NtTib.Version = 30 << 8;
Teb->ClientId = *ClientId;
Teb->RealClientId = *ClientId;
Teb->ProcessEnvironmentBlock = Process->Peb;//关键,teb中有个指针指向peb
Teb->CurrentLocale = PsDefaultThreadLocaleId;
if ((InitialTeb->PreviousStackBase == NULL) &&
(InitialTeb->PreviousStackLimit == NULL))
{
Teb->NtTib.StackBase = InitialTeb->StackBase;//栈底
Teb->NtTib.StackLimit = InitialTeb->StackLimit;//提交边界(最近未提交页的地址)
Teb->DeallocationStack = InitialTeb->AllocatedStackBase;
}
else
{
Teb->NtTib.StackBase = InitialTeb->PreviousStackBase;
Teb->NtTib.StackLimit = InitialTeb->PreviousStackLimit;
}
Teb->StaticUnicodeString.MaximumLength = sizeof(Teb->StaticUnicodeBuffer);
Teb->StaticUnicodeString.Buffer = Teb->StaticUnicodeBuffer;
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
KeDetachProcess();

*BaseTeb = Teb;
return Status;
}

这样经过以上的操作后,进程用户空间的典型布局就定出来了。

—————————————————————————————–>

- - - - - - - - - - -
64kb 64kb 64kb 一般1MB 一般在0x00400000处 n*4KB 4kb 4kb 60kb 64kb 0x80000000开始
禁区 环境变量块 参数块 主线程的栈 其它空间 exe文件各个节 其他空间 各teb peb 内核用户共享区 无效区, 隔离区, 系统空间

—————————————————————————————–>

用户空间的布局:一句口诀【环、参、栈、文、堆、t、p】

进程&线程的创建

流程分析
1.NtCreateThread函数中检查地址参数是否合法和可写,保存Teb作为PspCreateThread传入参数。
2.StartRoutine是否有值来决定当前模式是内核/用户模式。为NULL,通过ETHREAD->Tcb获得运行模式。
3.由ProcessHandle参数获得相应的进程对象,保存在局部变量Process。
4.调用ObCreateObject 创建ETHREAD的一个对象。
5.初始线程的停止保护锁(&Thread->RundownProtect), 用于跨线程初始化TEB,挂起线程。
6.设置线程的进程CID, 线程的CID句柄。函数在PspCidTable句柄表创建句柄表项。
7.初始读取的族大小
初始化LPC信号量对象
初始化跨进程通信LPC
初始化所有正在处理单尚末完成的I/O请求<IRP对象>
初始化配置管理器等级注册表的变化通知
初始化线程锁/时间旋转锁/当前线程的所有定时器
8.根据ThreadContext的值来确认此次创建是用户模式线程<非NULL>,或者系统线程.
用户模式线程: 创建一个TEB,并用InitialTeb初始化,接着初始线程的启动地址,WINDOWS子系统的启动地址。
9.调用KeInitThread函数 <继续初始新线程属性。> 同步Header, WaitBlock,系统服务表 ,APC ,定时器, 线程的内核栈等。
10.禁用当前线程内核APC的。且锁定进程。 确保当前进程的状态不是退出或正在终止。进程中Flags标记位判断当前进程是否是死进程。CrossThreadFlags跨线程访问的标志位。
包括Terminated 线程已执行终止操作 创建失败 等信息。
11.进程的活动线程数+1。挂入目标进程(EPROCESS中)的线程队列。
12.启动该线程运行 KeStartThread函数; 并再次初始化末完成的域,设置线程的优先级, 时限设置等。
13.局部变量OldActiveThreads 判断当前创建的线程是否是第一个线程。当为第一个线程: 通知线程创建标注的注册程序.
14.检测当前新创建线程的进程是否正处于在一个作业中。
15.线程对象引用数+2, 一个是当前创建的操作, 另一个返回线程的句柄。
16.CreateSuspended为TURE 挂起目标线程 不让其参与调度。KeSuspendThread挂起目标线程 , KeForceResumeThread 线程唤醒。
17.SeCreateAccessStateEx 创建ACCESS_STATE结构 用来插入进程的句柄表中,通过ObInsertObject函数将新线程对象插入。
18.KeQuerySystemTime 查询线程创建的时间。 PS_SET_THREAD_CREATE_TIME 设置线程创建的时间。
19.目标线程需要根据安全属性描述块确定其允许的访问权限.ObGetObjectSecurity 得到线程SD 。成员GrantedAccess赋值。
注: 已被挂起的线程即使处于就绪状态也不会被调度运行,而要到被解除挂起时才能被调度运行 KeReadyThread函数将线程进入就绪状态。
20.最后ObDereferenceObject将引用计数-1,操作完成。线程创建结束。

CreateProcessInternalW

现在具体看一下CreateProcess的创建过程,它会在内部直接转调CreateProcessInternalW函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
BOOL CreateProcessInternalW(HANDLE hToken,//暂时无用 
LPCWSTR lpApplicationName,//程序文件名
LPWSTR lpCommandLine,//命令行
LPSECURITY_ATTRIBUTES lpProcessAttributes,//SD
LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD
BOOL bInheritHandles,//是否继承父进程句柄表中的那些可继承句柄
DWORD dwCreationFlags,
LPVOID lpEnvironment,//环境变量块
LPCWSTR lpCurrentDirectory,//指定给子进程的当前目录
LPSTARTUPINFOW lpStartupInfo,//附加启动信息
LPPROCESS_INFORMATION lpProcessInformation,//返回创建结果
PHANDLE hNewToken)//暂时无用
{
BOOLEAN CmdLineIsAppName = FALSE;//表示文件名是否就是命令行
UNICODE_STRING ApplicationName = { 0, 0, NULL };
HANDLE hSection = NULL, hProcess = NULL, hThread = NULL, hDebug = NULL;
LPWSTR CurrentDirectory = NULL;
PPEB OurPeb = NtCurrentPeb();//当前进程即父进程的peb
SIZE_T EnvSize = 0;//环境变量块的大小
//检查下面的"映像劫持’键,略
//{HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options}

if ((dwCreationFlags & (DETACHED_PROCESS | CREATE_NEW_CONSOLE)) ==
(DETACHED_PROCESS | CREATE_NEW_CONSOLE))
{
SetLastError(ERROR_INVALID_PARAMETER);//这俩标志不可同时使用
return FALSE;
}

StartupInfo = *lpStartupInfo;
RtlZeroMemory(lpProcessInformation, sizeof(PROCESS_INFORMATION));

PriorityClass.Foreground = FALSE;//初始创建的进程作为后台进程看待
//根据创建标志计算对应的优先级类别(一种优先级类对应一种基本优先级)
PriorityClass.PriorityClass = (UCHAR)BasepConvertPriorityClass(dwCreationFlags);


GetAppName:
if (!lpApplicationName)//很常见
{
NameBuffer = RtlAllocateHeap(RtlGetProcessHeap(),0,MAX_PATH * sizeof(WCHAR));
lpApplicationName = lpCommandLine;
// 处理NameBuffer,略
lpApplicationName = NameBuffer;// 最终获得命令行中包含的应用程序文件名
}
else if (!lpCommandLine || *lpCommandLine == UNICODE_NULL)
{
CmdLineIsAppName = TRUE;
lpCommandLine = (LPWSTR)lpApplicationName;
}
//事实上只是为程序文件创建一个section,等待映射(函数名有误导)
Status = BasepMapFile(lpApplicationName, &hSection, &ApplicationName);
if (!NT_SUCCESS(Status))
{
If(是一个bat批脚本文件)
// 命令行改为“cmd /c bat文件名”,goto GetAppName,重新解析
Else …
}
if (!StartupInfo.lpDesktop)//继承父进程的桌面
StartupInfo.lpDesktop = OurPeb->ProcessParameters->DesktopInfo.Buffer;
//查询section对象的映像文件信息
Status = ZwQuerySection(hSection,SectionImageInformation,
&SectionImageInfo,sizeof(SectionImageInfo),NULL);
if (SectionImageInfo.ImageCharacteristics & IMAGE_FILE_DLL) 失败返回;
if (IMAGE_SUBSYSTEM_WINDOWS_GUI == SectionImageInfo.SubSystemType)
{
dwCreationFlags &= ~CREATE_NEW_CONSOLE;//GUI程序无需控制台
dwCreationFlags |= DETACHED_PROCESS;
}
ObjectAttributes = BasepConvertObjectAttributes(&LocalObjectAttributes,
lpProcessAttributes,NULL);
//if创建的是一个要被当前线程调试的子进程
if (dwCreationFlags & (DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS))
{

Status = DbgUiConnectToDbg();//连接到
hDebug = DbgUiGetThreadDebugObject();//为当前线程创建一个调试端口(用来父子进程通信)
}


//关键。调用系统服务,创建内核中的进程对象,并初始化其地址空间等N多内容
Status = NtCreateProcess(&hProcess,PROCESS_ALL_ACCESS,ObjectAttributes,
NtCurrentProcess(),bInheritHandles,hSection,
hDebug,); //当前线程的调试端口(将传给子进程)

//设置进程的优先级类别
if (PriorityClass.PriorityClass != PROCESS_PRIORITY_CLASS_INVALID)
{
Status = NtSetInformationProcess(hProcess,ProcessPriorityClass, &PriorityClass,sizeof(PROCESS_PRIORITY_CLASS));
}
Status = NtQueryInformationProcess(hProcess,ProcessBasicInformation,&ProcessBasicInfo, sizeof(ProcessBasicInfo),NULL);

if(lpEnvironment && !(dwCreationFlags & CREATE_UNICODE_ENVIRONMENT))
lpEnvironment = BasepConvertUnicodeEnvironment(&EnvSize, lpEnvironment);

RemotePeb = ProcessBasicInfo.PebBaseAddress;//子进程的peb地址(实际上是固定的)
//关键。创建子进程的参数块和环境变量块
Status = BasepInitializeEnvironment(hProcess,RemotePeb,lpApplicationName,
CurrentDirectory,lpCommandLine,
lpEnvironment,EnvSize,//环境变量块的地址、长度
&StartupInfo,dwCreationFlags,bInheritHandles);
//如果没有显式指定这三个标准句柄给子进程,就继承父进程中的那3个标准句柄(最常见)
if (!bInheritHandles && !(StartupInfo.dwFlags & STARTF_USESTDHANDLES) &&
SectionImageInfo.SubSystemType == IMAGE_SUBSYSTEM_WINDOWS_CUI)
{
PRTL_USER_PROCESS_PARAMETERS RemoteParameters;
Status = NtReadVirtualMemory(hProcess,&RemotePeb->ProcessParameters,
&RemoteParameters,sizeof(PVOID),NULL);
BasepDuplicateAndWriteHandle(hProcess,OurPeb->ProcessParameters->StandardInput,
&RemoteParameters->StandardInput);
BasepDuplicateAndWriteHandle(hProcess,OurPeb->ProcessParameters->StandardOutput,
&RemoteParameters->StandardOutput);
BasepDuplicateAndWriteHandle(hProcess,OurPeb->ProcessParameters->StandardError,
&RemoteParameters->StandardError);
}
//通知csrss.exe进程,一个新的进程已创建
Status = BasepNotifyCsrOfCreation(dwCreationFlags,ProcessBasicInfo.UniqueProcessId,
bInheritHandles);

//--------------------------------------华丽的分割线---------------------------------
//至此,已创建好了进程,接下来创建该进程中的第一个线程(主线程)



//这个函数创建主线程的用户栈、内核栈,并建立起初始的运行环境(内核栈帧)
hThread = BasepCreateFirstThread(hProcess,lpThreadAttributes,&SectionImageInfo, &ClientId);//返回线程的pid.tid
if (!(dwCreationFlags & CREATE_SUSPENDED))
NtResumeThread(hThread, &Dummy);//恢复线程,挂入就绪队列(即可以开始运行这个线程了)

lpProcessInformation->dwProcessId = (DWORD)ClientId.UniqueProcess;
lpProcessInformation->dwThreadId = (DWORD)ClientId.UniqueThread;
lpProcessInformation->hProcess = hProcess;
lpProcessInformation->hThread = hThread;
return TRUE;
}

NTSTATUS BasepMapFile(IN LPCWSTR lpApplicationName,OUT PHANDLE hSection, IN PUNICODE_STRING ApplicationName)
{
RelativeName.Handle = NULL;
//转为NT路径格式
RtlDosPathNameToNtPathName_U(lpApplicationName,ApplicationName,NULL,&RelativeName)
if (RelativeName.DosPath.Length)
ApplicationName = &RelativeName.DosPath;

InitializeObjectAttributes(&ObjectAttributes,ApplicationName,OBJ_CASE_INSENSITIVE,
RelativeName.Handle,NULL);
//打开程序文件
Status = NtOpenFile(&hFile,SYNCHRONIZE | FILE_EXECUTE | FILE_READ_DATA,&ObjectAttribute s,
&IoStatusBlock,FILE_SHARE_DELETE | FILE_SHARE_READ,
FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE);

//为文件创建一个公共section,等候映射(多个进程可以同用一个exe文件)
Status = NtCreateSection(hSection,SECTION_ALL_ACCESS,NULL,NULL,PAGE_EXECUTE,
SEC_IMAGE,hFile);
NtClose(hFile);
return Status;
}

如上这个函数的名字有误导,其实只是创建一个section,并没有立即映射到地址空间(多个进程可以共享同一程序文件的)
函数内部间接性调用了NtCreateProcess,BasepInitializeEnvironment,BasepCreateFirstThread

NtCreateProcess

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
NTSTATUS NtCreateProcess(OUT PHANDLE ProcessHandle, 
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ParentProcess,
IN BOOLEAN InheritObjectTable,
IN HANDLE SectionHandle OPTIONAL,
IN HANDLE DebugPort OPTIONAL,
IN HANDLE ExceptionPort OPTIONAL)
{
ULONG Flags = 0;
if ((ULONG)SectionHandle & 1) Flags = PS_REQUEST_BREAKAWAY;
if ((ULONG)DebugPort & 1) Flags |= PS_NO_DEBUG_INHERIT;
if (InheritObjectTable) Flags |= PS_INHERIT_HANDLES;
return NtCreateProcessEx(ProcessHandle,
DesiredAccess,
ObjectAttributes,
ParentProcess,
Flags,
SectionHandle,
DebugPort,
ExceptionPort,
FALSE);
}

NTSTATUS NtCreateProcessEx(OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ParentProcess,
IN ULONG Flags,
IN HANDLE SectionHandle OPTIONAL,
IN HANDLE DebugPort OPTIONAL,
IN HANDLE ExceptionPort OPTIONAL,
IN BOOLEAN InJob)

{
if (!ParentProcess)
Status = STATUS_INVALID_PARAMETER;
else
{
Status = PspCreateProcess(ProcessHandle,
DesiredAccess,
ObjectAttributes,
ParentProcess,
Flags,
SectionHandle,
DebugPort,
ExceptionPort,
InJob);
}
return Status;
}

如上CreateProcess API 调用NtCreateProcess系统服务,最终会调用下面的函数完成进程的创建工作

PspCreateProcess

下面的NtCreateProcessPspCreateProces只是创建了一个进程(它的内核对象、地址空间等)
进程本身是不能运行的,所以CreateProcessAPI 最终还会调用NtCreateThread创建并启动主线程。

线程从运行空间角度看,分为两种线程:

  • 1、 用户线程(主线程和CreateThread创建的普通线程都是用户线程):线程部分代码运行在用户空间
  • 2、 内核线程(由驱动程序调用PsCreateSystemThread创建的线程):线程的所有代码运行在内核空间 两种线程的运行路径分别为:
    • 1、 KiThreadStartup->PspUserThreadStartup->用户空间中的公共入口->映像文件中的入口
    • 2、 KiThreadStartup->PspSystemThreadStartup->内核空间中用户指定的入口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
NTSTATUS PspCreateProcess(OUT PHANDLE ProcessHandle,//返回子进程的句柄 
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ParentProcess OPTIONAL,//父进程可以是任意第三方进程
IN ULONG Flags,
IN HANDLE SectionHandle OPTIONAL,//exe文件的section对象
IN HANDLE DebugPort OPTIONAL,//调试器进程中某个线程的调试端口
IN HANDLE ExceptionPort OPTIONAL)
{
ULONG DirectoryTableBase[2] = {0,0};//为子进程分配的页目录所在的物理页面地址
PETHREAD CurrentThread = PsGetCurrentThread();
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
PEPROCESS CurrentProcess = PsGetCurrentProcess();
PACCESS_STATE AccessState = &LocalAccessState;//记录着当前线程的令牌和申请的访问权限
BOOLEAN NeedsPeb = FALSE;//表示是否需要为其创建一个peb,绝大多数都要
if (ParentProcess)//事实上只有”system”进程没有父进程
{
Status = ObReferenceObjectByHandle(ParentProcess,
PROCESS_CREATE_PROCESS,//表示要为其创建子进程
PsProcessType,PreviousMode, (PVOID*)&Parent);
Affinity = Parent->Pcb.Affinity;//继承父进程的cpu亲缘性
}
else
{

Parent = NULL;
Affinity = KeActiveProcessors;
}
MinWs = PsMinimumWorkingSet; MaxWs = PsMaximumWorkingSet;

//关键 ###################################
//关键。创建该进程的内核对象结构
//关键 ###################################
Status = ObCreateObject(PreviousMode,
PsProcessType,//进程对象类型
ObjectAttributes,PreviousMode,NULL,
sizeof(EPROCESS),//内核进程对象
0,0, (PVOID*)&Process);

RtlZeroMemory(Process, sizeof(EPROCESS));
InitializeListHead(&Process->ThreadListHead);//初始时该进程尚无任何线程
PspInheritQuota(Process, Parent);//继承父进程的资源配额块
ObInheritDeviceMap(Parent, Process);//继承父进程的磁盘卷设备位图

if (Parent)
Process->InheritedFromUniqueProcessId = Parent->UniqueProcessId;//记录父进程的pid
if (SectionHandle)//exe文件的section,一般都有
{
//获得对应的section对象
Status = ObReferenceObjectByHandle(SectionHandle,SECTION_MAP_EXECUTE,
MmSectionObjectType,PreviousMode,
(PVOID*)&SectionObject);
}
Else …


Process->SectionObject = SectionObject;//记录该进程的exe文件section


if (DebugPort)//由调试器启动的子进程,都会传递一个调试端口给子进程
{
Status = ObReferenceObjectByHandle(DebugPort,
DEBUG_OBJECT_ADD_REMOVE_PROCESS,
DbgkDebugObjectType,PreviousMode,
(PVOID*)&DebugObject);
//每个被调进程与调试器中的一个调试器线程通过一个调试端口连接,形成一个调试会话
Process->DebugPort = DebugObject; //可用于检测调试
if (Flags & PS_NO_DEBUG_INHERIT)//指示不可将调试端口再继承给它的子进程
InterlockedOr((PLONG)&Process->Flags, PSF_NO_DEBUG_INHERIT_BIT);
}
else
{
if (Parent)
DbgkCopyProcessDebugPort(Process, Parent);//继承父进程的调试端口
}
if (ExceptionPort)
{
Status = ObReferenceObjectByHandle(ExceptionPort,PORT_ALL_ACCESS,LpcPortObjectType,
PreviousMode, (PVOID*)&ExceptionPortObject);
Process->ExceptionPort = ExceptionPortObject;
}

Process->ExitStatus = STATUS_PENDING;//默认的退出码
if (Parent)
{
/*创建页目录和内核部分的页表,然后从系统公共的内核页表中复制内核空间中的那些页表项
(这样每个进程的内核地址空间的映射就相同了)*/
MmCreateProcessAddressSpace(MinWs,Process,DirectoryTableBase)
}
Else …
InterlockedOr((PLONG)&Process->Flags, PSF_HAS_ADDRESS_SPACE_BIT);

Process->Vm.MaximumWorkingSetSize = MaxWs;

//初始化进程对象的内部结构成员
KeInitializeProcess(&Process->Pcb,PROCESS_PRIORITY_NORMAL,Affinity,DirectoryTableBase);
Status = PspInitializeProcessSecurity(Process, Parent);//继承父进程的令牌
Process->PriorityClass = PROCESS_PRIORITY_CLASS_NORMAL;//初始创建时都是普通优先级类
Status = STATUS_SUCCESS;
if (SectionHandle) //一般都有
{
//初始化地址空间并将exe文件映射到用户空间中
Status = MmInitializeProcessAddressSpace(Process,SectionObject,&Flags,ImageFileNam e);
NeedsPeb = TRUE;
}
Else …

if (SectionObject)//映射(即加载)exe文件后,再映射ntdll.dll到用户空间(事实上固定映到某处)
PspMapSystemDll(Process, NULL, FALSE);

CidEntry.Object = Process; CidEntry.GrantedAccess = 0;

//进程id、线程id实际上都是全局PspCidTable句柄表中的句柄,他们也指向对应的对象
Process->UniqueProcessId = ExCreateHandle(PspCidTable, &CidEntry);//分配pid进程号
Process->ObjectTable->UniqueProcessId = Process->UniqueProcessId;

if ((Parent) && (NeedsPeb))//用户空间中的进程都会分配一个peb,且固定在某处
{
RtlZeroMemory(&InitialPeb, sizeof(INITIAL_PEB));
InitialPeb.Mutant = (HANDLE)-1;
if (SectionHandle)
Status = MmCreatePeb(Process, &InitialPeb, &Process->Peb);//创建peb(固定在某处)
Else …

}

/*将进程加入全局的“活动进程链表”中,这个链表仅供系统统计用,因此可以恣意篡改
如隐藏进程。任务管理器等其他绝大多数进程枚举工具内部就是遍历的这个进程链表*/
InsertTailList(&PsActiveProcessHead, &Process->ActiveProcessLinks);

//这个函数用来将进程对象插入句柄表,返回一个进程句柄
Status = ObInsertObject(Process,AccessState,DesiredAccess,1,NULL,&hProcess);


//根据进程的优先级类计算该进程的基本优先级和时间片(初始创建时作为后台进程)
Process->Pcb.BasePriority =PspComputeQuantumAndPriority(Process, PsProcessPriorityBackground,&Quantum);
Process->Pcb.QuantumReset = Quantum; KeQuerySystemTime(&Process->CreateTime);//记录进程的创建时间

PspRunCreateProcessNotifyRoutines(Process, TRUE);//发出一个进程创建通知消息
*ProcessHandle = hProcess;//返回对应的句柄
return Status;
}

如上面函数创建内核线程对象,然后调用下面的函数初始化对象结构,创建它的内核栈
然后构造好它的初始运行环境(指内核栈中的初始状态),设置好初始的优先级和时间片后,就启动线程运行(指加 入就绪队列)。

这样当该线程不久被调度运行时,就能跟着内核栈中初始的状态,一直运行下去

指调度时:恢复线程切换线程,从KiThreadStartup函数开始运行,然后恢复用户空间寄存器现场
回到用户空间的公共总入口处(kernel32模块中的BaseProcessStrartThunkBaseThreadStrartThunk)继续执行)

并调用MmInitializeProcessAddressSpace初始化地址空间并将exe文件映射到用户空间中

KeInitThread

下面这个函数就是用来实际执行构造线程的初始运行环境(即初始的内核栈状态)工作
初始的内核栈会模拟该线程仿佛以前曾经运行过,曾经被切换后的状态,这样该线程一旦得到初始调度 机会,就向得到重新调度机会一样继续运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
NTSTATUS KeInitThread(IN OUT PKTHREAD Thread, 
IN PKSYSTEM_ROUTINE SystemRoutine,//用户线程是PspUserThreadStartup
IN PKSTART_ROUTINE StartRoutine,//用户线程是NULL,使用公共的总入口
IN PVOID StartContext,//入口参数
IN PCONTEXT Context,//用户空间的初始寄存器上下文
IN PVOID Teb,//用户线程的初始teb
IN PKPROCESS Process)
{
BOOLEAN AllocatedStack = FALSE;//表示是否分配了内核栈
KeInitializeDispatcherHeader(&Thread->DispatcherHeader,ThreadObject,
sizeof(KTHREAD) / sizeof(LONG),FALSE);//线程也是可等待对象
InitializeListHead(&Thread->MutantListHead);
for (i = 0; i< (THREAD_WAIT_OBJECTS + 1); i++)
Thread->WaitBlock[i].Thread = Thread;//线程内部内置的四个预定等待块
Thread->EnableStackSwap = TRUE; //指示内核栈可以被置换到外存
Thread->IdealProcessor = 1;
Thread->SwapBusy = FALSE;//一个标记当前线程是否正在进行切换的标记
Thread->KernelStackResident = TRUE; //线程初始创建时,内核栈当然位于物理内存中
Thread->AdjustReason = AdjustNone;//优先级的调整原因
Thread->ServiceTable = KeServiceDescriptorTable;//该线程使用的系统服务表描述符表(非SSDT)
//初始时,线程的两个APC队列都为空
InitializeListHead(&Thread->ApcState.ApcListHead[0]);
InitializeListHead(&Thread->ApcState.ApcListHead[1]);
Thread->ApcState.Process = Process;//当前进程
Thread->ApcStatePointer[OriginalApcEnvironment] = &Thread->ApcState;

Thread->ApcStatePointer[AttachedApcEnvironment] = &Thread->SavedApcState;
Thread->ApcStateIndex = OriginalApcEnvironment;
Thread->ApcQueueable = TRUE;//标记初始时,APC队列可插入

//一个专用于挂起线程的APC,后文会有介绍
KeInitializeApc(&Thread->SuspendApc,Thread,
OriginalApcEnvironment,
KiSuspendNop,
KiSuspendRundown,
KiSuspendThread,//该apc真正的函数
KernelMode,NULL);
KeInitializeSemaphore(&Thread->SuspendSemaphore, 0, 2);
Timer = &Thread->Timer;//可复用
KeInitializeTimer(Timer);
TimerWaitBlock = &Thread->WaitBlock[TIMER_WAIT_BLOCK];//定时器固定占用一个等待快
TimerWaitBlock->Object = Timer;
TimerWaitBlock->WaitKey = STATUS_TIMEOUT;
TimerWaitBlock->WaitType = WaitAny;
TimerWaitBlock->NextWaitBlock = NULL;
TimerWaitBlock->WaitListEntry.Flink = &Timer->Header.WaitListHead;
TimerWaitBlock->WaitListEntry.Blink = &Timer->Header.WaitListHead;

Thread->Teb = Teb;//记录teb
KernelStack = MmCreateKernelStack(FALSE, 0);//关键。分配该线程的内核栈
AllocatedStack = TRUE;//标记为已分配

Thread->InitialStack = KernelStack;//初始的内核栈顶(即栈底)
Thread->StackBase = KernelStack;//内核栈底
Thread->StackLimit = KernelStack -12kb;//普通线程的内核栈的大小为12kb
Thread->KernelStackResident = TRUE;//初始时,内核栈当然位于物理内存中


Status = STATUS_SUCCESS;
//关键。下面这个函数构造初始的内核栈帧(模拟切换时的状态)
KiInitializeContextThread(Thread,
SystemRoutine,//用户线程为PspUserThreadStartup
StartRoutine,//用户线程为NULL(表示使用公共总入口)
StartContext,//入口参数
Context);//用户空间的初始寄存器上下文
Thread->State = Initialized;//标记为已初始化好,可以运行了
return Status;
}

KiInitializeContextThread

每个处于非运行状态的线程的内核栈的布局是:(从栈底到栈顶)【浮点、trap、函数、切】

浮点寄存器帧|trap 现场帧|内核各层函数参数、局部变量帧|线程切换帧

每次发生系统调用、中断、异常时线程都会进入内核,在内核栈先保存浮点寄存器,然后保存寄存器现场
进入内核函数嵌套调用,最后由于时间片等原因发生线程切换,保存切换时的现场,等待下次调度运行时, 从上次切换出时的断点处继续执行。
注意每当重回到用户空间后,线程的内核栈就是空白的。

一个线程的绝大多数时间都是运行在用户空间, 因此绝大多数时刻,线程的内核栈都呈现空白状态(里面没存放任何数据)。 下面这个函数就是用来初始构造模拟线程被切换出时的现场(实际线程还没运行过,即还没切换过)。

非常关键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
VOID KiInitializeContextThread(IN PKTHREAD Thread, 
IN PKSYSTEM_ROUTINE SystemRoutine,//用户线程为PspUserThreadStartup
IN PKSTART_ROUTINE StartRoutine, //用户线程为NULL(表示公共总入口)
IN PVOID StartContext, //入口参数
IN PCONTEXT ContextPointer) //用户线程的初始上下文(内核线程没有)
{
PFX_SAVE_AREA FxSaveArea;//内核栈中的浮点寄存器保存区
PFXSAVE_FORMAT FxSaveFormat;
PKSTART_FRAME StartFrame;//线程公共起始函数KiThreadStartup的栈帧
PKSWITCHFRAME CtxSwitchFrame;//切换帧
PKTRAP_FRAME TrapFrame;//trap现场帧
CONTEXT LocalContext;//临时变量
PCONTEXT Context = NULL;
ULONG ContextFlags;
PKUINIT_FRAME InitFrame;//线程的初始内核栈帧(由浮点帧、trap帧、起始函数帧、切换帧组成)
InitFrame = (PKUINIT_FRAME)( Thread->InitialStack - sizeof(KUINIT_FRAME));
FxSaveArea = &InitFrame->FxSaveArea;//初始帧中的浮点保存区

RtlZeroMemory(FxSaveArea,KTRAP_FRAME_LENGTH + sizeof(FX_SAVE_AREA));
TrapFrame = &InitFrame->TrapFrame;//初始帧中的trap现场帧(最重要)
StartFrame = &InitFrame->StartFrame;//起始函数(指KiThreadStartup)的参数帧(重要)
CtxSwitchFrame = &InitFrame->CtxSwitchFrame;//切换帧(非常重要)

if (ContextPointer)//如果是要构造用户线程的初始帧
{
RtlCopyMemory(&LocalContext, ContextPointer, sizeof(CONTEXT));
Context = &LocalContext;
ContextFlags = CONTEXT_CONTROL;
{初始化浮点寄存器部分略}
Context->ContextFlags &= ~CONTEXT_DEBUG_REGISTERS;//初始时不需要调试寄存器
//关键。模拟保存进入内核空间中时的现场

KeContextToTrapFrame(Context,NULL,TrapFrame,Context->ContextFlags | ContextFlags,
UserMode);//将Context中各个寄存器填写到Trap帧中(模拟自陷现场)
TrapFrame->HardwareSegSs |= RPL_MASK;
TrapFrame->SegDs |= RPL_MASK;TrapFrame->SegEs |= RPL_MASK;
TrapFrame->Dr7 = 0;//不需要调试寄存器
TrapFrame->DbgArgMark = 0xBADB0D00;
TrapFrame->PreviousPreviousMode = UserMode;
TrapFrame->ExceptionList = -1;

Thread->PreviousMode = UserMode;//模拟从用户空间自陷进来时构造的帧
StartFrame->UserThread = TRUE;//相当于push传参给KiThreadStartup
}
else
{
{内核线程则会初始化成不同的浮点寄存器,略}
Thread->PreviousMode = KernelMode; //模拟从内核空间发起系统调用时构造的帧
StartFrame->UserThread = FALSE; //相当于push传参给KiThreadStartup
}

StartFrame->StartContext = StartContext;//相当于push传参给KiThreadStartup
StartFrame->StartRoutine = StartRoutine; //相当于push传参给KiThreadStartup
StartFrame->SystemRoutine = SystemRoutine; //相当于push传参给KiThreadStartup

//关键。模拟线程仿佛上次在执行call KiThreadStartup时,被切换了出去
CtxSwitchFrame->RetAddr = KiThreadStartup;//以后线程一调度就从这儿开始执行下去
CtxSwitchFrame->ApcBypassDisable = TRUE;
CtxSwitchFrame->ExceptionList = -1;//线程的初始内核seh链表当然为空(-1表示空)
Thread->KernelStack = CtxSwitchFrame;//记录上次切换时的内核栈顶(模拟的)
}

为了弄懂线程初始时的内核栈布局,必须理解下面几个结构体定义和函数。

_KUINIT_FRAME

1
2
3
4
5
6
7
typedef struct _KUINIT_FRAME //每个线程的初始内核栈帧 
{
KSWITCHFRAME CtxSwitchFrame;//切换帧
KSTART_FRAME StartFrame;//KiThreadStartup函数的参数帧
KTRAP_FRAME TrapFrame;//trap现场帧
FX_SAVE_AREA FxSaveArea;//浮点保存区
} KUINIT_FRAME, *PKUINIT_FRAME;

其中浮点保存区就位于栈底,向上依次是 trap 现场帧、KiThreadStartup 的参数帧、切换帧

_KSTART_FRAME

1
2
3
4
5
6
7
8
typedef struct _KSTART_FRAME //KiThreadStartup的参数帧 
{
PKSYSTEM_ROUTINE SystemRoutine; //用户线程为PspUserThreadStartup
PKSTART_ROUTINE StartRoutine;//用户线程为NULL(表示使用公共总入口)
PVOID StartContext;//入口参数
BOOLEAN UserThread;//标志

} KSTART_FRAME, *PKSTART_FRAME;

_KSWITCHFRAME

1
2
3
4
5
6
7
8
9
typedef struct _KSWITCHFRAME //切换帧 
{
PVOID ExceptionList;//保存线程切换时的内核she链表(不是用户空间中的seh)
Union
{
BOOLEAN ApcBypassDisable;//用于首次调度 UCHAR WaitIrql;//用于保存切换时的WaitIrql
};
PVOID RetAddr;//保存发生切换时的断点地址(以后切换回来时从这儿继续执行)
} KSWITCHFRAME, *PKSWITCHFRAME;

KiThreadStartup

不管是用户线程还是内核线程,都是最开始从下面这个函数开始执行起来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Void KiThreadStartup(PKSYSTEM_ROUTINE  SystemRoutine //用户线程为 PspUserThreadStartup 
PKSTART_ROUTINE StartRoutine //转用作 PspUserThreadStartup 的参数
Void* StartContext//转用作 PspUserThreadStartup 的参数
BOOL UserThread)
{
Xor ebx,ebx
Xor esi,esi
Xor edi,edi
Xor ebp,ebp
Mov ecx,APC_LEVEL
Call KfLowerIrql //降到 APC 级别
Pop eax //弹出的第一个值刚好是 SystemRoutine
Call eax //调用 SystemRoutine(注意 StartRoutine,StartContext 又是它的参数)

----------------------------------华丽的分割线------------------------------------------
//注意若创建的是内核线程,那么上面的 eax 是 PspSystemThreadStartup,这个函数是“不返回的”,
// 它执行完毕后不会 ret 回来,而是直接跳去用户指定的内核入口了。反之,若能回来,那么可以肯定是用户线程
// (而且,StartRoutine 和 StartContext 这两个参数已被 PspSystemThreadStartup 在内部弹 出)。
// 那么现在就可以顺利弹出 trap 帧,恢复用户空间中的寄存器上下文继续执行,于是 Jmp Ki ServiceExit2, jmp 到那儿去,退回用户空间。
Pop ecx //此时 ecx=栈帧中 UserThread 字段的值
Or ecx,ecx
Jz BadThread //UserThread 不为 1 就显示蓝屏界面
Mov ebp,esp //此时的内核栈顶就是 trap 帧的地址。
Jmp KiServiceExit2 //此时内核栈中只剩余浮点保存区和 trap 帧,将恢复用户空间现场退回用户空间
}

PspUserThreadStartup

上面的线程公共入口函数内部会call SystemRoutine进入对应的函数。如果创建的是用户线程,调用的 就是PspUserThreadStartup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
VOID PspUserThreadStartup(IN PKSTART_ROUTINE StartRoutine,//对于用户线程无意义 
IN PVOID StartContext)//对于用户线程无意义
{
BOOLEAN DeadThread = FALSE;
KeLowerIrql(PASSIVE_LEVEL);
Thread = PsGetCurrentThread();
if (Thread->DeadThread)
DeadThread = TRUE;
Else …

if (!(Thread->DeadThread) && !(Thread->HideFromDebugger))
DbgkCreateThread(Thread, StartContext);//通知内核调试器一个新线程启动了

if (!DeadThread)
{
KeRaiseIrql(APC_LEVEL, &OldIrql);
//返回用户空间的总入口前先执行一下apc,完成其他初始工作(如加载其他依赖库)
KiInitializeUserApc(KeGetExceptionFrame(&Thread->Tcb),
KeGetTrapFrame(&Thread->Tcb),
PspSystemDllEntryPoint,//ntdll.LdrInitializeThunk
NULL,PspSystemDllBase,NULL);
KeLowerIrql(PASSIVE_LEVEL);
}
Else …
//这个函数是典型的"返回型’函数,会返回到上面函数的call eax指令后面,进而退回用户空间的 总入口函数去去执行。
// 不过在退回总入口前,这儿插入了一个APC
// 执行完这个附加的APC后才会正式退回用户空间的总入口(因为这个LdrInitializeThunk APC函数还有一些重要的附加工作要做)
Return;
}

相信大家一直有一个疑问,就是用户空间的总入口到底做了什么工作,我们后文再看。


现在我们回到 CreateProcess API,总结一下这个函数到底在内部干了什么,看以下总结

  • 1、 打开目标可执行文件
    若是 exe 文件,先检查”映像劫持’键,然后打开文件,创建一个 section,等候映射
    若是 bat、cmd 脚本文件,则启动的是 cmd.exe 进程,脚本文件作为命令行参数
    若是 DOS 的 exe、com 文件,启动 ntvdm.exe v86 进程,原文件作为命令行参数
    若是 posix、os2 文件,启动对应的子系统服务进程
  • 2、 创建、初始化进程对象;创建初始化地址空间;加载映射 exe 和 ntdll 文件;分配一个 PEB
  • 3、 创建、初始化主线程对象;创建 TEB;构造初始的运行环境(内核初始栈帧)
  • 4、 通知 windows 子系统(csrss.exe 进程)新进程创建事件(csrss.exe 进程含有绝大多数进程的句柄) 这样进程、主线程都创建起来了,只需等待得到 cpu 调度便可投入运行。


LdrInitializeThunk

当用户线程从内核的KiThreadStartup运行起来后,进入PspUserThreadStartup
最后回到用户空间的总入口处(主线程的用户空间根是BaseProcessStartThunk,其他线程的用户空间根是BaseThreadStartThunk)继续运行。

不过前文讲了,在正式从内核回到用户空间的的总入口前,会扫描执行中途插入的 APC 函数, 做完附加的 APC 工作后才从总入口处继续运行。
插入的这个 APC 函数是LdrInitializeThunk,它的主要工作是负责加载 exe 文件依赖的所有动态库以及其他工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
VOID LdrInitializeThunk() //APC 
{
Lea eax,[esp+16]
Mov [esp+4],eax //第一个参数=Context*
Xor ebp,ebp
Jmp LdrpInit //实际的工作
}

VOID LdrpInit(PCONTEXT Context,PVOID SystemArgument1,PVOID SystemArgument2) //APC
{
if (!LdrpInitialized)//if 主线程
{
LdrpInit2(Context, SystemArgument1, SystemArgument2);
LdrpInitialized = TRUE;
}
//各线程创建后都会通知进程中的所有模块一个ThreadAttach消息
LdrpAttachThread();
}

// 看看主线程的初始化工作,也即进程的初始化工作如下:
VOID LdrpInit2(PCONTEXT Context,PVOID SystemArgument1,PVOID SystemArgument2)
{
PPEB Peb = NtCurrentPeb();//现在就是子进程的peb啦(在子进程的地址空间中)
PVOID BaseAddress = SystemArgument1;//ntdll模块的地址
ImageBase = Peb->ImageBaseAddress;
PEDosHeader = (PIMAGE_DOS_HEADER) ImageBase;
if (PEDosHeader->e_magic != IMAGE_DOS_SIGNATURE || PEDosHeader->e_lfanew == 0L ||
*(PULONG)((PUCHAR)ImageBase + PEDosHeader->e_lfanew) != IMAGE_NT_SIGNATURE)
{
//验证PE文件签名
ZwTerminateProcess(NtCurrentProcess(), STATUS_INVALID_IMAGE_FORMAT);
}

RtlNormalizeProcessParams(Peb->ProcessParameters);
NTHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)ImageBase + PEDosHeader->e_lfanew);
Status = ZwQuerySystemInformation(SystemBasicInformation,&SystemInformation,
sizeof(SYSTEM_BASIC_INFORMATION),NULL);
Peb->NumberOfProcessors = SystemInformation.NumberOfProcessors;

RtlInitializeHeapManager();


//创建进程的默认堆
Peb->ProcessHeap = RtlCreateHeap(HEAP_GROWABLE,NULL,
NTHeaders->OptionalHeader.SizeOfHeapReserve,//一般为0
NTHeaders->OptionalHeader.SizeOfHeapCommit,//一般为4kb
NULL,NULL);
RtlpInitializeVectoredExceptionHandling();//初始化向量化异常

RtlInitializeCriticalSection(&PebLock);
Peb->FastPebLock = &PebLock;

//初始化peb中内置的动态tls位图
Peb->TlsBitmap = &TlsBitMap;
Peb->TlsExpansionBitmap = &TlsExpansionBitMap;
Peb->TlsExpansionCounter = 64;
RtlInitializeBitMap(&TlsBitMap, Peb->TlsBitmapBits,64);//固定指向内置的那个64位tls位图
RtlInitializeBitMap(&TlsExpansionBitMap, Peb->TlsExpansionBitmapBits,1024);


//初始化回调表
Peb->KernelCallbackTable = RtlAllocateHeap(RtlGetProcessHeap(),0,sizeof(PVOID) *
(USER32_CALLBACK_MAXIMUM + 1));
RtlInitializeCriticalSection(&LoaderLock);
Peb->LoaderLock = &LoaderLock;

//从默认堆中分配一个加载信息块
Peb->Ldr = (PPEB_LDR_DATA) RtlAllocateHeap(Peb->ProcessHeap,0,sizeof(PEB_LDR_DATA));
Peb->Ldr->Length = sizeof(PEB_LDR_DATA);
Peb->Ldr->Initialized = FALSE;//表示尚未完成初始化(也即尚未完成加载dll等工作)
Peb->Ldr->SsHandle = NULL;
InitializeListHead(&Peb->Ldr->InLoadOrderModuleList);//加载顺序的模块表
InitializeListHead(&Peb->Ldr->InMemoryOrderModuleList);//内存地址顺序的模块表
//初始化顺序模块表,初始化顺序与加载顺序相反(最底层的dll最先得到初始化)
InitializeListHead(&Peb->Ldr->InInitializationOrderModuleList);
LoadImageFileExecutionOptions(Peb);

ExeModule = RtlAllocateHeap(Peb->ProcessHeap,0,sizeof(LDR_DATA_TABLE_ENTRY));

ExeModule->DllBase = Peb->ImageBaseAddress;
RtlCreateUnicodeString(&ExeModule->FullDllName,
Peb->ProcessParameters->ImagePathName.Buffer);
RtlCreateUnicodeString(&ExeModule->BaseDllName,
wcsrchr(ExeModule->FullDllName.Buffer, L'\\') + 1);

ExeModule->Flags = LDRP_ENTRY_PROCESSED;//exe模块没有dll标志
ExeModule->LoadCount = -1;//标记为无法动态卸载
ExeModule->TlsIndex = -1;
ExeModule->SectionPointer = NULL;
ExeModule->CheckSum = 0;
NTHeaders = RtlImageNtHeader(ExeModule->DllBase);
ExeModule->SizeOfImage = LdrpGetResidentSize(NTHeaders);

ExeModule->TimeDateStamp = NTHeaders->FileHeader.TimeDateStamp;
//先插入exe文件的模块描述符
InsertTailList(&Peb->Ldr->InLoadOrderModuleList,&ExeModule->InLoadOrderLinks);



wcscpy(FullNtDllPath, SharedUserData->NtSystemRoot);//一般为C:\Windows
wcscat(FullNtDllPath, L"\\system32\\ntdll.dll");

NtModule = (PLDR_DATA_TABLE_ENTRY)
RtlAllocateHeap(Peb->ProcessHeap,0,sizeof(LDR_DATA_TABLE_ENTRY));
memset(NtModule, 0, sizeof(LDR_DATA_TABLE_ENTRY));

NtModule->DllBase = BaseAddress;
NtModule->EntryPoint = 0;
RtlCreateUnicodeString(&NtModule->FullDllName, FullNtDllPath);
RtlCreateUnicodeString(&NtModule->BaseDllName, L"ntdll.dll");
NtModule->Flags = LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED;

NtModule->LoadCount = -1;//标记无法动态卸载ntdll.dll
NtModule->TlsIndex = -1;
NtModule->SectionPointer = NULL;
NtModule->CheckSum = 0;
NTHeaders = RtlImageNtHeader(NtModule->DllBase);
NtModule->SizeOfImage = LdrpGetResidentSize(NTHeaders);
NtModule->TimeDateStamp = NTHeaders->FileHeader.TimeDateStamp;

//再插入ntdll文件的模块描述符
InsertTailList(&Peb->Ldr->InLoadOrderModuleList,&NtModule->InLoadOrderLinks);
InsertTailList(&Peb->Ldr->InInitializationOrderModuleList,
&NtModule->InInitializationOrderModuleList);//NTDLL不依赖其他库
LdrpInitLoader();//获取“\KnownDlls\KnownDllPath”路径

//PE加载器的核心函数,用来执行模块的重定位、加载导入库、处理tls
ExeModule->EntryPoint = LdrPEStartup(ImageBase, NULL, NULL, NULL);
Peb->Ldr->Initialized = TRUE;//标志该进程的所有dll都已加载完成
if (Peb->BeingDebugged)
DbgBreakPoint();//int 3通知调试器, 首次触发调试中断
}

LdrPEStartup

进程初始时的重点工作就是加载 exe 文件依赖的所有子孙 dll
由下面的函数完成这项工作 (注意这个函数专用来启动初始化进程的主 exe 文件,是启动阶段的核心函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
PEPFUNC LdrPEStartup (PVOID ImageBase,//exe文件的内存地址(进程的主exe文件) 
HANDLE SectionHandle,
PLDR_DATA_TABLE_ENTRY* Module,
PWSTR FullDosName)
{

PEPFUNC EntryPoint = NULL;
DosHeader = (PIMAGE_DOS_HEADER) ImageBase;
NTHeaders = (PIMAGE_NT_HEADERS) ((ULONG_PTR)ImageBase + DosHeader->e_lfanew);

//if 实际加载地址与pe头中的预期加载地址不同,执行重定位工作(常见于dll文件,exe文件也可能)
if (ImageBase != (PVOID) NTHeaders->OptionalHeader.ImageBase)
Status = LdrPerformRelocations(NTHeaders, ImageBase);//遍历.reloc节中项目,执行重定位

if (Module != NULL)//也即if 是dll文件(事实上这个条件永不满足)
{
*Module = LdrAddModuleEntry(ImageBase, NTHeaders, FullDosName);//加入加模块载顺序链表
(*Module)->SectionPointer = SectionHandle;
}
Else //也即进程的主exe文件,这才是正题
{
Module = &tmpModule;
Status = LdrFindEntryForAddress(ImageBase, Module);//直接在模块表中查找
}
if (ImageBase != (PVOID) NTHeaders->OptionalHeader.ImageBase)
(*Module)->Flags |= LDRP_IMAGE_NOT_AT_BASE;

Status = RtlAllocateActivationContextStack(&ActivationContextStack);
if (NT_SUCCESS(Status))
{
NtCurrentTeb()->ActivationContextStackPointer = ActivationContextStack;
NtCurrentTeb()->ActivationContextStackPointer->ActiveFrame = NULL;
}
Status = LdrFixupImports(NULL, *Module);//加载子孙dll,修正IAT导入表
Status = LdrpInitializeTlsForProccess();//初始化进程的静态tls,详见后文
if (NT_SUCCESS(Status))
{
LdrpAttachProcess();//发送一个ProcessAttach消息,调用该模块的DllMain函数
LdrpTlsCallback(*Module, DLL_PROCESS_ATTACH);
}
if (NTHeaders->OptionalHeader.AddressOfEntryPoint != 0)
EntryPoint = (ULONG_PTR)ImageBase+ NTHeaders->OptionalHeader.AddressOfEntryPoint;
return EntryPoint;//返回oep
}

LdrFixupImports

下面的函数加载指定模块依赖的所有子孙 dll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
NTSTATUS LdrFixupImports(IN PWSTR SearchPath OPTIONAL,//自定义的dll搜索路径(不提供的话就使用标准路径) 
IN PLDR_DATA_TABLE_ENTRY Module)//指定模块
{

ULONG TlsSize = 0;
NTSTATUS Status = STATUS_SUCCESS;

//获取tls目录
TlsDirectory = (PIMAGE_TLS_DIRECTORY)RtlImageDirectoryEntryToData(Module->DllBase,
TRUE,IMAGE_DIRECTORY_ENTRY_TLS,&Size);
if (TlsDirectory)
{
TlsSize = TlsDirectory->EndAddressOfRawData- TlsDirectory->StartAddressOfRawData
+ TlsDirectory->SizeOfZeroFill;
if (TlsSize > 0 && NtCurrentPeb()->Ldr->Initialized)//if 动态加载该模块
TlsDirectory = NULL;// 动态加载的模块不支持静态tls
}

ImportModuleDirectory = (PIMAGE_IMPORT_DESCRIPTOR)
RtlImageDirectoryEntryToData(Module->DllBase,
TRUE,IMAGE_DIRECTORY_ENTRY_IMPORT,&Size);
BoundImportDescriptor = (PIMAGE_BOUND_IMPORT_DESCRIPTOR)
RtlImageDirectoryEntryToData(Module->DllBase,TRUE,
MAGE_DIRECTORY_ENTRY_BOUND_IMPORT,&Size);

if (BoundImportDescriptor != NULL && ImportModuleDirectory == NULL)
return STATUS_UNSUCCESSFUL;
if (BoundImportDescriptor)
// 处理绑定导入表,略
else if (ImportModuleDirectory)
{
ImportModuleDirectoryCurrent = ImportModuleDirectory;//当前依赖的模块
while (ImportModuleDirectoryCurrent->Name)//遍历IMT导入模块表中的各个依赖模块
{
ImportedName = Module->DllBase + ImportModuleDirectoryCurrent->Name;//模块名
if (SearchPath == NULL) //如果没提供自定义搜索路径,就构造一个标准搜索路径
{
//标准搜索路径是:exe文件目录;当前目录;Sytem32目录;Windows目录;Path环境变量
ModulePath = LdrpQueryAppPaths(Module->BaseDllName.Buffer);
Status = LdrpGetOrLoadModule(ModulePath, ImportedName, &ImportedModule, TRU E);
if (NT_SUCCESS(Status))
goto Success;
}
//在模块加载表中查找该模块或者加载该模块(找不到就加载)
Status = LdrpGetOrLoadModule(SearchPath, ImportedName, &ImportedModule, TRUE);
Success:
//处理该依赖模块的IAT导入地址表(获取各个导入函数的实际地址,填到IAT对应的表项中)
Status = LdrpProcessImportDirectoryEntry(Module, ImportedModule, ImportModuleDirectoryCurrent);
ImportModuleDirectoryCurrent++;//下一个依赖的模块
}

}

if (TlsDirectory && TlsSize > 0)
LdrpAcquireTlsSlot(Module, TlsSize, FALSE);
return STATUS_SUCCESS;
}



NTSTATUS LdrpGetOrLoadModule(PWCHAR SearchPath,//搜索路径
PCHAR Name,//模块名,是ASC形式
PLDR_DATA_TABLE_ENTRY* Module,
BOOLEAN Load)//指找不到的话,是否加载
{
RtlInitAnsiString(&AnsiDllName, Name);
Status = RtlAnsiStringToUnicodeString(&DllName, &AnsiDllName, TRUE);

Status = LdrFindEntryForName (&DllName, Module, Load);
if (Load && !NT_SUCCESS(Status))
{
Status = LdrpLoadModule(SearchPath,0,&DllName,Module,NULL);
if (NT_SUCCESS(Status))
Status = LdrFindEntryForName (&DllName, Module, FALSE);
}
return Status;
}

LdrpLoadModule

之所以要在加载模块表中查找,找不到才加载,是因为避免同一个模块加载两次。
下面的函数用来加载一个模块。(注意这个函数也供LoadLibrary API内部间接调用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
NTSTATUS LdrpLoadModule(IN PWSTR SearchPath OPTIONAL, 
IN ULONG LoadFlags,
IN PUNICODE_STRING Name,//模块名
PLDR_DATA_TABLE_ENTRY *Module,
PVOID *BaseAddress OPTIONAL)//返回实际加载的地址
{

if (Module == NULL)
Module = &tmpModule;
LdrAdjustDllName(&AdjustedName, Name, FALSE);
MappedAsDataFile = FALSE;
Status = LdrFindEntryForName(&AdjustedName, Module, TRUE);//仍要先查找
if (NT_SUCCESS(Status))
{
if (NULL != BaseAddress)
*BaseAddress = (*Module)->DllBase;
}
else
{
//先尝试在\KnownDlls对象目录中查找该dll文件的section对象
Status = LdrpMapKnownDll(&AdjustedName, &FullDosName, &SectionHandle);
if (!NT_SUCCESS(Status))//若找不到,则为该dll文件创建一个映像文件section
{
MappedAsDataFile = (0 != (LoadFlags & LOAD_LIBRARY_AS_DATAFILE));
//内部会调用NtCreateSection系统服务,创建一个section
Status = LdrpMapDllImageFile(SearchPath, &AdjustedName, &FullDosName, MappedAsDataFile, &SectionHandle);
}
ViewSize = 0;//表示映射整个dll文件
ImageBase = 0;//表示不指定映射的地址
ArbitraryUserPointer = NtCurrentTeb()->NtTib.ArbitraryUserPointer;
NtCurrentTeb()->NtTib.ArbitraryUserPointer = FullDosName.Buffer;
Status = NtMapViewOfSection(SectionHandle,NtCurrentProcess(),
&ImageBase,0,0,NULL,&ViewSize,ViewShare,0,…);
NtCurrentTeb()->NtTib.ArbitraryUserPointer = ArbitraryUserPointer;
if (NULL != BaseAddress)
*BaseAddress = ImageBase;
if (MappedAsDataFile)//dll可以当做纯数据文件加载
{
if (NULL != BaseAddress)
*BaseAddress = (PVOID) ((char *) *BaseAddress + 1);//复用标志
*Module = NULL;
return STATUS_SUCCESS;
}
//if 实际加载映射的地址与该dll期望的加载地址不同,执行重定位
if (ImageBase != (PVOID) NtHeaders->OptionalHeader.ImageBase)
Status = LdrPerformRelocations(NtHeaders, ImageBase);

*Module = LdrAddModuleEntry(ImageBase, NtHeaders, FullDosName.Buffer);
(*Module)->SectionPointer = SectionHandle;
if (ImageBase != (PVOID) NtHeaders->OptionalHeader.ImageBase)
(*Module)->Flags |= LDRP_IMAGE_NOT_AT_BASE;

if (NtHeaders->FileHeader.Characteristics & IMAGE_FILE_DLL)
(*Module)->Flags |= LDRP_IMAGE_DLL;

//又加载该dll本身依赖的所有其他dll,修正它的导入表
Status = LdrFixupImports(SearchPath, *Module);
//当所有子孙dll初始化完后,自己才初始化完毕,此时才加入初始化顺序链表中
InsertTailList(&NtCurrentPeb()->Ldr->InInitializationOrderModuleList,
&(*Module)->InInitializationOrderModuleList);
}
return STATUS_SUCCESS;
}

至此进程启动时的初始化工作已经初始完毕。
Exe文件及其依赖的所有dll以及tls工作最终都完成处理了, 这时候该APC函数将返回。

BaseProcessStartThunk

这个LdrInitializeThunk要返回哪里呢?

答案是返回到内核,然后才恢复用户寄 存器现场,正式退回用户空间,
执行主线程的用户空间总入口函数BaseProcessStartThunk
换句话说当程序流执行到BaseProcessStartThunk这个函数时,进程已初始化,各dll已完成加载。
此时,万事俱备只欠东风了,线程可以放马在用户空间执行了。

主线程的用户空间总入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
_BaseProcessStartThunk@0://主线程的用户空间总入口(内核总入口是KiThreadStartup) 
{
xor ebp, ebp
push eax //oep
push 0 //表示不会返回 jmp _BaseProcessStartup@4
}

declspec(noreturn) VOID BaseProcessStartup(PPROCESS_START_ROUTINE lpStartAddress) //主线程的入口
{
UINT uExitCode = 0;
_SEH2_TRY //放在try块中保护
{
NtSetInformationThread(NtCurrentThread(),ThreadQuerySetWin32StartAddress, &lpStartAddress,sizeof(PPROCESS_START_ROUTINE));
//lpStartAddress即oep,一般就是WinMainCRTStartup/MainCRTStartup
uExitCode = (lpStartAddress)();//转去oep
}
_SEH2_EXCEPT(BaseExceptionFilter(_SEH2_GetExceptionInformation()))
{
uExitCode = _SEH2_GetExceptionCode();
}
_SEH2_END;

ExitProcess(uExitCode);//当WinMain函数正常退出后,进程才退出
}

普通线程的用户空间总入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
_BaseThreadStartupThunk@0: //一般普通线程的用户空间总入口(内核总入口是KiThreadStartup) 
{
xor ebp, ebp
push ebx //用户自己的context*参数
push eax //用户自己的线程入口函数
push 0 //表示不会返回 jmp _BaseThreadStartup@8
}

//一般普通线程的入口
declspec(noreturn)
VOID BaseThreadStartup(LPTHREAD_START_ROUTINE lpStartAddress,//用户自己的线程入口函数
LPVOID lpParameter)//用户自己函数的context*
{

volatile UINT uExitCode = 0;
_SEH2_TRY //也置于try块中保护
{
uExitCode = (lpStartAddress)((PVOID)lpParameter);
}
_SEH2_EXCEPT(BaseThreadExceptionFilter(_SEH2_GetExceptionInformation()))
{
uExitCode = _SEH2_GetExceptionCode();
} _SEH2_END;

ExitThread(uExitCode);//用户自己的线程入口函数返回后,线程自然退出
}

LoadLibraryW

Dll可以在进程启动初期,被PE加载器静态加载外,程序员也可以调用LoadLibraryAPI显式的动态加载。
看一下这个函数的原理,实际上这个函数不是API,是个宏,指向LoadLibraryW/LoadLibraryA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
HINSTANCE LoadLibraryW (LPCWSTR lpLibFileName) 
{
return LoadLibraryExW (lpLibFileName, 0, 0);
}


HINSTANCE LoadLibraryExW (
LPCWSTR lpLibFileName,
HANDLE hFile,
DWORD dwFlags)
{
if (dwFlags & DONT_RESOLVE_DLL_REFERENCES)
DllCharacteristics = IMAGE_FILE_EXECUTABLE_IMAGE;
dwFlags &= LOAD_WITH_ALTERED_SEARCH_PATH;
SearchPath = GetDllLoadPath(lpLibFileName);//构造该dll的标准搜索路径
RtlInitUnicodeString(&DllName, (LPWSTR)lpLibFileName);

if (dwFlags & LOAD_LIBRARY_AS_DATAFILE)
{
//在加载模块表中查找该dll
Status = LdrGetDllHandle(SearchPath, NULL, &DllName, (PVOID*)&hInst);
if (!NT_SUCCESS(Status))//若找不到
{
Status = LoadLibraryAsDatafile(SearchPath, DllName.Buffer, &hInst);
Return Status;
}
}
if (InWindows) //Windows中的实现
Status = LdrLoadDll(SearchPath,&DllCharacteristics,&DllName, (PVOID*)&hInst);
Else //ROS中的实现
Status = LdrLoadDll(SearchPath, &dwFlags, &DllName, (PVOID*)&hInst);

if ( !NT_SUCCESS(Status))
{
SetLastErrorByStatus (Status);
return NULL;
}

return hInst;
}

GetDllLoadPath

下面的函数用来从指定dll文件路径构造一个dll搜索路径(完整原代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
LPWSTR GetDllLoadPath(LPCWSTR lpModule) 
{
ULONG Pos = 0, Length = 0;
PWCHAR EnvironmentBufferW = NULL;
LPCWSTR lpModuleEnd = NULL;
UNICODE_STRING ModuleName;
DWORD LastError = GetLastError();

if ((lpModule != NULL) && (wcslen(lpModule) > 2) && (lpModule[1] == ':'))
lpModuleEnd = lpModule + wcslen(lpModule);
else
{
ModuleName = NtCurrentPeb()->ProcessParameters->ImagePathName;
lpModule = ModuleName.Buffer;
lpModuleEnd = lpModule + (ModuleName.Length / sizeof(WCHAR));
}
if (lpModule != NULL)
{
while (lpModuleEnd > lpModule && *lpModuleEnd != L'/' &&
*lpModuleEnd != L'\\' && *lpModuleEnd != L':')
{
--lpModuleEnd;
}
Length = (lpModuleEnd - lpModule) + 1;
}
//看到没,LoadLibrary的dll搜索路径顺序是这样(注意与静态加载时的搜索路径不同)
Length += GetCurrentDirectoryW(0, NULL);
Length += GetDllDirectoryW(0, NULL);
Length += GetSystemDirectoryW(NULL, 0);
Length += GetWindowsDirectoryW(NULL, 0);
Length += GetEnvironmentVariableW(L"PATH", NULL, 0);
EnvironmentBufferW = RtlAllocateHeap(RtlGetProcessHeap(), 0,Length * sizeof(WCHAR));

if (lpModule)
{
RtlCopyMemory(EnvironmentBufferW, lpModule, (lpModuleEnd - lpModule) *sizeof(WCHA R));
Pos += lpModuleEnd - lpModule;
EnvironmentBufferW[Pos++] = L';';
}
Pos += GetCurrentDirectoryW(Length, EnvironmentBufferW + Pos);
EnvironmentBufferW[Pos++] = L';';
Pos += GetDllDirectoryW(Length - Pos, EnvironmentBufferW + Pos);
EnvironmentBufferW[Pos++] = L';';
Pos += GetSystemDirectoryW(EnvironmentBufferW + Pos, Length - Pos);
EnvironmentBufferW[Pos++] = L';';
Pos += GetWindowsDirectoryW(EnvironmentBufferW + Pos, Length - Pos);
EnvironmentBufferW[Pos++] = L';';
Pos += GetEnvironmentVariableW(L"PATH", EnvironmentBufferW + Pos, Length - Pos);
SetLastError(LastError);
return EnvironmentBufferW;
}


NTSTATUS NTAPI
LdrLoadDll (IN PWSTR SearchPath OPTIONAL,
IN PULONG LoadFlags OPTIONAL,
IN PUNICODE_STRING Name,
OUT PVOID *BaseAddress)//也即返回的hModule
{
PPEB Peb = NtCurrentPeb();
Status = LdrpLoadModule(SearchPath, LoadFlags ? *LoadFlags : 0, Name, &Module, BaseAddr ess);

if (NT_SUCCESS(Status) && (!LoadFlags || 0 == (*LoadFlags & LOAD_LIBRARY_AS_DATAFILE)))
{
if (!(Module->Flags & LDRP_PROCESS_ATTACH_CALLED))
Status = LdrpAttachProcess();//通知一个ProcessAttach消息
}
*BaseAddress = NT_SUCCESS(Status) ? Module->DllBase : NULL;
return Status;
}

LdrpAttachThread

下面的函数在每次一个新线程创建时调用,用以调用各个模块的DllMain和tls回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
NTSTATUS  LdrpAttachThread (VOID)
{
Status = LdrpInitializeTlsForThread();

if (NT_SUCCESS(Status))
{
ModuleListHead = &NtCurrentPeb()->Ldr->InInitializationOrderModuleList;
Entry = ModuleListHead->Flink;
while (Entry != ModuleListHead)//遍历初始化顺序模块表
{
Module = CONTAINING_RECORD(Entry, LDR_DATA_TABLE_ENTRY, InInitializationOrderM
oduleList);
if (Module->Flags & LDRP_PROCESS_ATTACH_CALLED &&
!(Module->Flags & LDRP_DONT_CALL_FOR_THREADS) &&
!(Module->Flags & LDRP_UNLOAD_IN_PROGRESS))
{
//调用DllMain,注意是DLL_THREAD_ATTACH通知码
LdrpCallDllEntry(Module, DLL_THREAD_ATTACH, NULL);
}
Entry = Entry->Flink;
}

Entry = NtCurrentPeb()->Ldr->InLoadOrderModuleList.Flink;//exe模块
Module = CONTAINING_RECORD(Entry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
LdrpTlsCallback(Module, DLL_THREAD_ATTACH);
}
return Status;
}

LdrpAttachProcess

下面的函数在主线程创建时调用,用以调用各个模块的DllMain和tls回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
NTSTATUS LdrpAttachProcess(VOID) { 
NTSTATUS Status = STATUS_SUCCESS;
ModuleListHead = &NtCurrentPeb()->Ldr->InInitializationOrderModuleList;
Entry = ModuleListHead->Flink;
while (Entry != ModuleListHead)
{
Module = CONTAINING_RECORD(Entry, LDR_DATA_TABLE_ENTRY,
InInitializationOrderModuleList);
if (!(Module->Flags & (LDRP_LOAD_IN_PROGRESS|LDRP_UNLOAD_IN_PROGRESS|
LDRP_ENTRY_PROCESSED)))
{
Module->Flags |= LDRP_LOAD_IN_PROGRESS;
//调用DllMain,注意是DLL_PROCESS_ATTACH通知码
Result = LdrpCallDllEntry(Module, DLL_PROCESS_ATTACH, (Module->LoadCount ==
LDRP_PROCESS_CREATION_TIME ? 1 : 0));

if (Module->Flags & LDRP_IMAGE_DLL && Module->EntryPoint != 0)
Module->Flags |= LDRP_PROCESS_ATTACH_CALLED|LDRP_ENTRY_PROCESSED;
else
Module->Flags |= LDRP_ENTRY_PROCESSED;

Module->Flags &= ~LDRP_LOAD_IN_PROGRESS;
}
Entry = Entry->Flink;
}
return Status;
}




线程调度与切换

众所周知:Windows系统是一个分时抢占式系统,分时指每个线程分配时间片,抢占指时间片到期前,中途可以被其他更高优先级的线程强制抢占。
背景知识:每个cpu都有一个TSS,叫”任务状态段’。
这个TSS内部中的一些字段记录着该cpu上当前正在 运行的那个线程的一些信息(如ESP0记录着该线程的内核栈位置,IO权限位图记录着当前线程的IO空间权限)
IO空间有64KB,IO权限位图中的每一位记录着对应IO地址的IN、OUT许可权限,所以IO权限位图本身有8KB 大小,TSS中就就记录着当前线程IO权限位图的偏移位置。

每当切换线程时:自然要跟着修改TSS中的ESP0和IO权限位图。

TSS0中为什么要保存当前线程的内核栈位置?

原因是:每当一个线程内部,从用户模式进入内核模式时,需要将cpu中的esp换成该线程的内核栈(各 线程的内核栈是不同的)
每当进入内核模式时,cpu就自动从TSS中找到ESP0,然后MOV ESP, TSS.ESP0,换 成内核栈后,cpu然后在内核栈中压入浮点寄存器和标准的5个寄存器:原cs、原eip、原ss、原esp、原ef lags。
这就是为什么需要在TSS中记录当前线程的内核栈地址。(注意ESP0并不是栈底地址,而是要压入保 存寄存器处的存放地址)

线程切换相关数据结构

KPCR

1
2
3
4
5
6
7
8
9
10
11
Struct KPCR //处理器控制块(内核中的fs寄存器总是指向这个结构体的基址) 
{
KPCR_TIB Tib;
KPCR* self;//方便寻址
KPRCB* Prcb;
KIRQL irql;//物理上表示cpu的当前中断级,逻辑上理解为当前线程的中断级更好
USHORT* IDT;//本cpu的中断描述符表的地址
USHORT* GDT;//本cpu的全局描述符表的地址
KTSS* TSS;//本cpu上当前线程的信息(ESP0)

}

KPCR_TIB

1
2
3
4
5
6
7
8
9
Struct KPCR_TIB 
{
Void* ExceptionList;//当前线程的内核seh链表头结点地址
Void* StackBase;//内核栈底地址
Void* StackLimit;//栈的提交边界

KPCR_TIB* self;//方便寻址

}

KPRCB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Struct KPRCB 
{

KTHREAD* CurrentThread;//本cpu上当前正在运行的线程
KTHREAD* NextThread;//将剥夺(即抢占)当前线程的下一个线程
KTHREAD* IdleThread;//空转线程
BOOL QuantumEnd;//重要字段。指当前线程的时间片是否已经用完。
LIST_ENTRY WaitListHead;//本cpu的等待线程队列
ULONG ReadSummary;//各就绪队列中是否为空的标志
ULONG SelectNextLast;
LIST_ENTRY DispatcherReadyListHead[32];//对应32个优先级的32个就绪线程队列
FX_SAVE_AREA NpxSaveArea;

}

_KSWITCHFRAME

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _KSWITCHFRAME //切换帧(用来保存切换线程) 
{
PVOID ExceptionList;//保存线程切换时的内核she链表(不是用户空间中的seh) Union
{
BOOLEAN ApcBypassDisable;
//用于首次调度 UCHAR
WaitIrql;
//用于保存切换时的WaitIrql
};
//实际上首次时为KiThreadStartup,以后都固定为call KiSwapContextInternal后面的那条指令

PVOID RetAddr;//保存发生切换时的断点地址(以后切换回来时从这儿继续执行)
} KSWITCHFRAME, *PKSWITCHFRAME;

Trap现场帧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
typedef struct _KTRAP_FRAME  
{
// ------------------这些是KiSystemService保存的---------------------------
ULONG DbgEbp;
ULONG DbgEip;
ULONG DbgArgMark;
ULONG DbgArgPointer;
ULONG TempSegCs;
ULONG TempEsp;
ULONG Dr0;
ULONG Dr1;
ULONG Dr2;
ULONG Dr3;
ULONG Dr6;
ULONG Dr7;
ULONG SegGs;
ULONG SegEs;

ULONG SegDs;
ULONG Edx;//xy 这个位置不是用来保存edx的,而是用来保存上个Trap帧,因为Trap帧是可以嵌套的
ULONG Ecx; //中断和异常引起的自陷要保存eax,系统调用则不需保存ecx
ULONG Eax;//中断和异常引起的自陷要保存eax,系统调用则不需保存eax
ULONG PreviousPreviousMode;
struct _EXCEPTION_REGISTRATION_RECORD FAR *ExceptionList;//上次seh链表的开头地址
ULONG SegFs;
ULONG Edi;
ULONG Esi;
ULONG Ebx; ULONG Ebp;
// ----------------------------------------------------------------------------------------
ULONG ErrCode;//发生的不是中断,而是异常时,cpu还会自动在栈中压入对应的具体异常码在这儿

// -----------下面5个寄存器是由int 2e内部本身保存的或KiFastCallEntry模拟保存的现场---------
ULONG Eip;
ULONG SegCs;
ULONG EFlags;
ULONG HardwareEsp; ULONG HardwareSegSs;
// ---------------以下用于用于保存V86模式的4个寄存器也是cpu自动压入的-------------------
ULONG V86Es;
ULONG V86Ds;
ULONG V86Fs; ULONG V86Gs;
} KTRAP_FRAME, *PKTRAP_FRAME;

KiSwapContex切换线程核心

下面这个核心函数用来切换线程(从当前线程切换到新线程去)。
这个函数的原型是:BOOL FASTCALL KiSwapContex(KTHREAD* Currentthread*, KTHREAD* NewThread);
返回值表示下次切换回来时是否需要手动扫描执行内核APC。

这个函数的汇编代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@KiSwapContext@8:  //开头的@表示fastcall调用约定 
{
sub esp, 4 * 4 //腾出局部变量空间
//保存这4个寄存器,因为KiSwapContextInternal函数内部要使用这几个寄存器
mov [esp+12], ebx
mov [esp+8], esi
mov [esp+4], edi
mov [esp+0], ebp

mov ebx, fs:[KPCR_SELF] //ebx=当前cpu的KPCR*
mov edi, ecx //edi= KiSwapContext的第一个参数,即CurrentThread
mov esi, edx //edi= KiSwapContext的第而个参数,即NewThread
movzx ecx, byte ptr [edi+KTHREAD_WAIT_IRQL] //ecx=当前线程的WaitIrql

call @KiSwapContextInternal@0 //调用真正的切换工作函数
// 这中间已经被切换到新线程去了,当前线程已经让出cpu,挂入了就绪队列。
// 需要等到下次重新被调度运行时,才又从这儿的断点处继续向下执行下去

mov ebp, [esp+0] //这条指令就是断点处,以后切换回来时就从这个断点处继续执行
mov edi, [esp+4]
mov esi, [esp+8]
mov ebx, [esp+12
add esp, 4 * 4
ret
}

KiSwapContextInternal

下面的函数完成真正的切换工作(返回值表示切换回来后是否需要手动扫描执行内核apc)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
@KiSwapContextInternal@0: //edi指向当前线程,esi指向要切换到的新线程,ebx指向当前KPCR* 
{
inc dword ptr es:[ebx+KPCR_CONTEXT_SWITCHES] //递增当前cpu上发生的历史线程切换计数
push ecx //保存本线程切换时的WaitIrql
push [ebx+KPCR_EXCEPTION_LIST] //保存本线程切换时的内核seh链表

// ----------至此,上面的两条push连同本函数的返回地址(即断点地址),就构成了一个切换帧。
// ----------当前线程切换时的内核栈顶位置就在此处
AfterTrace:
mov ebp, cr0
mov edx, ebp //将cr0寄存器保存在edx中(cr0的Bit3位“TaskSwitched”标志位,与浮点运算相关)

SetStack:
mov [edi+KTHREAD_KERNEL_STACK], esp //保存本线程切换时的内核栈顶位置
mov eax, [esi+KTHREAD_INITIAL_STACK] //eax=新线程的内核栈底地址
--------------------------------------------------------------------------------
cli //下面检查Npx浮点寄存器,要关中断
movzx ecx, byte ptr [esi+KTHREAD_NPX_STATE] //ecx=新线程的Npx状态

and edx, ~(CR0_MP + CR0_EM + CR0_TS)
or ecx, edx
or ecx, [eax - (NPX_FRAME_LENGTH - FN_CR0_NPX_STATE)] //获得新线程需要的cr0
cmp ebp, ecx
jnz NewCr0 //如果新线程需要的cr0不同于当前的cr0,则修改当前cr0为新线程的cr0 StackOk:
Sti
--------------------------------------------------------------------------------
mov esp, [esi+KTHREAD_KERNEL_STACK] //关键。恢复成新线程当初被切换时的内核栈顶
mov ebp, [esi+KTHREAD_APCSTATE_PROCESS] //ebp=目标进程
mov eax, [edi+KTHREAD_APCSTATE_PROCESS] //eax=当前进程
cmp ebp, eax //检查是否是切换到同一个进程中的其他线程(若是。就不用切换LDT和cr3)
jz SameProcess //若切换到其他进程中的线程,则要同时修改LDT和CR3

mov ecx, [ebp+KPROCESS_LDT_DESCRIPTOR0]
or ecx, [eax+KPROCESS_LDT_DESCRIPTOR0]
jnz LdtReload //如果两个进程的LDT不同,就要换用不同的LDT

UpdateCr3:
mov eax, [ebp+KPROCESS_DIRECTORY_TABLE_BASE]
mov cr3, eax //关键。将cr3换成目标进程的页目录

SameProcess:
xor eax, eax
mov gs, ax
mov eax, [esi+KTHREAD_TEB] //新线程的TEB地址
mov [ebx+KPCR_TEB], eax //当前KPCR中的TEB指向新线程的TEB
mov ecx, [ebx+KPCR_GDT]
//修改GDT中的TEB描述符,指向新线程的TEB
mov [ecx+0x3A], ax
shr eax, 16
mov [ecx+0x3C], al
mov [ecx+0x3F], ah
mov eax, [esi+KTHREAD_INITIAL_STACK] //eax=新线程的内核栈底位置
sub eax, NPX_FRAME_LENGTH //跳过浮点保存区空间
test dword ptr [eax - KTRAP_FRAME_SIZE + KTRAP_FRAME_EFLAGS], EFLAGS_V86_MASK
jnz NoAdjust //检查新线程是否运行在V86模式
sub eax, KTRAP_FRAME_V86_GS - KTRAP_FRAME_SS //跳过V86保存区

NoAdjust:
mov ecx, [ebx+KPCR_TSS]
mov [ecx+KTSS_ESP0], eax //关键,修改TSS中的ESP0,指向新线程的内核栈底
mov ax, [ebp+KPROCESS_IOPM_OFFSET]
mov [ecx+KTSS_IOMAPBASE], ax //修改TSS中的IO权限位图偏移指向新进程中的IO权限位图
inc dword ptr [esi+KTHREAD_CONTEXT_SWITCHES] //递增线程的切换次数(也即历史调度次数)
pop [ebx+KPCR_EXCEPTION_LIST] //将当前KPCR中记录的seh链表恢复成新线程的seh链表
pop ecx //ecx=新线程原来切换前的WaitIrql

cmp byte ptr [ebx+KPCR_PRCB_DPC_ROUTINE_ACTIVE], 0 //检查当前是否有DPC函数处于活动状态
jnz BugCheckDpc //蓝屏
//至此,cpu中的寄存器内容全部换成了新线程的那些寄存器,从这个意思上说,此时就已完成了全部 切换工作,下面的代码都是在新线程的环境中运行了。


--------------------------------新线程环境---------------------------------------
--------------------------------新线程环境---------------------------------------
--------------------------------新线程环境---------------------------------------

cmp byte ptr [esi+KTHREAD_PENDING_KERNEL_APC], 0
jnz CheckApc //看到没,每次线程得到重新调度运行前,都会扫描执行内核apc队列中的函数
xor eax, eax
ret //此处返回值表示没有内核apc

CheckApc:
cmp word ptr [esi+KTHREAD_SPECIAL_APC_DISABLE], 0 //检查是否禁用了APC
jnz ApcReturn
test cl, cl //检查WaitIrql,如果是APC级,就在本函数内部返回前,发出apc中断

jz ApcReturn
//if(SPECIAL APC 没禁用 && WaitIrql!=PASSIVE_LEVEL),切换回来时就先执行内核APC
mov cl, APC_LEVEL
call @HalRequestSoftwareInterrupt@4 //发出一个apc中断
or eax, esp //既然发出apc中断了,那么就return FALSE表示无需手动扫描执行apc

ApcReturn:
setz al
ret //此处返回值表示切回来后是否需要手动扫描执行apc


//当这个函数返回时,之前已经换成新线程的内核栈了。
// 当函数返回后,将回到KiSwapContext中,当 KiSwapContext返回到调用方时,那个调用方就是新线程当初调用的KiSwapContext的函数
// 这样就沿着 新线程的内核栈,逐级向上回溯到新线程中了。
// 因此,可以说切换内核栈,即是切换线程。

LdtReload:
mov eax, [ebp+KPROCESS_LDT_DESCRIPTOR0]
test eax, eax //检测目标进程有没有LDT
jz LoadLdt
mov ecx, [ebx+KPCR_GDT]
mov [ecx+KGDT_LDT], eax //改指目标进程的LDT
mov eax, [ebp+KPROCESS_LDT_DESCRIPTOR1]
mov [ecx+KGDT_LDT+4], eax//改指目标进程的LDT
/* Write the INT21 handler */
mov ecx, [ebx+KPCR_IDT]
mov eax, [ebp+KPROCESS_INT21_DESCRIPTOR0]
mov [ecx+0x108], eax
mov eax, [ebp+KPROCESS_INT21_DESCRIPTOR1]
mov [ecx+0x10C], eax
mov eax, KGDT_LDT

LoadLdt:
lldt ax
jmp UpdateCr3

NewCr0:
mov cr0, ecx
jmp StackOk

BugCheckDpc:
mov eax, [edi+KTHREAD_INITIAL_STACK]
push 0
push eax
push esi
push edi
push ATTEMPTED_SWITCH_FROM_DPC
call _KeBugCheckEx@20 //蓝屏提示:“尝试从活动DPC例程中切换线程”
}

如上:线程从KiSwapContextInternal这个函数内部切换出去,某一时刻又切换回这个函数内。
或者也可以理解为:线程从KiSwapContext这个函数切换出去,某一时刻又切换回这个函数内。
(注:可以hook这两个函数,来达到检测隐藏进程的目的)




线程的调度策略与切换时机

明白了线程切换的过程,所做的工作后
接下来看:线程的切换时机(也即一个线程什么时候会调用KiSwapContext这个函数把自己切换出去),相信这是大伙最感兴趣的问题。

调度策略:Windows严格按优先级调度线程。
优先级分成32个,每个cpu对应有32个就绪线程队列。
每当要发生线程切换时,就根据调度策略从32条就绪 队列中,按优先级从高到低的顺序扫描(同一个就绪队列中,由于优先级相同,则按FIFO顺序扫描)
这样从32条就绪队列中,找到优先级最高的那个候选就绪线程,给予调度执行。

当一个线程得到调度执行时,如果一直没有任何其他就绪线程的优先级高于本线程,本线程就可以畅通无 阻地一直执行下去,直到本次的时间片用完。
但是如果本次执行的过程中,如果有个就绪线程的优先级突 然高于了本线程,那么本线程将被抢占,cpu将转去执行那个线程。
但是这种抢占可能不是立即性的,只有在当前线程的irql在DISPATCH_LEVEL以下(不包括),才会被立即抢占,
否则推迟抢占(即把那个高 优先级的就绪线程暂时记录到当前cpu的KPCR结构中的NextThread字段中,标记要将抢占)。

切换时机:一句话【时片、抢占、等、主动】

  • 1、 时间片耗尽
  • 2、 被抢占
  • 3、 因等待事件、资源、信号时主动放弃cpu(如调用WaitForSingleObject)
  • 4、 主动切换(如主动调用SwitchToThread这个Win32 API)
    但是即使到了切换时机了,也只有当线程的irql在DISPATCH_LEVEL以下(不包括)时,才可以被切换出 去,否则线程将继续占有cpu,一直等到irql降到DISPATCH_LEVEL以下。

线程的状态(不含挂起态,其实挂起态本质上也是一种等待态)

  • 1、Ready就绪态(挂入相应的就绪队列)
  • 2、某一时刻得到调度变成Running运行态
  • 3、因等待某一事件、信号、资源等变成Waiting等待状态
  • 4、Standby状态。指处于抢占者状态(NextThread就是自己)
  • 5、DeferredReady状态。指”将’进入就绪态。

主动放弃cpu(NtYieldExecution)

先看一下主动放弃cpu,切换线程的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
NTSTATUS NtYieldExecution() 
{
NTSTATUS Status = STATUS_NO_YIELD_PERFORMED;
KIRQL OldIrql;
PKPRCB Prcb = KeGetCurrentPrcb();//当前cpu的控制块
PKTHREAD Thread = KeGetCurrentThread(), NextThread; if (Prcb->ReadySummary==0)
return Status;//如果没有其他线程处于就绪态,就不用切换了

//重要。线程的调度过程与切换过程,本身就运行在SynchLevel,目的是防止在执行调度、切换工作的 过程中又被切换了出去。
// 因此可以说,调度、切换这个过程是原子的。
OldIrql = KeRaiseIrqlToSynchLevel();//先提到SynchLevel,再做调度、切换工作
if (Prcb->ReadySummary!=0)//如果当前cpu上有就绪线程
{
KiAcquireThreadLock(Thread);
KiAcquirePrcbLock(Prcb);
if (Prcb->NextThread != NULL)
NextThread = Prcb->NextThread;//优先选择那个等待抢占的线程
Else //如果当前没有候选抢占线程,就从就绪队列调度出一个线程
NextThread = KiSelectReadyThread(1, Prcb);
if (NextThread)
{
Thread->Quantum = Thread->QuantumReset;//设置下次调度运行的时间片
Thread->Priority = KiComputeNewPriority(Thread, 1);//略微降低一个优先级
KiReleaseThreadLock(Thread);
KiSetThreadSwapBusy(Thread);//标记本线程正在被切换
Prcb->CurrentThread = NextThread;//标记已切换到下一个线程
Prcb->NextThread = NULL;//初始运行时尚未有任何抢占者线程
NextThread->State = Running;//标记线程状态正在运行
Thread->WaitReason = WrYieldExecution;//标记本线程上次被切换的原因是主动放弃
KxQueueReadyThread(Thread, Prcb);//将本线程转入就绪队列
Thread->WaitIrql = APC_LEVEL;//这将导致下次切换回来时会自动发出apc中断
MiSyncForContextSwitch(NextThread);
KiSwapContext(Thread, NextThread);//真正切换到目标线程

---------------------------华丽的分割线---------------------------------------
Status = STATUS_SUCCESS;//本线程下次切回来时继续从这里执行下去
}
else
{
KiReleasePrcbLock(Prcb);
KiReleaseThreadLock(Thread);
}
}
KeLowerIrql(OldIrql);//完成调度、切换过程后,降低到原irql(这个过程可能会执行apc)
return Status;
}

调度策略(KiSelectReadyThread)

下面就是调度策略:按优先级从高到低的顺序扫描32条就绪队列,取下最高优先级的线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
PKTHREAD KiSelectReadyThread(IN KPRIORITY Priority,//指调度出的线程必须>=这个优先级 
IN PKPRCB Prcb)//指定cpu
{
ULONG PrioritySet;
LONG HighPriority;//含有就绪线程的最高优先级队列
PLIST_ENTRY ListEntry;
PKTHREAD Thread = NULL;//调度出来的线程


PrioritySet = Prcb->ReadySummary >> Priority;
if (!PrioritySet) goto Quickie;
BitScanReverse((PULONG)&HighPriority, PrioritySet);//从高位到地位扫描那个标志位图
HighPriority += Priority;
ASSERT(IsListEmpty(&Prcb->DispatcherReadyListHead[HighPriority]) == FALSE);

ListEntry = Prcb->DispatcherReadyListHead[HighPriority].Flink;//队列中的第一个线程
Thread = CONTAINING_RECORD(ListEntry, KTHREAD, WaitListEntry);
ASSERT(HighPriority == Thread->Priority);//确保优先级符合
ASSERT(Thread->Affinity & AFFINITY_MASK(Prcb->Number));//确保cpu亲缘性
ASSERT(Thread->NextProcessor == Prcb->Number);//确保是在那个cpu中等待调度

if (RemoveEntryList(&Thread->WaitListEntry))//取下来
Prcb->ReadySummary ^= PRIORITY_MASK(HighPriority);//如果队列变空了,修改对应的标志位 Quickie:
return Thread;
}

KiComputeNewPriority

每当一个非实时线程被切换出去,放弃cpu后,系统都会略微降低该线程的优先级,以免该线程总是占住cpu不放。

下面的函数就是做这个目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SCHAR  KiComputeNewPriority(IN PKTHREAD Thread,//非实时线程 
IN SCHAR Adjustment)//"调减量"
{
SCHAR Priority;
Priority = Thread->Priority;//原优先级
if (Priority < LOW_REALTIME_PRIORITY)//只对非实时性线程做调整
{
//先减去"恢减量’(对应于唤醒线程时系统临时提高的优先级量,现在要把它恢复回去)
Priority -= Thread->PriorityDecrement;

//再减去"调减量’,这才是真正的调整,上面只是恢复优先级
Priority -= Adjustment;

if (Priority < Thread->BasePriority)
Priority = Thread->BasePriority;//优先级不管怎么调,不能低于基本优先级

Thread->PriorityDecrement = 0;
}
return Priority;
}

被动切换

前面说的主动切换。但主动切换是非常少见的,一般都是不情愿的,被动切换。

时间片切换

典型的被动切换情形是: 每触发一次时钟中断(通常每10毫秒触发一次),就会在时钟中断的isr中递减当前线程KTHREAD结构中的Quantum字段(表示剩余时间片),当减到0时(也即时间片耗尽时),会将KPCRB结构中的QuantumEnd字段 标记为TRUE。

同时当cpu在每次扫描执行完DPC队列中的函数后,irql将降到DISPATCH_LEVEL以下,这时 系统会检查QuantumEnd字段,若发现时间片已经用完(可能已经用完很久了),就会调用下面的函数切换线程,这时切换线程的一种典型时机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
VOID KiQuantumEnd() //每次时间片自然到期后执行这个函数 
{
PKPRCB Prcb = KeGetCurrentPrcb();
PKTHREAD NextThread, Thread = Prcb->CurrentThread;//当前线程
if (InterlockedExchange(&Prcb->DpcSetEventRequest, 0))//检查是否有"触发DPC事件’的请求
KeSetEvent(&Prcb->DpcEvent, 0, 0);
KeRaiseIrqlToSynchLevel();//提升到SynchLevel,准备调度、切换
KiAcquireThreadLock(Thread);
KiAcquirePrcbLock(Prcb);
if (Thread->Quantum <= 0)//确认该线程的时间片已到期
{
if ((Thread->Priority >= LOW_REALTIME_PRIORITY) &&
(Thread->ApcState.Process->DisableQuantum))
{
Thread->Quantum = MAX_QUANTUM;//实时线程可以禁用时间片机制
}
else
{
Thread->Quantum = Thread->QuantumReset;//设置下次调度时的时间片
Thread->Priority = KiComputeNewPriority(Thread,1);//降低一个优先级(以免占住cpu)
if (Prcb->NextThread != NULL)

{
NextThread = Prcb->NextThread//直接使用这个候选的线程
Thread->Preempted = FALSE;//因为是时间片到期发生的切换,所以不是被抢占
}
else
{
NextThread = KiSelectReadyThread(Thread->Priority, Prcb);//调度出一个线程
//表示这个线程已被选中处于候选抢占状态,将立马上架投入运行
NextThread->State = Standby;

}
}
}
KiReleaseThreadLock(Thread);

KiSetThreadSwapBusy(Thread);//标记当前线程正在被切换
Prcb->CurrentThread = NextThread;//标记为切换到下一个线程了
Prcb->NextThread = NULL;//初始运行时没有抢占者线程
NextThread->State = Running;//已在运行了
Thread->WaitReason = WrQuantumEnd;//标记上次被切换的原因是时间片到期
KxQueueReadyThread(Thread, Prcb);//当前线程转入就绪队列
Thread->WaitIrql = APC_LEVEL;// 这将导致下次切换回来时会自动发出apc中断

KiSwapContext(Thread, NextThread);//正式切换到新线程
---------------------------华丽的分割线---------------------------------------
KeLowerIrql(DISPATCH_LEVEL);
}

优先级切换

除了时间片自然到期,线程被切换外,线程还可以在运行的过程中被其他高优先级线程,强制抢占而切换。

ResumeThread

如一个线程调用ResumeThread将别的线程恢复调度时,自己会检查那个刚被恢复成就绪态的线程是否因优 先级高于自己而要抢占本线程,如果是,就会切换到那个线程。

因此这个api内部有切换线程的可能ResumeThread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 ULONG  KeResumeThread(IN PKTHREAD Thread) //恢复指定目标线程 
{
KLOCK_QUEUE_HANDLE ApcLock;
ULONG PreviousCount;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);//当前irql一定<=DISPATCH_LEVEL
KiAcquireApcLock(Thread, &ApcLock);//锁定apc队列,同时提升irql到DISPATCH_LEVEL
PreviousCount = Thread->SuspendCount;
if (PreviousCount)
{
Thread->SuspendCount--;//递减挂起计数
//若挂起计数减到0,唤醒目标线程,进入就绪队列或者变成抢占者线程
if ((!Thread->SuspendCount) && (!Thread->FreezeCount))
{
KiAcquireDispatcherLockAtDpcLevel();

Thread->SuspendSemaphore.Header.SignalState++;
KiWaitTest(&Thread->SuspendSemaphore.Header, IO_NO_INCREMENT);//尝试唤醒它
KiReleaseDispatcherLockFromDpcLevel();
}
}
KiReleaseApcLockFromDpcLevel(&ApcLock);//注意这个函数只释放apc队列锁,不降低irql
//关键函数。降低当前线程的irql,同时先检查是否有抢占者线程,若有,先执行抢占切换。
KiExitDispatcher(ApcLock.OldIrql);
return PreviousCount;//返回之前的挂起计数
}

IRQL降低

下面这个函数的主功能是降回当前线程的irql到指定OldIrql。
不过在正式的降低前,会先检查是否发生了抢占,若有就先执行线程切换,等下次切换回来后再降低当前线程的irql。

这个函数经常在系统中的其它线程的运行状态一改变后,就主动调用。

其目的是检测是否为此而发生了可能的抢占现象,若已发生,就立即进行抢占式切换。
比如,改变了某其它线程的优先级,唤醒了某其他线程,挂起恢复了某其他线程,给某线程挂入了一个APC等等操作后,都会调用,以尝试立即切换。

KiExitDispatcher
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 注意这个函数只能在DISPATCH_LEVEL及其以上irql级别调用  
//降低irql检测是否有抢占
VOID FASTCALL KiExitDispatcher(IN KIRQL OldIrql)
{
PKPRCB Prcb = KeGetCurrentPrcb();
PKTHREAD Thread, NextThread;
BOOLEAN PendingApc;
ASSERT(KeGetCurrentIrql() >= DISPATCH_LEVEL); //确保
KiCheckDeferredReadyList(Prcb);
if (OldIrql >= DISPATCH_LEVEL)//如果要降回的irql不在DISPATCH_LEVEL以下,那就不能切换
{
if ((Prcb->NextThread) && !(Prcb->DpcRoutineActive))
HalRequestSoftwareInterrupt(DISPATCH_LEVEL);
goto Quickie;
}
if (!Prcb->NextThread)//如果没有抢占者线程,那很好,直接降低irql就是
goto Quickie;

//若发现有抢占发生,下面将执行抢占切换
KiAcquirePrcbLock(Prcb);
NextThread = Prcb->NextThread;
Thread = Prcb->CurrentThread; KiSetThreadSwapBusy(Thread);
Prcb->CurrentThread = NextThread;
Prcb->NextThread = NULL;
NextThread->State = Running;
KxQueueReadyThread(Thread, Prcb);
Thread->WaitIrql = OldIrql;//可以肯定:OldIrql=APC_LEVEL或PASSIVE_LEVEL,并且:如果原irq l是在AP_LEVEL的话,KiSwapContext内部会在返回前发出apc中断
PendingApc = KiSwapContext(Thread, NextThread);

-------------------------------------华丽的分割线---------------------------------------
-------------------------------------华丽的分割线---------------------------------------
-------------------------------------华丽的分割线---------------------------------------

//如果切回来后发现阻塞有内核apc,需要手动扫描执行apc(可以肯定原irql不是APC_LEVEL)
if (PendingApc)
{
ASSERT(OldIrql == PASSIVE_LEVEL);//可以肯定原来是PASSIVE_LEVEL级
KeLowerIrql(APC_LEVEL);//当然要先降到APC级别去
KiDeliverApc(KernelMode, NULL, NULL);//切换回来后,自己手动扫描执行内核apc
} Quickie:
KeLowerIrql(OldIrql);//本函数真正的工作:降低到指定irql
}

上面的函数在降低irql前,先尝试检测是否发生了抢占式切换。若有立即切换。 否则,降低irql。
注意降低irql到DISPATCH_LEVEL下以后,也可能会因为之前时间片早已到期,但是在DISPATCH_LEVEL以上迟迟没有得到切换,现在降到下面了就会引发线程切换(迟来的切换!)

当一个线程被唤醒时(如isr中将某线程唤醒),往往会提高其优先级,导致发生抢占。
一旦发现某个线程的优先级高于当前线程的优先级(并且也高于上一个候选的抢占者线程的优先级)
系统就会把这个线程作为新的候选抢占者线程记录到KPCRB结构的NextThread字段中。
这样只要时机一成熟就会发生抢占式切换。

KiUnwaitThread

下面的函数用来唤醒一个线程

1
2
3
4
5
6
7
8
9
VOID FASTCALL KiUnwaitThread(IN PKTHREAD Thread, 
IN LONG_PTR WaitStatus,
IN KPRIORITY Increment)//略微提高的优先级量(以便目标线程尽快得到调度)
{
KiUnlinkThread(Thread, WaitStatus);//从所有等待对象的线程链表中脱链
Thread->AdjustIncrement = (SCHAR)Increment;//要调整的优先级量
Thread->AdjustReason = AdjustUnwait;//跳转原因为唤醒
KiReadyThread(Thread);//关键函数。将线程转为就绪态
}

KiReadyThread

下面的函数用来将一个线程转为就绪态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
VOID  KiReadyThread(IN PKTHREAD Thread) 
{
IN PKPROCESS Process = Thread->ApcState.Process;
if (Process->State != ProcessInMemory)
ASSERT(FALSE);//蓝屏
else if (!Thread->KernelStackResident)//如果该线程的内核栈被置换到外存了
{
ASSERT(Process->StackCount != MAXULONG_PTR);
Process->StackCount++;
ASSERT(Thread->State != Transition);
Thread->State = Transition;
ASSERT(FALSE);//蓝屏
}
else
KiInsertDeferredReadyList(Thread);//实质函数

}
VOID KiInsertDeferredReadyList(IN PKTHREAD Thread)
{
Thread->State = DeferredReady;//将进入就绪态
Thread->DeferredProcessor = 0;//0号cpu
KiDeferredReadyThread(Thread);//实质函数,就绪化指定线程
}

下面的函数将指定线程转换为"就绪态"或者"抢占态"
也可理解为"就绪化"某个线程,但特殊处理抢占情形(抢占态是一种特殊的就绪态)

KiDeferredReadyThread
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
VOID FASTCALL KiDeferredReadyThread(IN PKTHREAD Thread) 
{
PKPRCB Prcb; BOOLEAN Preempted;
ULONG Processor = 0;//一律挂入0号cpu的就绪队列 KPRIORITY OldPriority;//目标线程的当前优先级
PKTHREAD NextThread;
if (Thread->AdjustReason == AdjustBoost) //if是线程首次启动时的调整优先级 。。。
else if (Thread->AdjustReason == AdjustUnwait) //if是唤醒时调整的优先级 。。。
Preempted = Thread->Preempted;
OldPriority = Thread->Priority;
Thread->Preempted = FALSE;
Thread->NextProcessor = 0;
Prcb = KiProcessorBlock[0];
KiAcquirePrcbLock(Prcb);
if (KiIdleSummary)//如果0号cpu运行着空转线程,目标线程的优先级肯定高于那个空转线程
{
KiIdleSummary = 0;
Thread->State = Standby;//将目标程序改为"抢占态"
Prcb->NextThread = Thread;//指向自己
KiReleasePrcbLock(Prcb);
return;
}

Thread->NextProcessor = (UCHAR)Processor;//0
NextThread = Prcb->NextThread;//获得0号cpu上的原抢占者线程
if (NextThread)//如果原来已有一个抢占者线程
{
ASSERT(NextThread->State == Standby);//可以确定那个线程处于抢占态
if (OldPriority > NextThread->Priority)//若高于原"抢占者线程’的优先级
{
NextThread->Preempted = TRUE;//标志那个抢占者线程又被目标线程抢占了
Prcb->NextThread = Thread;//更改新的抢占者线程,时机一成熟就抢占
Thread->State = Standby;//更为抢占态
NextThread->State = DeferredReady;//原抢占者线程进入将就绪态

NextThread->DeferredProcessor = Prcb->Number;//0
KiReleasePrcbLock(Prcb);
KiDeferredReadyThread(NextThread);//原抢占者线程转入0号cpu就绪队列
return;
}
}
else//如果原来没有抢占者线程(最典型的情况)
{
NextThread = Prcb->CurrentThread;
if (OldPriority > NextThread->Priority)//如果优先级高于当前运行的那个线程
{
if (NextThread->State == Running)
NextThread->Preempted = TRUE;//标记已被抢占
Prcb->NextThread = Thread; //指定抢占者线程,时机一成熟就抢占
Thread->State = Standby;//标记目标线程处于抢占态了
KiReleasePrcbLock(Prcb);
if (KeGetCurrentProcessorNumber() != 0)
KiIpiSend(AFFINITY_MASK(Thread->NextProcessor), IPI_DPC);//给0号cpu发一个通知
return;
}
}


//如果目标线程的优先级低于当前的抢占者线程,也低于当前运行中的线程
Thread->State = Ready;//更为就绪态
Thread->WaitTime = KeTickCount.LowPart;//记录上次被切换的时间
//如果目标线程上次是因为被抢占而切出的cpu,现在就挂入队头(平衡怒气)
Preempted ? InsertHeadList(&Prcb->DispatcherReadyListHead[OldPriority],
&Thread->WaitListEntry) :
InsertTailList(&Prcb->DispatcherReadyListHead[OldPriority],
&Thread->WaitListEntry);
Prcb->ReadySummary |= PRIORITY_MASK(OldPriority);//更改相应就绪队列的标志
KiReleasePrcbLock(Prcb);
}

上面这个函数用于将线程挂入0号cpu的就绪队列或者置为抢占者线程。




进程、线程的优先级

线程的调度策略是严格按优先级的,因此优先级,不妨叫做"调度优先级"

那么优先级是啥,是怎么确定的呢?

先要弄清几个概念:

  • 进程的优先级类:每种优先级类对应一种基本优先级
  • 进程的基本优先级:为各个线程的默认基本优先级
  • 线程的基本优先级:每个线程刚创建时的基本优先级继承它所属进程的基本优先级,但可以人为调整
  • 线程的当前优先级:又叫时机优先级。当前优先级可以浮动,但永远不会降到该线程的基本优先级下面

系统调度线程时,是以线程的当前优先级为准的,它才不管你的基本优先级是什么,你所属的进程的基本 优先级又是什么,它只看你的当前优先级。

进程基本优先级与线程基本优先级是一种水涨船高的关系。
进程的基本优先级变高了,那么它里面的各个线程的基本优先级也会跟着升高对应的幅度。
各个线程初始创建时的基本优先级等于其进程的基本优先级

线程的基本优先级与线程的当前优先级也是一种水涨船高的关系。
线程的基本优先级升高了,那么线程的当前优先级也会跟着升高对应的幅度。

另外:线程的当前优先级可以随时变化(比如每次一让出cpu时就略微降低那么一点点优先级)
但是永远不会降到其基本优先级以下。
基本优先级就是它的最低保障!

综上可理解为:线程基本优先级相对于进程的基本优先级,线程的当前优先级相对于线程的基本优先级

系统中总共分32个优先级:0到31,其中又分为两段。0到15的是非实时优先级,16-31的表示实时优先级。

  • #define LOW_PRIORITY 0
  • #define LOW_RELATIVE_PRIORITY 15 //最低的实时优先级
  • #define HIGH_PRIORITY 31//最高的实时优先级,也是整个系统最高的优先级

修改优先级

SetPriorityClass

SetPriorityClass这个Win32 API改变的就是一个进程的优先级类,而一种优先级类对应一种基 本优先级,所以这个函数实际上改变的是进程的基本优先级。

实际上最终调用到下面的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
KPRIORITY KeSetPriorityAndQuantumProcess(IN PKPROCESS Process, 
IN KPRIORITY Priority,//新的基本优先级
IN UCHAR Quantum OPTIONAL)//新的时间片
{
KLOCK_QUEUE_HANDLE ProcessLock;
KPRIORITY Delta;
PLIST_ENTRY NextEntry, ListHead;
KPRIORITY NewPriority, OldPriority;
PKTHREAD Thread;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
if (Process->BasePriority == Priority)
return Process->BasePriority;

if (Priority==0)
Priority = 1;//只有空转线程的优先级才能是0

KiAcquireProcessLock(Process, &ProcessLock);//获得自旋锁,同时提升irql到DISPATCH_LEVEL
if (Quantum)
Process->QuantumReset = Quantum;//修改进程的时间片(也即里面各个线程的时间片)

OldPriority = Process->BasePriority;
Process->BasePriority = (SCHAR)Priority;//修改为新的基本优先级
Delta = Priority - OldPriority;//计算提升幅度(注意Delta可以是负数)

ListHead = &Process->ThreadListHead;
NextEntry = ListHead->Flink;

if (Priority >= LOW_REALTIME_PRIORITY)//如果将基本优先级提到了实时级别
{
while (NextEntry != ListHead)//遍历该进程中的每个线程
{
Thread = CONTAINING_RECORD(NextEntry, KTHREAD, ThreadListEntry);
if (Quantum)
Thread->QuantumReset = Quantum;//同时设置线程的时间片

KiAcquireThreadLock(Thread);
NewPriority = Thread->BasePriority + Delta;//水涨船高

if (NewPriority < LOW_REALTIME_PRIORITY)
NewPriority = LOW_REALTIME_PRIORITY;// 实时优先级的最小值
else if (NewPriority > HIGH_PRIORITY)
NewPriority = HIGH_PRIORITY;// 实时优先级的最大值

if (!(Thread->Saturation) || (OldPriority < LOW_REALTIME_PRIORITY))
{
Thread->BasePriority = (SCHAR)NewPriority; //水涨船高
Thread->Quantum = Thread->QuantumReset;//当前剩余时间片=初始时间片
Thread->PriorityDecrement = 0;
KiSetPriorityThread(Thread, NewPriority);//提高线程优先级要做的附加工作
}
KiReleaseThreadLock(Thread);
NextEntry = NextEntry->Flink;//下一个线程
}
}
else//如果将基本优先级提到了非实时级别
{
while (NextEntry != ListHead)
{
Thread = CONTAINING_RECORD(NextEntry, KTHREAD, ThreadListEntry);
if (Quantum)
Thread->QuantumReset = Quantum;

KiAcquireThreadLock(Thread);
NewPriority = Thread->BasePriority + Delta;

if (NewPriority >= LOW_REALTIME_PRIORITY)
NewPriority = LOW_REALTIME_PRIORITY - 1;//非实时优先级的最大值
else if (NewPriority <= LOW_PRIORITY)
NewPriority = 1;//非实时优先级的最小值

if (!(Thread->Saturation) || (OldPriority >= LOW_REALTIME_PRIORITY))
{
Thread->BasePriority = (SCHAR)NewPriority;//水涨船高
Thread->Quantum = Thread->QuantumReset;//当前剩余时间片=初始的时间片
Thread->PriorityDecrement = 0;
KiSetPriorityThread(Thread, NewPriority); //提高线程优先级要做的附加工作
}
KiReleaseThreadLock(Thread);
NextEntry = NextEntry->Flink;//下一个线程
}
}
KiReleaseDispatcherLockFromDpcLevel();
KiReleaseProcessLockFromDpcLevel(&ProcessLock);

//降低到原irql,同时先检查是否发生了抢占式切换
// 因为显式改变了线程的优先级,有可能让其他线程的优先级突然高于了当前线程而要发生抢占现象,所以要检测这种情况
KiExitDispatcher(ProcessLock.OldIrql);
return OldPriority;
}

线程的基本优先级一变了,它的当前优先级就会跟着变,线程的当前优先级一变了,那么就会有很多的附加工作要做,

KiSetPriorityThread

下面的函数就用来做这个工作(如改变就绪队列、置为抢占者等)。

这个函数改变目标线程的优先级为指定优先级,并根据目标线程的当前所处状态
最对应的就绪队列、抢占者线程调整。
可见强行改变某个线程的当前优先级并不是件简单的工作,需要全盘综合考虑各 方面因素,做出相应的调整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
//设置线程的当前优先级
VOID FASTCALL KiSetPriorityThread(IN PKTHREAD Thread,
IN KPRIORITY Priority)//新的当前优先级
{
PKPRCB Prcb;
ULONG Processor;
BOOLEAN RequestInterrupt = FALSE;
KPRIORITY OldPriority;
PKTHREAD NewThread;
if (Thread->Priority != Priority)//if 优先级变了
{
for (;;)
{
if (Thread->State == Ready)//如果目标线程处于就绪态
{
if (!Thread->ProcessReadyQueue)//其实一般都会满足这个条件
{
Processor = Thread->NextProcessor;
Prcb = KiProcessorBlock[Processor];
KiAcquirePrcbLock(Prcb);
//如果现在仍处于就绪态,并且仍在那个cpu上等待
if ((Thread->State == Ready) && (Thread->NextProcessor == Prcb->Number))
{
if (RemoveEntryList(&Thread->WaitListEntry))//从原就绪队列摘下
Prcb->ReadySummary ^= PRIORITY_MASK(Thread->Priority);
Thread->Priority = (SCHAR)Priority;//=更为新的优先级
KiInsertDeferredReadyList(Thread);//挂入新的就绪队列(或置为抢占态)
KiReleasePrcbLock(Prcb);
}
Else …
}
}
else if (Thread->State == Standby) //如果目标线程处于抢占态
{
Processor = Thread->NextProcessor;
Prcb = KiProcessorBlock[Processor];
KiAcquirePrcbLock(Prcb);
if (Thread == Prcb->NextThread)//如果仍处于抢占态
{
OldPriority = Thread->Priority;
Thread->Priority = (SCHAR)Priority;//更改优先级
if (Priority < OldPriority)//如果优先级降了(可能不再成为抢占者线程了)
{
NewThread = KiSelectReadyThread(Priority + 1, Prcb);
if (NewThread)//如果选出了一个比现在的优先级更高的线程
{
NewThread->State = Standby;
Prcb->NextThread = NewThread;//更为新的抢占者线程
KiInsertDeferredReadyList(Thread);//原抢占线程则转入就绪队列
}
}
KiReleasePrcbLock(Prcb);
}
Else …
}
else if (Thread->State == Running) //如果目标线程正在运行
{
Processor = Thread->NextProcessor;
Prcb = KiProcessorBlock[Processor];
KiAcquirePrcbLock(Prcb);
if (Thread == Prcb->CurrentThread)//如果仍在运行
{
OldPriority = Thread->Priority;
Thread->Priority = (SCHAR)Priority;//更改优先级
if ((Priority < OldPriority) && !(Prcb->NextThread))//可能会出现抢占
{
NewThread = KiSelectReadyThread(Priority + 1, Prcb);
if (NewThread)// 如果选出了一个比现在的优先级更高的线程
{
NewThread->State = Standby;
Prcb->NextThread = NewThread;//出现了新的抢占线程
RequestInterrupt = TRUE;//需要立即中断
}
}
KiReleasePrcbLock(Prcb);
if (RequestInterrupt)
{
//通知目标cpu进行抢占切换
if (KeGetCurrentProcessorNumber() != Processor)
KiIpiSend(AFFINITY_MASK(Processor), IPI_DPC);
}
}
Else …
}
Else …
break;
}
}
}

如上这个函数改变目标线程的优先级为指定优先级,并根据目标线程的当前所处状态,最对应的就绪队列、抢占者线程调整。
可见强行改变某个线程的当前优先级并不是件简单的工作,需要全盘综合考虑各 方面因素,做出相应的调整。

KeSetPriorityThread

下面的函数是一个小型的封装函数:(他还会还原时间片)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
KPRIORITY KeSetPriorityThread(IN PKTHREAD Thread, 
IN KPRIORITY Priority)
{
KIRQL OldIrql;
KPRIORITY OldPriority;
OldIrql = KiAcquireDispatcherLock();
KiAcquireThreadLock(Thread);
OldPriority = Thread->Priority;
Thread->PriorityDecrement = 0;
if (Priority != Thread->Priority)//if 优先级变了
{
Thread->Quantum = Thread->QuantumReset;//关键。还原时间片
KiSetPriorityThread(Thread, Priority);//再做真正的修改工作
}
KiReleaseThreadLock(Thread);
KiReleaseDispatcherLock(OldIrql);
return OldPriority;
}

// 除了修改进程的基本优先级会影响到里面每个线程的基本优先级和当前优先级外
// 也可以用下面的函数直接修改线程的基本优先级和当前优先级。

NTSTATUS NtSetInformationThread(IN HANDLE ThreadHandle,
IN THREADINFOCLASS ThreadInformationClass,
IN PVOID ThreadInformation,
IN ULONG ThreadInformationLength)
{

switch (ThreadInformationClass)
{
case ThreadPriority://设置当前优先级
Priority = *(PLONG)ThreadInformation;//这个值是相对于进程基本优先级的差值
KeSetPriorityThread(&Thread->Tcb, Priority);
break;

case ThreadBasePriority://设置基本优先级
Priority = *(PLONG)ThreadInformation;
KeSetBasePriorityThread(&Thread->Tcb, Priority);
break;
case

}//end switch
}//end func


// 线程的基本优先级(非当前优先级)可以用下面的函数设置
LONG KeSetBasePriorityThread(IN PKTHREAD Thread,
IN LONG Increment)//这个是相对于进程基本优先级的差值
{
KIRQL OldIrql;
KPRIORITY OldBasePriority, Priority, BasePriority;
LONG OldIncrement;
PKPROCESS Process;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
Process = Thread->ApcState.Process;
OldIrql = KiAcquireDispatcherLock();
KiAcquireThreadLock(Thread);
OldBasePriority = Thread->BasePriority;
OldIncrement = OldBasePriority - Process->BasePriority;
if (Thread->Saturation) //如果是个饱和增量
OldIncrement = 16 * Thread->Saturation;//16或-16

Thread->Saturation = 0;

if (abs(Increment) >= 16) //饱和增量
Thread->Saturation = (Increment > 0) ? 1 : -1;

BasePriority = Process->BasePriority + Increment;//算得现在的基本优先级
if (Process->BasePriority >= LOW_REALTIME_PRIORITY)
{
Priority = BasePriority;//实时线程例外,当前优先级=基本优先级
}
else
{
Priority = KiComputeNewPriority(Thread, 0);//其实就是当前优先级
//看到没,线程的基本优先级一升高,它的当前优先级跟着升高对应的幅度
Priority += (BasePriority - OldBasePriority);
}

Thread->BasePriority = (SCHAR)BasePriority;//更改线程的基本优先级
Thread->PriorityDecrement = 0;
if (Priority != Thread->Priority)//如果当前优先级变了,做相关的附加工作
{
Thread->Quantum = Thread->QuantumReset;
KiSetPriorityThread(Thread, Priority);
}

KiReleaseThreadLock(Thread);
KiReleaseDispatcherLock(OldIrql);
return OldIncrement;
}




线程局部存储:TLS

对TLS这个概念陌生的朋友请先自己查阅相关资料。

TLS分为两种方法:静态tls、动态tls。
两种方法都可以达到tls的目的。
静态tls:
在编写程序时:只需在要声明为tls的全局变量前加上 declspec(thread)关键字即可。如:

1
2
3
4
declspec(thread) int g_a = 1; 
declspec(thread) int g_b;
declspec(thread) int g_c = 0;
declspec(thread) int g_d;

编译器在遇到这样的变量时,自然会将这种变量当做tls变量看待,编译链接存放到pe文件的.tls节中, Exe文件中可使用静态tls,动态库文件中使用静态tls则会有很大的缺点,所以动态库文件中一般都使用动 态tls来达到tls的目的。

为此Windows专门提供了一组api和相关基础设施来实现动态tls

  • DWORD TlsAlloc():为当前线程分配一个 tls 槽。返回本线程分得的槽号
  • BOOL TlsSetValue(DWORD idx,void* val):写数据到指定槽中
  • VOID* TlsGetValue(DWORD idx ):从指定槽中读数据
  • BOOL TlsFree(DWORD idx);释放这个槽给进程,使得其他线程可以分得这个槽 相关

相关结构

PEB

1
2
3
4
5
6
7
8
Struct PEB 
{


RTL_BITMAP* TlsBitmap;//标准的 64 位动态 tls 分配标志位图(固定使用下面的 64 位结构)
DWORD TlsBitmapBits[2];//内置的 64bit 大小的 tls 位图(每一位标志表示对应 tls 槽的分配情况)

}

RTL_BITMAP

1
2
3
4
5
Struct RTL_BITMAP 
{
ULONG SizeOfBitmap;//动态 tls 位图的大小,默认就是 8B(64bit)
BYTE* Buffer;//动态 tls 位图的地址,默认就指向 PEB 结构中的那个内置的 tls 位图。当要使用的 tl s 槽个数超过 64 个时,将使用扩展的 tls 位图。
}

TEB

1
2
3
4
5
6
7
8
Struct TEB 
{

Void* ThreadLocalStoragePointer;//本线程的那片静态 tls 区的地址
Void* TlsSlots[64];//内置的 64 个 tls 槽(每个槽中可以存放 4B 大小的任意数据)
Void* TlsExpansionSlots;//另外扩展的 1024 个 tls 槽

}


动态Tls相关函数

TlsAlloc

下面的函数分配一个空闲的 tls 槽,返回分到的槽号(即索引)DWORD TlsAlloc()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
DWORD TlsAlloc() 
{
ULONG Index;
RtlAcquirePebLock();
//先从标准的64位tls位图中找到一个空闲的tls槽(也即未被其他线程占用的tls槽)
Index = RtlFindClearBitsAndSet(NtCurrentPeb()->TlsBitmap, 1, 0);
if (Index == -1)//如果找不到
{
//再去扩展的tls槽位图中查找
Index = RtlFindClearBitsAndSet(NtCurrentPeb()->TlsExpansionBitmap, 1, 0);
if (Index != -1)//如果找到了
{
if (NtCurrentTeb()->TlsExpansionSlots == NULL)
{
NtCurrentTeb()->TlsExpansionSlots = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,1024 * sizeof(PVOID));
}
NtCurrentTeb()->TlsExpansionSlots[Index] = 0;//分到对应的槽后,自动将内容清0
Index += 64;
}
else
SetLastError(ERROR_NO_MORE_ITEMS);
}
else

NtCurrentTeb()->TlsSlots[Index] = 0; //分到对应的槽后,自动将内容清0
RtlReleasePebLock();
return Index;
}

TlsSetValue

下面的函数将数据写入指定 tls 槽中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BOOL TlsSetValue(DWORD Index, LPVOID Value) 
{
if (Index >= 64) //扩展tls槽中
{
if (NtCurrentTeb()->TlsExpansionSlots == NULL)
{
NtCurrentTeb()->TlsExpansionSlots = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,1024 *sizeof(PVOID));
}
NtCurrentTeb()->TlsExpansionSlots[Index - 64] = Value;
}
else
NtCurrentTeb()->TlsSlots[Index] = Value;
return TRUE;
}

TlsGetValue

下面的函数读取指定 tls 槽中的值

1
2
3
4
5
6
7
LPVOID TlsGetValue(DWORD Index) 
{
if (Index >= 64)
return NtCurrentTeb()->TlsExpansionSlots[Index - 64];
else
return NtCurrentTeb()->TlsSlots[Index];
}

TlsFree

下面的函数用来释放一个 tls 槽给进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
BOOL TlsFree(DWORD Index) 
{
BOOL BitSet;
RtlAcquirePebLock();
if (Index >= 64)
{
//检测该tls槽是否已分配
BitSet = RtlAreBitsSet(NtCurrentPeb()->TlsExpansionBitmap,Index - 64,1);
if (BitSet)//若已分配,现在标记为空闲
RtlClearBits(NtCurrentPeb()->TlsExpansionBitmap,Index - 64,1);
}
else
{
BitSet = RtlAreBitsSet(NtCurrentPeb()->TlsBitmap, Index, 1);
if (BitSet)
RtlClearBits(NtCurrentPeb()->TlsBitmap, Index, 1);
}

if (BitSet)
{
//将所有线程的对应tls槽内容清0
NtSetInformationThread(NtCurrentThread(),ThreadZeroTlsCell,&Index,sizeof(DWORD));
}
else
SetLastError(ERROR_INVALID_PARAMETER);

RtlReleasePebLock();
return BitSet;
}

上面这些关于动态tls的函数都不难理解。
动态tls功能强大,但使用起来不方便。
静态tls不好用在动态库中,比较局限,但静态tls使用方便。
话又说回来,静态的tls的使用方便背后,又包含着较为复杂的初始化流程。
下面看静态tls的初始化流程。

静态Tls

初始化流程

回顾一下进程创建时的启动流程: 在进程启动时,初始化主exe文件的函数内部

LdrPEStartup
1
2
3
4
5
6
7
8
9
10
11
12
PEFUNC LdrPEStartup(…) 
{

Status = LdrFixupImports(NULL, *Module);//加载子孙dll,修正IAT导入表
Status = LdrpInitializeTlsForProccess();//初始化进程的静态tls
if (NT_SUCCESS(Status))
{
LdrpAttachProcess();//发送一个ProcessAttach消息,调用该模块的DllMain函数
LdrpTlsCallback(*Module, DLL_PROCESS_ATTACH);//调用各模块的tls回调函数
}

}

钻进各个函数里面去看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NTSTATUS LdrFixupImports(…) 
{


if (TlsDirectory)
{
TlsSize = TlsDirectory->EndAddressOfRawData- TlsDirectory->StartAddressOfRawData
+ TlsDirectory->SizeOfZeroFill;
if (TlsSize > 0 && NtCurrentPeb()->Ldr->Initialized)//if 动态加载该模块
TlsDirectory = NULL;// 动态加载的模块不支持静态tls
}

if (TlsDirectory && TlsSize > 0)//处理静态加载的dll模块中的静态tls节
LdrpAcquireTlsSlot(Module, TlsSize, FALSE);

}
LdrpAcquireTlsSlot

在修正每个exe、dll文件的导入表时,会检查该文件中.tls节的大小。
由于这个函数本身也会被LoadLibrary函数在内部调用,所以这个函数他会检测是不是在动态加载dll
若是如果发现dll中含有静态tls 节,就什么都不做。
反之若dll是在进程启动阶段静态加载的,就会调用LdrpAcquireTlsSlot处理那个模块中的tls节。

具体是怎么处理的呢?我们看:

1
2
3
4
5
6
7
8
9
10
11
12
VOID LdrpAcquireTlsSlot(PLDR_DATA_TABLE_ENTRY Module, ULONG Size, BOOLEAN Locked) 
{
if (!Locked)
RtlEnterCriticalSection (NtCurrentPeb()->LoaderLock);

Module->TlsIndex = LdrpTlsCount;//记录这个模块tls节的索引(即tls号)
LdrpTlsCount++;//递增进程中的tls节个数
LdrpTlsSize += Size;//递增进程中tls节总大小

if (!Locked)
RtlLeaveCriticalSection(NtCurrentPeb()->LoaderLock);
}

如上每个模块在进程启动时的静态加载过程中,只是递增一下进程中总的tls节个数与大小,以及分配该 模块的tls节编号,以便在进程完全初始化完成(即加载了所有模块)后,统一集中处理各模块中的静态tls节。

LdrpInitializeTlsForProccess

下面再看LdrPEStartup函数中调用的LdrpInitializeTlsForProccess函数,显然这个函数是在LdrFixup Imports函数加载了该exe依赖的所有子孙dll文件后才调用的。
前面已经统计完了该进程中所有模块的所有tls节的总大小以及tls节总个数,现在就到调用这个函数集中统一处理该进程的静态tls时候了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
NTSTATUS LdrpInitializeTlsForProccess() 
{
PLIST_ENTRY ModuleListHead;
PLIST_ENTRY Entry;
PLDR_DATA_TABLE_ENTRY Module;
PIMAGE_TLS_DIRECTORY TlsDirectory;
PTLS_DATA TlsData;
ULONG Size;
if (LdrpTlsCount > 0) //如果有模块中存在tls节
{
//分配一个tls描述符数组,用来记录各模块的tls节信息
// 注意分配的只是描述符,并不用来存放tls节体。
// 另外每个进程的tls描述符数组都记录在ntdll.dll模块中的LdrpTlsArray全局变量中
LdrpTlsArray = RtlAllocateHeap(RtlGetProcessHeap(),0, LdrpTlsCount * sizeof(TLS_DATA));

ModuleListHead = &NtCurrentPeb()->Ldr->InLoadOrderModuleList;
Entry = ModuleListHead->Flink;

//遍历所有含有tls节的静态加载模块
while (Entry != ModuleListHead)
{
Module = CONTAINING_RECORD(Entry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
if (Module->LoadCount ==-1 && Module->TlsIndex != -1)
{
//获得pe文件中tls目录的信息
TlsDirectory = RtlImageDirectoryEntryToData(Module->DllBase,
TRUE,IMAGE_DIRECTORY_ENTRY_TLS,&Size);

TlsData = &LdrpTlsArray[Module->TlsIndex];//指向该模块对应的描述符

//非0区在原模块中的地址
TlsData->StartAddressOfRawData = TlsDirectory->StartAddressOfRawData;
//非0区的大小
TlsData->TlsDataSize = TlsDirectory->EndAddressOfRawData - TlsDirectory->StartAddressOfRawData;
//0区的大小(即尚未初始化的tls变量总大小)
TlsData->TlsZeroSize = TlsDirectory->SizeOfZeroFill;
//tls回调函数数组的地址
if (TlsDirectory->AddressOfCallBacks)
TlsData->TlsAddressOfCallBacks = TlsDirectory->AddressOfCallBacks;
else
TlsData->TlsAddressOfCallBacks = NULL;

TlsData->Module = Module;//该tls节所在的原模块
//重要。回填到原模块中,该tls节分得的索引。(写复制机制可确保各进程一份)
*(PULONG)TlsDirectory->AddressOfIndex = Module->TlsIndex;
}
Entry = Entry->Flink;
}
}
return STATUS_SUCCESS;
}

如上这个函数为进程建立起一个tls描述符数组。

1
2
3
4
5
6
7
8
9
//tls节描述符 
typedef struct _TLS_DATA
{
PVOID StartAddressOfRawData; //非0区在原模块中的地址
DWORD TlsDataSize;// 非0区的大小
DWORD TlsZeroSize;// 0区大小
PIMAGE_TLS_CALLBACK *TlsAddressOfCallBacks;//回调函数数组
PLDR_DATA_TABLE_ENTRY Module;//所在模块
} TLS_DATA, *PTLS_DATA;

非0区与0区是什么意思呢?
tls节中各个变量可能有的没有初值,凡是没有初值的tls的变量都被安排到tls节的末尾,并且不予分配文件空间(这样,可以节省文件体积),只记录他们的总字节数即可。

1
2
3
4
declspec(thread) int g_a = 1;//已初始化,被安排到tls节中的非0区 
declspec(thread) int g_b;//被安排到0区
declspec(thread) int g_c = 0;//已初始化,被安排到tls节中的非0区
declspec(thread) int g_d; //被安排到0区 所
LdrpAttachThread

有未予初始化的tls变量都默认赋予初值0。
最后每当一个线程创建时的初始化工作

1
2
3
4
5
6
7
8
NTSTATUS LdrpAttachThread (VOID) 
{
...
Status = LdrpInitializeTlsForThread();
//关键处。初始化每个线程的静态tls
// 调用各dll的DllMain,略
return Status;
}

如上每当一个线程初始运行时,除了会调用进程中各个dll的DllMain函数外,还会初始化自己的静态tls,建立起本线程独立的一份静态tls副本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
NTSTATUS LdrpInitializeTlsForThread(VOID) 
{
PVOID* TlsPointers;
PTLS_DATA TlsInfo;
PVOID TlsData;
ULONG i;
PTEB Teb = NtCurrentTeb();
Teb->StaticUnicodeString.Length = 0;
Teb->StaticUnicodeString.MaximumLength = sizeof(Teb->StaticUnicodeBuffer);
Teb->StaticUnicodeString.Buffer = Teb->StaticUnicodeBuffer;

if (LdrpTlsCount > 0)//如果本进程中有包含tls节的静态模块
{
//将各模块内部的tls节提取出来,连成一片,形成一块‘tls片区’
TlsPointers = RtlAllocateHeap(RtlGetProcessHeap(),0, LdrpTlsCount * sizeof(PVOID) + LdrpTlsSize);//头部指针数组+所有tls块的总大 小

//指向头部后面的各tls节体部分
TlsData = (PVOID)((ULONG_PTR)TlsPointers + LdrpTlsCount * sizeof(PVOID));
Teb->ThreadLocalStoragePointer = TlsPointers;//指向本线程自己的那份tls的头部
TlsInfo = LdrpTlsArray;//指向本进程的tls描述符数组

for (i = 0; i < LdrpTlsCount; i++, TlsInfo++)
{
TlsPointers[i] = TlsData;//将数组指针指向对应的tls块
if (TlsInfo->TlsDataSize)
{
//提取对应模块内部的tls节体(非0区部分)到这儿来
memcpy(TlsData, TlsInfo->StartAddressOfRawData, TlsInfo->TlsDataSize);

TlsData = (PVOID)((ULONG_PTR)TlsData + TlsInfo->TlsDataSize);
}
if (TlsInfo->TlsZeroSize)//0区部分
{
memset(TlsData, 0, TlsInfo->TlsZeroSize);//自动初始化为0
TlsData = (PVOID)((ULONG_PTR)TlsData + TlsInfo->TlsZeroSize);//跨过0区部分
}
}
}
return STATUS_SUCCESS;
}

看到没,每个线程诞生之初,就将进程中各模块内部的tls节提取出来,复制到一个集中的地方存放,这样, 吗,每个线程都建立了一份自己连续的tls片区。
以后要访问tls变量时,访问的都是自己的那份tls片区

当然如何访问?
这离不开编译器对静态tls机制提供的支持。
编译器在遇到declspec(thread)关键字时,会认为那个变量是tls变量,将之编译链接到pe文件的.tls 节中存放,另外每条访问tls变量的高级语句都被做了恰当的编译。

每个tls变量都被编译为二级地址: "Tls节号.节内偏移",每个模块的tls节号(即索引)保存在那个模块的tls目录中的某个固定字段中(详 见: *(PULONG)TlsDirectory->AddressOfIndex = Module->TlsIndex这条语句)
这样编译器从模块的这个位置取得该模块的tls节分得的节号,以此节号为索引,根据TEB中的保存的那块"tls片区"的头部数组,找到对应于本模块tls节副本的位置,然后加上该tls变量在节内的偏移,就正确找到对应的内存单元了。




进程挂靠与跨进程操作

前面总在说:“将一个线程挂靠到其他进程的地址空间”,这是怎么回事?

当父进程要创建一个子进程时:会在父进程中调用CreateProcess
这个函数本身是运行在父进程的地址空间中的,但是由它创建了子进程,创建了子进程的地址空间,创建了子进程的PEB。
当要初始化子进程的PEB结构时,由于PEB本身位于子进程的地址空间中,如果直接访问PEB那是不对的,那将会映射到不同的物理内存。
所以必须挂靠到子进程的地址空间中,去读写PEB结构体中的值。

KeAttachProcess

下面的函数就是用来挂靠的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//将当前线程挂靠到指定进程的地址空间
VOID KeAttachProcess(IN PKPROCESS Process)
{
KLOCK_QUEUE_HANDLE ApcLock;
PKTHREAD Thread = KeGetCurrentThread();
if (Thread->ApcState.Process == Process)
return;//如果已经位于目标进程返回
if ((Thread->ApcStateIndex != OriginalApcEnvironment) || (KeIsExecutingDpc()))
KeBugCheckEx(~);//蓝屏错误
else
{
KiAcquireApcLock(Thread, &ApcLock);
KiAcquireDispatcherLockAtDpcLevel();//挂靠过程操作过程中禁止线程切换
KiAttachProcess(Thread, Process, &ApcLock, &Thread->SavedApcState);//实质函数
}
}

VOID KiAttachProcess(IN PKTHREAD Thread,//指定线程
IN PKPROCESS Process,//要挂靠到的目标进程
IN PKLOCK_QUEUE_HANDLE ApcLock,
IN PRKAPC_STATE SavedApcState)//保存原apc队列状态
{
Process->StackCount++;//目标线程的内核栈个数递增(也即增加线程个数)
KiMoveApcState(&Thread->ApcState, SavedApcState);//复制保存原apc队列状态

//每当一挂靠,必然要清空原apc队列
InitializeListHead(&Thread->ApcState.ApcListHead[KernelMode]);
InitializeListHead(&Thread->ApcState.ApcListHead[UserMode]);
Thread->ApcState.Process = Process;//关键。将表示当前进程的字段更为目标进程
Thread->ApcState.KernelApcInProgress = FALSE;
Thread->ApcState.KernelApcPending = FALSE;
Thread->ApcState.UserApcPending = FALSE;
if (SavedApcState == &Thread->SavedApcState)//一般满足
{
//修改指向,但不管怎么修改,ApcState字段总是表示当前apc状态
Thread->ApcStatePointer[OriginalApcEnvironment] = &Thread->SavedApcState;
Thread->ApcStatePointer[AttachedApcEnvironment] = &Thread->ApcState;
Thread->ApcStateIndex = AttachedApcEnvironment;
}

if (Process->State == ProcessInMemory)//if 没被置换出去
{
KiReleaseDispatcherLockFromDpcLevel();
KiReleaseApcLockFromDpcLevel(ApcLock);
KiSwapProcess(Process, SavedApcState->Process);//实质函数
//调用这个函数的目的是检测可能的抢占切换条件是否已发生。(若已发生就赶紧切换)
KiExitDispatcher(ApcLock->OldIrql);//降到指定irql(同时检查是否发生了抢占式切换)
}
Else …
}

KiSwapProcess

实质性的函数是KiSwapProcess继续看

1
2
3
4
5
6
7
8
VOID KiSwapProcess(IN PKPROCESS NewProcess,IN PKPROCESS OldProcess) 
{
PKIPCR Pcr = (PKIPCR)KeGetPcr();
//关键。修改cr3(存放进程页目录的物理地址)寄存器为目标进程的页表
writecr3(NewProcess->DirectoryTableBase[0]);
Ke386SetGs(0);//将gs寄存器清0
Pcr->TSS->IoMapBase = NewProcess->IopmOffset;//修改当前线程的IO权限位图为目标进程的那份
}

看到没进程挂靠的实质工作,就是将cr3寄存器改为目标寄存器的地址空间
这样线程的所有有关内存的操作,操作的都是目标进程的地址空间。
明白了进程挂靠后,理解跨进程操作就很容易了。

一个进程可以调用OpenProcess打开另一个进程,取得目标进程的句柄后,就可调用VirtualAllocExWri teProcessMemoryReadProcessMemoryCreateRemoteThread等函数操作那个进程的地址空间。
这些跨进程操作的函数功能强大,而且带有破坏性,以至于往往被杀毒软件重点封杀,特别是CreateRemoteThread这个函数,冤啊。

打开目标进程

所有的跨进程操作都必经一步:打开目标进程。(这是一道需要重点把手的关口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
HANDLE OpenProcess(DWORD dwDesiredAccess,//申请的权限 
BOOL bInheritHandle,//指本次打开得到的句柄是否可继承给子进程
DWORD dwProcessId)//目标进程的pid
{
NTSTATUS errCode;
HANDLE ProcessHandle;
OBJECT_ATTRIBUTES ObjectAttributes;
CLIENT_ID ClientId;
ClientId.UniqueProcess = UlongToHandle(dwProcessId);
ClientId.UniqueThread = 0;

InitializeObjectAttributes(&ObjectAttributes,NULL, (bInheritHandle ? OBJ_INHERIT : 0),NULL,NULL);
//调用系统服务打开进程
errCode = NtOpenProcess(&ProcessHandle,dwDesiredAccess,&ObjectAttributes,&ClientId);
if (!NT_SUCCESS(errCode))
{
SetLastErrorByStatus(errCode);
return NULL;
}
return ProcessHandle;
}


NTSTATUS NtOpenProcess(OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId)//pid.tid
{
KPROCESSOR_MODE PreviousMode = KeGetPreviousMode();
ULONG Attributes = 0;
BOOLEAN HasObjectName = FALSE;

PETHREAD Thread = NULL;
PEPROCESS Process = NULL;
if (PreviousMode != KernelMode)
{
_SEH2_TRY
{
ProbeForWriteHandle(ProcessHandle);
if (ClientId)
{
ProbeForRead(ClientId, sizeof(CLIENT_ID), sizeof(ULONG));
SafeClientId = *ClientId;
ClientId = &SafeClientId;
}
ProbeForRead(ObjectAttributes,sizeof(OBJECT_ATTRIBUTES),sizeof(ULONG));
HasObjectName = (ObjectAttributes->ObjectName != NULL);
Attributes = ObjectAttributes->Attributes;
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
else
{
HasObjectName = (ObjectAttributes->ObjectName != NULL);
Attributes = ObjectAttributes->Attributes;
}



if ((HasObjectName) && (ClientId))//不能同时给定进程名与id
return STATUS_INVALID_PARAMETER_MIX;


//传递当前令牌以及要求的权限到AccessState中
Status = SeCreateAccessState(&AccessState,&AuxData,DesiredAccess,
&PsProcessType->TypeInfo.GenericMapping);
//检查当前令牌是否具有调试特权(这就是为什么经常在打开目标进程前要启用调试特权)
if (SeSinglePrivilegeCheck(SeDebugPrivilege, PreviousMode))
{
if (AccessState.RemainingDesiredAccess & MAXIMUM_ALLOWED)
AccessState.PreviouslyGrantedAccess |= PROCESS_ALL_ACCESS;
else
AccessState.PreviouslyGrantedAccess |=AccessState.RemainingDesiredAccess;
AccessState.RemainingDesiredAccess = 0;
}
if (HasObjectName) //以对象名的方式查找该进程对象
{
Status = ObOpenObjectByName(ObjectAttributes,PsProcessType,PreviousMode, &AccessState,0,NULL,&hProcess);
SeDeleteAccessState(&AccessState);
}
else if (ClientId)
{
if (ClientId->UniqueThread)//根据tid查找线程、进程对象
Status = PsLookupProcessThreadByCid(ClientId, &Process, &Thread);
Else //根据pid从获活动进程链表中查找进程对象,最常见
Status = PsLookupProcessByProcessId(ClientId->UniqueProcess,&Process);
if (!NT_SUCCESS(Status))
{
SeDeleteAccessState(&AccessState);
return Status;
}

//在该进程对象上打开一个句柄
Status = ObOpenObjectByPointer(Process,Attributes,&AccessState,0,
PsProcessType,PreviousMode,&hProcess);
SeDeleteAccessState(&AccessState);
if (Thread)
ObDereferenceObject(Thread);
ObDereferenceObject(Process);
}
else
return STATUS_INVALID_PARAMETER_MIX;



if (NT_SUCCESS(Status))
{
_SEH2_TRY
{
*ProcessHandle = hProcess;//返回打开得到的进程句柄
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
}
return Status;
}

如上这个函数在检测权限满足后,就打开目标进程,返回一个句柄给调用者。
看下面的典型跨进程写数据函数:

跨进程读写数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
NTSTATUS NtWriteVirtualMemory(IN HANDLE ProcessHandle,//远程进程 
IN PVOID BaseAddress,
IN PVOID Buffer,
IN SIZE_T NumberOfBytesToWrite,
OUT PSIZE_T NumberOfBytesWritten OPTIONAL)
{
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
PEPROCESS Process;
NTSTATUS Status = STATUS_SUCCESS;
SIZE_T BytesWritten = 0;
if (PreviousMode != KernelMode)
{
if ((((ULONG_PTR)BaseAddress + NumberOfBytesToWrite) < (ULONG_PTR)BaseAddress) ||
(((ULONG_PTR)Buffer + NumberOfBytesToWrite) < (ULONG_PTR)Buffer) ||
(((ULONG_PTR)BaseAddress + NumberOfBytesToWrite) > MmUserProbeAddress) ||
(((ULONG_PTR)Buffer + NumberOfBytesToWrite) > MmUserProbeAddress))
{
return STATUS_ACCESS_VIOLATION;
}
_SEH2_TRY
{
if (NumberOfBytesWritten)
ProbeForWriteSize_t(NumberOfBytesWritten);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}


if (NumberOfBytesToWrite)
{
Status = ObReferenceObjectByHandle(ProcessHandle,PROCESS_VM_WRITE,PsProcessType, PreviousMode, (PVOID*)&Process,NULL);
if (NT_SUCCESS(Status))
{
Status = MmCopyVirtualMemory(PsGetCurrentProcess(),Buffer,Process,
BaseAddress,NumberOfBytesToWrite,
PreviousMode,&BytesWritten);
ObDereferenceObject(Process);
}
}
if (NumberOfBytesWritten)
{
_SEH2_TRY
{
*NumberOfBytesWritten = BytesWritten;
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{

}
_SEH2_END;
}
return Status;
}



NTSTATUS MmCopyVirtualMemory(IN PEPROCESS SourceProcess,
IN PVOID SourceAddress,
IN PEPROCESS TargetProcess,
OUT PVOID TargetAddress,
IN SIZE_T BufferSize,
IN KPROCESSOR_MODE PreviousMode,
OUT PSIZE_T ReturnSize)
{
NTSTATUS Status;
PEPROCESS Process = SourceProcess;
if (SourceProcess == PsGetCurrentProcess())
Process = TargetProcess;

if (BufferSize > 512)//需要使用MDL
{
Status = MiDoMappedCopy(SourceProcess,SourceAddress,TargetProcess,TargetAddress,
BufferSize,PreviousMode,ReturnSize);
}
else
{
Status = MiDoPoolCopy(SourceProcess,SourceAddress,TargetProcess,TargetAddress,
BufferSize,PreviousMode,ReturnSize);
}
return Status;
}

NTSTATUS MiDoMappedCopy(IN PEPROCESS SourceProcess,
IN PVOID SourceAddress,
IN PEPROCESS TargetProcess,
OUT PVOID TargetAddress,
IN SIZE_T BufferSize,
IN KPROCESSOR_MODE PreviousMode,
OUT PSIZE_T ReturnSize)

{
PFN_NUMBER MdlBuffer[(sizeof(MDL) / sizeof(PFN_NUMBER)) + MI_MAPPED_COPY_PAGES + 1];
PMDL Mdl = (PMDL)MdlBuffer;

SIZE_T TotalSize, CurrentSize, RemainingSize;
volatile BOOLEAN FailedInProbe = FALSE, FailedInMapping = FALSE, FailedInMoving;
volatile BOOLEAN PagesLocked;

PVOID CurrentAddress = SourceAddress, CurrentTargetAddress = TargetAddress;
volatile PVOID MdlAddress;
KAPC_STATE ApcState;
BOOLEAN HaveBadAddress;
ULONG_PTR BadAddress;
NTSTATUS Status = STATUS_SUCCESS;
TotalSize = 14 * PAGE_SIZE;//每次拷贝14个页面大小

if (BufferSize <= TotalSize)
TotalSize = BufferSize;
CurrentSize = TotalSize;
RemainingSize = BufferSize;
while (RemainingSize > 0)
{
if (RemainingSize < CurrentSize)
CurrentSize = RemainingSize;

KeStackAttachProcess(&SourceProcess->Pcb, &ApcState);//挂靠到源进程
MdlAddress = NULL;
PagesLocked = FALSE;
FailedInMoving = FALSE;
_SEH2_TRY
{
if ((CurrentAddress == SourceAddress) && (PreviousMode != KernelMode))
{
FailedInProbe = TRUE;
ProbeForRead(SourceAddress, BufferSize, sizeof(CHAR));
FailedInProbe = FALSE;
}
MmInitializeMdl(Mdl, CurrentAddress, CurrentSize);
MmProbeAndLockPages(Mdl, PreviousMode, IoReadAccess);
PagesLocked = TRUE;
MdlAddress = MmMapLockedPagesSpecifyCache(Mdl,KernelMode,MmCached, NULL,
FALSE,HighPagePriority);
KeUnstackDetachProcess(&ApcState);//撤销挂靠
KeStackAttachProcess(&TargetProcess->Pcb, &ApcState);//挂靠到目标进程
if ((CurrentAddress == SourceAddress) && (PreviousMode != KernelMode))
{
FailedInProbe = TRUE;
ProbeForWrite(TargetAddress, BufferSize, sizeof(CHAR));
FailedInProbe = FALSE;
}

FailedInMoving = TRUE;
RtlCopyMemory(CurrentTargetAddress, MdlAddress, CurrentSize);//拷贝
}
_SEH2_EXCEPT().....

if (Status != STATUS_SUCCESS)
return Status;

KeUnstackDetachProcess(&ApcState);
MmUnmapLockedPages(MdlAddress, Mdl);
MmUnlockPages(Mdl);
RemainingSize -= CurrentSize;
CurrentAddress = (PVOID)((ULONG_PTR)CurrentAddress + CurrentSize);
CurrentTargetAddress = (PVOID)((ULONG_PTR)CurrentTargetAddress + CurrentSize);
}
*ReturnSize = BufferSize;
return STATUS_SUCCESS;
}


线程的挂起与恢复

SuspendThread->NtSuspendThread->PsSuspenThread-> KeSuspendThread

直接看KeSuspendThread函数

KeSuspendThread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
ULONG KeSuspendThread(PKTHREAD Thread) 
{
KLOCK_QUEUE_HANDLE ApcLock;
ULONG PreviousCount;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
KiAcquireApcLock(Thread, &ApcLock);
PreviousCount = Thread->SuspendCount;
if (Thread->ApcQueueable)
{
Thread->SuspendCount++;//递增挂起计数
if (!(PreviousCount) && !(Thread->FreezeCount))
{
if (!Thread->SuspendApc.Inserted)//if尚未插入那个挂起APC
{
Thread->SuspendApc.Inserted = TRUE;
KiInsertQueueApc(&Thread->SuspendApc, IO_NO_INCREMENT);//插入挂起APC
}
else
{
KiAcquireDispatcherLockAtDpcLevel();
Thread->SuspendSemaphore.Header.SignalState--;
KiReleaseDispatcherLockFromDpcLevel();

}
}
}
KiReleaseApcLockFromDpcLevel(&ApcLock);
KiExitDispatcher(ApcLock.OldIrql);
return PreviousCount;
}

这个专有的"挂起APC"是一个特殊的APC,我们看他的工作:

1
2
3
4
5
6
7
8
VOID KiSuspendThread(IN PVOID NormalContext, 
IN PVOID SystemArgument1,
IN PVOID SystemArgument2)
{
//等待挂起计数减到0
KeWaitForSingleObject(&KeGetCurrentThread()->SuspendSemaphore,Suspended,KernelMode,
FALSE,NULL);
}

如上向指定线程插入一个挂起APC后,那个线程下次一得到调度,就会先执行内核中的所有APC
当执行到这个APC的时候,就会一直等到挂起计数降到0。换言之线程刚一得到调度运行的就会,就又重新进入等待了。
因此挂起态也是一种特殊的等待态

KeResumeThread

什么时候挂起计数会减到0呢?
只有在别的线程恢复这个线程的挂起计数时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ULONG KeResumeThread(IN PKTHREAD Thread) 
{
KLOCK_QUEUE_HANDLE ApcLock;
ULONG PreviousCount;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
KiAcquireApcLock(Thread, &ApcLock);
PreviousCount = Thread->SuspendCount;
if (PreviousCount)
{
Thread->SuspendCount--;//递减挂起计数
if ((Thread->SuspendCount==0) && (!Thread->FreezeCount))
{
KiAcquireDispatcherLockAtDpcLevel();
Thread->SuspendSemaphore.Header.SignalState++;
//当挂起计数减到0时,唤醒目标线程
KiWaitTest(&Thread->SuspendSemaphore.Header, IO_NO_INCREMENT);
KiReleaseDispatcherLockFromDpcLevel();
}
}
KiReleaseApcLockFromDpcLevel(&ApcLock);
KiExitDispatcher(ApcLock.OldIrql);
return PreviousCount;
}

就这样简单。

当一个线程处于等待状态时,可以指示本次睡眠是否可被强制唤醒,不必等到条件满足

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
DWORD WaitForSingleObjectEx( 
HANDLE hHandle,
DWORD dwMilliseconds,
BOOL bAlertable //指示本次等待过程中是否可以被其他线程(或其他线程发来的APC)强制唤醒。
);



BOOLEAN KeAlertThread(IN PKTHREAD Thread,
IN KPROCESSOR_MODE AlertMode)
{
BOOLEAN PreviousState;
KLOCK_QUEUE_HANDLE ApcLock;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
KiAcquireApcLock(Thread, &ApcLock);
KiAcquireDispatcherLockAtDpcLevel();

PreviousState = Thread->Alerted[AlertMode];//检测是否收到了来自那个模式的强制唤醒要求

if (PreviousState==FALSE)
{
if ((Thread->State == Waiting) && //线程处于等待状态
(Thread->Alertable) && //线程可被强制唤醒
(AlertMode <= Thread->WaitMode)) //模式条件符合
{
//强制唤醒那个线程
KiUnwaitThread(Thread, STATUS_ALERTED, THREAD_ALERT_INCREMENT);
}
Else //仅仅标记已收到过来自那个模式的强制唤醒请求
Thread->Alerted[AlertMode] = TRUE;
}
KiReleaseDispatcherLockFromDpcLevel();
KiReleaseApcLockFromDpcLevel(&ApcLock);
KiExitDispatcher(ApcLock.OldIrql);
return PreviousState;
}

注意AlertMode <= Thread->WaitMode条件指:用户模式的强制唤醒请求不能唤醒内核模式的等待。


DLL注入

前面讲过每个进程在启动的时候会加载主exe文件依赖的所有子孙dll。
实际上一般的Win32 GUI进程都会加载user32.dll模块。
这个模块一加载就会自动搜索注册表键HKEY_LOCAL_MACHINE\Software\Mic rosoft\Windows NT\CurrentVersion\Windows下的值:AppInit_DLLs,该值是一个dll列表,user32.dll会读取这个值,调用LoadLibrary加载里面的每个dll

因此我们可以把我们的dll名称添加到这个列表中,达到dll注入的目的

DllMain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//User32.dll的DllMain 
INT DllMain(
IN PVOID hInstanceDll,
IN ULONG dwReason,
IN PVOID reserved)
{
switch (dwReason)
{
case DLL_PROCESS_ATTACH:
Init();//会调用这个函数


}
}

BOOL Init(VOID)
{

LoadAppInitDlls();//会调用这个函数加载那些dll

}

LoadAppInitDlls

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
VOID LoadAppInitDlls() 
{
szAppInit[0] = UNICODE_NULL;
if (GetDllList())//读取这册表键的值,将要加载的dll列表保存在全局变量szAppInit中
{
WCHAR buffer[KEY_LENGTH];
LPWSTR ptr;
size_t i;
RtlCopyMemory(buffer, szAppInit, KEY_LENGTH);

for (i = 0; i < KEY_LENGTH; ++ i)
{
if(buffer[i] == L' ' || buffer[i] == L',')//dll名称之间必须用空格或逗号隔开
buffer[i] = 0;
}

for (i = 0; i < KEY_LENGTH; )
{
if(buffer[i] == 0)
++ i;
else
{
ptr = buffer + i;
i += wcslen(ptr);
LoadLibraryW(ptr);//加载每个dll
}
}
}
}

GetDllList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
BOOL GetDllList() 
{
NTSTATUS Status;
OBJECT_ATTRIBUTES Attributes;
BOOL bRet = FALSE;
BOOL bLoad;
HANDLE hKey = NULL;
DWORD dwSize;
PKEY_VALUE_PARTIAL_INFORMATION kvpInfo = NULL;

UNICODE_STRING szKeyName = RTL_CONSTANT_STRING(L"\\Registry\\Machine\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows");

UNICODE_STRING szLoadName = RTL_CONSTANT_STRING(L"LoadAppInit_DLLs");
UNICODE_STRING szDllsName = RTL_CONSTANT_STRING(L"AppInit_DLLs");

InitializeObjectAttributes(&Attributes, &szKeyName, OBJ_CASE_INSENSITIVE, NULL, NULL);
Status = NtOpenKey(&hKey, KEY_READ, &Attributes);

if (NT_SUCCESS(Status))
{
dwSize = sizeof(KEY_VALUE_PARTIAL_INFORMATION) + sizeof(DWORD);
kvpInfo = HeapAlloc(GetProcessHeap(), 0, dwSize);
if (!kvpInfo)
goto end;

//先要在那个键中建立一个DWORD值:LoadAppInit_DLLs,并将数值设为1
Status = NtQueryValueKey(hKey,&szLoadName,KeyValuePartialInformation,
kvpInfo,dwSize,&dwSize);

RtlMoveMemory(&bLoad,kvpInfo->Data,kvpInfo->DataLength);
HeapFree(GetProcessHeap(), 0, kvpInfo);
kvpInfo = NULL;
if (bLoad)//if 需要加载初始列表的那些dll
{
Status = NtQueryValueKey(hKey,&szDllsName,KeyValuePartialInformation, NULL,0,&dwSize);
kvpInfo = HeapAlloc(GetProcessHeap(), 0, dwSize);
Status = NtQueryValueKey(hKey, &szDllsName,KeyValuePartialInformation,
kvpInfo,dwSize,&dwSize);
if (NT_SUCCESS(Status))
{
LPWSTR lpBuffer = (LPWSTR)kvpInfo->Data;
if (*lpBuffer != UNICODE_NULL)
{
INT bytesToCopy, nullPos;
bytesToCopy = min(kvpInfo->DataLength, KEY_LENGTH * sizeof(WCHAR));
if (bytesToCopy != 0)
{
//dll列表拷到全局变量
RtlMoveMemory(szAppInit,kvpInfo->Data,bytesToCopy);
nullPos = (bytesToCopy / sizeof(WCHAR)) - 1;
szAppInit[nullPos] = UNICODE_NULL;
bRet = TRUE;
}
}
}
}
}

end:
if (hKey)
NtClose(hKey);
if (kvpInfo)
HeapFree(GetProcessHeap(), 0, kvpInfo);
return bRet;
}

因此只需在那个键下面添加一个DWORD值:LoadAppInit_DLLs,设为1
然后在AppInit_DLLs值中添加我们的dll即可达到将我们的dll加载到任意GUI进程的地址空间中。




APC

异步过程调用, 这是一种常见的技术。
前面进程启动的初始过程就是:主线程在内核构造好运行环境后,从KiThreadStartup开始运行,然后调用PspUserThreadStartup,在该线程的apc队列中插入一个APC:LdrInitializeThunk

这样当PspUserThreadStartup返回后,正式退回用户空间的总入口 BaseProcessStartThunk前,会执行中途插入的那个 apc,完成进程的用户空间初始化工作(链接 dll 的加载等)

可见APC 的执行时机之一就是从内核空间返回用户空间的前夕。
也即在返回用户空间前,会中断那么一下。因此APC 就是一种软中断。

除了这种 APC 用途外,应用程序中也经常使用 APC。如Win32 API ReadFileEx 就可以使用 APC 机制来实现异步读写文件的功能。

ReadFileEx异步读写Apc实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
BOOL ReadFileEx(IN HANDLE hFile, 
IN LPVOID lpBuffer,
IN DWORD nNumberOfBytesToRead OPTIONAL,
IN LPOVERLAPPED lpOverlapped,//完成结果
IN LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine)//预置APC将调用的完成例程
{
LARGE_INTEGER Offset;
NTSTATUS Status;
Offset.u.LowPart = lpOverlapped->Offset;
Offset.u.HighPart = lpOverlapped->OffsetHigh;
lpOverlapped->Internal = STATUS_PENDING;
Status = NtReadFile(hFile,
NULL, //Event=NULL
ApcRoutine,//这个是内部预置的APC例程
lpCompletionRoutine,//APC的Context
(PIO_STATUS_BLOCK)lpOverlapped,
lpBuffer,
nNumberOfBytesToRead,
&Offset,
NULL);//Key=NULL


if (!NT_SUCCESS(Status))
{
SetLastErrorByStatus(Status);//
return FALSE;
}
return TRUE;
}




VOID ApcRoutine(PVOID ApcContext,//指向用户提供的完成例程
_IO_STATUS_BLOCK* IoStatusBlock,//完成结果
ULONG Reserved)
{
LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine = ApcContext;
DWORD dwErrorCode = RtlNtStatusToDosError(IoStatusBlock->Status);

//调用用户提供的完成例程
lpCompletionRoutine(dwErrorCode, IoStatusBlock->Information, (LPOVERLAPPED)IoStatusBlock);
}

因此应用层的用户提供的完成例程实际上是作为 APC 函数进行的,它运行在APC_LEVEL irql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NTSTATUS NtReadFile(IN HANDLE FileHandle, 
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,//内置的APC
IN PVOID ApcContext OPTIONAL,//应用程序中用户提供的完成例程
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID Buffer,
IN ULONG Length,
IN PLARGE_INTEGER ByteOffset OPTIONAL,
IN PULONG Key OPTIONAL)
{

Irp = IoAllocateIrp(DeviceObject->StackSize, FALSE);//分配一个 irp
Irp->Overlay.AsynchronousParameters.UserApcRoutine = ApcRoutine;//记录
Irp->Overlay.AsynchronousParameters.UserApcContext = ApcContext;//记录

Status = IoCallDriver(DeviceObject, Irp);//把这个构造的 irp 发给底层驱动

}

当底层驱动完成这个 irp 后,会调用IoCompleteRequest完成掉这个 irp,这个IoCompleteRequest实际上内部最终调用IopCompleteRequest来做一些完成时的工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
VOID IopCompleteRequest(IN PKAPC Apc, 
IN PKNORMAL_ROUTINE* NormalRoutine,
IN PVOID* NormalContext,
IN PVOID* SystemArgument1,
IN PVOID* SystemArgument2)
{

if (Irp->Overlay.AsynchronousParameters.UserApcRoutine)//上面传入的APC
{
//构造一个APC
KeInitializeApc(&Irp->Tail.Apc,KeGetCurrentThread(),CurrentApcEnvironment,
IopFreeIrpKernelApc,
IopAbortIrpKernelApc,

(PKNORMAL_ROUTINE)Irp->Overlay.AsynchronousParameters.UserApcRoutine,
Irp->RequestorMode,
Irp->Overlay.AsynchronousParameters.UserApcContext);//应用层的完成例程

//插入到APC队列
KeInsertQueueApc(&Irp->Tail.Apc, Irp->UserIosb, NULL, 2);
}//end if

}

如上ReadFileEx函数的异步 APC 机制是:在这个请求完成后,IO 管理器会将一个 APC 插入队列中,然后在返回用户空间前夕调用那个内置 APC,最终调用应用层用户提供的完成例程。

明白了 APC 大致原理后,现在详细看一下 APC 的工作原理。
APC分两种,用户 APC、内核 APC。
前者指在用户空间执行的 APC,后者指在内核空间执行的 APC。

基础结构

先看一下内核为支持 APC 机制提供的一些基础结构设施。

_KAPC_STATE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Typedef struct _KTHREAD 
{

KAPC_STATE ApcState;//表示本线程当前使用的 APC 状态(即 apc 队列的状态)
KAPC_STATE SavedApcState;//表示保存的原 apc 状态,备份用
KAPC_STATE* ApcStatePointer[2];//状态数组,包含两个指向 APC 状态的指针
UCHAR ApcStateIndex;//0 或 1,指当前的 ApcState 在 ApcStatePointer 数组中的索引位置
UCHAR ApcQueueable;//指本线程的 APC 队列是否可插入 apc
ULONG KernelApcDisable;//禁用标志
//专用于挂起操作的 APC(这个函数在线程一得到调度就重新进入等待态,等待挂起计数减到 0)
KAPC SuspendApc;

} KTHREAD;

//APC 队列的状态描述符
Typedef struct _KAPC_STATE
{
LIST_EBTRY ApcListHead[2];//每个线程有两个 apc 队列
PKPROCESS Process;//当前线程所在的进程
BOOL KernelApcInProgress;//指示本线程是否当前正在 内核 apc
BOOL KernelApcPending;//表示内核 apc 队列中是否有 apc
BOOL UserApcPending;//表示用户 apc 队列中是否 apc
}

_KAPC_ENVIRONMENT

1
2
3
4
5
6
7
Typedef enum _KAPC_ENVIRONMENT 
{
OriginalApcEnvironment,//0,状态数组索引
AttachedApcEnvironment;//1,状态数组索引
CurrentApc Environment;//2,表示使用当前 apc 状态
CurrentApc Environment;//3,表示使用插入 apc 时那时的线程的 apc 状态
}

一个线程可以挂靠到其他进程的地址空间中,因此,一个线程的状态分两种:常态、挂靠态。
常态下状态数组中 0 号元素指向ApcState(即当前 apc 状态),1 号元素指向SavedApcState(非当前 apc 状态);
挂靠态下,两个元素的指向刚好相反。但无论如何KTHREAD结构中的ApcStateIndex总是指当前状态的位置,ApcState 则总是表示线程当前使用的 apc 状态。

于是有

1
2
3
4
#define PsGetCurrentProcess IoGetCurrentProces PEPROCESS IoGetCurrentProces() 
{
Return PsGetCurrentThread()->Tcb.ApcState.Process;//ApcState 中的进程字段总是表示当前进程
}

不管当前线程是处于常态还是挂靠态下,它都有两个 apc 队列,一个内核,一个用户。
把 apc 插入对应的队列后就可以在恰当的时机得到执行。

注意:每当一个线程挂靠到其他进程时,挂靠初期,两个 apc 队列都会变空。
下面看下每个 apc 本身的结构

_KAPC

若这个 apc 是内核 apc,那么NormalRoutine表示用户自己提供的内核 apc 函数,NormalContext则是该 apc 函数的context*,SystemArgument1SystemArgument2 表示插入队列时的附加参数

若这个 apc 是用户 apc,那么NormalRoutine表示该 apc 的用户空间总 apc 函数,NormalContext才是真正用户自己提供的用户空间 apc 函数,SystemArgument1则表示该真正 apc 的 context*。(一切错位了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct _KAPC 
{
UCHAR Type;//结构体的类型
UCHAR Size;//结构体的大小
struct _KTHREAD *Thread;//目标线程
LIST_ENTRY ApcListEntry;//用来挂入目标apc队列
PKKERNEL_ROUTINE KernelRoutine;//该apc的内核总入口
PKRUNDOWN_ROUTINE RundownRoutine;
PKNORMAL_ROUTINE NormalRoutine;//该apc的用户空间总入口或者用户真正的内核apc函数
PVOID NormalContext;//真正用户提供的用户空间apc函数或者用户真正的内核apc函数的context*
PVOID SystemArgument1;//挂入时的附加参数1。真正用户apc的context*
PVOID SystemArgument2;//挂入时的附加参数2
CCHAR ApcStateIndex;//指要挂入目标线程的哪个状态时的apc队列
KPROCESSOR_MODE ApcMode;//指要挂入用户apc队列还是内核apc队列
BOOLEAN Inserted;//表示本apc是否已挂入队列
} KAPC, *PKAPC;

Api流程

QueueUserAPC

下面这个 Win32 API 可以用来手动插入一个 apc 到指定线程的用户 apc 队列中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
DWORD  
QueueUserAPC(PAPCFUNC pfnAPC, HANDLE hThread, ULONG_PTR dwData)
{
NTSTATUS Status;
//调用对应的系统服务
Status = NtQueueApcThread(hThread,//目标线程
IntCallUserApc,//用户空间中的总apc入口
pfnAPC,//用户自己真正提供的apc函数 (PVOID)dwData,//SysArg1=context*
NULL);//SysArg2=NULL

if (!NT_SUCCESS(Status))
{
SetLastErrorByStatus(Status);
return 0;
}
return 1;
}

NtQueueApcThread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
NTSTATUS NtQueueApcThread(IN HANDLE ThreadHandle,//目标线程 
IN PKNORMAL_ROUTINE ApcRoutine,//用户空间中的总apc
IN PVOID NormalContext,//用户自己真正的apc函数
IN PVOID SystemArgument1,//用户自己apc的context*
IN PVOID SystemArgument2)//其它
{
PKAPC Apc;
PETHREAD Thread;
NTSTATUS Status = STATUS_SUCCESS;
Status = ObReferenceObjectByHandle(ThreadHandle,THREAD_SET_CONTEXT,PsThreadType,
ExGetPreviousMode(), (PVOID)&Thread,NULL);
//分配一个apc结构,这个结构最终在PspQueueApcSpecialApc中释放
Apc = ExAllocatePoolWithTag(NonPagedPool |POOL_QUOTA_FAIL_INSTEAD_OF_RAISE,
sizeof(KAPC),TAG_PS_APC);
//构造一个apc
KeInitializeApc(Apc,
&Thread->Tcb,//目标线程
OriginalApcEnvironment,//目标apc状态(此服务固定为OriginalApcEnvironment)
PspQueueApcSpecialApc,//内核apc总入口
NULL,//Rundown Rounine=NULL
ApcRoutine,//用户空间的总apc
UserMode,//此系统服务固定插入到用户apc队列
NormalContext);//用户自己真正的apc函数
//插入到目标线程的用户apc队列
KeInsertQueueApc(Apc,
SystemArgument1,//插入时的附加参数1,此处为用户自己apc的context*
SystemArgument2, //插入时的附加参数2
IO_NO_INCREMENT)//表示不予调整目标线程的调度优先级
return Status;
}

KeInitializeApc

这个函数用来构造一个要插入指定目标队列的 apc 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
VOID KeInitializeApc(IN PKAPC Apc, 
IN PKTHREAD Thread,//目标线程
IN KAPC_ENVIRONMENT TargetEnvironment,//目标线程的目标apc状态
IN PKKERNEL_ROUTINE KernelRoutine,//内核apc总入口
IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,
IN PKNORMAL_ROUTINE NormalRoutine,//用户空间的总apc
IN KPROCESSOR_MODE Mode,//要插入用户apc队列还是内核apc队列
IN PVOID Context) //用户自己真正的apc函数
{
Apc->Type = ApcObject;
Apc->Size = sizeof(KAPC);
if (TargetEnvironment == CurrentApcEnvironment)//CurrentApcEnvironment表示使用当前apc状态
Apc->ApcStateIndex = Thread->ApcStateIndex;
else
Apc->ApcStateIndex = TargetEnvironment;
Apc->Thread = Thread;
Apc->KernelRoutine = KernelRoutine;
Apc->RundownRoutine = RundownRoutine;
Apc->NormalRoutine = NormalRoutine;
if (NormalRoutine)//if 提供了用户空间总apc入口
{
Apc->ApcMode = Mode;
Apc->NormalContext = Context;
}
Else//若没提供,肯定是内核模式
{
Apc->ApcMode = KernelMode;
Apc->NormalContext = NULL;
}
Apc->Inserted = FALSE;//表示初始构造后,尚未挂入apc队列
}

KeInsertQueueApc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
BOOLEAN KeInsertQueueApc(IN PKAPC Apc,IN PVOID SystemArgument1,IN PVOID SystemArgument2, 
IN KPRIORITY PriorityBoost)
{
PKTHREAD Thread = Apc->Thread;
KLOCK_QUEUE_HANDLE ApcLock;
BOOLEAN State = TRUE;
KiAcquireApcLock(Thread, &ApcLock);//插入过程需要独占队列
if (!(Thread->ApcQueueable) || (Apc->Inserted))//检查队列是否可以插入apc
State = FALSE;

else
{
Apc->SystemArgument1 = SystemArgument1;//记录该apc的附加插入时的参数
Apc->SystemArgument2 = SystemArgument2; //记录该apc的附加插入时的参数
Apc->Inserted = TRUE;//标记为已插入队列
//插入目标线程的目标apc队列(如果目标线程正处于睡眠状态,可能会唤醒它)
KiInsertQueueApc(Apc, PriorityBoost);
}

KiReleaseApcLockFromDpcLevel(&ApcLock);
KiExitDispatcher(ApcLock.OldIrql);//可能引发一次线程切换,以立即切换到目标线程执行apc
return State;
}

KiInsertQueueApc

这个函数既可以给当前线程发送 apc,也可以给目标线程发送 apc。
若给当前线程发送内核 apc 时, 会立即请求发出一个 apc 中断。若给其他线程发送 apc 时,可能会唤醒目标线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
VOID FASTCALL KiInsertQueueApc(IN PKAPC Apc,IN KPRIORITY PriorityBoost)//唤醒目标线程后的优先级增量 
{
PKTHREAD Thread = Apc->Thread;
BOOLEAN RequestInterrupt = FALSE;
if (Apc->ApcStateIndex == InsertApcEnvironment) //if要动态插入到当前的apc状态队列
Apc->ApcStateIndex = Thread->ApcStateIndex;

ApcState = Thread->ApcStatePointer[(UCHAR)Apc->ApcStateIndex];//目标状态
ApcMode = Apc->ApcMode;
//先插入apc到指定位置
/* 插入位置的确定:分三种情形
* 1) Kernel APC with Normal Routine or User APC : Put it at the end of the List
* 2) User APC which is PsExitSpecialApc : Put it at the front of the List
* 3) Kernel APC without Normal Routine : Put it at the end of the No-Normal Routine Kernel APC list
*/

if (Apc->NormalRoutine)//有NormalRoutine的APC都插入尾部(用户模式发来的线程终止APC除外)
{
if ((ApcMode == UserMode) && (Apc->KernelRoutine == PsExitSpecialApc))
{
Thread->ApcState.UserApcPending = TRUE;
InsertHeadList(&ApcState->ApcListHead[ApcMode],&Apc->ApcListEntry);
}
else
InsertTailList(&ApcState->ApcListHead[ApcMode],&Apc->ApcListEntry);
}
Else //无NormalRoutine的特殊类APC(内核APC),少见
{
ListHead = &ApcState->ApcListHead[ApcMode];
NextEntry = ListHead->Blink;
while (NextEntry != ListHead)
{
QueuedApc = CONTAINING_RECORD(NextEntry, KAPC, ApcListEntry);
if (!QueuedApc->NormalRoutine) break;

NextEntry = NextEntry->Blink;
}
InsertHeadList(NextEntry, &Apc->ApcListEntry);//插在这儿
}

//插入到相应的位置后,下面检查Apc状态是否匹配
if (Thread->ApcStateIndex == Apc->ApcStateIndex)//if 插到了当前apc状态的apc队列中
{
if (Thread == KeGetCurrentThread())//if就是给当前线程发送的apc
{
ASSERT(Thread->State == Running);//当前线程肯定没有睡眠,这不废话吗?
if (ApcMode == KernelMode)
{
Thread->ApcState.KernelApcPending = TRUE;
if (!Thread->SpecialApcDisable)//发出一个apc中断,待下次降低irql时将执行apc
HalRequestSoftwareInterrupt(APC_LEVEL); //关键
}
}
Else //给其他线程发送的内核apc
{
KiAcquireDispatcherLock();
if (ApcMode == KernelMode)
{
Thread->ApcState.KernelApcPending = TRUE;
if (Thread->State == Running)
RequestInterrupt = TRUE;//需要给它发出一个apc中断
else if ((Thread->State == Waiting) && (Thread->WaitIrql == PASSIVE_LEVEL) &&
!(Thread->SpecialApcDisable) && (!(Apc->NormalRoutine) ||
(!(Thread->KernelApcDisable) &&
!(Thread->ApcState.KernelApcInProgress))))
{
Status = STATUS_KERNEL_APC;
KiUnwaitThread(Thread, Status, PriorityBoost);//临时唤醒目标线程执行apc
}
else if (Thread->State == GateWait) …
}
else if ((Thread->State == Waiting) && (Thread->WaitMode == UserMode) &&
((Thread->Alertable) || (Thread->ApcState.UserApcPending)))
{
Thread->ApcState.UserApcPending = TRUE;
Status = STATUS_USER_APC;

KiUnwaitThread(Thread, Status, PriorityBoost);//强制唤醒目标线程
}
KiReleaseDispatcherLockFromDpcLevel();
KiRequestApcInterrupt(RequestInterrupt, Thread->NextProcessor);
}
}
}




用户态APC的执行时机

回顾一下从内核返回用户时的流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
KiSystemService()//int 2e 的 isr,内核服务函数总入口,注意这个函数可以嵌套、递归!!! 
{
SaveTrap();//保存 trap 现场 Sti //开中断

---------------上面保存完寄存器等现场后,开始查 SST 表调用系统服务------------------
FindTableCall();
---------------------------------调用完系统服务函数后------------------------------

Move esp,kthread.TrapFrame; //将栈顶回到 trap 帧结构体处 Cli //关中断
If(上次模式==UserMode)
{
Call KiDeliverApc //遍历执行本线程的内核 APC 和用户 APC 队列中的所有 APC 函数 清理 Trap 帧,恢复寄存器现场
Iret //返回用户空间
}
Else
{
返回到原 call 处后面的那条指令处
}
}

不光是从系统调用返回用户空间要扫描执行 apc,从异常和中断返回用户空间也同样需要扫描执行。
现在我们只看从系统调用返回时 apc 的执行过程。

上面是伪代码,实际的从 Cli 后面的代码,是下面这样的。

Cli后面处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Test dword ptr[ebp+KTRAP_FRAME_EFLAGS], EFLAGS_V86_MASK //检查 eflags 是否标志运行在 V86 模式 
Jnz 1 //若运行在 V86 模式,那么上次模式肯定是从用户空间进入内核的,跳过下面的检查

Test byte ptr[ebp+KTRAP_FRAME_CS],1
Je 2 //若上次模式不是用户模式,跳过下面的流程,不予扫描 apc

1:
Mov ebx,PCR[KPCR_CURRENT_THREAD] //ebx=KTHREAD*(当前线程对象的地址)
Mov byte ptr[ebx+KTHREAD_ALERTED],0 //kthread.Alert 修改为不可提醒
Cmp byte ptr[ebx+KTHREAD_PENDING_USER_APC],0
Je 2 //如果当前线程的用户 apc 队列为空,直接跳过
Mov ebx,ebp //ebx=TrapFrame 帧的地址
Mov [ebx,KTRAP_FRAME_EAX],eax //保存

Mov ecx,APC_LEVEL
Call KfRaiseIrql //call KfRaiseIrql(APC_LEVEL)
Push eax //保存提升 irql 之前的 irql

Sti
Push ebx //TrapFrame 帧的地址 Push NULL
Push UserMode
Call KiDeliverApc //call KiDeliverApc(UserMode, NULL, TrapFrame*)
Pop ecx // ecx=之前的 irql

Call KfLowerIrql //call KfLowerIrql(之前的 irql)


Move eax, [ebx,KTRAP_FRAME_EAX] //恢复 eax
Cli
Jmp 1 //再次跳回 1 处循环,扫描 apc 队列

KiDeliverApc

关键的函数是KiDeliverApc,这个函数用来真正扫描 apc 队列执行所有 apc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
VOID KiDeliverApc(IN KPROCESSOR_MODE DeliveryMode,//指要执行哪个apc队列中的函数 
IN PKEXCEPTION_FRAME ExceptionFrame,//传入的是NULL
IN PKTRAP_FRAME TrapFrame)//即将返回用户空间前的Trap现场帧
{
PKTHREAD Thread = KeGetCurrentThread();
PKPROCESS Process = Thread->ApcState.Process;
OldTrapFrame = Thread->TrapFrame;
Thread->TrapFrame = TrapFrame;
Thread->ApcState.KernelApcPending = FALSE;
if (Thread->SpecialApcDisable) goto Quickie;

//先固定执行掉内核apc队列中的所有apc函数
while (!IsListEmpty(&Thread->ApcState.ApcListHead[KernelMode]))
{
KiAcquireApcLockAtApcLevel(Thread, &ApcLock);//锁定apc队列
ApcListEntry = Thread->ApcState.ApcListHead[KernelMode].Flink;//队列头部中的apc
Apc = CONTAINING_RECORD(ApcListEntry, KAPC, ApcListEntry);
KernelRoutine = Apc->KernelRoutine;//内核总apc函数
NormalRoutine = Apc->NormalRoutine;//用户自己真正的内核apc函数
NormalContext = Apc->NormalContext;//真正内核apc函数的context*
SystemArgument1 = Apc->SystemArgument1;
SystemArgument2 = Apc->SystemArgument2;

if (NormalRoutine==NULL) //称为Special Apc,少见
{
RemoveEntryList(ApcListEntry);//关键,移除队列
Apc->Inserted = FALSE;

KiReleaseApcLock(&ApcLock);
//执行内核中的总apc函数
KernelRoutine(Apc,&NormalRoutine,&NormalContext,
&SystemArgument1,&SystemArgument2);
}
Else //典型,一般程序员都会提供一个自己的内核apc函数
{
if ((Thread->ApcState.KernelApcInProgress) || (Thread->KernelApcDisable))
{
KiReleaseApcLock(&ApcLock);
goto Quickie;
}
RemoveEntryList(ApcListEntry); //关键,移除队列
Apc->Inserted = FALSE;
KiReleaseApcLock(&ApcLock);

//执行内核中的总apc函数
KernelRoutine(Apc,
&NormalRoutine,//注意,内核中的总apc可能会在内部修改NormalRoutine
&NormalContext,
&SystemArgument1,
&SystemArgument2);
if (NormalRoutine)//如果内核总apc没有修改NormalRoutine成NULL
{
Thread->ApcState.KernelApcInProgress = TRUE;//标记当前线程正在执行内核apc
KeLowerIrql(PASSIVE_LEVEL);
//直接调用用户提供的真正内核apc函数
NormalRoutine(NormalContext, SystemArgument1, SystemArgument2);
KeRaiseIrql(APC_LEVEL, &ApcLock.OldIrql);
}
Thread->ApcState.KernelApcInProgress = FALSE;
}
}

//上面的循环,执行掉所有内核apc函数后,下面开始执行用户apc队列中的第一个apc
if ((DeliveryMode == UserMode) &&
!(IsListEmpty(&Thread->ApcState.ApcListHead[UserMode])) &&
(Thread->ApcState.UserApcPending))
{
KiAcquireApcLockAtApcLevel(Thread, &ApcLock);//锁定apc队列
Thread->ApcState.UserApcPending = FALSE;

ApcListEntry = Thread->ApcState.ApcListHead[UserMode].Flink;//队列头
Apc = CONTAINING_RECORD(ApcListEntry, KAPC, ApcListEntry);
KernelRoutine = Apc->KernelRoutine; //内核总apc函数
NormalRoutine = Apc->NormalRoutine; //用户空间的总apc函数

NormalContext = Apc->NormalContext;//用户真正的用户空间apc函数
SystemArgument1 = Apc->SystemArgument1;//真正apc的context*
SystemArgument2 = Apc->SystemArgument2;
RemoveEntryList(ApcListEntry);//关键,移除队列

Apc->Inserted = FALSE;
KiReleaseApcLock(&ApcLock);

KernelRoutine(Apc,
&NormalRoutine,// 注意,内核中的总apc可能会在内部修改NormalRoutine
&NormalContext,
&SystemArgument1,
&SystemArgument2);

if (!NormalRoutine)
KeTestAlertThread(UserMode);
Else //典型,准备提前回到用户空间调用用户空间的总apc函数
{
KiInitializeUserApc(ExceptionFrame,//NULL
TrapFrame,//Trap帧的地址
NormalRoutine, //用户空间的总apc函数
NormalContext, //用户真正的用户空间apc函数
SystemArgument1, //真正apc的context*
SystemArgument2);
}
}


Quickie:
Thread->TrapFrame = OldTrapFrame;
}

如上这个函数既可以用来投递处理内核 apc 函数,也可以用来投递处理用户 apc 队列中的函数。
特别的当要调用这个函数投递处理用户 apc 队列中的函数时,它每次只处理一个用户 apc。 由于正式回到用户空间前,会循环调用这个函数。

因此实际的处理顺序是:
扫描执行内核 apc 队列所有 apc->执行用户 apc 队列中一个 apc->再次扫描执行内核 apc 队列所有 apc->执行用户 apc 队列中下一个 apc->再次扫描执行内核 apc 队列所有 apc->再次执行用户 apc 队列中下一个 apc

如此循环直到将用户 apc 队列中的所有 apc 都执行掉。

执行用户 apc 队列中的 apc 函数与内核 apc 不同,因为用户 apc 队列中的 apc 函数自然是要在用户空间中执行的
KiDeliverApc这个函数本身位于内核空间,因此不能直接调用用户 apc 函数,需要提前 回到用户空间去执行队列中的每个用户 apc,然后重新返回内核,再次扫描整个内核 apc 队列,再执行用 户 apc 队列中遗留的下一个用户 apc。
如此循环直至执行完所有用户 apc 后,才正式返回用户空间。

KiInitializeUserApc

下面的函数就是用来为执行用户 apc 做准备的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
VOID KiInitializeUserApc(IN PKEXCEPTION_FRAME ExceptionFrame, 
IN PKTRAP_FRAME TrapFrame,//原真正的断点现场帧

IN PKNORMAL_ROUTINE NormalRoutine,
IN PVOID NormalContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2)
{
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
//将原真正的Trap帧打包保存在一个Context结构中
KeTrapFrameToContext(TrapFrame, ExceptionFrame, &Context);

_SEH2_TRY
{
AlignedEsp = Context.Esp & ~3;//对齐4B
//为用户空间中KiUserApcDisatcher函数的参数腾出空间(4个参数+ CONTEXT + 8B的seh节点)
ContextLength = CONTEXT_ALIGNED_SIZE + (4 * sizeof(ULONG_PTR));
Stack = ((AlignedEsp - 8) & ~3) - ContextLength;//8表示seh节点的大小
//模拟压入KiUserApcDispatcher函数的4个参数
*(PULONG_PTR)(Stack + 0 * sizeof(ULONG_PTR)) = (ULONG_PTR)NormalRoutine;
*(PULONG_PTR)(Stack + 1 * sizeof(ULONG_PTR)) = (ULONG_PTR)NormalContext;
*(PULONG_PTR)(Stack + 2 * sizeof(ULONG_PTR)) = (ULONG_PTR)SystemArgument1;
*(PULONG_PTR)(Stack + 3 * sizeof(ULONG_PTR)) = (ULONG_PTR)SystemArgument2;
//将原真正trap帧保存在用户栈的一个CONTEXT结构中,方便以后还原
RtlCopyMemory( (Stack + (4 * sizeof(ULONG_PTR))),&Context,sizeof(CONTEXT));

//强制修改当前Trap帧中的返回地址与用户栈地址(偏离原来的返回路线)
TrapFrame->Eip = (ULONG)KeUserApcDispatcher;//关键,新的返回断点地址
TrapFrame->HardwareEsp = Stack;//关键,新的用户栈顶
TrapFrame->SegCs = Ke386SanitizeSeg(KGDT_R3_CODE, UserMode);
TrapFrame->HardwareSegSs = Ke386SanitizeSeg(KGDT_R3_DATA, UserMode);
TrapFrame->SegDs = Ke386SanitizeSeg(KGDT_R3_DATA, UserMode);
TrapFrame->SegEs = Ke386SanitizeSeg(KGDT_R3_DATA, UserMode);
TrapFrame->SegFs = Ke386SanitizeSeg(KGDT_R3_TEB, UserMode);
TrapFrame->SegGs = 0;
TrapFrame->ErrCode = 0;
TrapFrame->EFlags = Ke386SanitizeFlags(Context.EFlags, UserMode);
if (KeGetCurrentThread()->Iopl) TrapFrame->EFlags |= EFLAGS_IOPL;
}
_SEH2_EXCEPT((RtlCopyMemory(&SehExceptRecord, _SEH2_GetExceptionInformation()->ExceptionRecord, sizeof(EXCEPTION_RECORD)), EXCEPTION_EXECUTE_HANDLER))
{
SehExceptRecord.ExceptionAddress = (PVOID)TrapFrame->Eip;
KiDispatchException(&SehExceptRecord,ExceptionFrame,TrapFrame,UserMode,TRUE);
}
_SEH2_END;
}

至于为什么要放在一个 try 块中保护,是因为用户空间中的栈地址,谁也无法保证会不会出现崩溃。

KiUserApcDisatcher

如上这个函数修改返回地址,回到用户空间中的KiUserApcDisatcher函数处去。然后把原trap 帧保存在用户栈中。
由于KiUserApcDisatcher这个函数有参数,所以需要模拟压入这个函数的参数
这样当返回到用户空间时,就仿佛是在调用这个函数。

看下那个函数的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
KiUserApcDisatcher(NormalRoutine, 
NormalContext,
SysArg1,
SysArg2
)
{
Lea eax,[esp+ CONTEXT_ALIGNED_SIZE+16] //eax 指向 seh 异常节点的地址
Mov ecx,fs:[TEB_EXCEPTION_LIST]
Mov edx,offset KiUserApcExceptionHandler
--------------------------------------------------------------------------------------
Mov [eax],ecx //seh 节点的 next 指针成员
Mov [eax+4],edx //she 节点的 handler 函数指针成员
Mov fs:[TEB_EXCEPTION_LIST],eax

--------------------上面三条指令在栈中构造一个 8B 的标准 seh 节点-----------------------
Pop eax //eax=NormalRoutine(即 IntCallUserApc 这个总 apc 函数)
Lea edi,[esp+12] //edi=栈中保存的 CONTEXT 结构的地址
Call eax //相当于 call IntCallUserApc(NormalContext,SysArg1,SysArg2)

Mov ecx,[edi+ CONTEXT_ALIGNED_SIZE]
Mov fs:[ TEB_EXCEPTION_LIST],ecx //撤销栈中的 seh 节点

Push TRUE //表示回到内核后需要继续检测执行用户 apc 队列中的 apc 函数
Push edi //传入原栈帧的 CONTEXT 结构的地址给这个函数,以做恢复工作
Call NtContinue //调用这个函数重新进入内核(注意这个函数正常情况下是不会返回到下面的)

----------------------------------华丽的分割线-------------------------------------------
Mov esi,eax
Push esi
Call RtlRaiseStatus //若 ZwContinue 返回了,那一定是内部出现了异常
Jmp StatusRaiseApc
Ret 16
}

如上每当要执行一个用户空间 apc 时,都会提前偏离原来的路线返回用户空间的这个函数处去执行用户的 apc。
在执行这个函数前,会先构造一个 seh 节点,也即相当于把这个函数的调用放在 try 块中保护。
这个函数内部会调用IntCallUserApc,执行完真正的用户 apc 函数后,调用ZwContinue重返内核。

IntCallUserApc

1
2
3
4
5
6
//用户空间的总 apc 函数 
Void CALLBACK IntCallUserApc(void* RealApcFunc, void* SysArg1,void* SysArg2)
{
(*RealApcFunc)(SysArg1);//也即调用 RealApcFunc(void* context)

}

ZwContinue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
NTSTATUS NtContinue(CONTEXT* Context, //原真正的 TraFrame  
BOOL TestAlert //指示是否继续执行用户 apc 队列中的 apc 函数
)
{
Push ebp //此时 ebp=本系统服务自身的 TrapFrame 地址
Mov ebx,PCR[KPCR_CURRENT_THREAD] //ebx=当前线程的 KTHREAD 对象地址
Mov edx,[ebp+KTRAP_FRAME_EDX] //注意 TrapFrame 中的这个 edx 字段不是用来保存 edx 的
Mov [ebx+KTHREAD_TRAP_FRAME],edx //将当前的 TrapFrame 改为上一个 TrapFrame 的地址
Mov ebp,esp
Mob eax,[ebp] //eax=本系统服务自身的 TrapFrame 地址
Mov ecx,[ebp+8] /本函数的第一个参数,即 Context
Push eax
Push NULL
Push ecx
Call KiContinue //call KiContinue(Context*,NULL,TrapFrame*)
Or eax,eax
Jnz error
Cmp dword ptr[ebp+12],0 //检查 TestAlert 参数的值
Je DontTest
Mov al,[ebx+KTHREAD_PREVIOUS_MODE]
Push eax
Call KeTestAlertThread //检测用户 apc 队列是否为空
DontTest:
Pop ebp
Mov esp,ebp
Jmp KiServiceExit2 //返回用户空间(返回前,又会去扫描执行 apc 队列中的下一个用户 apc)
}

NTSTATUS KiContinue(IN PCONTEXT Context,//原来的断点现场
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame) //NtContinue自身的TrapFrame地址
{
NTSTATUS Status = STATUS_SUCCESS;
KIRQL OldIrql = APC_LEVEL;
KPROCESSOR_MODE PreviousMode = KeGetPreviousMode(); if (KeGetCurrentIrql() < APC_LEVEL)
KeRaiseIrql(APC_LEVEL, &OldIrql);
_SEH2_TRY
{
if (PreviousMode != KernelMode)
KiContinuePreviousModeUser(Context,ExceptionFrame,TrapFrame);//恢复成原TrapFrame
else
{
KeContextToTrapFrame(Context,ExceptionFrame,TrapFrame,Context->ContextFlags,
KernelMode); //恢复成原TrapFrame
}
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
if (OldIrql < APC_LEVEL)
KeLowerIrql(OldIrql);
return Status;
}


VOID KiContinuePreviousModeUser(IN PCONTEXT Context,//原来的断点现场
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame)//NtContinue自身的TrapFrame地址
{
CONTEXT LocalContext;
ProbeForRead(Context, sizeof(CONTEXT), sizeof(ULONG));
RtlCopyMemory(&LocalContext, Context, sizeof(CONTEXT)); Context = &LocalContext;
//看到没,将原Context中的成员填写到NtContinue系统服务的TrapFrame帧中(也即修改成原来的 TrapFrame)
KeContextToTrapFrame(&LocalContext,ExceptionFrame,TrapFrame, LocalContext.ContextFlags,UserMode);
}

上面的函数,就把NtContinueTrapFrame强制还原成原来的TrapFrame,以好正式返回到用 户空间的真正断点处
不过在返回用户空间前,又要去扫描用户 apc 队列,若仍有用户 apc 函数,就先执行掉内核 apc 队列中的所有 apc 函数
然后又偏离原来的返回路线,提前返回到用户空间的KiUserApcDispatcher函数去执行用户 apc,这是一个不断循环的过程。
可见NtContinue这个函数不仅含有继续回到原真正用户空间断点处的意思,还含有继续执行用户 apc 队列中下一个 apc 函数的意思

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BOOLEAN  KeTestAlertThread(IN KPROCESSOR_MODE AlertMode) 
{
PKTHREAD Thread = KeGetCurrentThread();
KiAcquireApcLock(Thread, &ApcLock);
OldState = Thread->Alerted[AlertMode];
if (OldState)
Thread->Alerted[AlertMode] = FALSE;

else if ((AlertMode != KernelMode) && (!IsListEmpty(&Thread->ApcState.ApcListHead[UserMode])))
{
Thread->ApcState.UserApcPending = TRUE;//关键。又标记为不空,从而又去执行用户apc
}

KiReleaseApcLock(&ApcLock);
return OldState;
}

上面这个函数的关键工作是检测到用户 apc 队列不为空,就又将UserApcPending标志置于TRUE。




内核态APC的执行时机

前面我们看到的是用户 apc 队列的执行机制与时机,那是用户 apc 唯一的执行时机。
内核 apc 队列中的 apc 执行时机是不相同的,而且有很多执行时机。

内核 apc 的执行时机主要有

  • 1、 每次返回用户空间前,每执行一个用户 apc 前,就会扫描执行整个内核 apc 队列
  • 2、 每当调用 KeLowerIrql,从APC_LEVEL以上(不包括 APC_LEVEL) 降到APC_LEVEL以下(不包括APC_ LEVEL)前,中途会检查是否有阻塞的 apc 中断请求,若有就扫描执行内核 apc 队列
  • 3、 每当线程重新得到调度,开始运行前,会扫描执行内核 apc 队列或者发出 apc 中断请求内核

apc 的执行时机:【调度、返、降】apc

KeLowerIrql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
VOID FASTCALL KfLowerIrql(IN KIRQL OldIrql) 
{
ULONG EFlags;
ULONG PendingIrql, PendingIrqlMask;
PKPCR Pcr = KeGetPcr();
PIC_MASK Mask;
EFlags = readeflags();//保存原eflags
_disable();//关中断
Pcr->Irql = OldIrql;//降到目标irql
//检测是否有高于目标irql的阻塞中的软中断
PendingIrqlMask = Pcr->IRR & FindHigherIrqlMask[OldIrql];
if (PendingIrqlMask)//若有
{
BitScanReverse(&PendingIrql, PendingIrqlMask);//找到最高级别的软中断
if (PendingIrql > DISPATCH_LEVEL)
{
Mask.Both = Pcr->IDR;
outbyte(PIC1_DATA_PORT, Mask.Master);
outbyte(PIC2_DATA_PORT, Mask.Slave);
Pcr->IRR ^= (1 << PendingIrql);
}

SWInterruptHandlerTable[PendingIrql]();//处理阻塞的软中断(即扫描执行队列中的函数)
}
writeeflags(EFlags);//恢复原eflags
}

这个函数在从当前 irql 降到目标 irql 时,会按 irql 高低顺序执行各个软中断的 isr。 软中断是用来模拟硬件中断的一种中断。

1
2
3
4
#define PASSIVE_LEVEL          0 
#define APC_LEVEL 1
#define DISPATCH_LEVEL 2
#define CMCI_LEVEL 5

比如,当调用KfLowerIrql要将 cpu 的 irql 从CMCI_LEVEL降低到PASSIVE_LEVEL

  • 这个函数中途会先看看当前 cpu 是否收到了CMCI_LEVEL级的软中断,若有就调用那个软中断的 isr 处理之。
  • 然后再检查是否收到有DISPATCH_LEVEL级的软中断,若有调用那个软中断的 isr 处理之
  • 然后检查是否有 APC 中断,若有同样处理之。
  • 最后降到目标 irql,即PASSIVE_LEVEL
    换句话说在 irql 的降低过程中会一路检查、处理中途的软中断。
    Cpu 数据结构中有一个IRR字段,即表示当前 cpu 累积收到了哪些级别的软中断。

HalRequestSoftwareInterrupt

下面的函数可用于模拟硬件,向 cpu 发出任意 irql 级别的软中断,请求 cpu 处理执行那种中断。
那么什么时候,系统会调用这个函数,向 cpu 发出 apc 中断呢?

典型的情形 1:
在切换线程时,若将线程的WaitIrql置为APC_LEVEL,将导致KiSwapContextInternal函数内部在重新切回来后,立即自动发出一个 apc 中断,以在下次降低 irql 到PASSIVE_LEVEL时处理执行队列中那些阻塞的 apc。
反之若将线程的WaitIrql置为PASSIVE_LEVEL,将导致KiSwapContextInternal函数内部在重新切回来后,不会发出 apc 中断,然后系统会自行显式调用KiDeliverApc给予扫描执行

典型情形 2:
在给自身线程发送一个内核 apc 时,在 apc 进队的同时,会发出 apc 中断,以请求 cpu 在下次降低 irql 时,扫描执行 apc。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
VOID FASTCALL HalRequestSoftwareInterrupt(IN KIRQL Irql)//Irql一般是APC_LEVEL/DPC_LEVEL 
{
ULONG EFlags;
PKPCR Pcr = KeGetPcr();
KIRQL PendingIrql;
EFlags = readeflags();//保存老的eflags寄存器
_disable();//关中断
Pcr->IRR |= (1 << Irql);//关键。标志向cpu发出了一个对应irql级的软中断
PendingIrql = SWInterruptLookUpTable[Pcr->IRR & 3];//IRR后两位表示是否有阻塞的apc中断

//若有阻塞的apc中断,并且当前irql是PASSIVE_LEVEL,立即执行apc
//也即在PASSIVE_LEVEL级时发出 任意软中断后,会立即检查执行现有的apc中断。
if (PendingIrql > Pcr->Irql)
SWInterruptHandlerTable[PendingIrql]();//调用执行apc中断的isr,处理apc中断
writeeflags(EFlags);//恢复原eflags寄存器
}

HalpApcInterruptHandler

Apc 是一种软中断,既然是中断,他也有类似的 isr, Apc 中断的 isr 最终进入HalpApcInterruptHandler

1
2
3
4
5
6
7
8
9
10
VOID FASTCALL HalpApcInterruptHandler(IN PKTRAP_FRAME TrapFrame) 
{
//模拟硬件中断压入保存的寄存器
TrapFrame->EFlags = readeflags();
TrapFrame->SegCs = KGDT_R0_CODE;
TrapFrame->Eip = TrapFrame->Eax;
KiEnterInterruptTrap(TrapFrame);//构造Trap现场帧
扫描执行当前线程的内核apc队列,略…
KiEoiHelper(TrapFrame);
}




线程同步

一个线程可以等待一个对象或多个对象而进入等待状态(也叫睡眠状态),另一个线程可以触发那个等待对象,唤醒在那个对象上等待的所有线程。
一个线程可以等待一个对象或多个对象,而一个对象也可以同时被 N 个线程等待。
这样线程与等待对象之间是多对多的关系。
他们之间的等待关系由一个队列和一个等待块来控制,等待块就是线程与等待目标对象之间的纽带。

WaitForSingleObject可以等待那些可等待对象,哪些对象是可等待的呢?

进程、线程、作业、文件对象、IO 完成端口、可等待定时器、互斥、事件、信号量等,这些都是可等待对象,可用于WaitForSingleObject 等函数。

直接间接等待对象

可等待对象又分为可直接等待对象可间接等待对象
互斥、事件、信号量、进程、线程这些对象由于内部结构中的自第一个字段是DISPATCHER_HEADER结构(可以看成是继承了DISPATCHER_HEADER),因此是可直接等待的。
而文件对象不带这个结构,但文件对象内部有一个事件对象,因此文件对象是可间接等待对象

WaitForSingleObject

WaitForSingleObject内部最终调用下面的系统服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
NTSTATUS NtWaitForSingleObject(IN HANDLE ObjectHandle,//直接或间接可等待对象的句柄 
IN BOOLEAN Alertable,//表示本次等待操作是否可被吵醒(即被强制唤醒)
IN PLARGE_INTEGER TimeOut OPTIONAL)//超时
{
PVOID Object, WaitableObject;
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
LARGE_INTEGER SafeTimeOut;
NTSTATUS Status;
if ((TimeOut) && (PreviousMode != KernelMode))
{
_SEH2_TRY
{
SafeTimeOut = ProbeForReadLargeInteger(TimeOut);
TimeOut = &SafeTimeOut;
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}

Status = ObReferenceObjectByHandle(ObjectHandle,SYNCHRONIZE,NULL,PreviousMode, &Object,NULL);
if (NT_SUCCESS(Status))
{
//得到那个对象的`可直接等待对象`DefaultObject
WaitableObject = OBJECT_TO_OBJECT_HEADER(Object)->Type->DefaultObject;
if (IsPointerOffset(WaitableObject))//if DefaultObject是个偏移,不是指针
{
//加上偏移值,获得内部的`可直接等待对象`
WaitableObject = (PVOID)((ULONG_PTR)Object + (ULONG_PTR)WaitableObject);
}
_SEH2_TRY
{
Status = KeWaitForSingleObject(WaitableObject,//这个函数只能等待`直接等待对象`
UserRequest,PreviousMode,Alertable,TimeOut);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
ObDereferenceObject(Object);
}
return Status;
}

#define IsPointerOffset(Ptr) ((LONG)(Ptr) >= 0)

每个对象的对象类型都有一个默认的可直接等待对象,要么直接指向对象,要么是个偏移值。
如果是个偏移值,那么DefaultObject值的最高位为 0,否则为 1。

NtWaitForSingleObject可以等待直接的、间接的可等待对象,然而KeWaitForSingleObject只能等待真正的可直接等待对象,所以必须将间接可等待对象转换为直接可等待对象,而每个对象的可直接等待对象记录在其对象类型的DefaultObject字段中。

KeWaitForSingleObject

下面这个函数是重点。这个函数很不好理解,我也是认真看了好几遍才有所明白,这个函数本身逻辑量比较大,函数也较长。
重要的是对唤醒原因的理解。(把WaitStatus理解为唤醒原因就好了)

注意下面的函数只能在DISPATCH_LEVEL以下调用,否则蓝屏。(除非Timeout!=NULL && *Timeout==0)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
NTSTATUS KeWaitForSingleObject(IN PVOID Object,//要等待的`可直接等待对象` 
IN KWAIT_REASON WaitReason,//线程上次被切出原因
IN KPROCESSOR_MODE WaitMode,//表示这是来自用户模式/内核模式的等待请求
IN BOOLEAN Alertable,//表示本次等待操作是否可以被强制唤醒
IN PLARGE_INTEGER Timeout OPTIONAL)//超时值
{
PKTHREAD Thread = KeGetCurrentThread();
PKMUTANT CurrentObject = (PKMUTANT)Object;//其实意思是(DISPATCHER_HEADER*)Object
PKWAIT_BLOCK WaitBlock = &Thread->WaitBlock[0];

//内置的第4个等待块固定用于定时器(TIMER_WAIT_BLOCK是索引3)
PKWAIT_BLOCK TimerBlock = &Thread->WaitBlock[TIMER_WAIT_BLOCK];
PKTIMER Timer = &Thread->Timer;//复用这个超时定时器
NTSTATUS WaitStatus;//其实表示‘上次唤醒原因’
BOOLEAN Swappable;//内核栈是否可换到外存
LARGE_INTEGER DueTime, NewDueTime, InterruptTime;
PLARGE_INTEGER OriginalDueTime = Timeout; ULONG Hand = 0;

//首次等待时需从WaitStart处开始,加锁,提升irql至DISPATCH_LEVEL
if (!Thread->WaitNext) goto WaitStart;

Thread->WaitNext = FALSE;//复位
KxSingleThreadWait();//这是一个宏,主要用来构造当前线程的等待块链表


for (;;)//首次循环、以后每次临时唤醒切回来时的入口
{
Thread->Preempted = FALSE;//表示因等待而主动放弃的cpu,不是被抢占

//if进入等待态前的原irql是APC_LEVEL,每次切回来时会自动在KiSwapContextInternal执行掉所有内核APC。
//if进入等待态前的原irql是PASSIVE_LEVEL,则用下面的语句手动执行掉所有内核APC。
//总之确保每次切回来时,总是会先执行掉所有内核APC
if ((Thread->ApcState.KernelApcPending) && !(Thread->SpecialApcDisable) && (Thread->WaitIrql == PASSIVE_LEVEL))
{
KiReleaseDispatcherLock(Thread->WaitIrql);//执行掉所有Pending中的内核APC
}

//每轮循环中(即每次切回来时),测试是否条件成熟,可以退出睡眠。可以退出的3种条件为:
// 1、 所等待的对象有信号了
// 2、 可被强制唤醒了
// 3、 超时了
Else
{
//先检测所等待的对象是否有信号了,不过互斥对象需要特殊判断。
if (CurrentObject->Header.Type == MutantObject)
{

//if等待的互斥对象有信号了,或者当前线程本身就是该互斥对象的拥有者(一个线程 可以反复多次等待同一个互斥对象的)
if ((CurrentObject->Header.SignalState > 0) || (Thread == CurrentObject->OwnerThread))
{
if (CurrentObject->Header.SignalState != (LONG)MINLONG)
{
KiSatisfyMutantWait(CurrentObject, Thread);
WaitStatus = Thread->WaitStatus;//唤醒原因
goto DontWait;//退出函数
}
Else 抛出异常…
}
}
//普通的等待对象只要DISPATCHER_HEADER头部中的SignalState > 0就表示有信号了
else if (CurrentObject->Header.SignalState > 0)
{
KiSatisfyNonMutantWait(CurrentObject);//递减信号状态量计数
WaitStatus = STATUS_WAIT_0;//唤醒原因为‘真唤醒’
goto DontWait; //退出函数
}


//若所等等待的对象没有信号,就检查当前状态是否可被强制唤醒
WaitStatus = KiCheckAlertability(Thread, Alertable, WaitMode);
if (WaitStatus != STATUS_WAIT_0)//if 可以强制唤醒了,就break退出循环,退出函数
break;

//如果无法强制唤醒,就再检测是否已经等待超时了
if (Timeout)//if用户提供了一个超时值,就检测超时情况
{
InterruptTime.QuadPart = KeQueryInterruptTime();
if ((ULONGLONG)InterruptTime.QuadPart >= Timer->DueTime.QuadPart)
{
WaitStatus = STATUS_TIMEOUT;//唤醒原因为‘超时’
goto DontWait; //退出函数
}
Timer->Header.Inserted = TRUE;
}

//如果所有条件都不满足,就需要进入睡眠(首轮检测)或继续进入睡眠(每次临时唤醒切 回来时的检查)
//将等待块挂入目标等待对象内部的等待块链表中
InsertTailList(&CurrentObject->Header.WaitListHead,&WaitBlock->WaitListEntry);

if (Thread->Queue) KiActivateWaiterQueue(Thread->Queue);

Thread->State = Waiting;//将线程标记为等待状态(即睡眠状态)
KiAddThreadToWaitList(Thread, Swappable);//加入到当前cpu的等待线程链表中
if (Timeout)
KxInsertTimer(Timer, Hand);
else
KiReleaseDispatcherLockFromDpcLevel();

KiSetThreadSwapBusy(Thread);//标记线程正在进行切换
WaitStatus = KiSwapThread(Thread, KeGetCurrentPrcb());//关键。切出线程,进入睡眠

-------------------------------华丽的分割线--------------------------------------
-------------------------------华丽的分割线--------------------------------------
-------------------------------华丽的分割线--------------------------------------

//上面函数的返回值表示线程唤醒切回来的原因(大体分为三种)【临时、强制、真唤醒】
//1、 临时唤醒。指被其他线程发来的内核APC临时唤醒,要求执行紧急APC任务
//2、 强制唤醒。指被其他线程发来的强制唤醒要求唤醒/被发来的用户APC强制唤醒
//3、 真唤醒。指所等待的对象真的有信号了而被唤醒
if (WaitStatus != STATUS_KERNEL_APC)//if唤醒原因不是临时唤醒,就退出函数了
return WaitStatus;

//下面的代码,是当被临时唤醒回来后,需要继续执行的。临时唤醒回来后,需要重新计算 剩余超时值、执行内核APC,继续进入下轮循环,测试是否可以退出睡眠。
if (Timeout)//重新计算剩余超时值
Timeout = KiRecalculateDueTime(OriginalDueTime,&DueTime,&NewDueTime);
}

WaitStart://首次开始进入等待时,从这儿开始,提升irql
Thread->WaitIrql = KeRaiseIrqlToSynchLevel();
KxSingleThreadWait();//构造好线程的等待块链表
KiAcquireDispatcherLockAtDpcLevel();
}
KiReleaseDispatcherLock(Thread->WaitIrql);//break方式退出到这儿
return WaitStatus;


DontWait:
KiReleaseDispatcherLockFromDpcLevel();
KiAdjustQuantumThread(Thread);//调整时间片
return WaitStatus;//返回本次睡眠的唤醒原因
}

如上这个函数是一个循环测试,概念上有点类似忙式等待。

首先第一轮循环,从WaitStart处开始, 提升 irql(目的是防止被切换),然后回到 for 循环开头处,测试条件。
如果所要等待的对象本来就有信号,那么第一轮循环时,就不用进入睡眠了,直接退出函数。
否则就切出线程,让出cpu,构造好线程的等待块链表,并将等待块(此处意指线程自身)挂入目标等待对象的等待块队列中(可理解为等待者线程队列)。
然后当以后条件成熟,唤醒回来时,检测唤醒原因。若是被临时唤醒的,就继续进入下轮循环测试等待条件,否则即可退出函数,退出睡眠了。

_KWAIT_BLOCK

下面的等待块结构是线程的等待机制核心,它即用来挂入线程的等待块链表,也用来挂入等待对象的队列 中。
当挂入前者时,等待块就可理解为一个等待对象,当挂入后者时就可理解为一个等待者线程。

1
2
3
4
5
6
7
8
9
10
11
typedef struct _KWAIT_BLOCK 
{
LIST_ENTRY WaitListEntry;//用来挂入目标对象的等待者线程队列
struct _KTHREAD *Thread;//所属线程
PVOID Object;//要等待的目标对象

struct _KWAIT_BLOCK *NextWaitBlock;//下一个等待块(用来挂入线程的等待块链表)
USHORT WaitKey;//本等待块是所属线程的第几个等待对象
UCHAR WaitType;//WaitAll/WaitAny
volatile UCHAR BlockState;
} KWAIT_BLOCK, *PKWAIT_BLOCK, *PRKWAIT_BLOCK;

上面的函数中牵涉到一个重要宏和几个子函数,我们看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#define KxSingleThreadWait()                                                                                    
Thread->WaitBlockList = WaitBlock;//等待块链表
WaitBlock->WaitKey = 0;//即等待块的索引
WaitBlock->Object = Object;//要等待的目标对象
WaitBlock->WaitType = WaitAny; //只等待一个对象的话就固定是WaitAny
Thread->WaitStatus = 0; //复位唤醒原因
if (Timeout) //if给定了超时值
{
KxSetTimerForThreadWait(Timer, *Timeout, &Hand);//设置好定时器对象
DueTime.QuadPart = Timer->DueTime.QuadPart;
WaitBlock->NextWaitBlock = TimerBlock; //定时器对象等待块也挂入链表中
TimerBlock->NextWaitBlock = WaitBlock;//单循环链表
Timer->Header.WaitListHead.Flink = &TimerBlock->WaitListEntry;
Timer->Header.WaitListHead.Blink = &TimerBlock->WaitListEntry;
}
else
{
WaitBlock->NextWaitBlock = WaitBlock; //单循环链表
}
Thread->Alertable = Alertable;//线程睡眠模式,是否可被提醒(即强制唤醒)
Thread->WaitMode = WaitMode;//来自用户还是内核模式的等待请求
Thread->WaitReason = WaitReason; //上次线程切换原因
Thread->WaitListEntry.Flink = NULL;
Swappable = KiCheckThreadStackSwap(Thread, WaitMode);//检测本线程的内核栈是否可以换到外存
Thread->WaitTime = KeTickCount.LowPart;//记录上次切出时间

如上这个宏的主要功能就是用来构造好该线程的等待块链表,以及一些其它乱七八糟的工作,上面
的函数KiCheckThreadStackSwap用来检测本线程的内核栈是否可以置换到外存

1
2
3
4
5
6
7
8
9
10
11
12
BOOLEAN  KiCheckThreadStackSwap(IN PKTHREAD Thread,IN KPROCESSOR_MODE WaitMode) 
{
if ((WaitMode == UserMode) && (Thread->EnableStackSwap) &&
(Thread->Priority >= (LOW_REALTIME_PRIORITY + 9)))
{
return TRUE;
}
else
{
return FALSE;
}
}

如上,超级实时类的线程在处理来自用户模式的等待请求时,内核栈可以置换到外存。

KiCheckAlertability

下面的函数用来在线程被临时唤醒后,测试线程的状态是否可被强制唤醒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
NTSTATUS KiCheckAlertability(IN PKTHREAD Thread, 
IN BOOLEAN Alertable,//本次睡眠操作是否支持强制唤醒
IN KPROCESSOR_MODE WaitMode)//来自哪个模式的等待请求
{
if (Alertable)//if本次睡眠操作支持强制唤醒(即可被强制唤醒)
{
if (Thread->Alerted[WaitMode])//if 真收到了来自那个模式的强制唤醒要求
{
Thread->Alerted[WaitMode] = FALSE;//复位
return STATUS_ALERTED;//唤醒原因设为强制唤醒
}
//若没收到其他线程发来对应模式的强制唤醒要求,但收到了用户APC
else if ((WaitMode != KernelMode) &&
(!IsListEmpty(&Thread->ApcState.ApcListHead[UserMode])))
{
Thread->ApcState.UserApcPending = TRUE;
return STATUS_USER_APC;//标记为被用户APC给强制唤醒了
}
//只要收到了内核模式的强制唤醒要求,就可被强制唤醒(不必模式匹配)
else if (Thread->Alerted[KernelMode])
{
Thread->Alerted[KernelMode] = FALSE;
return STATUS_ALERTED; //唤醒原因设为强制唤醒
}
}
//即使本次睡眠不支持强制唤醒,但其他线程发来的用户APC仍可强制唤醒本线程
else if ((WaitMode != KernelMode) && (Thread->ApcState.UserApcPending))
{
return STATUS_USER_APC; //标记为被用户APC给强制唤醒了
}
return STATUS_WAIT_0;
}

如上当线程的本次睡眠支持强制唤醒的情况下,可被对应模式或内核模式的强制唤醒要求给唤醒,即使不支持强制唤醒,也会被其他线程发来的用户 APC 给强制搞醒。

关于模式匹配记住下面一点。来自内核模式的强制唤醒要求可以强制唤醒来自内核模式、用户模式的等待 请求
而来自用户模式的强制唤醒要求只能强制唤醒来自用户模式的等待请求。

这就好比:内核空间的程序可以访问内核空间的代码和数据,而用户空间的程序只能访问用户空间的代码和数据。这样记就容易了。


线程等待唤醒

线程的睡眠其工作之一就是线程切换。线程一被切出了,要么进入就绪态,要么进入等待态。
因等待对象而引起的线程切换,将使线程处于等待态。等待态与就绪态的本质区别就是处于就绪态的线程直接挂入就 绪队列,随时等候被调度运行,处于等待态的线程,则要将自己挂入 cpu 的等待队列,挂入各个目标等待 对象的等待线程队列,然后一直等待别的线程触发等待对象,唤醒自己,重新进入就绪态或运行态。

1
2
3
4
5
#define KiAddThreadToWaitList(Thread, Swappable)                             
{
if (Swappable) //为什么要满足这个条件,我也搞不清
InsertTailList(&KeGetCurrentPrcb()->WaitListHead, &Thread->WaitListEntry);
}

当一个线程处于睡眠态时,最典型的唤醒原因就是所等待的对象有了信号。

KiWaitTest

当一个等待对象有了信号时,系统(指别的线程)会调用下面的函数尝试唤醒在该等待对象上等待的所有线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
VOID FASTCALL KiWaitTest(IN PVOID ObjectPointer,//目标等待对象 
IN KPRIORITY Increment)//唤醒线程后的优先级增量(以便尽快得到调度运行)
{
PLIST_ENTRY WaitEntry, WaitList;
PKWAIT_BLOCK WaitBlock;
PKTHREAD WaitThread;
PKMUTANT FirstObject = ObjectPointer;
NTSTATUS WaitStatus;
WaitList = &FirstObject->Header.WaitListHead; WaitEntry = WaitList->Flink;
//遍历等待者线程队列,尝试唤醒所有线程,直到信号状态量分配完毕
while ((FirstObject->Header.SignalState > 0) && (WaitEntry != WaitList))
{
WaitBlock = CONTAINING_RECORD(WaitEntry, KWAIT_BLOCK, WaitListEntry);
WaitThread = WaitBlock->Thread;
WaitStatus = STATUS_KERNEL_APC;//模拟给那个等待者线程发送内核apc而临时唤醒它
if (WaitBlock->WaitType == WaitAny)//WaitAnt类型的话,肯定满足分配了
{
WaitStatus = (NTSTATUS)WaitBlock->WaitKey;//唤醒原因改为‘真唤醒’,此处即索引
KiSatisfyObjectWait(FirstObject, WaitThread);//分配资源,递减对象的信号状态量计数
}
KiUnwaitThread(WaitThread, WaitStatus, Increment);//关键函数
WaitEntry = WaitList->Flink;//下一个线程
}
}

如上这个函数会在每一个等待对象变成有信号时,尝试唤醒所有在该对象上等待的所有线程。
注意,若队列中的某个等待块类型是WaitAny时,那么那个等待者线程必然真的满足了等待条件,所以需要将唤 醒原因改为真唤醒类型。
反之若那个线程的等待类型是WaitAll,也即它还要等待其他对象,那么就模拟给它发送内核 apc 的方式(其实没发送),临时唤醒它,进入下轮循环,继续测试它所等待的其他对象,这一点务必要注意。

KiSatisfyObjectWait(分配唤醒信号)

下面的宏用于当满足分配条件时,分配信号状态量给指定线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#define KiSatisfyObjectWait(Object, Thread)                                  
{

if ((Object)->Header.Type == MutantObject) //互斥对象要特殊处理
{
(Object)->Header.SignalState--;//递减信号状态量计数(此处为拥有计数)
if ((Object)->Header.SignalState == 0)//if拥有计数==0
{
(Object)->OwnerThread = Thread;
Thread->KernelApcDisable = Thread->KernelApcDisable - (Object)->ApcDisable;
if ((Object)->Abandoned)//如果该互斥对象是因为原拥有者线程意外终止了而让出的
{
(Object)->Abandoned = FALSE;//复位
Thread->WaitStatus = STATUS_ABANDONED;//唤醒原因
}
//插入本线程获得的所有互斥对象链表中
InsertHeadList(Thread->MutantListHead.Blink, &(Object)->MutantListEntry);
}
}
//如果是`自动复位`型事件,触发对象后,又立马复位它的状态
else if (((Object)->Header.Type & TIMER_OR_EVENT_TYPE) == EventSynchronizationObject)
(Object)->Header.SignalState = 0;
else if ((Object)->Header.Type == SemaphoreObject)
(Object)->Header.SignalState--;//递减信号状态量计数(此处为真的信号量计数)
}

熟悉 Win32 多线程编程的朋友想必不用解释这段代码了吧。
只是要提醒一下,不管是什么同步对象,其内部的SignalState表示信号状态量计数,当该值<=0 时表示无信号,>0 时表示有信号。

其实这个字段本用于信号量的,不过所有的同步对象都可以看做是SignalState只有 0 和 1 两种情况的特殊信号量

KiUnwaitThread(唤醒指定线程)

下面的这个函数可以说是线程的等待唤醒机制的核心,其功能用来唤醒指定线程

1
2
3
4
5
6
7
8
9
VOID FASTCALL KiUnwaitThread(IN PKTHREAD Thread,//目标线程 
IN LONG_PTR WaitStatus,//唤醒原因
IN KPRIORITY Increment)//唤醒后的优先级增量,以便唤醒后尽快得到调度运行
{
KiUnlinkThread(Thread, WaitStatus);//将所有等待块脱链
Thread->AdjustIncrement = (SCHAR)Increment;//上次优先级调整增量
Thread->AdjustReason = AdjustUnwait;//调整原因
KiReadyThread(Thread);//‘就绪化’指定线程(也即转入就绪队列或者置为抢占者线程)
}

任意一个线程都可以调用这个函数唤醒目标线程。唤醒原因大体分为:临时唤醒强制唤醒真唤醒
每当插入一个 apc 的时候,将调用下面的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
NTSTATUS KiInsertQueueApc(…) 
{

…….
if (Thread != KeGetCurrentThread())
{
if (ApcMode == KernelMode)//若要给其他线程插入一个内核apc
{
Thread->ApcState.KernelApcPending = TRUE;
if (Thread->State == Running)
{
RequestInterrupt = TRUE;
}
else if ((Thread->State == Waiting) && (Thread->WaitIrql == PASSIVE_LEVEL) &&
!(Thread->SpecialApcDisable) && (!(Apc->NormalRoutine) ||
(!(Thread->KernelApcDisable)&&!(Thread->ApcState.KernelApcInProgress))))
{
Status = STATUS_KERNEL_APC;
KiUnwaitThread(Thread, Status, PriorityBoost);//临时唤醒目标线程
}
…….
}
else if ((Thread->State == Waiting) && (Thread->WaitMode == UserMode) &&
((Thread->Alertable) || (Thread->ApcState.UserApcPending)))
{
Thread->ApcState.UserApcPending = TRUE;
Status = STATUS_USER_APC;
KiUnwaitThread(Thread, Status, PriorityBoost);//插入用户APC,强制唤醒目标线程
}
……
}

看到没,插入内核 apc 的时候可能会临时唤醒目标线程,插入用户 apc 的时候可能会强制唤醒目标线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
VOID FASTCALL KiUnlinkThread(IN PKTHREAD Thread, IN NTSTATUS WaitStatus) 
{
PKWAIT_BLOCK WaitBlock;
PKTIMER Timer;

//关键。唤醒原因记录在线程结构的这个字段中,KiSwapContext返回的就是这个字段值
Thread->WaitStatus |= WaitStatus;
WaitBlock = Thread->WaitBlockList;

//让各个等待块脱离各自等待对象的队列
do
{
RemoveEntryList(&WaitBlock->WaitListEntry);
WaitBlock = WaitBlock->NextWaitBlock;
} while (WaitBlock != Thread->WaitBlockList);

//脱离cpu的等待线程队列
if (Thread->WaitListEntry.Flink)
RemoveEntryList(&Thread->WaitListEntry);

Timer = &Thread->Timer;
if (Timer->Header.Inserted) KxRemoveTreeTimer(Timer);
if (Thread->Queue) Thread->Queue->CurrentCount++;
}

注意上面的这个函数只是脱离各个等待对象的等待块队列。
线程自己的等待块队列还在。

现在看一下KeWaitForSingleObject那个函数内部调用的KiSwapThread。注意是KiSwapThread不是KiSwapContext

KiSwapThread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
LONG FASTCALL KiSwapThread(IN PKTHREAD CurrentThread,IN PKPRCB Prcb) 
{
BOOLEAN ApcState = FALSE;
KIRQL WaitIrql;//上次切出时的irql
LONG_PTR WaitStatus;//上次唤醒原因
PKTHREAD NextThread;
KiAcquirePrcbLock(Prcb);
NextThread = Prcb->NextThread;//当前的抢占者线程
if (NextThread)
{
Prcb->NextThread = NULL;
Prcb->CurrentThread = NextThread;
NextThread->State = Running;
}
else
{
NextThread = KiSelectReadyThread(0, Prcb);//调度处一个
if (NextThread)
{
Prcb->CurrentThread = NextThread;
NextThread->State = Running;
}
else
{
InterlockedOr((PLONG)&KiIdleSummary, Prcb->SetMember);
NextThread = Prcb->IdleThread;//使用空转线程
Prcb->CurrentThread = NextThread;
NextThread->State = Running;
}
}
KiReleasePrcbLock(Prcb);

WaitIrql = CurrentThread->WaitIrql;//记录上次切出时的irql
MiSyncForContextSwitch(NextThread);

ApcState = KiSwapContext(CurrentThread, NextThread);

----------------------------------------华丽的分割线---------------------------------------
----------------------------------------华丽的分割线---------------------------------------
----------------------------------------华丽的分割线---------------------------------------

if (ApcState)//切回来后,例行执行内核apc
{
KeLowerIrql(APC_LEVEL);
KiDeliverApc(KernelMode, NULL, NULL);
ASSERT(WaitIrql == PASSIVE_LEVEL);
}
KeLowerIrql(WaitIrql);
WaitStatus = CurrentThread->WaitStatus;//关键。返回该线程上次唤醒的原因
return WaitStatus;
}

相信到此为止,大家理解了线程如何等待单个对象的机制了吧?

KeWaitForMultipleObjects

一个线程可以同时等待对个对象我们看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172

NTSTATUS NTAPI KeWaitForMultipleObjects(IN ULONG Count,//数组元素个数
IN PVOID Object[],//等待对象数组
IN WAIT_TYPE WaitType,//WaitAll/AitAny
IN KWAIT_REASON WaitReason,//上次切换原因
IN KPROCESSOR_MODE WaitMode,//来自用户模式/内核模式的等待请求
IN BOOLEAN Alertable,//本次等待是否可提醒(指是否可被强制唤醒)
IN PLARGE_INTEGER Timeout OPTIONAL,//超时
OUT PKWAIT_BLOCK WaitBlockArray OPTIONAL)//等待块数组
{
PKMUTANT CurrentObject;
PKWAIT_BLOCK WaitBlock;
PKTHREAD Thread = KeGetCurrentThread();
PKWAIT_BLOCK TimerBlock = &Thread->WaitBlock[TIMER_WAIT_BLOCK];//定时器等待块是固定的
PKTIMER Timer = &Thread->Timer;
NTSTATUS WaitStatus = STATUS_SUCCESS;
BOOLEAN Swappable;

PLARGE_INTEGER OriginalDueTime = Timeout;
LARGE_INTEGER DueTime, NewDueTime, InterruptTime;
ULONG Index, Hand = 0;

if (!WaitBlockArray)//没提供就使用内置的等待块数组(3个等待块+一个定时器等待块)
WaitBlockArray = &Thread->WaitBlock[0];

if (!Thread->WaitNext) goto WaitStart;//首轮循环从WaitStart处开始
Thread->WaitNext = FALSE;
KxMultiThreadWait();//关键。与等待单个对象时使用的宏不相同
for (;;)
{
Thread->Preempted = FALSE;
if ((Thread->ApcState.KernelApcPending) && !(Thread->SpecialApcDisable) && (Thread->WaitIrql < APC_LEVEL))
{
KiReleaseDispatcherLock(Thread->WaitIrql);
}
else
{
Index = 0;
if (WaitType == WaitAny)
{
do
{
CurrentObject = (PKMUTANT)Object[Index];
if (CurrentObject->Header.Type == MutantObject)
{
if ((CurrentObject->Header.SignalState > 0) || (Thread == CurrentObject->OwnerThread))
{
if (CurrentObject->Header.SignalState != (LONG)MINLONG)
{
KiSatisfyMutantWait(CurrentObject, Thread);
WaitStatus = Thread->WaitStatus | Index;
goto DontWait;//只要满足一个就退出函数
}
else
{
KiReleaseDispatcherLock(Thread->WaitIrql);
ExRaiseStatus(STATUS_MUTANT_LIMIT_EXCEEDED);
}
}
}
else if (CurrentObject->Header.SignalState > 0)
{
KiSatisfyNonMutantWait(CurrentObject);
WaitStatus = Index;
goto DontWait; //只要满足一个就退出函数
}
Index++;
} while (Index < Count);
}
Else //WaitAll
{

do
{
CurrentObject = (PKMUTANT)Object[Index];
if (CurrentObject->Header.Type == MutantObject)
{
if ((Thread == CurrentObject->OwnerThread) &&
(CurrentObject->Header.SignalState == (LONG)MINLONG))
{
KiReleaseDispatcherLock(Thread->WaitIrql);
ExRaiseStatus(STATUS_MUTANT_LIMIT_EXCEEDED);
}
else if ((CurrentObject->Header.SignalState <= 0) &&
(Thread != CurrentObject->OwnerThread))
{
break;//只要任有一个对象不满足,就要继续进入等待状态
}
}
else if (CurrentObject->Header.SignalState <= 0)
{
break; //只要任有一个对象不满足,就要继续进入等待状态
}
Index++;
} while (Index < Count);


if (Index == Count)//if 所有对象都有信号了
{
WaitBlock = WaitBlockArray;
do
{
CurrentObject = (PKMUTANT)WaitBlock->Object;
KiSatisfyObjectWait(CurrentObject, Thread);
WaitBlock = WaitBlock->NextWaitBlock;
} while(WaitBlock != WaitBlockArray);

WaitStatus = Thread->WaitStatus;//唤醒原因为‘真唤醒’类型
goto DontWait;
}
}
WaitStatus = KiCheckAlertability(Thread, Alertable, WaitMode);

if (WaitStatus != STATUS_WAIT_0) break;

if (Timeout)
{
InterruptTime.QuadPart = KeQueryInterruptTime();
if ((ULONGLONG)InterruptTime.QuadPart >= Timer->DueTime.QuadPart)
{

WaitStatus = STATUS_TIMEOUT;
goto DontWait;
}
Timer->Header.Inserted = TRUE;
WaitBlock->NextWaitBlock = TimerBlock;
}
WaitBlock = WaitBlockArray;
do
{
CurrentObject = WaitBlock->Object;
InsertTailList(&CurrentObject->Header.WaitListHead,&WaitBlock->WaitListEntry);
WaitBlock = WaitBlock->NextWaitBlock;
} while (WaitBlock != WaitBlockArray);

if (Thread->Queue) KiActivateWaiterQueue(Thread->Queue);

Thread->State = Waiting;
KiAddThreadToWaitList(Thread, Swappable);
KiSetThreadSwapBusy(Thread);

if (Timeout)
KxInsertTimer(Timer, Hand);
else
KiReleaseDispatcherLockFromDpcLevel();
WaitStatus = KiSwapThread(Thread, KeGetCurrentPrcb());

----------------------------------------华丽的分割线---------------------------------------
----------------------------------------华丽的分割线---------------------------------------
----------------------------------------华丽的分割线---------------------------------------
//if 唤醒原因不是临时唤醒,直接退出整个函数,退出睡眠状态
if (WaitStatus != STATUS_KERNEL_APC) return WaitStatus;

//否则,若是临时唤醒回来的,则进入下一轮循环,继续去测试等待对象的信号情况
if (Timeout)
Timeout = KiRecalculateDueTime(OriginalDueTime,&DueTime,&NewDueTime);
}

WaitStart:
Thread->WaitIrql = KeRaiseIrqlToSynchLevel();
KxMultiThreadWait();
KiAcquireDispatcherLockAtDpcLevel();
}

KiReleaseDispatcherLock(Thread->WaitIrql);
return WaitStatus;

DontWait:
KiReleaseDispatcherLockFromDpcLevel();
KiAdjustQuantumThread(Thread);
return WaitStatus;
}

使用了以下宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#define KxMultiThreadWait()                                                                           
Thread->WaitBlockList = WaitBlockArray;
Index = 0;
//构造好本线程的等待块链表
do
{
WaitBlock = &WaitBlockArray[Index];
WaitBlock->Object = Object[Index];
WaitBlock->WaitKey = (USHORT)Index;//关键
WaitBlock->WaitType = WaitType;//所有等待块的等待类型都相同
WaitBlock->Thread = Thread;
WaitBlock->NextWaitBlock = &WaitBlockArray[Index + 1];
Index++;
} while (Index < Count);

WaitBlock->NextWaitBlock = WaitBlockArray; //单循环链表
Thread->WaitStatus = STATUS_WAIT_0;
if (Timeout)
{
TimerBlock->NextWaitBlock = WaitBlockArray;
KxSetTimerForThreadWait(Timer, *Timeout, &Hand);
DueTime.QuadPart = Timer->DueTime.QuadPart;
InitializeListHead(&Timer->Header.WaitListHead);
}
Thread->Alertable = Alertable;//是否可被强制唤醒
Thread->WaitMode = WaitMode;//来自用户模式/内核模式的等待请求
Thread->WaitReason = WaitReason;//上次被切原因
Thread->WaitListEntry.Flink = NULL;
Swappable = KiCheckThreadStackSwap(Thread, WaitMode);
Thread->WaitTime = KeTickCount.LowPart;//记录上次被切时间

这段代码我想不用多解释了吧。唯一需要注意的是使用了不同的宏;


同步对象

弄懂了线程的等待唤醒机制后,下面我们看各种具体等待对象(又叫同步对象)的原理
同步对象:(互斥、事件、信号量、自旋锁)

信号量

1
2
3
4
typedef struct _KSEMAPHORE { 
DISPATCHER_HEADER Header;//公共头部
LONG Limit;//信号量的最大信号个数
} KSEMAPHORE, *PKSEMAPHORE;

NtCreateSemaphore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
NTSTATUS NtCreateSemaphore(OUT PHANDLE SemaphoreHandle,//返回信号量对象的句柄 
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,//信号量的名称及其他属性
IN LONG InitialCount,//信号量的初始信号个数
IN LONG MaximumCount)//支持的最大信号个数
{
PKSEMAPHORE Semaphore;
HANDLE hSemaphore;
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
NTSTATUS Status;
if (PreviousMode != KernelMode)
{
_SEH2_TRY
{
ProbeForWriteHandle(SemaphoreHandle);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
if ((MaximumCount <= 0) || (InitialCount < 0) || (InitialCount > MaximumCount))
return STATUS_INVALID_PARAMETER;
//信号量也是一种内核对象
Status = ObCreateObject(PreviousMode,ExSemaphoreObjectType,ObjectAttributes,PreviousMode,
NULL,sizeof(KSEMAPHORE),0,0, (PVOID*)&Semaphore);
if (NT_SUCCESS(Status))
{
//初始化对象结构
KeInitializeSemaphore(Semaphore,InitialCount,MaximumCount);
//插入对象目录和句柄表
Status = ObInsertObject((PVOID)Semaphore,NULL,DesiredAccess,0,NULL,&hSemaphore);

if (NT_SUCCESS(Status))
{
_SEH2_TRY
{
*SemaphoreHandle = hSemaphore;
}
_SEH2_EXCEPT(ExSystemExceptionFilter())
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
}
}
return Status;
}

KeInitializeSemaphore

关键看下面的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
VOID KeInitializeSemaphore(IN PKSEMAPHORE Semaphore,IN LONG Count,IN LONG Limit) 
{
KeInitializeDispatcherHeader(&Semaphore->Header,SemaphoreObject,
sizeof(KSEMAPHORE) / sizeof(ULONG),Count);
Semaphore->Limit = Limit;
}

#define KeInitializeDispatcherHeader(Header, t, s, State) \
{ \
(Header)->Type = t; \
(Header)->Absolute = 0; \
(Header)->Size = s; \
(Header)->Inserted = 0; \
(Header)->SignalState = State;//初始信号个数 \
InitializeListHead(&((Header)->WaitListHead));//初始化等待线程队列 \
}

信号量对象可用于WaitFor系列函数中。
每得到一个信号量,SignalState就递减。 当SignalState减到 0 时,就需要等待其他线程释放信号量。

NtReleaseSemaphore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
NTSTATUS NtReleaseSemaphore(IN HANDLE SemaphoreHandle, 
IN LONG ReleaseCount,//一次可以释放多个信号量,这将一次唤醒多个线程
OUT PLONG PreviousCount OPTIONAL)//返回之前的信号个数
{
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
PKSEMAPHORE Semaphore;
NTSTATUS Status;
if ((PreviousCount) && (PreviousMode != KernelMode))
{
_SEH2_TRY
{
ProbeForWriteLong(PreviousCount);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
if (ReleaseCount <= 0)
return STATUS_INVALID_PARAMETER;

Status = ObReferenceObjectByHandle(SemaphoreHandle,SEMAPHORE_MODIFY_STATE,
ExSemaphoreObjectType,PreviousMode,
(PVOID*)&Semaphore,NULL);
if (NT_SUCCESS(Status))
{
_SEH2_TRY
{
//实质函数
LONG PrevCount = KeReleaseSemaphore(Semaphore,IO_NO_INCREMENT, ReleaseCount,FALSE);
if (PreviousCount) *PreviousCount = PrevCount;
}
_SEH2_EXCEPT(ExSystemExceptionFilter())
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
ObDereferenceObject(Semaphore);
}
return Status;
}


LONG KeReleaseSemaphore(IN PKSEMAPHORE Semaphore,
IN KPRIORITY Increment,//优先级增量
IN LONG Adjustment,// 一次可以释放多个信号量,这将一次唤醒多个线程
IN BOOLEAN Wait)
{
LONG InitialState, State;
KIRQL OldIrql;
PKTHREAD CurrentThread;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
OldIrql = KiAcquireDispatcherLock();

InitialState = Semaphore->Header.SignalState;
State = InitialState + Adjustment;//一次增加N个信号
if ((Semaphore->Limit < State) || Adjustment< 0))
{
KiReleaseDispatcherLock(OldIrql);
ExRaiseStatus(STATUS_SEMAPHORE_LIMIT_EXCEEDED);
}
Semaphore->Header.SignalState = State;

//没释放一次信号量,就会尝试唤醒在该信号量对象上等待的所有线程
if (!(InitialState) && !(IsListEmpty(&Semaphore->Header.WaitListHead)))
KiWaitTest(&Semaphore->Header, Increment);

if (Wait == FALSE)
KiReleaseDispatcherLock(OldIrql);
else
{
CurrentThread = KeGetCurrentThread();
CurrentThread->WaitNext = TRUE;
CurrentThread->WaitIrql = OldIrql;
}
return InitialState;
}

如上如此简单而已。只是需要注意信号量对象一有信号,就会调用KiWaitTest


互斥对象

互斥对象的原理:互斥对象有点特殊,他有一个拥有者线程,并且同一时刻最多只能让一个线程拥有

1
2
3
4
5
6
7
typedef struct _KMUTANT { 
DISPATCHER_HEADER Header;//公共头
LIST_ENTRY MutantListEntry;//用来挂入拥有者线程的互斥对象链表
struct _KTHREAD *RESTRICTED_POINTER OwnerThread;//关键。当前拥有者线程
BOOLEAN Abandoned;//是否被拥有者线程因意外终止而抛弃了
BOOL ApcDisable;//表示获得互斥体,进入临界区前,是否需要禁用内核APC(用于用户模式的Mutex)
} KMUTANT, *PKMUTANT, KMUTEX, *PKMUTEX;

KeInitializeMutant

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
VOID 
KeInitializeMutant(IN PKMUTANT Mutant,
IN BOOLEAN InitialOwner)//指当前线程是否初始为该互斥对象的拥有者
{
PKTHREAD CurrentThread;
KIRQL OldIrql;
if (InitialOwner)
{
CurrentThread = KeGetCurrentThread();
Mutant->OwnerThread = CurrentThread;//看
OldIrql = KiAcquireDispatcherLock();
//挂入本线程当前拥有的互斥对象链表
InsertTailList(&CurrentThread->MutantListHead,&Mutant->MutantListEntry);
KiReleaseDispatcherLock(OldIrql);
}
else
Mutant->OwnerThread = NULL;

KeInitializeDispatcherHeader(&Mutant->Header,MutantObject, sizeof(KMUTANT) / sizeof(ULONG), InitialOwner ? 0: 1);//初始对外信号个数为0或者1

Mutant->Abandoned = FALSE;

Mutant->ApcDisable = 0;
}

因为互斥对象有一个拥有者线程,所以互斥对象是可以重入的,也即一个线程可以反复多次调用,等待同一个互斥对象

如:

  • WaitForSingleObject(mutex1); //获得互斥对象,同时递增拥有计数其他代码;
  • WaitForSingleObject(mutex1);//递增拥有计数 其他代码;
  • ReleaseMutex(mutex1); //释放拥有计数 其他代码;
  • ReleaseMurex(mutex1);// 释放拥有计数

NtReleaseMutant

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
NTSTATUS NtReleaseMutant(IN HANDLE MutantHandle, 
IN PLONG PreviousCount OPTIONAL)
{
PKMUTANT Mutant;
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
NTSTATUS Status;
if ((PreviousCount) && (PreviousMode != KernelMode))
{
_SEH2_TRY
{
ProbeForWriteLong(PreviousCount);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
Status = ObReferenceObjectByHandle(MutantHandle,MUTANT_QUERY_STATE,ExMutantObjectType,
PreviousMode, (PVOID*)&Mutant,NULL);
if (NT_SUCCESS(Status))
{
_SEH2_TRY
{
//释放互斥对象,释放一次本线程对它的一个拥有计数
LONG Prev = KeReleaseMutant(Mutant,MUTANT_INCREMENT,FALSE,FALSE);
if (PreviousCount) *PreviousCount = Prev;
}
_SEH2_EXCEPT(ExSystemExceptionFilter())
{
Status = _SEH2_GetExceptionCode();

}
_SEH2_END;
ObDereferenceObject(Mutant);
}
return Status;
}



LONG KeReleaseMutant(IN PKMUTANT Mutant,
IN KPRIORITY Increment,
IN BOOLEAN Abandon,//是否是因为线程意外终止而抛弃的
IN BOOLEAN Wait)
{
KIRQL OldIrql;
LONG PreviousState;
PKTHREAD CurrentThread = KeGetCurrentThread();
BOOLEAN EnableApc = FALSE;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
OldIrql = KiAcquireDispatcherLock();
PreviousState = Mutant->Header.SignalState;//先前的拥有计数
if (Abandon == FALSE)//最典型
{
if (Mutant->OwnerThread != CurrentThread)
{
KiReleaseDispatcherLock(OldIrql);
ExRaiseStatus(Mutant->Abandoned ? STATUS_ABANDONED :STATUS_MUTANT_NOT_OWNED);
}
Mutant->Header.SignalState++;//递增对外(指对其他线程)的信号个数
}
else
{
Mutant->Header.SignalState = 1;//复位成1,表示直接对外有信号
Mutant->Abandoned = TRUE;//标记为被拥有者线程意外抛弃了
}
if (Mutant->Header.SignalState == 1)//如果该互斥对象变得对其他线程可用了
{
if (PreviousState <= 0)//若是正常释放的互斥对象(非意外终止原因)
{
RemoveEntryList(&Mutant->MutantListEntry);//本线程不再拥有该互斥对象
EnableApc = Mutant->ApcDisable;
}
Mutant->OwnerThread = NULL; //不再有拥有者线程了

if (!IsListEmpty(&Mutant->Header.WaitListHead))
KiWaitTest(&Mutant->Header, Increment);//关键。尝试唤醒其他线程
}
if (Wait == FALSE)
KiReleaseDispatcherLock(OldIrql);
else
{
CurrentThread->WaitNext = TRUE;
CurrentThread->WaitIrql = OldIrql;
}
if (EnableApc) KeLeaveCriticalRegion();
return PreviousState;
}

互斥对象的原理就这样,除了普通的互斥体,内核实际上还提供了一种快速互斥体,那种互斥体不再可重入。 互斥对象之所以设计成可重入的,可嵌套的,是有其原因的,有其好处的。
比如如果不能重入,那么在内层的WaiFor操作就会一直阻塞,等待外层的WaitFor操作释放互斥对象,这样就会死锁。


事件对象

事件对象的原理,事件分为两种事件

  • 1、 自动复位型,又叫SynchronizationEvent
  • 2、 手动复位型事件,又叫NotificationEvent

前者类型由于事件有信号号,马上又自动复位,所以一次只能唤醒一个线程,后者类型则类似广播通知, 一次可以唤醒等待该事件的所有线程。

KeSetEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
LONG KeSetEvent(IN PKEVENT Event, 
IN KPRIORITY Increment,
IN BOOLEAN Wait)
{
KIRQL OldIrql;
LONG PreviousState;
PKTHREAD Thread;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
if ((Event->Header.Type == EventNotificationObject) && (Event->Header.SignalState == 1) && !(Wait))
{
return TRUE;
}
OldIrql = KiAcquireDispatcherLock();
PreviousState = Event->Header.SignalState;
Event->Header.SignalState = 1;//置为有信号状态

if (!(PreviousState) && !(IsListEmpty(&Event->Header.WaitListHead)))//如果变得有信号
{
if (Event->Header.Type == EventNotificationObject) //唤醒所有正在等待的线程
KxUnwaitThread(&Event->Header, Increment);
Else //唤醒队列中的第一个等待类型为WaitAny的线程
KxUnwaitThreadForEvent(Event, Increment);
}
if (!Wait)
KiReleaseDispatcherLock(OldIrql);
else
{
Thread = KeGetCurrentThread();
Thread->WaitNext = TRUE;
Thread->WaitIrql = OldIrql;
}
return PreviousState;
}

KeResetEvent

手动复位型事件事件需要手动调用下面的函数复位事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
LONG  KeResetEvent(IN PKEVENT Event) 
{
KIRQL OldIrql;
LONG PreviousState;
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);

OldIrql = KiAcquireDispatcherLock();
PreviousState = Event->Header.SignalState;

Event->Header.SignalState = 0;//复位成无信号状态
KiReleaseDispatcherLock(OldIrql);
return PreviousState;
}

互斥、事件、信号量的原理都这么简单。在此不多说了。


自旋锁

#define KeAcquireSpinLock(SpinLock,OldIrql) *(OldIrql) = KeAcquireSpinLockRaiseToDpc(SpinLock)

ReactOS 的实现原理与 Windows 的有出入,在此引用另外一位作者的分析成果:

来源地址:http://bbs.pediy.com/showthread.php?t=74502

总结下自旋锁的实现

  • 1、在单核系统中就是将cpu的irql提到DISPATCH_LEVEL,防止线程切换,从而达到同步的目的
  • 2、在多核系统中,先将当前cpu提到DISPATCH_LEVEL,防止当前cpu上发生线程切换。然后不停的使用lock 前缀指令,锁定内存总线,忙式测试自旋锁的最低位是否标记处于了空闲状态。

由此可以看出,在多cpu系统中,自旋锁可以用来实现多个cpu之间的同步。原理就是lock总线。
自旋锁的用途之一:内联钩子的多cpu同步问题。

先讨论单cpu上的同步问题:
内联钩子一般修改了函数的前5B为jmp xxxxxxxx指令,共5个字节。
由于内联函数在hook的过程中,其他线程可能正在执行这个函数,因此必须确保同步,否则,极易引发蓝屏崩溃。

为什么呢?

巧就巧在hook 修改的是5个字节,若是<=4B的话,由于32位系统是以4B为单位访问内存单元的,4B区域的修改必定能用一 条简单的MOV类似指令完成修改,就不会存在同步问题。

而现在要改的是5个字节,若不加同步措施,假设下面一种情形:正在进行hook的线程刚好使用了一条mov指令改完了前4B,正准备改下一个字节的时候结果 发生了线程切换,另外一个线程来到这个函数入口时,因为前4B已经被改成jmp xxxxxxxx指令的前4B内容, 而第5个字节尚未改变,于是此时的前5B就是Jmp xxxxxx xx 形式,这导致错误的构造了jmp指令的 操作码部分,将跳转到一个未知地方,引发不可预料的错误,常见现象就是蓝屏。

前面5B,实际上是指前面8B的修改操作必须作为原子过程完成。

怎么让他原子呢?

第一步:修改前,将当前cpu的irql提升至DISPATCH_LEVEL,防止当前cpu上发生线程切换。
然后讨论多cpu同步问题
在多cpu机器上同步问题更复杂,除了需要保证同一个cpu上的线程同步,还需要保证各cpu之间同步
第二步:给其他各cpu发送一个DPC函数过去,DPC函数本身运行在DISPATCH_LEVEL
所以可以让其他cpu 始终运行在我们指定的DPC函数内,防止其他cpu上发生线程切换。
第三步:当确保了所有cpu都不会发生线程切换后,修改前5B进行hook,完事后通知其他cpu的DPC函数退出。

在上面的过程中,当前cpu与其他cpu之间,必须使用自选锁方式,读取各个DPC函数的运行状态和hook 状态。(详见《Rootkit—Windows内核安全防护》)

附:
Lock指令前缀的用途:一句话用来将一条指令变成原子的 假设内存单元0x11110000处存放着一个公共的计时器变量,且初值为100
那么考虑两个cpu同时执行inc [0x11110000] 这条指令的情况。

本来想要的目标是两个cpu一前一后执行 这条指令,最终结果变成102的。但是如果不加lock前缀,直接执行的话,由于这条指令内部分为取指、 分析、取数、运算、回写这几个阶段,假设第一个cpu刚好取出了数据100,然后释放了内存总线(同时另 一个cpu在此时将获得内存总线,然后他也同样去取数据,取出的也是100)

然后计算得出101,然后占用内存总线,回写数据到0x11110000处,变成101,完事后,另外一个cpu也将进行同样的计算,得出101,回 写到0x11110000处。最终0x11110000处的值是101而不是102。

这就是一种典型的多cpu同步引起的问题, 其根源在于指令内部是分阶段占用内存总线的
而lock前缀指示cpu:在整个指令的执行过程中全程独占总线,直到指令执行完毕后才释放。




消息与钩子

众所周知,Windows 系统是消息驱动的,现在我们就来看 Windows 的消息机制.

早期的 Windows 的窗口图形机制是在用户空间实现的,后来为了提高图形处理效率,将这部分移入内核空间, 在Win32k.sys模块中实现。
这个模块作为一个扩展的内核模块,提高了一个扩展额系统服务表,专用于窗口图形操作,相应的这个模块中添加了一个扩展系统调用服务表Shadow SSDT
以及一个扩展的系统调用服务表描述符表:KeServiceDescriptorTableShadow(系统中不仅有两张 SSDT,还有两张系统服务表描述符表)。
当一个线程首次调用这个模块中的系统服务函数时,这个线程就自然变成了 GUI 线程。

GUI 线程结构的ServiceTable指向的就是这个shadow描述符表。

那么消息机制是如何运作的呢?

当用户运行一个应用程序,通过对鼠标的点击或键盘按键,产生一些特定事件。由于Windows一直监控着I/O设备,该事件首先会被翻译成消息,由系统捕获,存放于系统消息队列。
经分析Windows知道该消息应由那个应用程序处理,则拷贝到相应的应用程序消息队列。
由于消息循环不断检索自身的消息队列,当发现应用程序消息队列里有消息,就用GetMessage()取出消息,封装成Msg()结构。如果该消息是由键盘按键产生的,用TranslateMessage()翻译为WM_CHAR消息,否则用DisPatchMessage()将取出的消息分发到相应的应用程序窗口,交由窗口处理程序处理。

Windows为每个窗体预留了过程窗口函数,该函数是一个回掉函数,由系统调用应用程序不能调用。
程序员可以通过重载该函数处理我们感兴趣的消息。
对于不感兴趣的消息,则由系统默认的窗口过程处理程序做出处理。

转变GUI线程

指向这个表的系统服务号的 bit12 位(也即第 13 位)为 1,如0x1XXX表示使用的是shadow服务表。 每个线程创建时都是普通线程,但是只要那个线程在运行的过程中发起了一次对win32k.sys模块中的系统调用,就会转变成 GUI 线程

下面的函数就是这个用途。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
NTSTATUS PsConvertToGuiThread(VOID) 
{
ULONG_PTR NewStack;
PVOID OldStack;
PETHREAD Thread = PsGetCurrentThread();
PEPROCESS Process = PsGetCurrentProcess();
NTSTATUS Status;

if (KeGetPreviousMode() == KernelMode)
return STATUS_INVALID_PARAMETER;

ASSERT(PspW32ProcessCallout != NULL);//确保win32k.sys模块已加载到内存
if (Thread->Tcb.ServiceTable != KeServiceDescriptorTable)
return STATUS_ALREADY_WIN32;//表示先前已经转换为GUI线程了

if (!Thread->Tcb.LargeStack)//if 尚未换成大内核栈
{
NewStack = (ULONG_PTR)MmCreateKernelStack(TRUE, 0);//分配一个64KB的大内核栈
//更为大内核栈
OldStack = KeSwitchKernelStack(NewStack, (NewStack - KERNEL_STACK_SIZE));
MmDeleteKernelStack(OldStack, FALSE);//销毁原来的普通内核栈
}

if (!Process->Win32Process)//if 尚未分配W32PROCESS结构(也即if是该进程中的第一个GUI线程)
Status = PspW32ProcessCallout(Process, TRUE);//分配Win32Process结构(表示GUI进程)

Thread->Tcb.ServiceTable = KeServiceDescriptorTableShadow;//关键。更改描述符表
//为当前线程分配一个W32THREAD结构
Status = PspW32ThreadCallout(Thread, PsW32ThreadCalloutInitialize);
if (!NT_SUCCESS(Status))
Thread->Tcb.ServiceTable = KeServiceDescriptorTable;//改为原来 的
return Status;
}

如上每个线程在转换为 GUI 线程时,必须换用64KB的大内核栈,因为普通的内核栈只有12KB大小,不能支持开销大的图形任务。
然后分配一个W32PROCESS结构,将进程转换为 GUI 进程,然后分配W32THREAD结构,更改系统服务表描述符表

上面的PspW32ProcessCalloutPspW32ThreadCallout函数都是回调函数
分别指向win32k.sys模块中的Win32kProcessCallbackWin32kThreadCallback函数。

Win32kProcessCallback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
NTSTATUS Win32kProcessCallback(struct _EPROCESS *Process, 
BOOLEAN Create)//指是要创建还是要销毁
{
PPROCESSINFO Win32Process;
Win32Process = PsGetProcessWin32Process(Process);//获得当前进程的W32PROCESS结构指针
if (!Win32Process)//if 尚未分配该结构,就分配一个
{
Win32Process = ExAllocatePoolWithTag(NonPagedPool,sizeof(PROCESSINFO),'p23W');
RtlZeroMemory(Win32Process, sizeof(PROCESSINFO));
PsSetProcessWin32Process(Process, Win32Process);
}
if (Create)
{
SIZE_T ViewSize = 0;
LARGE_INTEGER Offset;
PVOID UserBase = NULL;
NTSTATUS Status;
extern PSECTION_OBJECT GlobalUserHeapSection;
Offset.QuadPart = 0;
//将全局用户堆映射到本进程的地址空间
Status = MmMapViewOfSection(GlobalUserHeapSection,PsGetCurrentProcess(),&UserBase,
0,0,&Offset,&ViewSize,ViewUnmap,SEC_NO_CHANGE,
PAGE_EXECUTE_READ);
Win32Process->HeapMappings.Next = NULL;
Win32Process->HeapMappings.KernelMapping = (PVOID)GlobalUserHeap;
Win32Process->HeapMappings.UserMapping = UserBase;
Win32Process->HeapMappings.Count = 1;

InitializeListHead(&Win32Process->ClassList);
InitializeListHead(&Win32Process->MenuListHead);
InitializeListHead(&Win32Process->GDIBrushAttrFreeList);
InitializeListHead(&Win32Process->GDIDcAttrFreeList);
InitializeListHead(&Win32Process->PrivateFontListHead);
ExInitializeFastMutex(&Win32Process->PrivateFontListLock);
InitializeListHead(&Win32Process->DriverObjListHead);
ExInitializeFastMutex(&Win32Process->DriverObjListLock);

Win32Process->KeyboardLayout = W32kGetDefaultKeyLayout();

if(Process->Peb != NULL)
{

//映射全局的GDI对象句柄表到本进程的地址空间中
Process->Peb->GdiSharedHandleTable = GDI_MapHandleTable(GdiTableSection, Process);
Process->Peb->GdiDCAttributeList = GDI_BATCH_LIMIT;
}

Win32Process->peProcess = Process;
Win32Process->W32PF_flags = 0;
}
else
{
IntCleanupMenus(Process, Win32Process);
IntCleanupCurIcons(Process, Win32Process);
CleanupMonitorImpl();
DestroyProcessClasses(Win32Process);
GDI_CleanupForProcess(Process);
co_IntGraphicsCheck(FALSE);
if(LogonProcess == Win32Process)
LogonProcess = NULL;
}
return STATUS_SUCCESS;
}

每个含有 GUI 线程的进程都是 GUI 进程,每个 GUI 进程的EPROCESS结构含有W32PROCESS结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct _W32PROCESS 
{
PEPROCESS peProcess;
DWORD RefCount;
ULONG W32PF_flags;
PKEVENT InputIdleEvent;
DWORD StartCursorHideTime;
struct _W32PROCESS * NextStart;
PVOID pDCAttrList;
PVOID pBrushAttrList;
DWORD W32Pid;
LONG GDIHandleCount;
LONG UserHandleCount;
PEX_PUSH_LOCK GDIPushLock; /* Locking Process during access to structure. */
RTL_AVL_TABLE GDIEngUserMemAllocTable; /* Process AVL Table. */
LIST_ENTRY GDIDcAttrFreeList;
LIST_ENTRY GDIBrushAttrFreeList;
} W32PROCESS, *PW32PROCESS;

Win32kThreadCallback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
NTSTATUS Win32kThreadCallback(struct _ETHREAD *Thread, 
PSW32THREADCALLOUTTYPE Type) //指是初始化还是清理
{
struct _EPROCESS *Process;
PTHREADINFO Win32Thread;
DECLARE_RETURN(NTSTATUS);
Process = Thread->ThreadsProcess;
Win32Thread = PsGetThreadWin32Thread(Thread);
if (!Win32Thread)//if 尚未分配Win32Thread结构,就分配
{
Win32Thread = ExAllocatePoolWithTag(NonPagedPool,sizeof(THREADINFO),'t23W');
RtlZeroMemory(Win32Thread, sizeof(THREADINFO));
PsSetThreadWin32Thread(Thread, Win32Thread);
}
if (Type == PsW32ThreadCalloutInitialize)//if 初始化
{
HWINSTA hWinSta = NULL;
PTEB pTeb;
HDESK hDesk = NULL;
NTSTATUS Status;
PUNICODE_STRING DesktopPath;
PRTL_USER_PROCESS_PARAMETERS ProcessParams = (Process->Peb ? Process->Peb->ProcessPara meters : NULL);
InitializeListHead(&Win32Thread->WindowListHead);
InitializeListHead(&Win32Thread->W32CallbackListHead);
InitializeListHead(&Win32Thread->PtiLink);

DesktopPath = (ProcessParams ? ((ProcessParams->DesktopInfo.Length > 0) ? &ProcessPara
ms->DesktopInfo : NULL) : NULL);
Status = IntParseDesktopPath(Process,DesktopPath,&hWinSta,&hDesk);
if(NT_SUCCESS(Status))
{

Win32Thread->MessageQueue = MsqCreateMessageQueue(Thread);//关键。创建消息队列
Win32Thread->KeyboardLayout = W32kGetDefaultKeyLayout();
Win32Thread->pEThread = Thread;
}
}
Else …

Return STATUS_SUCCESS;
}



typedef struct _W32THREAD
{
PETHREAD pEThread;
ULONG RefCount;
PTL ptlW32;
PVOID pgdiDcattr;
PVOID pgdiBrushAttr;
PVOID pUMPDObjs;
PVOID pUMPDHeap;
DWORD dwEngAcquireCount;
PVOID pSemTable;
PVOID pUMPDObj;
} W32THREAD, *PW32THREAD;


typedef struct _THREADINFO
{
W32THREAD; //开头是一个W32THREAD结构
PTL ptl;
PPROCESSINFO ppi;
struct _USER_MESSAGE_QUEUE* MessageQueue;
struct _KBL* KeyboardLayout;
PCLIENTTHREADINFO pcti;
struct _DESKTOP* rpdesk;
PDESKTOPINFO pDeskInfo;
PCLIENTINFO pClientInfo;
FLONG TIF_flags;
PUNICODE_STRING pstrAppName;
LONG timeLast;
ULONG_PTR idLast;
INT exitCode;
HDESK hdesk;
UINT cPaintsReady; /* Count of paints pending. */
UINT cTimersReady; /* Count of timers pending. */
DWORD dwExpWinVer;
DWORD dwCompatFlags;
DWORD dwCompatFlags2;
struct _USER_MESSAGE_QUEUE* pqAttach;
PTHREADINFO ptiSibling;
ULONG fsHooks;
PHOOK sphkCurrent;
LPARAM lParamHkCurrent;
WPARAM wParamHkCurrent;
struct tagSBTRACK* pSBTrack;
HANDLE hEventQueueClient;

PKEVENT pEventQueueServer;
LIST_ENTRY PtiLink;

CLIENTTHREADINFO cti; // Used only when no Desktop or pcti NULL.
/* ReactOS */
LIST_ENTRY WindowListHead;
LIST_ENTRY W32CallbackListHead;
SINGLE_LIST_ENTRY ReferencesList;
} THREADINFO;


//每个窗口对象的内部结构
typedef struct _WINDOW_OBJECT
{
THRDESKHEAD head;
PWND Wnd;//内部结构
PTHREADINFO pti; //所属线程
HMENU SystemMenu;//左上角的系统菜单
HWND hSelf;//窗口句柄是内核全局的
ULONG state;
HANDLE hrgnUpdate;//当前无效区域(指更新区域)的句柄
HANDLE hrgnClip;//剪裁区域的句柄
struct _WINDOW_OBJECT* spwndChild;//第一个子窗口
struct _WINDOW_OBJECT* spwndNext;//下一个兄弟窗口
struct _WINDOW_OBJECT* spwndPrev;//上一个兄弟窗口
struct _WINDOW_OBJECT* spwndParent;//父窗口
struct _WINDOW_OBJECT* spwndOwner;//拥有者窗口与父窗口是两码事
PSBINFOEX pSBInfo;//滚动条信息
LIST_ENTRY ThreadListEntry;//用来挂入线程的窗口链表
} WINDOW_OBJECT;

typedef struct _WND
{
THRDESKHEAD head;
DWORD state;
DWORD state2;
DWORD ExStyle;//扩展样式
DWORD style;//标准样式
HINSTANCE hModule;//创建本窗口的模块
DWORD fnid;
struct _WND *spwndNext;
struct _WND *spwndPrev;
struct _WND *spwndParent;
struct _WND *spwndChild;
struct _WND *spwndOwner;
RECT rcWindow;//整个区域

RECT rcClient;//客户区域
WNDPROC lpfnWndProc;//关键。窗口过程
PCLS pcls;//窗口类
HRGN hrgnUpdate;
LIST_ENTRY PropListHead;//属性链表
ULONG PropListItems;
PSBINFO pSBInfo;//滚动条信息
HMENU SystemMenu;
UINT IDMenu;
HRGN hrgnClip;
HRGN hrgnNewFrame;
LARGE_UNICODE_STRING strName;//窗口标题
ULONG cbwndExtra;//附加数据区的大小
HWND hWndLastActive;
struct _WND *spwndLastActive;
LONG dwUserData;//用户自定义数据
struct _WND *spwndClipboardListener;
DWORD ExStyle2;
struct
{
RECT NormalRect;
POINT IconPos;
POINT MaxPos;
} InternalPos;
UINT Unicode : 1; // !(WNDS_ANSICREATOR|WNDS_ANSIWINDOWPROC) ?
UINT InternalPosInitialized : 1;
UINT HideFocus : 1; // WS_EX_UISTATEFOCUSRECTHIDDEN ?
UINT HideAccel : 1; // WS_EX_UISTATEKBACCELHIDDEN ?
} WND, *PWND;




消息循环机制重点函数

下面重点讲述消息循环机制中的几个重点函数

  • GetMessage
  • DispatchMessage
  • SendMessage
  • PostMessage
  • PeekMessage
1
2
3
4
5
while(GetMessage(&Msg, NULL, 0, 0) > 0)
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}

当运行程序->事件操作引发消息->消息先存在系统消息队列->再存入到应用程序消息队列->用消息循环提取消息->处理消息->再返回消息队列....

上面代码的执行过程为

    1. 消息循环调用GetMessage()从消息队列中查找消息进行处理,如果消息队列为空,程序将停止执行并等待(程序阻塞)。
    1. 事件发生时导致一个消息加入到消息队列(例如系统注册了一个鼠标点击事件)
      GetMessage()将返回一个正值,这表明有消息需要被处理,并且消息已经填充到传入的MSG参数中;
      当传入WM_QUIT消息时返回0, 如果返回值为负表明发生了错误。
    1. 取出消息(在Msg变量中)并将其传递给TranslateMessage()函数这个函数做一些额外的处理:将虚拟键值信息转换为字符信息。这一步实际上是可选的但有些地方需要用到这一步。
    1. 上面的步骤执行完后,将消息传递给DispatchMessage()函数。DispatchMessage()函数将消息分发到消息的目标窗口,并且查找目标窗口过程函数,给窗口过程函数传递窗口句柄、消息、wParam、lParam等参数然后调用该函数。
    1. 在窗口过程函数中检查消息和其他参数,你可以用它来实现你想要的操作。
      如果不想处理某些特殊的消息,你应该总是调用DefWindowProc()函数,系统将按按默认的方式处理这些消息(通常认为是不做任何操作)。
    1. 一旦一个消息处理完成,窗口过程函数返回DispatchMessage()函数返回,继续循环处理下一个消息。

GetMessageW

下面的函数从本线程的消息队列中取出一条符合指定过滤条件的消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
BOOL GetMessageW(LPMSG lpMsg,//返回从队列中摘下来的消息 
HWND hWnd,//过滤条件一:发给这个窗口的消息
UINT wMsgFilterMin, //过滤条件二:最小值
UINT wMsgFilterMax) //过滤条件三:最大值
{
BOOL Res;
MSGCONVERSION Conversion;//用于内核消息、用户模式消息互转
NTUSERGETMESSAGEINFO Info;//一个专用于NtUserGetMessage函数的中间结构
PUSER32_THREAD_DATA ThreadData = User32GetThreadData();//每个线程的User32 tls数据

MsgConversionCleanup(lpMsg, FALSE, FALSE, NULL);

//实质函数。从本线程的消息队列中寻找符合对应过滤条件的一条消息,摘下来。
Res = NtUserGetMessage(&Info, hWnd, wMsgFilterMin, wMsgFilterMax);
if (-1 == (int) Res) return Res;
Conversion.LParamSize = Info.LParamSize;
Conversion.KMMsg = Info.Msg;

//将内核模式的消息格式转换为用户模式的消息格式
if (! MsgiKMToUMMessage(&Conversion.KMMsg, &Conversion.UnicodeMsg))
return (BOOL) -1;

*lpMsg = Conversion.UnicodeMsg;//返回取得的转换后的消息
Conversion.Ansi = FALSE;
Conversion.FinalMsg = lpMsg;
MsgConversionAdd(&Conversion);//加入全局的转换数组

if (Res && lpMsg->message != WM_PAINT && lpMsg->message != WM_QUIT)
ThreadData->LastMessage = Info.Msg;//记录本线程上次取下来的消息
return Res;
}

typedef struct tagNTUSERGETMESSAGEINFO //中间结构
{
MSG Msg;
ULONG LParamSize;//L参数附件包的长度(注意有的消息类型是带有L附件包的)
} NTUSERGETMESSAGEINFO, *PNTUSERGETMESSAGEINFO;


typedef struct tagMSG
{
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;//lparam有可能是个指针
DWORD time;//消息进队时间
POINT pt;//消息进队时,桌面光标的位置
} MSG,*LPMSG,*PMSG;

NtUserGetMessage

我们看实质的内核函数:NtUserGetMessage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
BOOL NtUserGetMessage( PNTUSERGETMESSAGEINFO UnsafeInfo,//用户空间中的不安全地址 
HWND hWnd,
UINT MsgFilterMin,
UINT MsgFilterMax )
{
BOOL GotMessage;
NTUSERGETMESSAGEINFO Info;//内核中的安全缓冲
NTSTATUS Status;

PWINDOW_OBJECT Window = NULL;
PMSGMEMORY MsgMemoryEntry;
PVOID UserMem;
UINT Size;
USER_MESSAGE Msg;//消息队列中的原生消息结构
DECLARE_RETURN(BOOL);
UserEnterExclusive();
//检测窗口句柄是否无效
if (hWnd && !(Window = UserGetWindowObject(hWnd)))
RETURN(-1);

//过滤条件自相矛盾,就不过滤
if (MsgFilterMax < MsgFilterMin)
{
MsgFilterMin = 0;
MsgFilterMax = 0;
}
//循环等待,直到取出一条符合指定条件的消息出来
do
{
GotMessage = co_IntPeekMessage(&Msg, Window, MsgFilterMin, MsgFilterMax, PM_REMOVE);
if (GotMessage)//如果取出了
{
Info.Msg = Msg.Msg;
//检测这种消息是否带有L参数附件包,若有,返回它的L附件包大小计算法
MsgMemoryEntry = FindMsgMemory(Info.Msg.message);

if (NULL == MsgMemoryEntry)//若不带L附件包
Info.LParamSize = 0;
else
{
//根据这种消息的L附件包长度计算法 计算出该消息的L附件包大小
Size = MsgMemorySize(MsgMemoryEntry, Info.Msg.wParam,Info.Msg.lParam);
Info.LParamSize = Size;
UserMem = NULL;//用户空间的L附件包地址
//在当前进程的用户空间分配一块L附件包大小的内存
Status = ZwAllocateVirtualMemory(NtCurrentProcess(), &UserMem, 0,
&Info.LParamSize, MEM_COMMIT, PAGE_READWRITE);
//关键。将内核中的L附件包转移到用户空间
Status = MmCopyToCaller(UserMem, (PVOID) Info.Msg.lParam, Size);
Info.Msg.lParam = (LPARAM) UserMem;//重定向指向用户空间中的地址
}
if (Msg.FreeLParam && NULL != Msg.Msg.lParam)
ExFreePool((void *) Msg.Msg.lParam);//释放内核中的L附件包

//将取下来的消息上传给用户
Status = MmCopyToCaller(UnsafeInfo, &Info, sizeof(NTUSERGETMESSAGEINFO));

if (! NT_SUCCESS(Status))
{
SetLastNtError(Status);
RETURN( (BOOL) -1);
}
}
//一直等待消息队列中出现对应过滤条件的消息
else if (! co_IntWaitMessage(Window, MsgFilterMin, MsgFilterMax))
{
RETURN( (BOOL) -1);
}
}
while (! GotMessage);
RETURN( WM_QUIT != Info.Msg.message); CLEANUP:

UserLeave();
END_CLEANUP;
}

上面这个函数首先会判断窗口局部是否有效。它根据窗口句柄从内核中的全局用户对象句柄表中检索出对应的用户对象
(所谓用户对象指窗口、菜单、快捷键、光标、钩子等相对于内核对象的 GUI 对象),
由于该句柄表是全局的,所有用户对象的句柄因此也是全局的,各个进程通用,不像内核对象的句柄是局限在各个进程的句柄表中。

UserGetWindowObject(找窗口对象)

下面的函数根据窗口句柄找到对应的窗口对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
WINDOW_OBJECT* FASTCALL UserGetWindowObject(HWND hWnd) 
{
THREADINFO* ti;
WINDOW_OBJECT* Window;
if (PsGetCurrentProcess() != PsInitialSystemProcess)//若不是’system’进程
{
ti = GetW32ThreadInfo();
if (ti == NULL)
{
SetLastWin32Error(ERROR_ACCESS_DENIED);
return NULL;
}
}

//实质函数,gHandleTable指向全局的GUI对象句柄表
Window = (WINDOW_OBJECT*)UserGetObject(gHandleTable, hWnd, otWindow);
if (!Window || 0 != (Window->state & WINDOWSTATUS_DESTROYED))
{
SetLastWin32Error(ERROR_INVALID_WINDOW_HANDLE);
return NULL;
}

return Window;
}


PVOID UserGetObject(PUSER_HANDLE_TABLE ht,//句柄表
HANDLE handle,//用户对象的句柄
USER_OBJECT_TYPE type )//用户对象类型
{
PUSER_HANDLE_ENTRY entry = handle_to_entry(ht, handle);//根据句柄值找到对应的句柄表项
if (entry == NULL || entry->type != type)//若找不到或者类型不匹配
{
SetLastWin32Error(ERROR_INVALID_HANDLE);
return NULL;
}
return entry->ptr;//返回对应的对象
}


PUSER_HANDLE_ENTRY handle_to_entry(PUSER_HANDLE_TABLE ht, HANDLE handle )
{
unsigned short generation;
int index = (((unsigned int)handle & 0xffff) - FIRST_USER_HANDLE) >> 1;
if (index < 0 || index >= ht->nb_handles)
return NULL;
if (!ht->handles[index].type)
return NULL;

generation = (unsigned int)handle >> 16;
if (generation == ht->handles[index].generation || !generation || generation == 0xffff)
return &ht->handles[index];
return NULL;
}

如上根据handle找到对应的句柄表项,有点类似内核对象的句柄,可以把 GUI 对象的句柄也看作是一个简单的数组索引值。
下面是具体的 GUI 对象句柄表项的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _USER_HANDLE_ENTRY 
{
void *ptr;//指向对应的GUI对象
union
{
PVOID pi;
PTHREADINFO pti; // 指向所属线程
PPROCESSINFO ppi; // 指向所属进程
};
unsigned char type;
unsigned char flags;
unsigned short generation;//不明
} USER_HANDLE_ENTRY, * PUSER_HANDLE_ENTRY;

下面是 GUI 对象的句柄表

1
2
3
4
5
6
7
typedef struct _USER_HANDLE_TABLE //句柄表描述符 
{
PUSER_HANDLE_ENTRY handles;//句表的地址
PUSER_HANDLE_ENTRY freelist;//空闲表项链表
int nb_handles;//表的容量
int allocated_handles;//表中实际已分配的表项个数
} USER_HANDLE_TABLE, * PUSER_HANDLE_TABLE;


FindMsgMemory(查找指定类型消息的L附件包)

前面的函数会将消息从队列取下来后,将L附件包转移到用户空间。
绝大多数消息都不带L附件包,有的消息则带有L附加包
内核中有一个全局表给出了所有含有L附件包的消息类型以及他们的L附件包大小的计算方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static MSGMEMORY MsgMemory[] =    //全局表 
{
{ WM_CREATE, MMS_SIZE_SPECIAL, MMS_FLAG_READWRITE },
{ WM_DDE_ACK, sizeof(KMDDELPARAM), MMS_FLAG_READ },
{ WM_DDE_EXECUTE, MMS_SIZE_WPARAM, MMS_FLAG_READ },
{ WM_GETMINMAXINFO, sizeof(MINMAXINFO), MMS_FLAG_READWRITE },
{ WM_GETTEXT, MMS_SIZE_WPARAMWCHAR, MMS_FLAG_WRITE },
{ WM_NCCALCSIZE, MMS_SIZE_SPECIAL, MMS_FLAG_READWRITE },
{ WM_NCCREATE, MMS_SIZE_SPECIAL, MMS_FLAG_READWRITE },
{ WM_SETTEXT, MMS_SIZE_LPARAMSZ, MMS_FLAG_READ },
{ WM_STYLECHANGED, sizeof(STYLESTRUCT), MMS_FLAG_READ },
{ WM_STYLECHANGING, sizeof(STYLESTRUCT), MMS_FLAG_READWRITE },
{ WM_COPYDATA, MMS_SIZE_SPECIAL, MMS_FLAG_READ },
{ WM_WINDOWPOSCHANGED, sizeof(WINDOWPOS), MMS_FLAG_READ },
{ WM_WINDOWPOSCHANGING, sizeof(WINDOWPOS), MMS_FLAG_READWRITE },
};


typedef struct tagMSGMEMORY
{
UINT Message;//消息ID,即消息类型
UINT Size;//该类消息的L附件包大小或计算方法
INT Flags;//数据流动方向
}
MSGMEMORY, *PMSGMEMORY;

如上面WM_GETTEXT这种消息的L附件包大小计算方法是:蕴含在它的wparam参数中,wparam参数给定了L附件包的宽字符个数
MMS_FLAG_WRITE标志则表示需要写用户空间,也即将L附件包回写复制到用户空间。

WM_SETTEXT这种消息的L附件包大小计算方法是:蕴含在它的lparam参数,lparam参数是一个以0结尾的字符串
MMS_FLAG_READ标志则表示需要读用户空间,也即将用户空间中的文本复制到内核空间中。
MMS_SIZE_SPECIAL则表示L附件包的大小特定于具体的消息。

除开上面表中的那些消息,其他消息都不带L附件包。

1
2
3
4
5
6
7
8
9
10
11
12
PMSGMEMORY FASTCALL FindMsgMemory(UINT Msg) 
{
PMSGMEMORY MsgMemoryEntry;
for (MsgMemoryEntry = MsgMemory;
MsgMemoryEntry < MsgMemory + sizeof(MsgMemory) / sizeof(MSGMEMORY);
MsgMemoryEntry++)
{
if (Msg == MsgMemoryEntry->Message)
return MsgMemoryEntry;
}
return NULL;
}

下面的函数根据计算的计算法计算对应消息的L附件包大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
UINT FASTCALL MsgMemorySize(PMSGMEMORY MsgMemoryEntry, WPARAM wParam, LPARAM lParam) 
{
CREATESTRUCTW *Cs;
PUNICODE_STRING WindowName;
PUNICODE_STRING ClassName;
UINT Size = 0;
_SEH2_TRY
{
if (MMS_SIZE_WPARAM == MsgMemoryEntry->Size)
Size = (UINT)wParam;
else if (MMS_SIZE_WPARAMWCHAR == MsgMemoryEntry->Size)
Size = (UINT) (wParam * sizeof(WCHAR));
else if (MMS_SIZE_LPARAMSZ == MsgMemoryEntry->Size)
Size = (UINT) ((wcslen((PWSTR) lParam) + 1) * sizeof(WCHAR));
else if (MMS_SIZE_SPECIAL == MsgMemoryEntry->Size)//具体消息具体计算
{
switch(MsgMemoryEntry->Message)
{

case WM_COPYDATA:
Size = sizeof(COPYDATASTRUCT) + ((PCOPYDATASTRUCT)lParam)->cbData;
break;

}
}
else

Size = MsgMemoryEntry->Size;
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
Size = 0;
}
_SEH2_END;
return Size;
}


co_IntPeekMessage

前面的NtUserGetMessage函数实际上调用co_IntPeekMessage函数从消息队列中取下消息。

消息队列的结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//线程的消息队列描述符
typedef struct _USER_MESSAGE_QUEUE
{
LONG References;//本队列的引用计数
struct _ETHREAD *Thread;//本队列的所属线程
LIST_ENTRY SentMessagesListHead;//通过SendMessage方式发来的消息进入这个队列(send队列)
LIST_ENTRY PostedMessagesListHead; //通过PostMessage方式发来的消息进入这个队列(post队列)
LIST_ENTRY NotifyMessagesListHead;//专用于存放SendMessage回调函数调用通知的消息队列
LIST_ENTRY HardwareMessagesListHead;//来自硬件设备的消息(指鼠标消息)队列
KMUTEX HardwareLock;
PUSER_MESSAGE MouseMoveMsg;//当前的MouseMove消息(所有MouseMove消息合并成一个消息)
BOOLEAN QuitPosted;//指队列中是否收到了一个WM_QUIT消息正处于Pengding中
ULONG QuitExitCode;//收到的WM_QUIT消息的退出码(wparam)
PKEVENT NewMessages;//一个标记队列中是否含有WakeMask掩码消息的事件
HANDLE NewMessagesHandle;//上面事件对象的句柄
ULONG LastMsgRead;//上次发出PeekMessage请求的时间
HWND FocusWindow;//当前键盘焦点窗口
ULONG PaintCount;//阻塞中的WM_PAINT消息计数
HWND ActiveWindow; //当前活动窗口
HWND CaptureWindow;//当前鼠标焦点窗口(一般是没有的,鼠标消息一般发给光标处所在窗口)
HWND MoveSize; /* Current move/size window for this queue */
HWND MenuOwner; /* Current menu owner window for this queue */
BYTE MenuState; /* Identifes the menu state */
PTHRDCARETINFO CaretInfo; /* Caret information for this queue */
PHOOKTABLE Hooks;//每个线程局部的钩子表(还有一个全局的钩子表)
WORD WakeMask;//唤醒消息掩码
WORD QueueBits;
WORD ChangedBits;
LPARAM ExtraInfo;
LIST_ENTRY DispatchingMessagesHead;//本线程已发送出去的,但尚未被目标线程处理的消息队列
LIST_ENTRY LocalDispatchingMessagesHead;//本线程正在进行Dispatching处理的消息队列
struct _DESKTOP *Desktop;//本队列所属的Desktop
} USER_MESSAGE_QUEUE, *PUSER_MESSAGE_QUEUE;

消息队列按用途分类实际微观上包含好几种,只不过从宏观上我们理解每个线程一个消息队列。 微观上分为 4 个接收队列

当线程收到消息时会先从接收队列取下来,转入处理中队列,当处理完毕后再退出处理中队列

当一个线程通过SendMesage发送消息时,发出的消息会被放到DispatchingMessagesHead队列,当被接收方处理后,从中移出。

消息队列中的每个消息的结构定义如下:

1
2
3
4
5
6
typedef struct _USER_MESSAGE 
{
LIST_ENTRY ListEntry;//用来挂入消息队列
BOOLEAN FreeLParam;//表示本消息被取出后是否需要是否内核lparam
MSG Msg;//消息主体
} USER_MESSAGE, *PUSER_MESSAGE;

PostMessageSendMessage的区别。
前者是异步的发生方直接把消息发送到接收方的消息队列中就返 回,不管消息有没有被处理完成。
而后者是同步的,要一直等到消息被接收方处理后才会返回。

回到NtUserGetMessage我们看到实际调用的正题函数是下面的这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
BOOL FASTCALL co_IntPeekMessage( PUSER_MESSAGE Msg,//返回取下来的原生消息 
PWINDOW_OBJECT Window,//过滤条件
UINT MsgFilterMin,//过滤条件
UINT MsgFilterMax,//过滤条件
UINT RemoveMsg )//表示是否需要从队列中移除
{
PTHREADINFO pti;
LARGE_INTEGER LargeTickCount;
PUSER_MESSAGE_QUEUE ThreadQueue;
PUSER_MESSAGE Message;
BOOL Present, RemoveMessages;
USER_REFERENCE_ENTRY Ref;
USHORT HitTest;
pti = PsGetCurrentThreadWin32Thread();
ThreadQueue = pti->MessageQueue;
RemoveMessages = RemoveMsg & PM_REMOVE;

CheckMessages:
HitTest = HTNOWHERE;
Present = FALSE;//表示是否找到了对应的消息
KeQueryTickCount(&LargeTickCount);
ThreadQueue->LastMsgRead = LargeTickCount.u.LowPart;//记录上次取消息请求的时间


//在取消息前,先处理完所有其他线程send过来的消息(指SendMessage方式)
while (co_MsqDispatchOneSentMessage(ThreadQueue));

//检测是否收到了WM_QUIT消息,特殊处理
if (ThreadQueue->QuitPosted)
{
Msg->Msg.hwnd = NULL;
Msg->Msg.message = WM_QUIT;
Msg->Msg.wParam = ThreadQueue->QuitExitCode;
Msg->Msg.lParam = 0;
Msg->FreeLParam = FALSE;
if (RemoveMessages)
ThreadQueue->QuitPosted = FALSE;
goto MsgExit;
}

//先查找post队列中是否有对应的消息(注意键盘消息也是在post队列中)
Present = co_MsqFindMessage( ThreadQueue,FALSE,RemoveMessages, Window,MsgFilterMin,MsgFilterMax,&Message);

if (Present)
{
RtlCopyMemory(Msg, Message, sizeof(USER_MESSAGE));//取下来,返回给用户
if (RemoveMessages)
MsqDestroyMessage(Message);
goto MessageFound;
}

//再检查硬件消息队列中是否有鼠标消息
Present = co_MsqFindMessage( ThreadQueue,TRUE,RemoveMessages, Window,MsgFilterMin,MsgFilterMax,&Message);

if (Present)
{
RtlCopyMemory(Msg, Message, sizeof(USER_MESSAGE)); //取下来,返回给用户

if (RemoveMessages)
MsqDestroyMessage(Message);

goto MessageFound;
}

//上面的查找过程会耗时一段时间,因此可能会又积累了send消息,处理掉
while (co_MsqDispatchOneSentMessage(ThreadQueue));

//检测是否有WM_PAINT消息
if ( IntGetPaintMessage( Window,MsgFilterMin,MsgFilterMax,pti,&Msg->Msg,RemoveMessages))
{
Msg->FreeLParam = FALSE;

goto MsgExit;
}

if (PostTimerMessages(Window))//检测是否有WM_TIMER消息
goto CheckMessages;

if(Present)//如果在队列中找到了消息
{

MessageFound:
if(RemoveMessages)
{
PWINDOW_OBJECT MsgWindow = NULL;
if( Msg->Msg.hwnd && ( MsgWindow = UserGetWindowObject(Msg->Msg.hwnd) )
&& Msg->Msg.message >= WM_MOUSEFIRST && Msg->Msg.message <= WM_MOUSELAST )
{
USHORT HitTest;
UserRefObjectCo(MsgWindow, &Ref);
if ( co_IntTranslateMouseMessage( ThreadQueue,&Msg->Msg,&HitTest,TRUE))
{
UserDerefObjectCo(MsgWindow);
goto CheckMessages;//丢弃该条消息,重新查找
}

if(ThreadQueue->CaptureWindow == NULL)
{
co_IntSendHitTestMessages(ThreadQueue, &Msg->Msg);

if (( Msg->Msg.message != WM_MOUSEMOVE && Msg->Msg.message != WM_NCMOUSEMOVE)
&& IS_BTN_MESSAGE(Msg->Msg.message, DOWN)
&& co_IntActivateWindowMouse(ThreadQueue, &Msg->Msg, MsgWindow, &HitTest))
{
UserDerefObjectCo(MsgWindow);
goto CheckMessages; //丢弃该条消息,重新查找
}
}
UserDerefObjectCo(MsgWindow);
}
else
co_IntSendHitTestMessages(ThreadQueue, &Msg->Msg);
goto MsgExit;
}
if ( ( Msg->Msg.hwnd && Msg->Msg.message >= WM_MOUSEFIRST && Msg->Msg.message <= WM_MOUSELAST )
&& co_IntTranslateMouseMessage( ThreadQueue,&Msg->Msg,&HitTest,FALSE) )
{
goto CheckMessages; //丢弃该条消息,重新查找

}

MsgExit:
if ( ISITHOOKED(WH_MOUSE) && IS_MOUSE_MESSAGE(Msg->Msg.message))
{
if(!ProcessMouseMessage(&Msg->Msg, HitTest, RemoveMsg))
return FALSE;
}

if ( ISITHOOKED(WH_KEYBOARD) && IS_KBD_MESSAGE(Msg->Msg.message))
{
if(!ProcessKeyboardMessage(&Msg->Msg, RemoveMsg))
return FALSE;
}

//每当取出一个消息后,都会调用WH_GETMESSAGE钩子
if (ISITHOOKED(WH_GETMESSAGE))
{
co_HOOK_CallHooks( WH_GETMESSAGE, HC_ACTION, RemoveMsg & PM_REMOVE, (LPARAM)&Msg->M sg);
}
return TRUE;
}
return Present;
}

如上这个函数会从post消息队列和硬件消息队列中查找,取出一个符合指定条件的消息出来。
不过在进行查找前,会优先处理掉send队列中其他线程发来的消息。
查找消息的顺序是:send队列、post队列、键盘鼠标消息、重绘消息、定时器消息【send、post、键鼠、 重、定时】

_USER_SENT_MESSAGE

注意send队列中的消息结构与post队列、硬件队列中的消息结构不同,

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _USER_SENT_MESSAGE 
{
LIST_ENTRY ListEntry;//用来挂入接收方的Dispatching处理队列
MSG Msg;//主体
PKEVENT CompletionEvent;//完成处理后,用于通知发送方的事件
LRESULT* Result;//处理结果
struct _USER_MESSAGE_QUEUE* SenderQueue;//发送方的消息队列(方便用)
SENDASYNCPROC CompletionCallback;//发送方提供的消息处理完成例程
ULONG_PTR CompletionCallbackContext;
LIST_ENTRY DispatchingListEntry;//用来挂入发送方的Dispatching未完成队列
INT HookMessage;//钩子相关
BOOL HasPackedLParam;//表示L附件包是否打包到用户空间了
} USER_SENT_MESSAGE, *PUSER_SENT_MESSAGE;
send队列处理
co_MsqDispatchOneSentMessage

下面的函数用于处理send队列中的一条消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
BOOLEAN FASTCALL co_MsqDispatchOneSentMessage(PUSER_MESSAGE_QUEUE MessageQueue) 
{
PUSER_SENT_MESSAGE Message;
PLIST_ENTRY Entry;
LRESULT Result;
if (IsListEmpty(&MessageQueue->SentMessagesListHead))
return(FALSE);

Entry = RemoveHeadList(&MessageQueue->SentMessagesListHead);
Message = CONTAINING_RECORD(Entry, USER_SENT_MESSAGE, ListEntry);

//转入接收方线程的正在Dispatching处理队列
InsertTailList(&MessageQueue->LocalDispatchingMessagesHead,&Message->ListEntry);


//if 这个消息本身就是‘请求执行钩子过程’,那么message表示HookId,hwnd表示code
//其他线程在调用底层键盘鼠标钩子时,会给钩子的原创建者线程发一个消息,请求执行钩子过程
if (Message->HookMessage == MSQ_ISHOOK)
{
//检查、调用指定类型的钩子
Result = co_HOOK_CallHooks(Message->Msg.message,Message->Msg.hwnd,
Message->Msg.wParam,Message->Msg.lParam);
}
else if (Message->HookMessage == MSQ_ISEVENT)
{
Result = co_EVENT_CallEvents( Message->Msg.message,Message->Msg.hwnd,
Message->Msg.wParam,Message->Msg.lParam);
}
Else //最典型
{

//关键。将消息派遣到目标窗口的窗口过程,处理消息,返回处理结果
Result = co_IntSendMessage(Message->Msg.hwnd,Message->Msg.message,
Message->Msg.wParam,Message->Msg.lParam);
}

RemoveEntryList(&Message->ListEntry);//处理完后从接收方的Dispatching队列移除


if (!(Message->HookMessage & MSQ_SENTNOWAIT))
{
if (Message->DispatchingListEntry.Flink != NULL)
RemoveEntryList(&Message->DispatchingListEntry);//从发送方的Dispatching队列移除
}

if (Message->Result != NULL)
*Message->Result = Result;//返回处理结果

if (Message->HasPackedLParam == TRUE && Message->Msg.lParam)
ExFreePool((PVOID)Message->Msg.lParam);

if (Message->CompletionEvent != NULL)//唤醒发送方
KeSetEvent(Message->CompletionEvent, IO_NO_INCREMENT, FALSE);

//发送方可以调用SendMessageCallback这个API设置一个完成回调函数
if (Message->CompletionCallback != NULL)
{
//给发送方发送一个通知消息,通知它调用这个回调函数
//为什么不直接执行回调函数?因为回调函数是发送方提供的,是在发送方进程地址空间中
co_IntCallSentMessageCallback(Message->CompletionCallback,Message->Msg.hwnd,
Message->Msg.message,Message->CompletionCallbackContext,
Result);
}
if (!(Message->HookMessage & MSQ_SENTNOWAIT))
{
IntDereferenceMessageQueue(Message->SenderQueue);
IntDereferenceMessageQueue(MessageQueue);
}
ExFreePoolWithTag(Message, TAG_USRMSG);
return(TRUE);
}
post消息队列处理
co_MsqFindMessage

下面的函数用于在硬件消息队列或post消息队列中查找消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
BOOLEAN APIENTRY co_MsqFindMessage(IN PUSER_MESSAGE_QUEUE MessageQueue, 
IN BOOLEAN Hardware,//指是在硬件消息队列还是post消息队列中查找
IN BOOLEAN Remove,//是否移除队列
IN PWINDOW_OBJECT Window,//过滤条件
IN UINT MsgFilterLow,//过滤条件
IN UINT MsgFilterHigh,//过滤条件
OUT PUSER_MESSAGE* Message)//返回找到的消息
{
PLIST_ENTRY CurrentEntry;
PUSER_MESSAGE CurrentMessage;
PLIST_ENTRY ListHead;
if (Hardware)//硬件消息(鼠标消息)需要特殊处理
{
return(co_MsqPeekHardwareMessage( MessageQueue,Window,MsgFilterLow,MsgFilterHigh, Remove,Message));
}

CurrentEntry = MessageQueue->PostedMessagesListHead.Flink;
ListHead = &MessageQueue->PostedMessagesListHead;

while (CurrentEntry != ListHead)
{
CurrentMessage = CONTAINING_RECORD(CurrentEntry, USER_MESSAGE,ListEntry);
if ( ( !Window ||PtrToInt(Window) == 1 || Window->hSelf == CurrentMessage->Msg.hwnd )
&& ((MsgFilterLow == 0 && MsgFilterHigh == 0)
|| (MsgFilterLow <= CurrentMessage->Msg.message
&& MsgFilterHigh >= CurrentMessage->Msg.message ) ) )
{
if (Remove)
RemoveEntryList(&CurrentMessage->ListEntry);

*Message = CurrentMessage;
return(TRUE);
}
CurrentEntry = CurrentEntry->Flink;
}

return(FALSE);
}

上面的代码我想不用解释了。

由此可以看到在GetMessagePeekMessage函数内部,会一直等待到消息队列中出现符合指定条件的消息,这就是为什么GUI线程绝大多数时刻都处于睡眠状态的原因, 因为它在等待消息。
另外在GetMessagePeekMessage内部,即使因为队列中一直没有符合指定条件的消息而等待,也为在内部不断的处理别的线程通过SendMessage 方式发来的消息,以尽快完成这种消息的处理。

回到NtUserGetMessage内部继续执行co_IntPeekMessage函数。这个函数前面看过,它会两次调用co_MsqFindMessage,先查找线程post队列,然后查找硬件消息队列。
当时因为硬件消息处理特殊,省略没看,现在我们可以看他是如何查找硬件消息队列的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
BOOL co_MsqPeekHardwareMessage(PUSER_MESSAGE_QUEUE MessageQueue, PWINDOW_OBJECT Window, 
UINT FilterLow, UINT FilterHigh, BOOL Remove,PUSER_MESSAGE* Message)
{
PWINDOW_OBJECT DesktopWindow = NULL;
PDESKTOPINFO Desk = NULL;
WaitObjects[0] = &HardwareMessageQueueLock;
WaitObjects[1] = MessageQueue->NewMessages;//主要是等待这个事件

do
{
IdlePing();
UserLeaveCo();
WaitStatus = KeWaitForMultipleObjects(2, WaitObjects, WaitAny, UserRequest,
UserMode, FALSE, NULL, NULL);
UserEnterCo();
}
while (NT_SUCCESS(WaitStatus) && STATUS_WAIT_0 != WaitStatus);
//上面的循环一直等到NewMessages事件触发后,才来到这里
DesktopWindow = UserGetWindowObject(IntGetDesktopWindow());

if (DesktopWindow)

{
UserRefObjectCo(DesktopWindow, &Ref);
Desk = DesktopWindow->pti->pDeskInfo;
}
IntLockHardwareMessageQueue(MessageQueue);//锁定线程的硬件消息队列
CurrentEntry = MessageQueue->HardwareMessagesListHead.Flink;
while (CurrentEntry != &MessageQueue->HardwareMessagesListHead)//遍历线程的硬件消息队列
{
PUSER_MESSAGE Current = CONTAINING_RECORD(CurrentEntry, USER_MESSAGE, ListEntry);
CurrentEntry = CurrentEntry->Flink;
//我们只要鼠标消息
if (Current->Msg.message >= WM_MOUSEFIRST && Current->Msg.message <= WM_MOUSELAST)
{
//判断这个鼠标消息是不是我们需要的消息(即是否满足3个过滤条件)
Accept = co_MsqTranslateMouseMessage(MessageQueue, Window, FilterLow, FilterHigh,
Current, Remove, &Freed,DesktopWindow, &ScreenPoint, FALSE, &CurrentEntry);
if (Accept)//如果是,也即如果找到了一个需要的消息
{
if (Remove) RemoveEntryList(&Current->ListEntry);
IntUnLockHardwareMessageQueue(MessageQueue);
IntUnLockSystemHardwareMessageQueueLock(FALSE);
*Message = Current;//返回找到的消息
if (Desk) Desk->LastInputWasKbd = FALSE;
RETURN(TRUE);
}
}
}
IntUnLockHardwareMessageQueue(MessageQueue);
IntLockSystemMessageQueue(OldIrql);//锁定系统的环形缓冲消息队列
while (SystemMessageQueueCount > 0)//将环形缓冲消息队列中的消息全部转入系统硬件消息队列
{
PUSER_MESSAGE UserMsg;
MSG Msg;
Msg = SystemMessageQueue[SystemMessageQueueHead];
SystemMessageQueueHead = (SystemMessageQueueHead + 1) % SYSTEM_MESSAGE_QUEUE_SIZE;
SystemMessageQueueCount--;
IntUnLockSystemMessageQueue(OldIrql);
//系统硬件消息队列中的消息结构 与 环形缓冲消息队列中的消息结构不相同,要转换
UserMsg = ExAllocateFromPagedLookasideList(&MessageLookasideList);
UserMsg->FreeLParam = FALSE;
UserMsg->Msg = Msg;
InsertTailList(&HardwareMessageQueueHead, &UserMsg->ListEntry);//转入系统硬件消息队列


IntLockSystemMessageQueue(OldIrql);
}
HardwareMessageQueueStamp++;
IntUnLockSystemMessageQueue(OldIrql);

//转移所有缓冲消息到系统硬件消息队列后,现在开始扫描查找
CurrentEntry = HardwareMessageQueueHead.Flink;
while (CurrentEntry != &HardwareMessageQueueHead)
{
PUSER_MESSAGE Current = CONTAINING_RECORD(CurrentEntry, USER_MESSAGE, ListEntry);
CurrentEntry = CurrentEntry->Flink;
RemoveEntryList(&Current->ListEntry);//不管如何,先从系统硬件消息队列中取下来
HardwareMessageQueueStamp++;
if (Current->Msg.message >= WM_MOUSEFIRST && Current->Msg.message <= WM_MOUSELAST)
{
const ULONG ActiveStamp = HardwareMessageQueueStamp;
//判断是不是我们需要的消息(根据三个过滤条件)
Accept = co_MsqTranslateMouseMessage(MessageQueue, Window, FilterLow, FilterHigh,
Current, Remove, &Freed,
DesktopWindow, &ScreenPoint, TRUE, NULL);
if (Accept)//如果是我们需要的消息,也即如果找到了
{
IntLockSystemMessageQueue(OldIrql);
if (SystemMessageQueueCount == 0 && IsListEmpty(&HardwareMessageQueueHead))
KeClearEvent(&HardwareMessageEvent);//若两个队列都空了,复位事件
IntUnLockSystemMessageQueue(OldIrql);
if (!Remove)//将取下来的消息插回线程的硬件消息队列中
{
IntLockHardwareMessageQueue(MessageQueue);
if(Current->Msg.message == WM_MOUSEMOVE)//不过MouseMove特殊处理
{
if(MessageQueue->MouseMoveMsg)
{
RemoveEntryList(&MessageQueue->MouseMoveMsg->ListEntry);
ExFreePool(MessageQueue->MouseMoveMsg);//销毁原来的MouseMove消息
}
MessageQueue->MouseMoveMsg = Current;//修改替换成现在刚取下的MouseMove消息
}
//插入到线程的硬件消息队列中,体现为NoRemove
InsertTailList(&MessageQueue->HardwareMessagesListHead,&Current->ListEntry);
IntUnLockHardwareMessageQueue(MessageQueue);
}
IntUnLockSystemHardwareMessageQueueLock(FALSE);
*Message = Current;//返回找到的消息


RETURN(TRUE);
}
//if 队列的内容改变了,重新从头开始处理
if (HardwareMessageQueueStamp != ActiveStamp)
{
CurrentEntry = HardwareMessageQueueHead.Flink;
continue;
}
}
}
IntLockSystemMessageQueue(OldIrql);
if (SystemMessageQueueCount == 0 && IsListEmpty(&HardwareMessageQueueHead))
KeClearEvent(&HardwareMessageEvent);
IntUnLockSystemMessageQueue(OldIrql);
IntUnLockSystemHardwareMessageQueueLock(FALSE);
RETURN(FALSE); CLEANUP:
if (DesktopWindow) UserDerefObjectCo(DesktopWindow);
END_CLEANUP;
}

co_IntWaitMessage

具体的ntUserGetMessage我们看过,内部会调用co_IntPeekMessage接收消息,如果找不到,就调用co_IntWaitMessage等待。
我们看这个函数的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
BOOL FASTCALL co_IntWaitMessage( PWINDOW_OBJECT Window,UINT MsgFilterMin,UINT MsgFilterMax ) 
{
PTHREADINFO pti;
PUSER_MESSAGE_QUEUE ThreadQueue;
NTSTATUS Status = STATUS_SUCCESS;
USER_MESSAGE Msg;
pti = PsGetCurrentThreadWin32Thread();
ThreadQueue = pti->MessageQueue;
do
{
//if 有消息,返回
if ( co_IntPeekMessage( &Msg,Window,MsgFilterMin,MsgFilterMax,PM_NOREMOVE))//不移除
return TRUE;
//否则,等待

Status = co_MsqWaitForNewMessages( ThreadQueue,Window,MsgFilterMin,MsgFilterMax);
}while ( (STATUS_WAIT_0 <= Status && Status <= STATUS_WAIT_63));

//上面的循环,每次一个新消息进队后,就回头去检测队列中是否有需要的消息
if (!NT_SUCCESS(Status))
SetLastNtError(Status);
return FALSE;
}

NTSTATUS FASTCALL co_MsqWaitForNewMessages(PUSER_MESSAGE_QUEUE MessageQueue, PWINDOW_OBJECT WndFilter,
UINT MsgFilterMin, UINT MsgFilterMax)
{
//等待线程的消息队列,全局的硬件消息队列两个队列的新消息事件
PVOID WaitObjects[2] = {MessageQueue->NewMessages, &HardwareMessageEvent};
NTSTATUS ret;
ret = KeWaitForMultipleObjects(2,WaitObjects,WaitAny,Executive,UserMode,FALSE,
NULL,NULL);
return ret;
}

上面个函数先在本地线程的硬件消息队列中查找,找不到就再去全局的系统硬件消息队列中查找


co_MsqTranslateMouseMessage

具体的查找判定函数看下面:(这个函数专用于判定鼠标消息是否符合过滤条件)
关键的判断比较操作就是比较光标处窗口是不是我们的窗口。
另外从以上可以看出鼠标消息的流动方向是:系统环形缓冲消息队列->系统硬件消息队列->目标线程硬件消息队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
BOOL co_MsqTranslateMouseMessage(PUSER_MESSAGE_QUEUE MessageQueue, 
PWINDOW_OBJECT Window, UINT FilterLow, UINT FilterHigh,//过滤条件
PUSER_MESSAGE Message,//IN\OUT,返回该消息的目标窗口
BOOL Remove,//没用
PBOOL Freed,//返回是否需要释放L附件包
PWINDOW_OBJECT ScopeWin, //一般就是桌面窗口
PPOINT ScreenPoint,//返回鼠标消息的产生位置
BOOL FromGlobalQueue)//指Message参数是否是全局硬件消息队列中的
{
USHORT Msg = Message->Msg.message;
PWINDOW_OBJECT CaptureWindow = NULL; //鼠标消息发往的目标窗口 HWND hCaptureWin;//鼠标消息发往的目标窗口
hCaptureWin = IntGetCaptureWindow();//当前的鼠标焦点窗口(很少有鼠标焦点)
if (hCaptureWin == NULL)//一般总是NULL
{
if (Msg == WM_MOUSEWHEEL)//滚轮消息总是发给键盘焦点窗口的
{

CaptureWindow = UserGetWindowObject(IntGetFocusWindow());//键盘焦点窗口
if (CaptureWindow) UserReferenceObject(CaptureWindow);
}
else
{
//关键。计算鼠标消息产生时,当时光标处的窗口
co_WinPosWindowFromPoint(ScopeWin, NULL, &Message->Msg.pt, &CaptureWindow);
if(CaptureWindow == NULL)
{
CaptureWindow = ScopeWin;//如果光标处没有窗口,就是桌面窗口
if (CaptureWindow) UserReferenceObject(CaptureWindow);
}
}
}
Else //很少见。若有鼠标焦点窗口,所有鼠标消息都发给当前鼠标焦点窗口
{
CaptureWindow = UserGetWindowObject(hCaptureWin);
if (CaptureWindow) UserReferenceObject(CaptureWindow);
}
if (CaptureWindow == NULL) …

//如果现在的光标处窗口 与 当初鼠标消息刚产生时的光标处窗口不同了,将消息从消息队列中移除, 转发给现在的光标处窗口的所属线程
if (CaptureWindow->pti->MessageQueue != MessageQueue)
{
if (! FromGlobalQueue)
{
RemoveEntryList(&Message->ListEntry);
if(MessageQueue->MouseMoveMsg == Message)
MessageQueue->MouseMoveMsg = NULL;
}
IntLockHardwareMessageQueue(CaptureWindow->pti->MessageQueue);
//转入新的目标线程
InsertTailList(&CaptureWindow->pti->MessageQueue->HardwareMessagesListHead,
&Message->ListEntry);
if(Message->Msg.message == WM_MOUSEMOVE) // WM_MOUSEMOV需要特殊处理
{
if(CaptureWindow->pti->MessageQueue->MouseMoveMsg)
{
RemoveEntryList(&CaptureWindow->pti->MessageQueue->MouseMoveMsg->ListEntry);
ExFreePool(CaptureWindow->pti->MessageQueue->MouseMoveMsg);
}
CaptureWindow->pti->MessageQueue->MouseMoveMsg = Message;


CaptureWindow->pti->MessageQueue->QueueBits |= QS_MOUSEMOVE;
CaptureWindow->pti->MessageQueue->ChangedBits |= QS_MOUSEMOVE;
if (CaptureWindow->pti->MessageQueue->WakeMask & QS_MOUSEMOVE)
KeSetEvent(CaptureWindow->pti->MessageQueue->NewMessages, 0, FALSE);
}
else
{
CaptureWindow->pti->MessageQueue->QueueBits |= QS_MOUSEBUTTON;
CaptureWindow->pti->MessageQueue->ChangedBits |= QS_MOUSEBUTTON;
if (CaptureWindow->pti->MessageQueue->WakeMask & QS_MOUSEBUTTON)
KeSetEvent(CaptureWindow->pti->MessageQueue->NewMessages, 0, FALSE);
}
IntUnLockHardwareMessageQueue(CaptureWindow->pti->MessageQueue);
*Freed = FALSE;
UserDereferenceObject(CaptureWindow);
return(FALSE);
}


//否则为同一个消息队列(也即光标处的窗口没发生变化)
*ScreenPoint = Message->Msg.pt;//返回鼠标消息产生时的光标位置

//关键。当计算出该鼠标消息发往的目标窗口后,现在比较判断这条消息是不是我们想要的消息
//if 不是我们想要的消息,简单转入线程的硬件消息队列
if((Window != NULL && PtrToInt(Window) != 1 && CaptureWindow->hSelf != Window->hSelf) ||
((FilterLow != 0 || FilterHigh != 0) && (Msg < FilterLow || Msg > FilterHigh)))
{
if(FromGlobalQueue)//if 是系统硬件消息队列中的消息
{
IntLockHardwareMessageQueue(CaptureWindow->pti->MessageQueue);
//转入线程的硬件消息队列
InsertTailList(&CaptureWindow->pti->MessageQueue->HardwareMessagesListHead,
&Message->ListEntry);
}
if (Message->Msg.message == WM_MOUSEMOVE) //特殊处理
{
if(CaptureWindow->pti->MessageQueue->MouseMoveMsg &&
(CaptureWindow->pti->MessageQueue->MouseMoveMsg != Message))
{
RemoveEntryList(&CaptureWindow->pti->MessageQueue->MouseMoveMsg->ListEntry);
ExFreePool(CaptureWindow->pti->MessageQueue->MouseMoveMsg);
}
CaptureWindow->pti->MessageQueue->MouseMoveMsg = Message;
}

if(FromGlobalQueue)
IntUnLockHardwareMessageQueue(CaptureWindow->pti->MessageQueue);
UserDereferenceObject(CaptureWindow);
*Freed = FALSE;
return(FALSE);
}

//关键。If是我们想要的消息,返回这条消息
Message->Msg.hwnd = CaptureWindow->hSelf;//返回目标窗口
Message->Msg.lParam = MAKELONG(Message->Msg.pt.x, Message->Msg.pt.y);//构造好l参数
if (Message->Msg.message == WM_MOUSEMOVE || Message->Msg.message == WM_NCMOUSEMOVE)
{
if(FromGlobalQueue) //销毁原MouseMove消息
{
IntLockHardwareMessageQueue(CaptureWindow->pti->MessageQueue);
if(CaptureWindow->pti->MessageQueue->MouseMoveMsg)
{
RemoveEntryList(&CaptureWindow->pti->MessageQueue->MouseMoveMsg->ListEntry);
ExFreePool(CaptureWindow->pti->MessageQueue->MouseMoveMsg);
CaptureWindow->pti->MessageQueue->MouseMoveMsg = NULL;
}
IntUnLockHardwareMessageQueue(CaptureWindow->pti->MessageQueue);
}
else if (CaptureWindow->pti->MessageQueue->MouseMoveMsg == Message)
CaptureWindow->pti->MessageQueue->MouseMoveMsg = NULL;
}
UserDereferenceObject(CaptureWindow);
*Freed = FALSE;
return(TRUE);
}




DispatchMessageW

Windows 消息的产生来源(也即发送方的类型)

  • 1、 一个线程调用SendMessagePostMessage等函数给自己或别的线程发消息,它就是一个发送方线程
  • 2、 内核中键盘输入守护线程(每当得到一个按键后就发送到当时键盘焦点线程的消息队列)
  • 3、 内核中的鼠标输入守护线程(每当得到一个鼠标消息后就发送到系统的环形缓冲消息队列)
  • 4、 系统win32k.sys模块自己生成、发送消息到目标线程的队列(如定时器消息)

当应用程序的消息循环中,通过GetMessage/PeekMessage从消息队列中取出来得到一个消息后,会调用TranslateMessage将键盘按键消息转为字符消息,然后调用DispatchMessage将消息派遣到目标窗口的窗口过程进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
LRESULT  DispatchMessageW(CONST MSG *lpmsg) 
{
LRESULT Ret = 0;
PWND Wnd;
if (lpmsg->hwnd != NULL)
{
Wnd = ValidateHwnd(lpmsg->hwnd);
if (!Wnd || Wnd->head.pti != GetW32ThreadInfo())
return 0;
}
else
Wnd = NULL;

if ((lpmsg->message == WM_TIMER || lpmsg->message == WM_SYSTIMER) && lpmsg->lParam != 0)
{
if ( lpmsg->message == WM_SYSTIMER )
return NtUserDispatchMessage( (PMSG) lpmsg );

WNDPROC WndProc = (WNDPROC)lpmsg->lParam;//有回调函数类型的定时器就在此直接调用了
Ret = WndProc(lpmsg->hwnd,lpmsg->message,lpmsg->wParam,GetTickCount());
}
else if (Wnd != NULL)
{
if ( (lpmsg->message != WM_PAINT) && !(Wnd->state & WNDS_SERVERSIDEWINDOWPROC) )
{
//调用窗口过程
Ret = IntCallMessageProc(Wnd,lpmsg->hwnd,lpmsg->message,
lpmsg->wParam,lpmsg->lParam,FALSE);

}
else
Ret = NtUserDispatchMessage( (PMSG) lpmsg );
}
return Ret;
}

LRESULT IntCallMessageProc(IN PWND Wnd, IN HWND hWnd, IN UINT Msg, IN WPARAM wParam, IN LPARAM lParam, IN BOOL Ansi)
{
WNDPROC WndProc;
BOOL IsAnsi;
PCLS Class;
Class = DesktopPtrToUser(Wnd->pcls);
WndProc = NULL;
if (Class->fnid <= FNID_GHOST && Class->fnid >= FNID_FIRST ) …
else
{
IsAnsi = !Wnd->Unicode;
WndProc = Wnd->lpfnWndProc;
}

if (!Ansi)
return IntCallWindowProcW(IsAnsi, WndProc, Wnd, hWnd, Msg, wParam, lParam);
else
return IntCallWindowProcA(IsAnsi, WndProc, Wnd, hWnd, Msg, wParam, lParam);
}

IntCallWindowProcW

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
LRESULT FASTCALL IntCallWindowProcW(BOOL IsAnsiProc,WNDPROC WndProc,PWND pWnd, 
HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam)
{
MSG AnsiMsg;
MSG UnicodeMsg;
BOOL Hook = FALSE, MsgOverride = FALSE, Dialog;
LRESULT Result = 0, PreResult = 0;
DWORD Data = 0;
if (pWnd)
Dialog = (pWnd->fnid == FNID_DIALOG);//检查是否是个对话框
else
Dialog = FALSE;

Hook = BeginIfHookedUserApiHook();
if (Hook)
{

if (!Dialog)
MsgOverride = IsMsgOverride( Msg, &guah.WndProcArray);
else
MsgOverride = IsMsgOverride( Msg, &guah.DlgProcArray);
}

if (IsAnsiProc) …
else
{
if (Hook && MsgOverride)//先预处理
{
if (!Dialog)
PreResult = guah.PreWndProc(hWnd, Msg, wParam, lParam, &Result, &Data );
else
PreResult = guah.PreDefDlgProc(hWnd, Msg, wParam, lParam, &Result, &Data);
}

if (PreResult) goto Exit;//若预处理完成了处理,直接退出
Result = WndProc(hWnd, Msg, wParam, lParam);//关键。调用目标窗口过程

if (Hook && MsgOverride)//后处理
{
if (!Dialog)
guah.PostWndProc(hWnd, Msg, wParam, lParam, &Result, &Data );
else
guah.PostDefDlgProc(hWnd, Msg, wParam, lParam, &Result, &Data );
}
}

Exit:
if (Hook)
EndUserApiHook();

return Result;
}

PostMessageW

下面看看消息的发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BOOL PostMessageW(HWND hWnd, 
UINT Msg,
WPARAM wParam,
LPARAM lParam)
{
LRESULT Ret;
if (Msg == CB_DIR || Msg == LB_DIR) …
//最典型
if ((Msg != WM_DROPFILES) || (NtUserQueryWindow( hWnd, QUERY_WINDOW_UNIQUE_PROCESS_ID) == PtrToUint(NtCurrentTeb()->ClientId.UniqueProcess) ) )
{
return PostMessageWorker(hWnd, Msg, wParam, lParam);
}

}

PostMessageWorker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
BOOL FASTCALL PostMessageWorker(  HWND Wnd, 
UINT Msg,
WPARAM wParam,
LPARAM lParam)
{
MSG UMMsg, KMMsg;
LRESULT Result;
UMMsg.hwnd = Wnd;
UMMsg.message = Msg;
UMMsg.wParam = wParam;
UMMsg.lParam = lParam; MsgiUMToKMMessage(&UMMsg, &KMMsg, TRUE);
//调用这个系统服务
Result = NtUserPostMessage( Wnd,KMMsg.message,KMMsg.wParam,KMMsg.lParam);
MsgiUMToKMCleanup(&UMMsg, &KMMsg);
return Result;
}

//系统服务
BOOL NtUserPostMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam)
{
RETURN(UserPostMessage(hWnd, Msg, wParam, lParam));
}

UserPostMessage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
BOOL FASTCALL UserPostMessage( HWND Wnd,UINT Msg,WPARAM wParam,LPARAM lParam) 
{
PTHREADINFO pti;
MSG Message;
LARGE_INTEGER LargeTickCount;
if (FindMsgMemory(Msg) != 0)
{
SetLastWin32Error(ERROR_MESSAGE_SYNC_ONLY );
return FALSE;
}

if (Wnd==NULL)
return UserPostThreadMessage(PtrToInt(PsGetCurrentThreadId()),Msg,wParam,lParam);

if (Wnd == HWND_BROADCAST) … //广播给桌面所有顶层窗口,略
else
{
PWINDOW_OBJECT Window;
Window = UserGetWindowObject(Wnd);
pti = Window->Wnd->head.pti;
if ( Window->state & WINDOWSTATUS_DESTROYING )
return FALSE;

if (WM_QUIT == Msg)//这种消息的发送特殊处理
MsqPostQuitMessage(Window->pti->MessageQueue, wParam);
else
{
Message.hwnd = Wnd;
Message.message = Msg;
Message.wParam = wParam;
Message.lParam = lParam;
Message.pt = gpsi->ptCursor;
KeQueryTickCount(&LargeTickCount);
pti->timeLast = Message.time = MsqCalculateMessageTime(&LargeTickCount);
//实质函数
MsqPostMessage(Window->pti->MessageQueue, &Message, FALSE, QS_POSTMESSAGE);
}
}
return TRUE;
}

MsqPostMessage

WM_QUIT消息特殊处理,实际上这种消息不进队,会被接收方优先处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
VOID FASTCALL MsqPostQuitMessage(PUSER_MESSAGE_QUEUE MessageQueue, ULONG ExitCode) 
{
MessageQueue->QuitPosted = TRUE;
MessageQueue->QuitExitCode = ExitCode;
MessageQueue->QueueBits |= QS_POSTMESSAGE;
MessageQueue->ChangedBits |= QS_POSTMESSAGE;
if (MessageQueue->WakeMask & QS_POSTMESSAGE) //符合唤醒条件,就唤醒目标线程
KeSetEvent(MessageQueue->NewMessages, IO_NO_INCREMENT, FALSE);
}


VOID FASTCALL MsqPostMessage(PUSER_MESSAGE_QUEUE MessageQueue, MSG* Msg, BOOLEAN FreeLParam,
DWORD MessageBits)

{
PUSER_MESSAGE Message;
Message = MsqCreateMessage(Msg, FreeLParam);//分配一个队列中的消息结构


//关键。将消息挂入目标线程的post队列
InsertTailList(&MessageQueue->PostedMessagesListHead,&Message->ListEntry);
MessageQueue->QueueBits |= MessageBits;
MessageQueue->ChangedBits |= MessageBits;
if (MessageQueue->WakeMask & MessageBits)
KeSetEvent(MessageQueue->NewMessages, IO_NO_INCREMENT, FALSE);
}

由上可以看出PostMesage的处理是很简单的,它仅仅将消息挂入目标线程的post消息队列,唤醒目标线程后立即返回。
因此我们说它是异步的。


SendMessage

下面看SendMessage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
LRESULT SendMessageW(HWND Wnd,UINT Msg,WPARAM wParam,LPARAM lParam) 
{
MSG UMMsg, KMMsg;
NTUSERSENDMESSAGEINFO Info;
LRESULT Result;
PWND Window;
PTHREADINFO ti = GetW32ThreadInfo();

Window = ValidateHwnd(Wnd);//验证窗口句柄是否有效

if (Window==NULL) return FALSE;

if (Wnd != HWND_BROADCAST && (Msg < WM_DDE_FIRST || Msg > WM_DDE_LAST))
{
if (Window != NULL && Window->head.pti == ti && !IsThreadHooked(GetWin32ClientInfo()))
return IntCallMessageProc(Window, Wnd, Msg, wParam, lParam, FALSE);
}
UMMsg.hwnd = Wnd;
UMMsg.message = Msg;
UMMsg.wParam = wParam;
UMMsg.lParam = lParam;

MsgiUMToKMMessage(&UMMsg, &KMMsg, FALSE);
Info.Ansi = FALSE;

//调用这个系统服务
Result = NtUserSendMessage( KMMsg.hwnd,KMMsg.message,KMMsg.wParam,KMMsg.lParam,&Info);

if (!Info.HandledByKernel)//if内部没有执行这条消息(因为同一个线程),就自己处理
{
MsgiUMToKMCleanup(&UMMsg, &KMMsg);
Result = IntCallWindowProcW( Info.Ansi, Info.Proc,//NtUserSendMessage返回的proc
Window,UMMsg.hwnd,UMMsg.message,UMMsg.wParam,UMMsg.lParam);
}

MsgiUMToKMReply(&UMMsg, &KMMsg, &Result);
return Result;
}

上面这个函数真正调用NtUserSendMessage这个系统服务完成消息的发送
不过若是要发到同一线程的话,就不用发了,直接回到下面

在本线程中直接调用窗口过程就 OK 了。

NtUserSendMessage

1
2
3
4
5
LRESULT NtUserSendMessage(HWND Wnd,UINT Msg,WPARAM wParam,LPARAM lParam, 
PNTUSERSENDMESSAGEINFO UnsafeInfo )
{
RETURN(co_IntDoSendMessage(Wnd, Msg, wParam, lParam, NULL, UnsafeInfo));
}

co_IntDoSendMessage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
LRESULT FASTCALL co_IntDoSendMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam, 
PDOSENDMESSAGE dsm,//一般为NULL
PNTUSERSENDMESSAGEINFO UnsafeInfo )//返回处理情况
{
LRESULT Result = TRUE;
PWINDOW_OBJECT Window = NULL;
NTUSERSENDMESSAGEINFO Info;
RtlZeroMemory(&Info, sizeof(NTUSERSENDMESSAGEINFO));
pti = PsGetCurrentThreadWin32Thread();

if (HWND_BROADCAST != hWnd && NULL != pti &&
Window->pti->MessageQueue == pti->MessageQueue && //同一个线程
!ISITHOOKED(WH_CALLWNDPROC) && !ISITHOOKED(WH_CALLWNDPROCRET) &&
(Msg < WM_DDE_FIRST || Msg > WM_DDE_LAST ) )
{
Info.HandledByKernel = FALSE;//内部就不发送了,直接由本线程调用窗口过程自己处理
Status = MmCopyFromCaller(&(Info.Ansi), &(UnsafeInfo->Ansi),sizeof(BOOL));
Info.Ansi = !Window->Wnd->Unicode;
Info.Proc = Window->Wnd->lpfnWndProc;
}
else
{
Info.HandledByKernel = TRUE;//内部处理
UserModeMsg.hwnd = hWnd;
UserModeMsg.message = Msg;
UserModeMsg.wParam = wParam;
UserModeMsg.lParam = lParam;
MsgMemoryEntry = FindMsgMemory(UserModeMsg.message);
Status = CopyMsgToKernelMem(&KernelModeMsg, &UserModeMsg, MsgMemoryEntry);
if(!dsm)//典型
{
Result = co_IntSendMessage( KernelModeMsg.hwnd,KernelModeMsg.message,
KernelModeMsg.wParam,KernelModeMsg.lParam );
}

else
{
Result = co_IntSendMessageTimeout( KernelModeMsg.hwnd,KernelModeMsg.message,
KernelModeMsg.wParam,KernelModeMsg.lParam,
dsm->uFlags,dsm->uTimeout,&dsm->Result );
}
Status = CopyMsgToUserMem(&UserModeMsg, &KernelModeMsg);
}

Status = MmCopyToCaller(UnsafeInfo, &Info, sizeof(NTUSERSENDMESSAGEINFO));

if (! NT_SUCCESS(Status))
SetLastWin32Error(ERROR_INVALID_PARAMETER);

return (LRESULT)Result;
}

上面的函数,发现窗口所属的目标线程就是当前现成的额话,就不用发送了,直接由本线程自己处理
否则调用co_IntSendMessage发送该消息到目标线程的send消息队列

co_IntSendMessage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
LRESULT FASTCALL co_IntSendMessage( HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam ) 
{
ULONG_PTR Result = 0;
if(co_IntSendMessageTimeout(hWnd, Msg, wParam, lParam, 0, 0, &Result))
return (LRESULT)Result;
return 0;
}

LRESULT FASTCALL co_IntSendMessageTimeout( HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam,
UINT uFlags,UINT uTimeout,ULONG_PTR *uResult )
{
PWINDOW_OBJECT DesktopWindow;
HWND *Children;
HWND *Child;
if (HWND_BROADCAST != hWnd)
return co_IntSendMessageTimeoutSingle(hWnd, Msg, wParam, lParam, uFlags, uTimeout, uResult);

DesktopWindow = UserGetWindowObject(IntGetDesktopWindow());
Children = IntWinListChildren(DesktopWindow);
for (Child = Children; NULL != *Child; Child++)//广播消息处理
co_IntSendMessageTimeoutSingle(*Child, Msg, wParam, lParam, uFlags, uTimeout, uResult);

ExFreePool(Children);
return (LRESULT) TRUE;
}
co_IntSendMessageTimeoutSingle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
LRESULT FASTCALL co_IntSendMessageTimeoutSingle( HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam, 

UINT uFlags,UINT uTimeout,ULONG_PTR *uResult )
{
PWINDOW_OBJECT Window = NULL;
if (!(Window = UserGetWindowObject(hWnd)))
RETURN( FALSE);

UserRefObjectCo(Window, &Ref);
Win32Thread = PsGetCurrentThreadWin32Thread();
IntCallWndProc( Window, hWnd, Msg, wParam, lParam);//调用钩子
//再次检查是否是同一消息队列(因为可能前面是广播窗口的原因),若是,就地处理
if ( NULL != Win32Thread && Window->pti->MessageQueue == Win32Thread->MessageQueue)
{
if (Win32Thread->TIF_flags & TIF_INCLEANUP)
RETURN( FALSE);

MsgMemoryEntry = FindMsgMemory(Msg);

if (NULL == MsgMemoryEntry)
lParamBufferSize = -1;
else
lParamBufferSize = MsgMemorySize(MsgMemoryEntry, wParam, lParam);

PackParam(&lParamPacked, Msg, wParam, lParam, FALSE);//L附件打包到用户空间
Result = co_IntCallWindowProc(Window->Wnd->lpfnWndProc,!Window->Wnd->Unicode, hWnd,Msg,wParam,lParamPacked,lParamBufferSize);
if(uResult)
*uResult = Result;

IntCallWndProcRet( Window, hWnd, Msg, wParam, lParam, (LRESULT *)uResult);//调用钩子

UnpackParam(lParamPacked, Msg, wParam, lParam, FALSE);
RETURN( TRUE);
}
//若不是同一线程,就真的发送了
if (uFlags & SMTO_ABORTIFHUNG && MsqIsHung(Window->pti->MessageQueue))
RETURN( FALSE);
if (Window->state & WINDOWSTATUS_DESTROYING)
RETURN( FALSE);
do
{
Status = co_MsqSendMessage( Window->pti->MessageQueue,hWnd,Msg,wParam,lParam, uTimeout, (uFlags & SMTO_BLOCK),MSQ_NORMAL,uResult);
}
while ((STATUS_TIMEOUT == Status) && (uFlags & SMTO_NOTIMEOUTIFNOTHUNG) && !MsqIsHung(Window->pti->MessageQueue));

IntCallWndProcRet( Window, hWnd, Msg, wParam, lParam, (LRESULT *)uResult);
if (STATUS_TIMEOUT == Status)
{
SetLastWin32Error(ERROR_TIMEOUT);
RETURN( FALSE);
}
else if (! NT_SUCCESS(Status))
{
SetLastNtError(Status);
RETURN( FALSE);
}
RETURN( TRUE);
}
co_MsqSendMessage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
NTSTATUS FASTCALL co_MsqSendMessage(PUSER_MESSAGE_QUEUE MessageQueue,HWND Wnd, UINT Msg, WPARAM wParam, 
LPARAM lParam,UINT uTimeout,
BOOL Block,//指是否一直要等到消息被接收方处理
INT HookMessage,//指是否是那个专用的‘底层键盘鼠标钩子调用请求’消息
ULONG_PTR *uResult)
{
PTHREADINFO pti;
PUSER_SENT_MESSAGE Message;//send队列中的消息结构
KEVENT CompletionEvent;
NTSTATUS WaitStatus;
LRESULT Result;
PUSER_MESSAGE_QUEUE ThreadQueue;
LARGE_INTEGER Timeout;
PLIST_ENTRY Entry;

Message = ExAllocatePoolWithTag(PagedPool, sizeof(USER_SENT_MESSAGE), TAG_USRMSG)
KeInitializeEvent(&CompletionEvent, NotificationEvent, FALSE);

pti = PsGetCurrentThreadWin32Thread();
ThreadQueue = pti->MessageQueue;//本线程的消息队列
ASSERT(ThreadQueue != MessageQueue);//可以肯定

Timeout.QuadPart = (LONGLONG) uTimeout * (LONGLONG) -10000;

Result = 0;
Message->Msg.hwnd = Wnd;
Message->Msg.message = Msg;
Message->Msg.wParam = wParam;
Message->Msg.lParam = lParam;
Message->CompletionEvent = &CompletionEvent;//关键字段
Message->Result = &Result;
Message->SenderQueue = ThreadQueue;//发送发线程的消息队列
IntReferenceMessageQueue(ThreadQueue);
Message->CompletionCallback = NULL;
Message->HookMessage = HookMessage;
Message->HasPackedLParam = FALSE;
IntReferenceMessageQueue(MessageQueue);


//关键。一头插入本线程的Dispatching阻塞队列中
InsertTailList(&ThreadQueue->DispatchingMessagesHead, &Message->DispatchingListEntry);
//关键。一头插入目标线程的send队列中
InsertTailList(&MessageQueue->SentMessagesListHead, &Message->ListEntry);
MessageQueue->QueueBits |= QS_SENDMESSAGE;
MessageQueue->ChangedBits |= QS_SENDMESSAGE;
if (MessageQueue->WakeMask & QS_SENDMESSAGE)
KeSetEvent(MessageQueue->NewMessages, IO_NO_INCREMENT, FALSE);
//下面的等待操作单独括起来
{
PVOID WaitObjects[2];
WaitObjects[0] = &CompletionEvent;
WaitObjects[1] = ThreadQueue->NewMessages;
Int count = Block? 1 : 2//等待的对象个数不同
do
{
WaitStatus = KeWaitForMultipleObjects(count, WaitObjects, WaitAny, UserRequest,
UserMode, FALSE, (uTimeout ? &Timeout : NULL), NULL);
if(WaitStatus == STATUS_TIMEOUT)
{
//超时醒来的第一件事:清除目标线程send队列中这个消息的完成事件
Entry = MessageQueue->SentMessagesListHead.Flink;
while (Entry != &MessageQueue->SentMessagesListHead)
{
if (CONTAINING_RECORD(Entry, USER_SENT_MESSAGE, ListEntry)== Message)
{
Message->CompletionEvent = NULL;
Message->Result = NULL;
break;
}
Entry = Entry->Flink;
}
//超时醒来的第二件事:清除本线程Disatching队列中的这个消息
Entry = ThreadQueue->DispatchingMessagesHead.Flink;
while (Entry != &ThreadQueue->DispatchingMessagesHead)
{
if (CONTAINING_RECORD(Entry, USER_SENT_MESSAGE, DispatchingListEntry) == Message)
{
Message->CompletionEvent = NULL;
Message->Result = NULL;
RemoveEntryList(&Message->DispatchingListEntry);//关键
Message->DispatchingListEntry.Flink = NULL;

break;
}
Entry = Entry->Flink;
}
break;
}
while (co_MsqDispatchOneSentMessage(ThreadQueue));//扫描执行本线程send队列中的消息

} while (NT_SUCCESS(WaitStatus) && STATUS_WAIT_0 != WaitStatus);
}
if(WaitStatus != STATUS_TIMEOUT)
*uResult = (STATUS_WAIT_0 == WaitStatus ? Result : -1);
return WaitStatus;
}


消息钩子

消息钩子:(相信这是大家最感兴趣的了)

SetWindowsHookExW

下面这个函数用来为指定线程安装一个指定类型的钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
HHOOK SetWindowsHookExW( 
int idHook,//钩子类型
HOOKPROC lpfn,//钩子函数
HINSTANCE hMod,//钩子函数所在模块
DWORD dwThreadId)//为0就表示全局钩子
{
return IntSetWindowsHook(idHook, lpfn, hMod, dwThreadId, FALSE);
}

HHOOK FASTCALL IntSetWindowsHook(
int idHook,
HOOKPROC lpfn,
HINSTANCE hMod,
DWORD dwThreadId,
BOOL bAnsi)
{
WCHAR ModuleName[MAX_PATH];
UNICODE_STRING USModuleName;
if (NULL != hMod)
{
if (0 == GetModuleFileNameW(hMod, ModuleName, MAX_PATH))
return NULL;
RtlInitUnicodeString(&USModuleName, ModuleName);
}
else
RtlInitUnicodeString(&USModuleName, NULL);

//调用系统服务
return NtUserSetWindowsHookEx(hMod, &USModuleName, dwThreadId, idHook, lpfn, bAnsi);
}

NtUserSetWindowsHookEx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
HHOOK NtUserSetWindowsHookEx(HINSTANCE Mod, 
PUNICODE_STRING UnsafeModuleName,
DWORD ThreadId,//指定线程,0表示全局
int HookId,//钩子类型
HOOKPROC HookProc,
BOOL Ansi)
{
BOOLEAN ThreadReferenced = FALSE;
ClientInfo = GetWin32ClientInfo();
if (ThreadId!=0)//if 特定线程
{
//这些钩子本就是全局的,不支持局部线程
if (HookId == WH_JOURNALRECORD ||HookId == WH_JOURNALPLAYBACK || HookId == WH_KEYBOARD_LL ||HookId == WH_MOUSE_LL ||HookId == WH_SYSMSGFILTER)
{
SetLastWin32Error(ERROR_INVALID_PARAMETER);
RETURN( NULL);
}

Mod = NULL;
Global = FALSE;
PsLookupThreadByThreadId(ThreadId, &Thread);//根据线程ID找到对应的线程对象


ThreadReferenced = TRUE;
if (Thread->ThreadsProcess != PsGetCurrentProcess())//只能是当前进程中的其他线程
{
ObDereferenceObject(Thread);
SetLastWin32Error(ERROR_INVALID_PARAMETER);

RETURN( NULL);
}
}
else //if全局钩子
{
if (HookId == WH_KEYBOARD_LL || HookId == WH_MOUSE_LL)
{
Mod = NULL;
Thread = PsGetCurrentThread();
Status = ObReferenceObjectByPointer(Thread,THREAD_ALL_ACCESS,
PsThreadType,KernelMode);
if (!NT_SUCCESS(Status))//if权限检查失败
{
SetLastNtError(Status);
RETURN( (HANDLE) NULL);
}

ThreadReferenced = TRUE;
}
else if (NULL == Mod)//全局钩子必须指定模块
{
SetLastWin32Error(ERROR_HOOK_NEEDS_HMOD);
RETURN( NULL);
}
else
Thread = NULL;//全局钩子的目标线程等于NULL,但是底层键盘鼠标类钩子除外
Global = TRUE;
}

Status = IntValidateWindowStationHandle(PsGetCurrentProcess()->Win32WindowStation,
KernelMode,0,&WinStaObj);
if (!NT_SUCCESS(Status)) …


//关键。在对应类型的钩子队列中挂入一个钩子
Hook = IntAddHook(Thread, HookId, Global, WinStaObj);

if (NULL == Hook)
{
if (ThreadReferenced)
ObDereferenceObject(Thread);

ObDereferenceObject(WinStaObj);
RETURN( NULL);
}
if (ThreadReferenced)
Hook->Flags |= HOOK_THREAD_REFERENCED;

if (NULL != Mod) //如果提供了模块,那HookProc就是模块内的相对偏移
{
Status = MmCopyFromCaller(&ModuleName, UnsafeModuleName, sizeof(UNICODE_STRING));

Hook->ModuleName.Buffer = ExAllocatePoolWithTag(PagedPool,
ModuleName.MaximumLength,TAG_HOOK);
Hook->ModuleName.MaximumLength = ModuleName.MaximumLength;
Status = MmCopyFromCaller(Hook->ModuleName.Buffer,ModuleName.Buffer,
ModuleName.MaximumLength);
Hook->ModuleName.Length = ModuleName.Length;
Hook->Proc = (void *)((char *)HookProc - (char *)Mod);//相对偏移
}
Else //否则,就是直接的地址
Hook->Proc = HookProc;
Hook->Ansi = Ansi;
Handle = UserHMGetHandle(Hook);//钩子句柄

ClientInfo->phkCurrent = 0;
UserDereferenceObject(Hook);
ObDereferenceObject(WinStaObj);
RETURN( Handle);

CLEANUP:…
}

IntAddHook

实质函数是IntAddHook,这个函数将指定钩子插入指定钩子表的对应类型的钩子队列中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
PHOOK IntAddHook(PETHREAD Thread, int HookId, BOOLEAN Global, PWINSTATION_OBJECT WinStaObj) 
{
PTHREADINFO W32Thread;
PHOOK Hook;
HANDLE Handle;
//全局的钩子表或每个线程局部的钩子表
PHOOKTABLE Table = Global ? GlobalHooks :
MsqGetHooks(((PTHREADINFO)Thread->Tcb.Win32Thread)->MessageQueue);

if (NULL == Table)//分配钩子表
{
Table = IntAllocHookTable();
if (Global)
GlobalHooks = Table;
else
MsqSetHooks(((PTHREADINFO)Thread->Tcb.Win32Thread)->MessageQueue, Table);
}

//创建钩子对象(钩子像窗口一样,也是一种GUI对象,放在全局的GUI对象句柄表中)
Hook = UserCreateObject(gHandleTable, NULL, &Handle, otHook, sizeof(HOOK));
Hook->Thread = Thread;
Hook->HookId = HookId;
if (Thread)//局部钩子
{
W32Thread = ((PTHREADINFO)Thread->Tcb.Win32Thread);

//fsHooks表示钩子掩码
W32Thread->fsHooks |= HOOKID_TO_FLAG(HookId);
if (W32Thread->pClientInfo)
W32Thread->pClientInfo->fsHooks = W32Thread->fsHooks;
if (W32Thread->pDeskInfo)
W32Thread->pDeskInfo->fsHooks= W32Thread->fsHooks;

Hook->head.pti = W32Thread;
Hook->head.rpdesk = W32Thread->rpdesk;
}
RtlInitUnicodeString(&Hook->ModuleName, NULL);//不需要模块


//关键。将钩子挂入钩子表中对应类型的钩子队列中。注意是插在队列头部,这就是为什么后安装的钩子优先得到处理的原因
InsertHeadList(&Table->Hooks[HOOKID_TO_INDEX(HookId)], &Hook->Chain);
return Hook;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct tagHOOKTABLE //钩子表(全局的或每个线程句柄的钩子表) 
{
LIST_ENTRY Hooks[16];//对应16种钩子的16个队列
UINT Counts[16];//各队列的引用计数(注意不是钩子个数)
} HOOKTABLE, *PHOOKTABLE;


typedef struct tagHOOK //钩子对象的结构
{
THRDESKHEAD head;
LIST_ENTRY Chain; //用来挂入相应类型的钩子队列
struct _ETHREAD* Thread;//钩子的目标线程,注意底层键盘鼠标类钩子的这个字段表示原创建者线程
int HookId;//钩子的类型
HOOKPROC Proc;//若给定了模块名,就是相对偏移,否则就是地址
BOOLEAN Ansi;
ULONG Flags;
UNICODE_STRING ModuleName;//全局钩子的模块名
} HOOK, *PHOOK;

通过以上函数的展示我们知道SetWindowsHookEx这个 API 最终就是在相应类型的钩子队列中挂入一个钩 子,如此简单而已。

co_HOOK_CallHooks

那么钩子是如何得到调用的呢?

当在Dispatching消息时,系统会检查相应类型的钩 子队列中有没有钩子,除此之外在其它每一个可能有钩子的地方也都会调用钩子,如取下消息后会检查调用WH_GETMESSAGE 钩子。
系统通过下面的函数检查、调用钩子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
LRESULT FASTCALL co_HOOK_CallHooks(INT HookId, INT Code, WPARAM wParam, LPARAM lParam) 
{
pti = PsGetCurrentThreadWin32Thread();
//先检查局部钩子表和全局钩子表中是否安装有HookId这种类型的钩子
Table = MsqGetHooks(pti->MessageQueue);//当前线程的局部钩子表
if (NULL == Table || ! (Hook = IntGetFirstValidHook(Table, HookId)))
{
Table = GlobalHooks;
if (NULL == Table || ! (Hook = IntGetFirstValidHook(Table, HookId)))
return 0;
}

//底层键盘鼠标钩子需要特殊方式调用(发一个内部消息通知原钩子创建者线程去调用钩子,注意原 线程必须提供一个消息循环)
if ((Hook->Thread != PsGetCurrentThread()) && (Hook->Thread != NULL))
return IntCallLowLevelHook(Hook, Code, wParam, lParam);

//每次调用钩子时,递增相应钩子队列(即钩子类型)的引用计数
Table->Counts[HOOKID_TO_INDEX(HookId)]++;
if (Table != GlobalHooks && GlobalHooks != NULL)
GlobalHooks->Counts[HOOKID_TO_INDEX(HookId)]++;

ClientInfo = GetWin32ClientInfo();
SaveHook = ClientInfo->phkCurrent;
ClientInfo->phkCurrent = Hook;//记录当前正在调用的钩子,为调用下一个钩子做准备

//关键。调用队列中的第一个钩子(也即最后安装的钩子,可以在你面调用CallNextHookEx)
Result = co_IntCallHookProc(HookId,Code,wParam,lParam,Hook->Proc,Hook->Ansi,
&Hook->ModuleName);
ClientInfo->phkCurrent = SaveHook;
Status = IntValidateWindowStationHandle(PsGetCurrentProcess()->Win32WindowStation,
KernelMode,0,&WinStaObj);

//递减钩子类型的引用计数,减到0后,就销毁那些已经撤销了,但尚未销毁的钩子
IntReleaseHookChain(MsqGetHooks(pti->MessageQueue), HookId, WinStaObj);
IntReleaseHookChain(GlobalHooks, HookId, WinStaObj);
ObDereferenceObject(WinStaObj);
return Result;
}


LRESULT FASTCALL IntCallLowLevelHook(PHOOK Hook, INT Code, WPARAM wParam, LPARAM lParam)
{
NTSTATUS Status;
ULONG_PTR uResult;

Timeout = HKEY_CURRENT_USER\Control Panel\Desktop\LowLevelHooksTimeout键的值
Status = co_MsqSendMessage(((PTHREADINFO)Hook->Thread->Tcb.Win32Thread)->MessageQueue,
IntToPtr(Code),Hook->HookId,wParam,lParam,5000,
TRUE,//HookMesage=TRUE,表示这是一条专用的‘钩子调用请求’消息
MSQ_ISHOOK,&uResult);
return NT_SUCCESS(Status) ? uResult : 0;
}


VOID FASTCALL IntReleaseHookChain(PHOOKTABLE Table, int HookId, PWINSTATION_OBJECT WinStaObj)
{
PLIST_ENTRY Elem;
PHOOK HookObj;
ASSERT(0 != Table->Counts[HOOKID_TO_INDEX(HookId)]);
//递减钩子类型的引用计数,减到0后,销毁那些已经撤销了的钩子
if (0 == --Table->Counts[HOOKID_TO_INDEX(HookId)])
{
Elem = Table->Hooks[HOOKID_TO_INDEX(HookId)].Flink;
while (Elem != &Table->Hooks[HOOKID_TO_INDEX(HookId)])
{
HookObj = CONTAINING_RECORD(Elem, HOOK, Chain);
Elem = Elem->Flink;
if (NULL == HookObj->Proc)
IntFreeHook(Table, HookObj, WinStaObj);
}
}
}

UnHookWindowsHookEx函数用来撤销钩子,它将钩子对象的HookProc置为 NULL,并检查该钩子是否正在被调用,若是就立即返回。否则销毁钩子后再返回。

键盘消息的产生、处理

熟悉了消息机制的原理后,我们看看具体键盘鼠标消息的处理流程。
系统在初始化时会创建两个内核守护线程,分别用来监视、处理键盘输入和鼠标输入

InitInputImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NTSTATUS FASTCALL InitInputImpl(VOID) 
{
NTSTATUS Status;
KeInitializeEvent(&InputThreadsStart, NotificationEvent, FALSE);
MasterTimer = ExAllocatePoolWithTag(NonPagedPool, sizeof(KTIMER), TAG_INPUT);
KeInitializeTimer(MasterTimer);
Status = PsCreateSystemThread(&RawInputThreadHandle,THREAD_ALL_ACCESS,NULL,NULL,
&RawInputThreadId,RawInputThreadMain,NULL);
//键盘输入线程:KeyboardThreadMain
Status = PsCreateSystemThread(&KeyboardThreadHandle,THREAD_ALL_ACCESS,NULL,NULL,
&KeyboardThreadId,KeyboardThreadMain,NULL);
//鼠标输入线程:MouseThreadMain
Status = PsCreateSystemThread(&MouseThreadHandle,THREAD_ALL_ACCESS,NULL,NULL,
&MouseThreadId,MouseThreadMain,NULL);
InputThreadsRunning = TRUE;//标志现在可以开始读取键盘鼠标输入
KeSetEvent(&InputThreadsStart, IO_NO_INCREMENT, FALSE);
return STATUS_SUCCESS;
}

KeyboardThreadMain

我们看键盘输入线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
VOID  KeyboardThreadMain(PVOID StartContext) 
{
//键盘类驱动中的键盘类设备
UNICODE_STRING KeyboardDeviceName = RTL_CONSTANT_STRING(L"\\Device\\KeyboardClass0");
PKEYBOARD_INDICATOR_TRANSLATION IndicatorTrans = NULL;//LED指示灯状态
UINT ModifierState = 0;//ctrl、shift、alt、win四个修正键的按住状态
USHORT LastMakeCode = 0;
USHORT LastFlags = 0;
UINT RepeatCount = 0;//后续的重复计数(按住不放引起的)
InitializeObjectAttributes(&KeyboardObjectAttributes,&KeyboardDeviceName,0,NULL,NULL);
do
{
LARGE_INTEGER DueTime;
KEVENT Event;
DueTime.QuadPart = (LONGLONG)(-10000000);//1秒
KeInitializeEvent(&Event, NotificationEvent, FALSE);
Status = KeWaitForSingleObject(&Event, Executive, KernelMode, FALSE, &DueTime);
Status = NtOpenFile(&KeyboardDeviceHandle,FILE_ALL_ACCESS,&KeyboardObjectAttributes,
&Iosb,0,FILE_SYNCHRONOUS_IO_ALERT);
} while (!NT_SUCCESS(Status));

//上面的循环尝试打开键盘类设备,直到成功为止
Status = Win32kInitWin32Thread(PsGetCurrentThread());
//键盘输入和鼠标输入线程是一种实时性较高的内核线程,比普通应用程序的线程的优先级高
KeSetPriorityThread(&PsGetCurrentThread()->Tcb,LOW_REALTIME_PRIORITY + 3);
//获取键盘初始的LED指示灯状态
IntKeyboardGetIndicatorTrans(KeyboardDeviceHandle,&IndicatorTrans);
for (;;)
{
Status = KeWaitForSingleObject(&InputThreadsStart,0,KernelMode,TRUE,NULL);
//每轮循环读取、处理一个键或者两个键(复合键:修正键+普通键)
while (InputThreadsRunning)// InputThreadsRunning一般总是TRUE
{
BOOLEAN NumKeys = 1;
BOOLEAN bLeftAlt;
KEYBOARD_INPUT_DATA KeyInput;
KEYBOARD_INPUT_DATA NextKeyInput;
LPARAM lParam = 0;
UINT fsModifiers, fsNextModifiers;
struct _ETHREAD *Thread;
HWND hWnd;
int id;

//先读一个键
Status = NtReadFile (KeyboardDeviceHandle,NULL,NULL,NULL,&Iosb, &KeyInput,sizeof(KEYBOARD_INPUT_DATA),NULL,NULL);

if(Status == STATUS_ALERTED && !InputThreadsRunning) break;

if(Status == STATUS_PENDING)
{
NtWaitForSingleObject(KeyboardDeviceHandle, FALSE, NULL);
Status = Iosb.Status;
}
if (Status == STATUS_ALERTED && !InputThreadsRunning) break;

IntLastInputTick(TRUE);//记录上次读得输入的时间
//判断这个键是否是个修正键,若是,返回修正键ID
fsModifiers = IntKeyboardGetModifiers(&KeyInput);
if (fsModifiers)//if 是个修正键
{
if (KeyInput.Flags & KEY_BREAK)//if 弹起,修改状态
{
ModifierState &= ~fsModifiers;//去掉这个键
if(fsModifiers == MOD_ALT)
{
if(KeyInput.Flags & KEY_E0)//if 弹起的是右边alt
gQueueKeyStateTable[VK_RMENU] = 0;//修改右alt键状态
else

gQueueKeyStateTable[VK_LMENU] = 0;
//修改alt键状态
if (gQueueKeyStateTable[VK_RMENU] == 0 &&gQueueKeyStateTable[VK_LMENU] == 0)
gQueueKeyStateTable[VK_MENU] = 0;
}
}
Else //if 按下
{
ModifierState |= fsModifiers;//加上这个修正键
//if 当前仅仅按着ALT或WIN键,特殊处理
if (ModifierState == fsModifiers && (fsModifiers == MOD_ALT || fsModifiers == MOD_WIN))
{
bLeftAlt = FALSE;
if(fsModifiers == MOD_ALT)//if alt键,修改alt键的状态
{
if(KeyInput.Flags & KEY_E0)
gQueueKeyStateTable[VK_RMENU] = 0x80;//最高位为1表示处于按着状态
else
{
gQueueKeyStateTable[VK_LMENU] = 0x80;
bLeftAlt = TRUE;
}
gQueueKeyStateTable[VK_MENU] = 0x80;//标记有个alt键按着
}
do
{
Status = NtReadFile (KeyboardDeviceHandle,NULL,NULL,NULL,&Iosb,
&NextKeyInput,sizeof(KEYBOARD_INPUT_DATA),
NULL,NULL);
if (Status == STATUS_ALERTED && !InputThreadsRunning)
goto KeyboardEscape;
}
while ((!(NextKeyInput.Flags & KEY_BREAK)) &&
NextKeyInput.MakeCode == KeyInput.MakeCode);
//上面的循环,跳过因按住alt、win不放产生的那些重复按键,直到读到一个原按键 弹起输入或者另外一个键为止
//判断读到的下一个键是否为修正键
fsNextModifiers = IntKeyboardGetModifiers(&NextKeyInput);
if (fsNextModifiers)
ModifierState ^= fsNextModifiers;//状态切换
if (ModifierState == 0)//也即if读到的下个键是原按键的弹起
{
if (fsModifiers == MOD_WIN)
IntKeyboardSendWinKeyMsg();//生成单个win键单击消息
else if (fsModifiers == MOD_ALT)
{
gQueueKeyStateTable[VK_MENU] = 0;

if(bLeftAlt)
gQueueKeyStateTable[VK_LMENU] = 0;
else
gQueueKeyStateTable[VK_RMENU] = 0;

co_IntKeyboardSendAltKeyMsg();//生成单个alt键单击消息
}
continue;//继续读取下个输入
}
//若下个输入是另外某个键按下
NumKeys = 2;//表示alt+x或win+x的复合键
}
}
}
//处理本轮循环读到的键(一个键或者复合键)
for (;NumKeys;memcpy(&KeyInput, &NextKeyInput, sizeof(KeyInput)),NumKeys--)
{
PKBL keyboardLayout = NULL;
lParam = 0;
//修正对应的指示灯
IntKeyboardUpdateLeds(KeyboardDeviceHandle,&KeyInput,IndicatorTrans);
//下面构造该KeyDown/KeyUp消息的lparam,其格式如下:
* 0-15: 重复计数
* 16-23: 扫描码
* 24: 修正键标志
* 29: alt键是否按着标志
* 30: 标志是否与上次的按键状态相同
* 31: 标志当前按键状态是按下还是弹起
if (!(KeyInput.Flags & KEY_BREAK))//if 按下
{
//也即是同一按键后续的重复KeyDown
if (((KeyInput.Flags & (KEY_E0 | KEY_E1)) == LastFlags) &&
(KeyInput.MakeCode == LastMakeCode))
{
RepeatCount++;//递增重复按键计数
lParam |= (1 << 30);//bit30标记为与上次的按键状态的相同
}
Else//首次按下某键
{
RepeatCount = 1;

LastFlags = KeyInput.Flags & (KEY_E0 | KEY_E1);
LastMakeCode = KeyInput.MakeCode;
}
}
Else //if 弹起
{
LastFlags = 0;
LastMakeCode = 0;
lParam |= (1 << 30) | (1 << 31);
}
lParam |= RepeatCount;
lParam |= (KeyInput.MakeCode & 0xff) << 16;
if (KeyInput.Flags & KEY_E0)
lParam |= (1 << 24);
if (ModifierState & MOD_ALT)//如果按着alt键,那就是WM_SYSKEYDOWN、WM_SYSKEYUP
{
lParam |= (1 << 29);
if (!(KeyInput.Flags & KEY_BREAK))
msg.message = WM_SYSKEYDOWN;
else
msg.message = WM_SYSKEYUP;
}
else
{
if (!(KeyInput.Flags & KEY_BREAK))
msg.message = WM_KEYDOWN;
else
msg.message = WM_KEYUP;
}
//获得当前的‘键盘焦点线程’(注意不是焦点窗口)
FocusQueue = IntGetFocusMessageQueue();
if (FocusQueue)
{
msg.hwnd = FocusQueue->FocusWindow;
FocusThread = FocusQueue->Thread;
if (FocusThread && FocusThread->Tcb.Win32Thread)
keyboardLayout = (FocusThread->Tcb.Win32Thread)->KeyboardLayout;
}
msg.lParam = lParam;
if (!keyboardLayout)
keyboardLayout = W32kGetDefaultKeyLayout();
//将扫描码转为虚拟码保存在wparam中
W32kKeyProcessMessage(&msg,keyboardLayout->KBTables, KeyInput.Flags & KEY_E0 ? 0xE0 : (KeyInput.Flags & KEY_E1 ? 0xE1 : 0));

//检查是否这个键被注册为了热键
if (GetHotKey(ModifierState,msg.wParam,&Thread,&hWnd,&id))
{
if (!(KeyInput.Flags & KEY_BREAK))
{
MsqPostHotKeyMessage (Thread,hWnd, (WPARAM)id,
MAKELPARAM((WORD)ModifierState, (WORD)msg.wParam));
}
continue;//看到没,热键会阻断正常的按键分派流程
}
if (!FocusQueue)
continue;
//关键。将消息发送给当前键盘焦点线程
co_MsqPostKeyboardMessage(msg.message,msg.wParam,msg.lParam);
}
}
}
}

如上这个函数会循环从键盘驱动读取键盘输入,然后生成WM_HotKey/KeyDown/KeyUp消息,发给应用线 程。

读到的每个输入是一个结构,是由键盘驱动提交上来的

1
2
3
4
5
6
7
typedef struct _KEYBOARD_INPUT_DATA { 
USHORT UnitId;//键盘ID,一台机器可以安装多个键盘
USHORT MakeCode;//扫描码
USHORT Flags;//E0、E1、Up等标志
USHORT Reserved;
ULONG ExtraInformation;
} KEYBOARD_INPUT_DATA, *PKEYBOARD_INPUT_DATA;

其中 Flags 包含的常见三个标志位:

1
2
3
4
#define KEY_MAKE                       0x000  //表示按下 
#define KEY_BREAK 0x001 //表示弹起
#define KEY_E0 0x010 //表示win键或右边的ctrl、lt
#define KEY_E1 0x100 //表示普通键(非修正键

IntKeyboardGetModifiers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
UINT  IntKeyboardGetModifiers(KEYBOARD_INPUT_DATA *InputData) 
{
if (InputData->Flags & KEY_E1)//普通键
return 0;//返回FALSE
if (!(InputData->Flags & KEY_E0))//if 不是win键和右边的ctrl、lt
{
switch (InputData->MakeCode)
{

case 0x2a: /* left shift */
case 0x36: /* right shift */
return MOD_SHIFT;
case 0x1d: /* left control */
return MOD_CONTROL;
case 0x38: /* left alt */
return MOD_ALT;
default:
return 0;
}
}
else
{
switch (InputData->MakeCode)
{
case 0x1d: /* right control */
return MOD_CONTROL;
case 0x38: /* right alt */
return MOD_ALT;
case 0x5b: /* left gui (windows) */
case 0x5c: /* right gui (windows) */
return MOD_WIN;
default:
return 0;
}
}
}

用户可以调用RegisterHotkeyapi 注册热键,它内部调用下面的系统服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
BOOL NtUserRegisterHotKey(HWND hWnd,int id,UINT fsModifiers,UINT vk) 
{
PHOT_KEY_ITEM HotKeyItem;
PWINDOW_OBJECT Window;
PETHREAD HotKeyThread;
DECLARE_RETURN(BOOL);
if (IsHotKey (fsModifiers, vk))//如果这个热键组合已被抢占了,注册失败
RETURN( FALSE);
if (hWnd == NULL)//可为NULL,热键消息发给当前线程
HotKeyThread = PsGetCurrentThread();
else
{
if(!(Window = UserGetWindowObject(hWnd)))
RETURN( FALSE);

HotKeyThread = Window->pti->pEThread;//热键消息将发给的目标线程
}
HotKeyItem = ExAllocatePoolWithTag (PagedPool, sizeof(HOT_KEY_ITEM), TAG_HOTKEY);
HotKeyItem->Thread = HotKeyThread;//目标线程
HotKeyItem->hWnd = hWnd;//目标窗口
HotKeyItem->id = id;//热键id
HotKeyItem->fsModifiers = fsModifiers;//修正键组合
HotKeyItem->vk = vk;//虚拟码
InsertHeadList (&gHotkeyList, &HotKeyItem->ListEntry);//插入全局的热键表
RETURN( TRUE);
}

当读到的按键被发现注册为了热键的话,就发给热键注册的目标线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//检测是不是热键,若是,再返回该热键的目标线程、窗口和id 
BOOL FASTCALL GetHotKey (UINT fsModifiers,UINT vk,struct _ETHREAD **Thread,HWND *hWnd,int *id)
{
PHOT_KEY_ITEM HotKeyItem;
LIST_FOR_EACH(HotKeyItem, &gHotkeyList, HOT_KEY_ITEM, ListEntry)
{
if (HotKeyItem->fsModifiers == fsModifiers && HotKeyItem->vk == vk)
{
if (Thread != NULL)
*Thread = HotKeyItem->Thread;
if (hWnd != NULL)
*hWnd = HotKeyItem->hWnd;
if (id != NULL)
*id = HotKeyItem->id;
return TRUE;
}
}
return FALSE;
}

下面的函数将热键消息以Post方式发给目标线程

MsqPostHotKeyMessage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
VOID FASTCALL MsqPostHotKeyMessage(PVOID Thread, HWND hWnd, WPARAM wParam, LPARAM lParam) 
{
PWINDOW_OBJECT Window;
PTHREADINFO Win32Thread;
MSG Mesg;
LARGE_INTEGER LargeTickCount;
NTSTATUS Status;
Status = ObReferenceObjectByPointer (Thread,THREAD_ALL_ACCESS,PsThreadType,KernelMode);
Win32Thread = ((PETHREAD)Thread)->Tcb.Win32Thread;

Window = IntGetWindowObject(hWnd);
Mesg.hwnd = hWnd;
Mesg.message = WM_HOTKEY;
Mesg.wParam = wParam;
Mesg.lParam = lParam;
KeQueryTickCount(&LargeTickCount);
Mesg.time = MsqCalculateMessageTime(&LargeTickCount);//消息的产生时间
Mesg.pt = gpsi->ptCursor; //消息产生时的光标位置

MsqPostMessage(Window->pti->MessageQueue, &Mesg, FALSE, QS_HOTKEY);//发给目标线程
UserDereferenceObject(Window);
ObDereferenceObject (Thread);
}

co_MsqPostKeyboardMessage

普通的按键消息都会发给当前键盘焦点线程(注意可能当前没有键盘焦点窗口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
VOID FASTCALL co_MsqPostKeyboardMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) 
{
PUSER_MESSAGE_QUEUE FocusMessageQueue;
MSG Msg;
LARGE_INTEGER LargeTickCount;
KBDLLHOOKSTRUCT KbdHookData;
BOOLEAN Entered = FALSE;
FocusMessageQueue = IntGetFocusMessageQueue();//当前焦点线程的消息队列
Msg.hwnd = NULL;
Msg.message = uMsg;
Msg.wParam = wParam;
Msg.lParam = lParam;
KeQueryTickCount(&LargeTickCount);
Msg.time = MsqCalculateMessageTime(&LargeTickCount);
KbdHookData.vkCode = Msg.wParam;
KbdHookData.scanCode = (Msg.lParam >> 16) & 0xff;
KbdHookData.flags = (0 == (Msg.lParam & 0x01000000) ? 0 : LLKHF_EXTENDED) |
(0 == (Msg.lParam & 0x20000000) ? 0 : LLKHF_ALTDOWN) |
(0 == (Msg.lParam & 0x80000000) ? 0 : LLKHF_UP);
KbdHookData.time = Msg.time;
KbdHookData.dwExtraInfo = 0;

//进队前,先检查、调用底层键盘钩子,若钩子处理结果返回非0值,则不再继续发送
if (co_HOOK_CallHooks(WH_KEYBOARD_LL, HC_ACTION, Msg.message, (LPARAM) &KbdHookData))
return;
if (FocusMessageQueue->FocusWindow != NULL)
{
Msg.hwnd = FocusMessageQueue->FocusWindow;
FocusMessageQueue->Desktop->pDeskInfo->LastInputWasKbd = TRUE;
Msg.pt = gpsi->ptCursor;//光标位置

MsqPostMessage(FocusMessageQueue, &Msg, FALSE, QS_KEY);//发给焦点线程
}
return;
}


鼠标消息的产生、处理

MouseThreadMain

这个线程会不断的从鼠标驱动读取鼠标输入-处理-读取鼠标输入-处理
看看是怎么处理鼠标输入的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
VOID  MouseThreadMain(PVOID StartContext) 
{
//鼠标类设备
UNICODE_STRING MouseDeviceName = RTL_CONSTANT_STRING(L"\\Device\\PointerClass0");
OBJECT_ATTRIBUTES MouseObjectAttributes;
IO_STATUS_BLOCK Iosb;
NTSTATUS Status;
MOUSE_ATTRIBUTES MouseAttr;

Status = Win32kInitWin32Thread(PsGetCurrentThread());
KeSetPriorityThread(&PsGetCurrentThread()->Tcb,LOW_REALTIME_PRIORITY + 3);
InitializeObjectAttributes(&MouseObjectAttributes,&MouseDeviceName,0,NULL,NULL);
do
{
LARGE_INTEGER DueTime;
KEVENT Event;
DueTime.QuadPart = (LONGLONG)(-10000000);
KeInitializeEvent(&Event, NotificationEvent, FALSE);
Status = KeWaitForSingleObject(&Event, Executive, KernelMode, FALSE, &DueTime);
Status = NtOpenFile(&MouseDeviceHandle,FILE_ALL_ACCESS,&MouseObjectAttributes,
&Iosb,0,FILE_SYNCHRONOUS_IO_ALERT);
} while (!NT_SUCCESS(Status));

//上面的循环尝试打开鼠标类设备,直到成功
for(;;)
{
Status = KeWaitForSingleObject(&InputThreadsStart,0,KernelMode,TRUE,NULL);
Status = NtDeviceIoControlFile(MouseDeviceHandle,NULL,NULL,NULL,&Iosb,
IOCTL_MOUSE_QUERY_ATTRIBUTES,
&MouseAttr, sizeof(MOUSE_ATTRIBUTES),
NULL, 0);
while(InputThreadsRunning)
{
MOUSE_INPUT_DATA MouseInput;
Status = NtReadFile(MouseDeviceHandle,NULL,NULL,NULL,&Iosb,
&MouseInput,sizeof(MOUSE_INPUT_DATA),NULL,NULL);
if(Status == STATUS_ALERTED && !InputThreadsRunning) break;
if(Status == STATUS_PENDING)
{

NtWaitForSingleObject(MouseDeviceHandle, FALSE, NULL);
Status = Iosb.Status;
}

IntLastInputTick(TRUE);//记录上次得到输入的时间
UserEnterExclusive();

//处理得到的鼠标输入
ProcessMouseInputData(&MouseInput, Iosb.Information / sizeof(MOUSE_INPUT_DATA));
UserLeave();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
VOID FASTCALL ProcessMouseInputData(PMOUSE_INPUT_DATA Data, ULONG InputCount) 
{
PMOUSE_INPUT_DATA mid;//当前鼠标输入
MOUSEINPUT mi;
ULONG i;
ClearMouseInput(mi);//全部清0
mi.time = 0;
mi.dwExtraInfo = 0;
for(i = 0; i < InputCount; i++)//遍历每个鼠标输入
{
mid = (Data + i);
mi.dx += mid->LastX;//X方向移动距离
mi.dy += mid->LastY;//Y方向移动距离
if (mid->Flags == MOUSE_MOVE_ABSOLUTE)//指LastX、LastY是绝对值
mi.dwFlags |= MOUSEEVENTF_ABSOLUTE;
if(mid->ButtonFlags)
{
if(mid->ButtonFlags & MOUSE_LEFT_BUTTON_DOWN)
{
mi.dwFlags |= MOUSEEVENTF_LEFTDOWN;
SendMouseEvent(mi);
}
if(mid->ButtonFlags & MOUSE_LEFT_BUTTON_UP)
{
mi.dwFlags |= MOUSEEVENTF_LEFTUP;
SendMouseEvent(mi);
}
if(mid->ButtonFlags & MOUSE_MIDDLE_BUTTON_DOWN)
{

mi.dwFlags |= MOUSEEVENTF_MIDDLEDOWN;
SendMouseEvent(mi);
}
if(mid->ButtonFlags & MOUSE_MIDDLE_BUTTON_UP)
{
mi.dwFlags |= MOUSEEVENTF_MIDDLEUP;
SendMouseEvent(mi);
}
if(mid->ButtonFlags & MOUSE_RIGHT_BUTTON_DOWN)
{
mi.dwFlags |= MOUSEEVENTF_RIGHTDOWN;
SendMouseEvent(mi);
}
if(mid->ButtonFlags & MOUSE_RIGHT_BUTTON_UP)
{
mi.dwFlags |= MOUSEEVENTF_RIGHTUP;
SendMouseEvent(mi);
}
if(mid->ButtonFlags & MOUSE_WHEEL)
{
mi.mouseData = mid->ButtonData;
mi.dwFlags |= MOUSEEVENTF_WHEEL;
SendMouseEvent(mi);
}
}
}// end for
SendMouseEvent(mi);
}


//SendMouseEvent 是个宏
#define SendMouseEvent(mi) \
if(mi.dx != 0 || mi.dy != 0) \ //if鼠标有移动
mi.dwFlags |= MOUSEEVENTF_MOVE; \
if(mi.dwFlags) \
IntMouseInput(&mi); \ //生成相应的鼠标消息,放入系统的环形缓冲队列
ClearMouseInput(mi);//又清 0

IntMouseInput

可以看出,第一个满足的 if 条件语句,在调用SendMouseEvent时,才可能含有标志 MOUSEEVENTF_MOVE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
BOOL FASTCALL IntMouseInput(MOUSEINPUT *mi) 
{
//鼠标的左右按键可以互换
const UINT SwapBtnMsg[2][2] =
{ {WM_LBUTTONDOWN, WM_RBUTTONDOWN},{WM_LBUTTONUP, WM_RBUTTONUP} };
const WPARAM SwapBtn[2] ={MK_LBUTTON, MK_RBUTTON};

POINT MousePos;
PSYSTEM_CURSORINFO CurInfo;
BOOL SwapButtons;
MSG Msg;
CurInfo = IntGetSysCursorInfo();//光标的一些属性
SwapButtons = gspv.bMouseBtnSwap;//全局变量,表示鼠标左右按键是否互换
MousePos = gpsi->ptCursor; //当前光标的位置
if(mi->dwFlags & MOUSEEVENTF_MOVE)//前面讲了,第一个满足条件的if块才可能带有这个标志
{
if(mi->dwFlags & MOUSEEVENTF_ABSOLUTE)
{
MousePos.x = mi->dx * UserGetSystemMetrics(SM_CXVIRTUALSCREEN) >> 16;
MousePos.y = mi->dy * UserGetSystemMetrics(SM_CYVIRTUALSCREEN) >> 16;
}
Else //计算新的光标位置
{
MousePos.x += mi->dx;
MousePos.y += mi->dy;
}
}
if(mi->dwFlags & MOUSEEVENTF_MOVE)
UserSetCursorPos(MousePos.x, MousePos.y, TRUE);//系统会自动物理移动桌面上的光标位置
Msg.wParam = 0;
Msg.lParam = MAKELPARAM(MousePos.x, MousePos.y);
Msg.pt = MousePos;
//上面构造了一个鼠标消息(注意这个消息尚未确定目标窗口)
if (gQueueKeyStateTable[VK_SHIFT] & 0xc0)
Msg.wParam |= MK_SHIFT;
if (gQueueKeyStateTable[VK_CONTROL] & 0xc0)
Msg.wParam |= MK_CONTROL;
if(mi->dwFlags & MOUSEEVENTF_LEFTDOWN)
{
gQueueKeyStateTable[VK_LBUTTON] |= 0xc0;
Msg.message = SwapBtnMsg[0][SwapButtons];
CurInfo->ButtonsDown |= SwapBtn[SwapButtons];
Msg.wParam |= CurInfo->ButtonsDown;
MsqInsertSystemMessage(&Msg);//关键。系统生成鼠标消息后,插入到系统的环形缓冲消息队列
}
else if(mi->dwFlags & MOUSEEVENTF_LEFTUP)
{
gQueueKeyStateTable[VK_LBUTTON] &= ~0x80;
Msg.message = SwapBtnMsg[1][SwapButtons];
CurInfo->ButtonsDown &= ~SwapBtn[SwapButtons];
Msg.wParam |= CurInfo->ButtonsDown;

MsqInsertSystemMessage(&Msg);
}
if(mi->dwFlags & MOUSEEVENTF_MIDDLEDOWN)
{
gQueueKeyStateTable[VK_MBUTTON] |= 0xc0;
Msg.message = WM_MBUTTONDOWN;
CurInfo->ButtonsDown |= MK_MBUTTON;
Msg.wParam |= CurInfo->ButtonsDown;
MsqInsertSystemMessage(&Msg);
}
else if(mi->dwFlags & MOUSEEVENTF_MIDDLEUP)
{
gQueueKeyStateTable[VK_MBUTTON] &= ~0x80;
Msg.message = WM_MBUTTONUP;
CurInfo->ButtonsDown &= ~MK_MBUTTON;
Msg.wParam |= CurInfo->ButtonsDown;
MsqInsertSystemMessage(&Msg);
}
if(mi->dwFlags & MOUSEEVENTF_RIGHTDOWN)
{
gQueueKeyStateTable[VK_RBUTTON] |= 0xc0;
Msg.message = SwapBtnMsg[0][!SwapButtons];
CurInfo->ButtonsDown |= SwapBtn[!SwapButtons];
Msg.wParam |= CurInfo->ButtonsDown;
MsqInsertSystemMessage(&Msg);
}
else if(mi->dwFlags & MOUSEEVENTF_RIGHTUP)
{
gQueueKeyStateTable[VK_RBUTTON] &= ~0x80;
Msg.message = SwapBtnMsg[1][!SwapButtons];
CurInfo->ButtonsDown &= ~SwapBtn[!SwapButtons];
Msg.wParam |= CurInfo->ButtonsDown;
MsqInsertSystemMessage(&Msg);
}
if((mi->dwFlags & (MOUSEEVENTF_XDOWN | MOUSEEVENTF_XUP)) &&
(mi->dwFlags & MOUSEEVENTF_WHEEL))
{
/* fail because both types of events use the mouseData field */
return FALSE;//两种消息的mouseData字段意思不一样
}
if(mi->dwFlags & MOUSEEVENTF_WHEEL)
{
Msg.message = WM_MOUSEWHEEL;
Msg.wParam = MAKEWPARAM(CurInfo->ButtonsDown, mi->mouseData);

MsqInsertSystemMessage(&Msg);
}

return TRUE;
}

MsqInsertSystemMessage

如上这个函数就是把鼠标输入转为一个鼠标消息,插入到系统的环形缓冲消息队列中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
VOID FASTCALL MsqInsertSystemMessage(MSG* Msg) 
{
LARGE_INTEGER LargeTickCount;
KIRQL OldIrql;
ULONG Prev;
MSLLHOOKSTRUCT MouseHookData;
KeQueryTickCount(&LargeTickCount);
Msg->time = MsqCalculateMessageTime(&LargeTickCount);//计算消息的进队时间
MouseHookData.pt.x = LOWORD(Msg->lParam);
MouseHookData.pt.y = HIWORD(Msg->lParam);
switch(Msg->message)
{
case WM_MOUSEWHEEL:
MouseHookData.mouseData = MAKELONG(0, GET_WHEEL_DELTA_WPARAM(Msg->wParam));
break;
case WM_XBUTTONDOWN:
case WM_XBUTTONUP:
case WM_XBUTTONDBLCLK:
case WM_NCXBUTTONDOWN:
case WM_NCXBUTTONUP:
case WM_NCXBUTTONDBLCLK:
MouseHookData.mouseData = MAKELONG(0, HIWORD(Msg->wParam));
break;
default:
MouseHookData.mouseData = 0;
break;
}
MouseHookData.flags = 0;
MouseHookData.time = Msg->time;//消息的进队时间
MouseHookData.dwExtraInfo = 0;

//检查、调用底层鼠标钩子,如果钩子过程返回非0值,那么不再处理该鼠标消息
if (co_HOOK_CallHooks(WH_MOUSE_LL, HC_ACTION, Msg->message, (LPARAM) &MouseHookData))
return;

IntLockSystemMessageQueue(OldIrql);

//由此可以看出,鼠标消息可能会因缓冲区满而丢失
if (SystemMessageQueueCount == SYSTEM_MESSAGE_QUEUE_SIZE)//缓冲容量:256个消息
{
IntUnLockSystemMessageQueue(OldIrql);
return;
}

if (Msg->message == WM_MOUSEMOVE && SystemMessageQueueCount !=0)
{
if (SystemMessageQueueTail == 0)
Prev = SYSTEM_MESSAGE_QUEUE_SIZE - 1;
else
Prev = SystemMessageQueueTail - 1;

//若环形缓冲队列中的上一个消息也是WM_MOUSEMOVE,就更新替换(相当于合并MouseMove消息)
if (SystemMessageQueue[Prev].message == WM_MOUSEMOVE)
{
SystemMessageQueueTail = Prev;
SystemMessageQueueCount--;
}
}
SystemMessageQueue[SystemMessageQueueTail] = *Msg; //关键。插入到环形缓冲队列尾部
SystemMessageQueueTail = (SystemMessageQueueTail + 1) % SYSTEM_MESSAGE_QUEUE_SIZE;
SystemMessageQueueCount++;
IntUnLockSystemMessageQueue(OldIrql);
KeSetEvent(&HardwareMessageEvent, IO_NO_INCREMENT, FALSE);//触发事件,唤醒其他线程
}

当键盘按键消息插入到系统环形缓冲队列中后,会触发事件HardwareMessageEventGetMessagePeekMessage内部会等待这个事件。

模拟键盘鼠标动作

除了由真实的硬件(键盘、鼠标)产生键盘消息外,用户也可以模拟键盘鼠标动作。
keybd_eventmouse_eventSendInput这几个 API 都是用来模拟鼠标键盘的

keybd_event&mouse_event

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
VOID keybd_event(BYTE bVk,BYTE bScan,DWORD dwFlags,ULONG_PTR dwExtraInfo) 
{
INPUT Input;
Input.type = INPUT_KEYBOARD;
Input.ki.wVk = bVk;
Input.ki.wScan = bScan;
Input.ki.dwFlags = dwFlags;

Input.ki.time = 0;
Input.ki.dwExtraInfo = dwExtraInfo;
NtUserSendInput(1, &Input, sizeof(INPUT));
}
VOID mouse_event(DWORD dwFlags,DWORD dx,DWORD dy,DWORD dwData,ULONG_PTR dwExtraInfo)
{
INPUT Input;
Input.type = INPUT_MOUSE;
Input.mi.dx = dx;
Input.mi.dy = dy;
Input.mi.mouseData = dwData;
Input.mi.dwFlags = dwFlags;
Input.mi.time = 0;
Input.mi.dwExtraInfo = dwExtraInfo;
NtUserSendInput(1, &Input, sizeof(INPUT));
}

SendInput

1
2
3
4
UINT SendInput(UINT nInputs, LPINPUT pInputs, int cbSize) 
{
return NtUserSendInput(nInputs, pInputs, cbSize);
}

这几个 API 都是通过调用 NtUserSendInput 这个系统服务实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
UINT NtUserSendInput(UINT nInputs,LPINPUT pInput,INT cbSize) 
{
PTHREADINFO W32Thread;
UINT cnt;
DECLARE_RETURN(UINT);
UserEnterExclusive();
W32Thread = PsGetCurrentThreadWin32Thread();
if(!W32Thread->rpdesk)
RETURN( 0);
if(!nInputs || !pInput || (cbSize != sizeof(INPUT)))
{
SetLastWin32Error(ERROR_INVALID_PARAMETER);
RETURN( 0);
}
//检查权限和活动桌面
if(!ThreadHasInputAccess(W32Thread) || !IntIsActiveDesktop(W32Thread->rpdesk))
{
SetLastWin32Error(ERROR_ACCESS_DENIED);
RETURN( 0);

}
cnt = 0;
while(nInputs--)//处理每个输入
{
INPUT SafeInput;
NTSTATUS Status;
Status = MmCopyFromCaller(&SafeInput, pInput++, sizeof(INPUT));
switch(SafeInput.type)
{
case INPUT_MOUSE:
if(IntMouseInput(&SafeInput.mi))//将模拟的鼠标消息插入系统的环形缓冲队列
cnt++;
break;
case INPUT_KEYBOARD://将模拟的键盘消息发给当前键盘焦点线程的post队列
if(IntKeyboardInput(&SafeInput.ki))
cnt++;
break;
case INPUT_HARDWARE:
break;
}
}
RETURN( cnt); CLEANUP:
UserLeave();
END_CLEANUP;
}




DPC

DPC 不同 APC,DPC 的全名是延迟过程调用

DPC 最初作用是设计为中断服务程序的一部分。
因为每次触发中断,都会关中断,然后执行中断服务例程。
由于关中断了,所以中断服务例程必须短小精悍,不能消耗过多时间,否则会导致系统丢失大量其他中断。

但是有的中断,其中断服务例程要做的事情本来就很多,那怎么办?
于是可以在中断服务例程中先执行最紧迫的那部分工作,然后把剩余的相对来说不那么重要的工作移入到 DPC 函数中去执行。
因此,DPC 又叫 ISR 的后半部。(比如每次时钟中断后,其 isr 会扫描系统中的所有定时器是否到点,若到点就调用各定时器的函数。
但是这个扫描过程比较耗时,因此时钟中断的 isr 会将扫描工作纳入 dpc 中进行)
每当触发一个中断时,中断服务例程可以在当前 cpu 中插入一个 DPC,当执行完 isr,退出 isr 后, cpu 就会扫描它的 dpc 队列,依次执行里面的每个 dpc,
当执行完 dpc 后,才又回到当前线程的中断处继续执行

基本结构

1
2
3
4
5
6
7
8
9
10
11
typedef struct _KDPC { 
UCHAR Type; //DPC类型(分为普通DPC和线程化DPC)
UCHAR Importance;//该DPC的重要性,将决定挂在队列头还是尾
volatile USHORT Number;//第5位为0就表示当前cpu,否则,最低4位表示目标cpu号
LIST_ENTRY DpcListEntry;//用来挂入dpc链表
PKDEFERRED_ROUTINE DeferredRoutine;//dpc函数
PVOID DeferredContext;//dpc函数的参数
PVOID SystemArgument1;//挂入时的系统附加参数1
PVOID SystemArgument2;//挂入时的系统附加参数2
volatile PVOID DpcData;//所在的dpc队列
} KDPC, *PKDPC, *RESTRICTED_POINTER PRKDPC;

关键函数

KeInitializeDpc

这个函数将 dpc 挂入目标 cpu 的指定 dpc 队列(每个 cpu 有两个 dpc 队列,一个普通的,一个线程化的)。
然后检查当前是否可以立即向目标 cpu 发出一个 dpc 软中断,这样以在下次降低到DISPATCH_LEVEL以下时扫描执行 dpc
其实上面的这个函数一般用于在 isr 中调用,但用户也可以随时手动调用
一般来说,挂入的都是中等重要性的 dpc,一般在 dpc 进队的同时就会顺势发出一个 dpc 中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
VOID KeInitializeDpc(IN PKDPC Dpc,//DPC对象(DPC也是一种内核对象) 
IN PKDEFERRED_ROUTINE DeferredRoutine, //DPC函数
IN PVOID DeferredContext)//DPC函数的参数
{
KiInitializeDpc(Dpc, DeferredRoutine, DeferredContext, DpcObject);
}

VOID KiInitializeDpc(IN PKDPC Dpc,
IN PKDEFERRED_ROUTINE DeferredRoutine,
IN PVOID DeferredContext,
IN KOBJECTS Type)
{
Dpc->Type = Type;
Dpc->Number = 0;//初始的目标cpu为当前cpu
Dpc->Importance= MediumImportance;
Dpc->DeferredRoutine = DeferredRoutine;
Dpc->DeferredContext = DeferredContext;
Dpc->DpcData = NULL;//表示该DPC尚未挂入任何DPC队列
}


// 初始化构造时的目标 cpu 默认都是当前 cpu。
DPC BOOLEAN KeInsertQueueDpc(IN PKDPC Dpc,IN PVOID SystemArgument1,IN PVOID SystemArgument2)
{
KIRQL OldIrql;
PKPRCB Prcb, CurrentPrcb;
ULONG Cpu;
PKDPC_DATA DpcData;
BOOLEAN DpcConfigured = FALSE, DpcInserted = FALSE;
KeRaiseIrql(HIGH_LEVEL, &OldIrql);//插入过程的中断级是最高的,这个过程不会被打断。
CurrentPrcb = KeGetCurrentPrcb();

//检查目标cpu号的第5位为1(32 = 00100000),就表示其它cpu,低4位表示cpu号
if (Dpc->Number >= 32)
{
Cpu = Dpc->Number - 32;
Prcb = KiProcessorBlock[Cpu];
}
Else //否则,表示当前cpu
{
Cpu = Prcb->Number;
Prcb = CurrentPrcb;//目标cpu就是当前cpu
}
//if 要插入的是一个线程化dpc并且那个cpu启用了线程化dpc机制
if ((Dpc->Type == ThreadedDpcObject) && (Prcb->ThreadDpcEnable))
DpcData = &Prcb->DpcData[DPC_THREADED]; //目标cpu的线程化dpc队列
else
DpcData = &Prcb->DpcData[DPC_NORMAL];//目标cpu的普通dpc队列 KiAcquireSpinLock(&DpcData->DpcLock);


//if 尚未挂入任何dpc队列
if (!InterlockedCompareExchangePointer(&Dpc->DpcData, DpcData, NULL))
{
Dpc->SystemArgument1 = SystemArgument1;
Dpc->SystemArgument2 = SystemArgument2;
DpcData->DpcQueueDepth++;
DpcData->DpcCount++;
DpcConfigured = TRUE;
//不管如何,先把dpc挂到目标cpu的dpc队列中
if (Dpc->Importance == HighImportance)
InsertHeadList(&DpcData->DpcListHead, &Dpc->DpcListEntry);
else
InsertTailList(&DpcData->DpcListHead, &Dpc->DpcListEntry);

if (&Prcb->DpcData[DPC_THREADED] == DpcData)
{


if (!(Prcb->DpcThreadActive) && !(Prcb->DpcThreadRequested))
线程化DPC,ReactOS目前尚不支持,略

}
Else //若挂入的是一个普通dpc(最常见),检查是否需要立即发出一个dpc软中断给cpu
{
//if 目标cpu当前没在执行dpc,并且它尚未收到dpc中断请求
if (!(Prcb->DpcRoutineActive) && !(Prcb->DpcInterruptRequested))
{
if (Prcb != CurrentPrcb)//if 目标cpu是其它cpu
{
if (((Dpc->Importance == HighImportance) ||
(DpcData->DpcQueueDepth >= Prcb->MaximumDpcQueueDepth)) &&
(!(AFFINITY_MASK(Cpu) & KiIdleSummary) || (Prcb->Sleeping)))
{
Prcb->DpcInterruptRequested = TRUE;
DpcInserted = TRUE; //表示需要立即给cpu发出dpc软中断
}
}
Else //if 目标cpu就是自身cpu,最常见
{
//一般插入的都是中等重要性的dpc,因此,一般会立即发出一个dpc中断。
if ((Dpc->Importance != LowImportance) ||
(DpcData->DpcQueueDepth >= Prcb->MaximumDpcQueueDepth) ||
(Prcb->DpcRequestRate < Prcb->MinimumDpcRate))
{
Prcb->DpcInterruptRequested = TRUE;
DpcInserted = TRUE; //表示需要立即给cpu发出dpc软中断
}
}
}
}
}
KiReleaseSpinLock(&DpcData->DpcLock);
if (DpcInserted)//if 需要立即发出一个dpc软中断
{
if (Prcb != CurrentPrcb)
KiIpiSend(AFFINITY_MASK(Cpu), IPI_DPC);
else
HalRequestSoftwareInterrupt(DISPATCH_LEVEL);//给当前cpu发出一个dpc软中断
}
KeLowerIrql(OldIrql);//降低irql

return DpcConfigured;
}


HalRequestSoftwareInterrupt

下面的函数可用于模拟硬件,向 cpu 发出任意irql级别的软中断,请求 cpu 处理执行那种中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Irql一般是APC_LEVEL/DPC_LEVEL 
VOID FASTCALL HalRequestSoftwareInterrupt(IN KIRQL Irql)
{
ULONG EFlags;
PKPCR Pcr = KeGetPcr();
KIRQL PendingIrql;
EFlags = readeflags();//保存老的eflags寄存器

_disable();//关中断

Pcr->IRR |= (1 << Irql);//关键。标志向cpu发出了一个对应irql级的软中断

PendingIrql = SWInterruptLookUpTable[Pcr->IRR & 3];//IRR后两位表示是否有阻塞的apc中断

//若有阻塞的apc中断,并且当前irql是PASSIVE_LEVEL,立即执行apc。
//也即在PASSIVE_LEVEL级时发出 任意软中断后,会立即检查执行现有的apc中断。
if (PendingIrql > Pcr->Irql)
SWInterruptHandlerTable[PendingIrql]();//调用执行apc中断的isr,处理apc中断
writeeflags(EFlags);//恢复原eflags寄存器
}

DPC 函数的执行时机

KfLowerIrql

windows内核什么时候会扫描DPC请求队列,执行这些DPC函数呢?
答案是每当CPU的运行级别从DISPATCH_LEVEL或以上降低到DISPATCH_LEVEL以下时,如果有扫描DPC请求队列的要求存在,内核就会扫描DPC请求队列并执行DPC函数(如果队列非空的话)。

为此不妨从KfLowerIrql开始往下看。

[KfLowerIrql() > HalpLowerIrql() > KiDispatchInterrupt() > KiRetireDpcList()]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
VOID FASTCALL KfLowerIrql(IN KIRQL OldIrql)//降回到原irql 
{
ULONG EFlags;
ULONG PendingIrql, PendingIrqlMask;
PKPCR Pcr = KeGetPcr();
PIC_MASK Mask;
EFlags = readeflags();//保存老的eflags寄存器
_disable();//关中断
Pcr->Irql = OldIrql;//降低到指定irql

//检查是否有在目标irql以上的阻塞中的软中断
PendingIrqlMask = Pcr->IRR & FindHigherIrqlMask[OldIrql];
if (PendingIrqlMask)
{
//从高位到低位扫描,找到阻塞中的最高irql级的软中断

BitScanReverse(&PendingIrql, PendingIrqlMask);
if (PendingIrql > DISPATCH_LEVEL) …
SWInterruptHandlerTable[PendingIrql]();//处理那个软中断,似乎这儿有问题,应该是
SWInterruptHandlerTable[31-PendingIrql]();
}
writeeflags(EFlags);//恢复原eflags寄存器
}

unsigned char BitScanReverse(ULONG * const Index, unsigned long Mask)
{
*Index = 0;
while (Mask && ((Mask & (1 << 31)) == 0))
{
Mask <<= 1;
++(*Index);
}
return Mask ? 1 : 0;
}
```

各个软中断的处理函数如下(怀疑这个表的布局有问题)

```c++
PHAL_SW_INTERRUPT_HANDLER SWInterruptHandlerTable[20] =
{
KiUnexpectedInterrupt,//PASSIVE_LEVEL最低了,永远不会中断别的irql
HalpApcInterrupt,//APC中断的isr
HalpDispatchInterrupt2,//DPC中断的isr
KiUnexpectedInterrupt,
HalpHardwareInterrupt0,
HalpHardwareInterrupt1,
HalpHardwareInterrupt2,
HalpHardwareInterrupt3,
HalpHardwareInterrupt4,
HalpHardwareInterrupt5,
HalpHardwareInterrupt6,
HalpHardwareInterrupt7,
HalpHardwareInterrupt8,
HalpHardwareInterrupt9,
HalpHardwareInterrupt10,
HalpHardwareInterrupt11,
HalpHardwareInterrupt12,
HalpHardwareInterrupt13,
HalpHardwareInterrupt14,
HalpHardwareInterrupt15
};

下面是处理 DPC 软中断的 isr

HalpDispatchInterrupt2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
VOID HalpDispatchInterrupt2(VOID) 
{
ULONG PendingIrqlMask, PendingIrql;
KIRQL OldIrql;
PIC_MASK Mask;
PKPCR Pcr = KeGetPcr();
//这个函数里面会提高irql到DISPACH_LEVEL去扫描执行dpc队列中的所有dpc
OldIrql = _HalpDispatchInterruptHandler();//关键函数 Pcr->Irql = OldIrql;//恢复成原来的irql

//再去检测是否仍有更高irql的阻塞软中断
PendingIrqlMask = Pcr->IRR & FindHigherIrqlMask[OldIrql];
if (PendingIrqlMask)
{
BitScanReverse(&PendingIrql, PendingIrqlMask);
if (PendingIrql > DISPATCH_LEVEL) …
SWInterruptHandlerTable[PendingIrql]();//应该是[31 - PendingIrql]
}
}

KIRQL _HalpDispatchInterruptHandler(VOID)
{
KIRQL CurrentIrql;
PKPCR Pcr = KeGetPcr();
CurrentIrql = Pcr->Irql;
Pcr->Irql = DISPATCH_LEVEL;//将irql临时提高到DISPATCH_LEVEL
Pcr->IRR &= ~(1 << DISPATCH_LEVEL);//清除对应的软中断位
_enable();//开中断
KiDispatchInterrupt();//关键函数。开中断后扫描执行所有dpc
_disable();
return CurrentIrql;//返回原irql
}

KiDispatchInterrupt

下面的函数扫描当前 cpu 的 dpc 队列执行所有

此函数在执行 dpc 前会先将内核栈切换为 dpc 函数专用栈。
因为 dpc 函数运行在任意线程的上下文中, 而 dpc 函数可能太大,局部变量太多而占用了过多的内核栈
所以需要为 dpc 函数的执行专门配备一个栈。

这个函数还有一个注意地方,就是在扫描 dpc 队列执行完所有 dpc 函数后,会检查当前线程的时间片是否 耗尽,若耗尽就进行线程切换,若尚未耗尽,就检查当前是否有一个抢占者线程,若有也进行线程切换。
【总之: 系统在每次扫描执行完 dpc 队列后,都会尝试进行线程切换】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
dpc KiDispatchInterrupt 
{
push ebx
mov ebx, PCR[KPCR_SELF] //ebc = kpcr*
cli //关中断
//检查dpc队列是否为空
mov eax, [ebx+KPCR_PRCB_DPC_QUEUE_DEPTH]
or eax, [ebx+KPCR_PRCB_TIMER_REQUEST]
or eax, [ebx+KPCR_PRCB_DEFERRED_READY_LIST_HEAD]
jz CheckQuantum

push ebp //保存
push dword ptr [ebx+KPCR_EXCEPTION_LIST] //保存原seh
mov dword ptr [ebx+KPCR_EXCEPTION_LIST], -1 //将当前色seh置空
/* Save the stack and switch to the DPC Stack */
mov edx, esp
mov esp, [ebx+KPCR_PRCB_DPC_STACK] //切换为DPC函数专用的内核栈
push edx //保存原来的内核栈顶位置

mov ecx, [ebx+KPCR_PRCB]
call @KiRetireDpcList@4 //关键。扫描执行dpc
pop esp //恢复成原来的内核栈顶
pop dword ptr [ebx+KPCR_EXCEPTION_LIST] //恢复
pop ebp //恢复
CheckQuantum:
Sti //开中断
cmp byte ptr [ebx+KPCR_PRCB_QUANTUM_END], 0 //现在回头检查当前线程的时间片是否耗尽
jnz QuantumEnd //若已耗尽,直接跳到QuantumEnd处执行线程切换
cmp byte ptr [ebx+KPCR_PRCB_NEXT_THREAD], 0 //再检查当前是否有一个抢占者线程
je Return

/* Make space on the stack to save registers */
sub esp, 3 * 4
mov [esp+8], esi //保存
mov [esp+4], edi //保存
mov [esp+0], ebp //保存
mov edi, [ebx+KPCR_CURRENT_THREAD]

#ifdef CONFIG_SMP //if多处理器
call _KeRaiseIrqlToSynchLevel@0 //提升irql到SynchLevel
mov byte ptr [edi+KTHREAD_SWAP_BUSY], 1 //标记该线程正在进行切换
/* Acquire the PRCB Lock */
lock bts dword ptr [ebx+KPCR_PRCB_PRCB_LOCK], 0
jnb GetNext
lea ecx, [ebx+KPCR_PRCB_PRCB_LOCK]
call @KefAcquireSpinLockAtDpcLevel@4
#endif
GetNext:
mov esi, [ebx+KPCR_PRCB_NEXT_THREAD]
and dword ptr [ebx+KPCR_PRCB_NEXT_THREAD], 0
mov [ebx+KPCR_CURRENT_THREAD], esi
mov byte ptr [esi+KTHREAD_STATE_], Running
mov byte ptr [edi+KTHREAD_WAIT_REASON], WrDispatchInt

mov ecx, edi
lea edx, [ebx+KPCR_PRCB_DATA]
call @KiQueueReadyThread@8
mov cl, APC_LEVEL
call @KiSwapContextInternal@0

#ifdef CONFIG_SMP
mov cl, DISPATCH_LEVEL
call @KfLowerIrql@4
#endif
mov ebp, [esp+0] //恢复
mov edi, [esp+4] //恢复
mov esi, [esp+8] //恢复
add esp, 3*4 Return:
pop ebx
ret QuantumEnd:
mov byte ptr [ebx+KPCR_PRCB_QUANTUM_END], 0
call _KiQuantumEnd@0 //调用这个函数切换线程
pop ebx
ret
}

KiRetireDpcList

下面的函数才是最终扫描执行 dpc 的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
VOID FASTCALL KiRetireDpcList(IN PKPRCB Prcb) 
{
PKDPC_DATA DpcData;
PLIST_ENTRY ListHead, DpcEntry;
PKDPC Dpc;
PKDEFERRED_ROUTINE DeferredRoutine;
PVOID DeferredContext, SystemArgument1, SystemArgument2;
ULONG_PTR TimerHand;
DpcData = &Prcb->DpcData[DPC_NORMAL];//当前cpu的普通dpc队列
ListHead = &DpcData->DpcListHead;
do
{

Prcb->DpcRoutineActive = TRUE;//标记当前cpu正在执行dpc
if (Prcb->TimerRequest) //if收到有定时器到期dpc中断(定时器是一种特殊的dpc)
{
TimerHand = Prcb->TimerHand;
Prcb->TimerRequest = 0;
_enable();
KiTimerExpiration(NULL, NULL, (PVOID)TimerHand, NULL);//处理定时器队列
_disable();
}
while (DpcData->DpcQueueDepth != 0)//遍历dpc队列
{
KeAcquireSpinLockAtDpcLevel(&DpcData->DpcLock);
DpcEntry = ListHead->Flink;
if (DpcEntry != ListHead)
{
RemoveEntryList(DpcEntry);//取下来
Dpc = CONTAINING_RECORD(DpcEntry, KDPC, DpcListEntry);
Dpc->DpcData = NULL;
DeferredRoutine = Dpc->DeferredRoutine;
DeferredContext = Dpc->DeferredContext;
SystemArgument1 = Dpc->SystemArgument1;
SystemArgument2 = Dpc->SystemArgument2;
DpcData->DpcQueueDepth--;
Prcb->DebugDpcTime = 0;
KeReleaseSpinLockFromDpcLevel(&DpcData->DpcLock);
_enable();//开中断
//关键。执行DPC例程
DeferredRoutine(Dpc,DeferredContext,SystemArgument1,SystemArgument2);
ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL);
_disable();//关中断
}
else
{
ASSERT(DpcData->DpcQueueDepth == 0);//肯定执行完了所有dpc
KeReleaseSpinLockFromDpcLevel(&DpcData->DpcLock);
}
}
Prcb->DpcRoutineActive = FALSE;//当前不再用dpc正在执行
Prcb->DpcInterruptRequested = FALSE;
} while (DpcData->DpcQueueDepth != 0);
}

注意:DPC 函数运行在DISPATCH_LEVEL,并且开中断,因此 dpc 函数本身又可能被其他中断给中断。
因为 dpc 函数本身就是一种软中断,因此它支持中断嵌套。

【总之:在降低过程中检查是否有 dpc 中断,若有执行之】

一句口诀:【降低、检断、DPC】 不像 APC 的执行时机有很多,DPC 的执行时机就一处。

那么在什么时候系统会降低 irql 呢?
除了用户显式调用这个内核函数外, isr 一般工作在比DISPATCH_LEVEL高的 irql,当 isr 退出时,必然会降低 irql 到原来的 irql。
因此常常在 isr 中插入一 个 dpc 到 dpc 队列,发出一个 dpc 中断给 cpu,然后退出 isr 时,降低 irql,顺理成章的执行 dpc。


DPC切目标CPU

KeSetTargetProcessorDpc

KeInitializeDpc 初始化的 dpc,默认的目标 cpu 都是当前 cpu,如果需要将 dpc 发给其他 cpu,让其在其他 cpu 上运行的话

可以采用下面的函数

1
2
3
4
VOID KeSetTargetProcessorDpc(IN PKDPC Dpc,IN CCHAR Number) 
{
Dpc->Number = Number + 32;
}

这是一个非常有用的函数,因为他可以使你的代码运行在你想要的 cpu 上。比如,你写了一个函数,你只想那个函数运行在 3 号 cpu 上,那么你可以构造一个在 3 号 cpu 上运行的 dpc,然后在 dpc 里调用你自己的函数。
这种技术常用于保障内联 hook 的多 cpu 线程安全 和 IDT hook。

当然也可使用KeSetSystemAffinityThread这个内核函数,修改当前线程的 cpu 亲缘性为只能运行在目标 cpu上,这样也会立即导致当前线程立刻挪到其它 cpu 上去运行,KeSetSystemAffinityThread的代码,有兴趣的读者自己看。


系统工作者线程

本篇既然谈到了 DPC,那就要讲下与之紧密相关的另一个话题:系统工作者线程

DPC 函数是运行在DISPATCH_LEVEL的,而内核中的绝大多数函数的运行时 irql 都不能处在这个中断级别
ZwCreateFieddk文档规定了,这个内核函数必须运行在PASSIVE_LEVEL

如果我们需要在某个 DPC 函 数中调用ZwCreateFie,怎么办呢?
一个办法便是将这个工作打包成一条工作项委派给系统工作者线程去执行。
内核中有一个守护线程(其实分成 9 个线程),运行在PASSIVE_LEVEL,专门用来提供服务执行别的线 程委派给它的工作,这个守护线程就是系统工作者线程

按照工作项的紧迫程度,分成三种。系统中相应的有三种工作项队列

1
2
3
4
5
typedef enum _WORK_QUEUE_TYPE { 
CriticalWorkQueue,//较紧迫
DelayedWorkQueue,//最常见
HyperCriticalWorkQueue,//最紧迫
} WORK_QUEUE_TYPE;

CriticalWorkQueue工作项队列上配有 5 个服务线程,DelayedWorkQueue队列上配有 3 个服务线程,HyperCriticalWorkQueue上配有 1 个服务线程。

ExInitializeWorkItem

1
2
3
4
5
6
#define ExInitializeWorkItem(Item,Routine,Context) \ 
{ \
Item->WorkRoutine=Routine;\
Item->Parameter=Context;\
Item->List.Flink=NULL;\
}

ExQueueWorkItem

构造好一条工作项后,就可以把这条工作项挂入指定紧迫程度的系统工作项队列中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
VOID ExQueueWorkItem(IN PWORK_QUEUE_ITEM WorkItem, 
IN WORK_QUEUE_TYPE QueueType)//工作项紧迫程度类型
{
PEX_WORK_QUEUE WorkQueue = &ExWorkerQueue[QueueType];//相应的工作项队列
if ((ULONG_PTR)WorkItem->WorkerRoutine < MmUserProbeAddress)//必须位于内核空间
{
KeBugCheckEx(WORKER_INVALID,1, (ULONG_PTR)WorkItem,
(ULONG_PTR)WorkItem->WorkerRoutine,0);
}

KeInsertQueue(&WorkQueue->WorkerQueue, &WorkItem->List);//关键。

if ((WorkQueue->Info.MakeThreadsAsNecessary) &&
(!IsListEmpty(&WorkQueue->WorkerQueue.EntryListHead)) &&
(WorkQueue->WorkerQueue.CurrentCount < WorkQueue->WorkerQueue.MaximumCount) &&
(WorkQueue->DynamicThreadCount < 16))
{
KeSetEvent(&ExpThreadSetManagerEvent, 0, FALSE);
}
}

当把工作项插入到系统对应的工作项队列后,系统中的某个服务线程便会在某一时刻处理该工作项。
9个服务线程的函数都是同一个函数,只是参数不同。我们看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
VOID ExpWorkerThreadEntryPoint(IN PVOID Context)//context参数主要表示工作项队列类型 
{
PLARGE_INTEGER TimeoutPointer = NULL;
PETHREAD Thread = PsGetCurrentThread();
if ((ULONG_PTR)Context & EX_DYNAMIC_WORK_THREAD)
{

Timeout.QuadPart = Int32x32To64(10, -10000000 * 60);//1分钟
TimeoutPointer = &Timeout;
}
WorkQueueType = (WORK_QUEUE_TYPE)((ULONG_PTR)Context &~ EX_DYNAMIC_WORK_THREAD);
WorkQueue = &ExWorkerQueue[WorkQueueType];
WaitMode = (UCHAR)WorkQueue->Info.WaitMode;
ASSERT(Thread->ExWorkerCanWaitUser == FALSE);
if (WaitMode == UserMode) Thread->ExWorkerCanWaitUser = TRUE;
if (!ExpWorkersCanSwap) KeSetKernelStackSwapEnable(FALSE);
do
{
if (WorkQueue->Info.QueueDisabled)
{
KeSetKernelStackSwapEnable(TRUE);
PsTerminateSystemThread(STATUS_SYSTEM_SHUTDOWN);//立即终止
}
OldValue = WorkQueue->Info;
NewValue = OldValue;
NewValue.WorkerCount++;//递增该队列上的工作线程计数
}
while (InterlockedCompareExchange((PLONG)&WorkQueue->Info,*(PLONG)&NewValue,
*(PLONG)&OldValue) != *(PLONG)&OldValue);
Thread->ActiveExWorker = TRUE;//标记正式成为一个工作者线程了 ProcessLoop:
for (;;)
{
//等待本服务线程的工作项队列中出现工作项,然后取下来
QueueEntry = KeRemoveQueue(&WorkQueue->WorkerQueue,WaitMode,TimeoutPointer);
if ((NTSTATUS)(ULONG_PTR)QueueEntry == STATUS_TIMEOUT)
break;//等待超时就退出for循环

InterlockedIncrement((PLONG)&WorkQueue->WorkItemsProcessed);//递增已处理计数
WorkItem = CONTAINING_RECORD(QueueEntry, WORK_QUEUE_ITEM, List);

WorkItem->WorkerRoutine(WorkItem->Parameter);//关键。调用执行工作项

if (Thread->Tcb.SpecialApcDisable) Thread->Tcb.SpecialApcDisable = FALSE;

//我们的工作项函数运行在PASSIVE_LEVEL,内部不要修改irql,否则蓝屏
if (KeGetCurrentIrql() != PASSIVE_LEVEL)
{
KeBugCheckEx(WORKER_THREAD_RETURNED_AT_BAD_IRQL,
(ULONG_PTR)WorkItem->WorkerRoutine,KeGetCurrentIrql(),
(ULONG_PTR)WorkItem->Parameter, (ULONG_PTR)WorkItem);
}

if (Thread->ActiveImpersonationInfo)//工作项函数内部不能冒用令牌
{
KeBugCheckEx(IMPERSONATING_WORKER_THREAD, (ULONG_PTR)WorkItem->WorkerRoutine,
(ULONG_PTR)WorkItem->Parameter, (ULONG_PTR)WorkItem,0);
}
}
if (!IsListEmpty(&Thread->IrpList)) goto ProcessLoop; //继续服务
if (WorkQueue->Info.QueueDisabled) goto ProcessLoop; //继续服务
//下面退出服务线程
do
{
OldValue = WorkQueue->Info;
NewValue = OldValue;
NewValue.WorkerCount--;//递减该队列上的服务线程计数
}
while (InterlockedCompareExchange((PLONG)&WorkQueue->Info,*(PLONG)&NewValue,
*(PLONG)&OldValue) != *(PLONG)&OldValue);
InterlockedDecrement(&WorkQueue->DynamicThreadCount);
Thread->ActiveExWorker = FALSE;
KeSetKernelStackSwapEnable(TRUE);
return;
}

上面这段代码想必不用过多解释了




中断处理

每个 cpu 有一张中断表,简称IDT

IDT的整体布局:【异常->空白->5 系->硬】(推荐采用 7 字口诀的方式重点记忆)

  • 异常:前 20 个表项存放着各个异常的描述符(IDT 表不仅可以放中断描述符,还放置了所有异常的异常处理描述符0x00-0x13)
  • 保留:0x14-0x1F,忽略这块号段
  • 空白:接下来存放一组空闲的保留项(0x20-0x29),供系统和程序员自己分配注册使用
  • 5 系:然后是系统自己注册的 5 个预定义的软中断向量(软中断指手动的 INT 指令)

    0x2A-0x2E 5 个系统预注册的中断向量

    • 0x2A:KiGetTickCount,
    • 0x2B:KiCallbaclReturn
    • 0x2C:KiRaiseAssertion
    • 0x2D:KiDebugService
    • 0x2E:KiSystemService
  • 硬:最后的表项供驱动程序注册硬件中断使用和自定义注册其他软中断使用(0x30-0xFF)

下面是中断号的具体的分配情况:

  • 0x00-0x13 固定分配给异常:
  • 0x14-0x1f:Intel 保留给他公司将来自己使用(OS 和用户都不要试图去使用这个号段,不安全)

———————-以下的号段可用于自由分配给 OS、硬件、用户使用———————–

linux 等其他系统是怎么划分这块号段的,不管我们只看 Windows 的情况

  • 0x20-0x29:Windows 没占用,因此这块号段我们也可以自由使用
  • 0x2A-0x2E:Windows 自己本身使用的 5 个中断号
  • 0x30-0xFF:Windows 决定把这块剩余的号段让给硬件和用户使用

参见《寒江独钓》一书 P93 页注册键盘中断时,搜索空闲未用表项是从0x20开始,到0x29结束的;
就知道为什么寒江独钓是在这段范围内搜索空白表项了(其实我们也完全可以从 0x14 开始搜索)

Windows 系统中,0x30-0xFF这块号段让给了硬件和用户自己使用。事实上这块号段的开头部分默认都是让给硬件IRQ使用的,也即是分配给硬件IRQ的。
IRQ N默认映射到中断号0x30+N
IRQ0用于系统时钟,系统时钟中断号默认对应就是0x30
当然程序员也可以修改APIC(可编程中断控制器)IRQ映射到自定义的中断号。

IRQ 对外部设备分配,但 IRQ0,IRQ2,IRQ13 必须如下分配:

  • IRQ0 —->间隔定时设备
  • IRQ2 —->8259A 芯片
  • IRQ13 —->外部数学协处理器
  • 其余的 IRQ 可以任意分配给外部设备。

虽然一个 IRQ 只对应一个中断号,但是由于 IRQ 数量有限,而设备种类成千上万,因此多个设备可以使用同一个 IRQ,进而多个设备可以分配同一个中断号。
因此一个中断号可以共享给多个设备同时使用。


IoConnectInterrupt

Pnp设备在插入系统后,相应的总线驱动会自动为其创建一个用作栈底基石的pdo,然后给这个pdo发出一个IRP_MN_QUERY_RESOURCE_REQUIREMENTS查询得到初步的资源需求。
然后pnp管理器会找到相应的硬件端口驱动,调用其AddDevice函数,当这个函数返回后该硬件设备的设备栈已经建立起立了

pnp管理器就给栈顶设备发出一个IRP_MN_FILTER_RESOURCE_REQUIREMENTS再次询问该硬件需要的资源(功能驱动此时可以拦截处理这个 irp,修改资源需求)
当确定好最终的资源需求后,系统就协调分配端口号、 中断号、DIRQL等硬件资源给它。分配完后就发出一个IRP_MN_START_DEVICE给栈顶设备请求启动该硬件设备。
当该 irp 下发来到端口驱动(指真正的硬件驱动)时, 端口驱动这时就需要在分配的中断号上注册一个中断服务例程,以处理硬件中断,与设备进行交互。

下面的函数就是用来注册中断服务例程的(准确的说法叫挂接中断)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
NTSTATUS IoConnectInterrupt(OUT PKINTERRUPT *InterruptObject,//返回创建的中断对象(一般是一个数组) 
IN PKSERVICE_ROUTINE ServiceRoutine,//我们的isr(our isr)
IN PVOID ServiceContext,//isr的参数
IN PKSPIN_LOCK SpinLock,//我们isr的自旋锁,用于多cpu互斥(一般传NULL即可)
IN ULONG Vector,//分配到的中断号
IN KIRQL Irql,//isr对应的irql
IN KIRQL SynchronizeIrql,//必须>=Irql,一般=Irql即可(isr实际运行在这个irql)
IN KINTERRUPT_MODE InterruptMode,//表示是否允许执行本中断的下一个中断
IN BOOLEAN ShareVector,//表示本中断对象是否想要共享中断号以及是否允许共享
IN KAFFINITY ProcessorEnableMask,//本isr的cpu亲缘性,一般全部cpu都亲缘。
IN BOOLEAN FloatingSave)//一般为FALSE
{
PKINTERRUPT Interrupt;
PKINTERRUPT InterruptUsed;//当前的中断对象
PIO_INTERRUPT IoInterrupt;//中断对象数组的头部
PKSPIN_LOCK SpinLockUsed;//实际使用的自旋锁
BOOLEAN FirstRun;
CCHAR Count = 0;//cpu号
KAFFINITY Affinity;//cpu亲缘性掩码
PAGED_CODE();
*InterruptObject = NULL;
Affinity = ProcessorEnableMask & KeActiveProcessors;//本isr的cpu亲缘性与实有cpu的交集
while (Affinity)
{
if (Affinity & 1) Count++;
Affinity >>= 1;
}


//上面的循环根据本isr可以在哪些cpu上运行,得出可运行的cpu个数 if (!Count) return STATUS_INVALID_PARAMETER;
//分配一个中断对象数组
IoInterrupt = ExAllocatePoolWithTag(NonPagedPool, (Count - 1) * sizeof(KINTERRUPT) +
sizeof(IO_INTERRUPT),TAG_KINTERRUPT);
if (!IoInterrupt) return STATUS_INSUFFICIENT_RESOURCES;
*InterruptObject = &IoInterrupt->FirstInterrupt;

//if 用户没提供自旋锁,就使用内置的自旋锁。一般用户不需自己提供自旋锁
SpinLockUsed = SpinLock ? SpinLock : &IoInterrupt->SpinLock;
Interrupt = (PKINTERRUPT)(IoInterrupt + 1);//后面的中断对象数组地址
FirstRun = TRUE;
RtlZeroMemory(IoInterrupt, sizeof(IO_INTERRUPT));
Affinity = ProcessorEnableMask & KeActiveProcessors;
for (Count = 0; Affinity; Count++, Affinity >>= 1) //Count其实表示cpu号
{
if (Affinity & 1)
{
//第一次使用头部中的那个内置中断对象
InterruptUsed = FirstRun ? &IoInterrupt->FirstInterrupt : Interrupt;
//构造一个中断对象
KeInitializeInterrupt(InterruptUsed,ServiceRoutine,ServiceContext,
SpinLockUsed,Vector,Irql,SynchronizeIrql,
InterruptMode,ShareVector,Count,FloatingSave);
if (!KeConnectInterrupt(InterruptUsed))//关键,挂接中断对象到目标cpu的指定中断号
{
//if 挂接失败
if (FirstRun)
ExFreePool(IoInterrupt);
else
IoDisconnectInterrupt(&IoInterrupt->FirstInterrupt);
return STATUS_INVALID_PARAMETER;
}
if (FirstRun)
FirstRun = FALSE;
Else //记录各cpu的那个中断号上挂接的中断对象地址
IoInterrupt->Interrupt[(UCHAR)Count] = Interrupt++;
}
}
return STATUS_SUCCESS;
}

如上这个函数用来将指定 isr 挂接到各个 cpu 的指定中断号上。
因为在多 cpu 系统中,一个设备可以向每个 cpu 都发出中断,因此必须在每个 cpu 的 IDT 中都要挂接登记那个中断的 isr。

具体是怎么挂接的呢?
这个函数会创建一个中断对象数组,然后将各个中断对象对应挂接到各 cpu 的同一中断号上。

KeInitializeInterrupt

由于老式机器是单 cpu 的,因此早期的中断对象结构IO_INTERRUPT就包含一个中断对象任意,后来的机器对其 进行了扩展,在这个结构后面是一个中断对象数组,用来挂接到其他 cpu 上。 另外由于多个设备可以共用同一中断号,所以每个中断号需要一个自己的链表来记录所有挂接在此中断号上的所有中断对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
typedef struct _IO_INTERRUPT 
{
KINTERRUPT FirstInterrupt;//内置的中断对象
PKINTERRUPT Interrupt[MAXIMUM_PROCESSORS];//记录各cpu上挂接的中断对象地址
KSPIN_LOCK SpinLock;//内置的isr自旋锁,如果用户没提供,就默认使用这个公共的自旋锁。
} IO_INTERRUPT, *PIO_INTERRUPT;

// 该结构体后面紧跟一个 INTERRUPT 结构体数组
typedef struct _KINTERRUPT //中断对象
{
CSHORT Type;

CSHORT Size;
LIST_ENTRY InterruptListEntry;//用来挂入中断对象链表
PKSERVICE_ROUTINE ServiceRoutine;//我们的isr(用户的isr)
PVOID ServiceContext;//isr 参数
KSPIN_LOCK SpinLock; //一般无用
ULONG TickCount;//没用
PKSPIN_LOCK ActualLock;//我们isr实际使用的自旋锁 PKINTERRUPT_ROUTINE DispatchAddress;//中间的Dispatch isr函数地址 CCHAR Number;//要挂往的目标cpu
ULONG Vector;//要挂往的目标中断号
KIRQL Irql;//isr对应的isr
KIRQL SynchronizeIrql;//isr实际运行在的irql
BOOLEAN FloatingSave;//一般为FALSE
BOOLEAN Connected;//表示本中断对象是否挂上去了
BOOLEAN ShareVector;//是否想要共享中断号,以及是否允许后来的中断对象共享
KINTERRUPT_MODE Mode;//是否允许继续执行本中断对象后面的中断对象的isr
ULONG ServiceCount;//没用
ULONG DispatchCount;//没用
ULONG DispatchCode[DISPATCH_LENGTH];//这不是数组,而是一段代码,表示本中断对象的模板isr
} KINTERRUPT;

下面的函数用来构造、初始化一个中断对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
VOID KeInitializeInterrupt(IN PKINTERRUPT Interrupt, 
IN PKSERVICE_ROUTINE ServiceRoutine,IN PVOID ServiceContext,
IN PKSPIN_LOCK SpinLock,IN ULONG Vector,IN KIRQL Irql,
IN KIRQL SynchronizeIrql,IN KINTERRUPT_MODE InterruptMode,
IN BOOLEAN ShareVector,IN CHAR ProcessorNumber,IN BOOLEAN FloatingSave)
{
ULONG i;
PULONG DispatchCode = &Interrupt->DispatchCode[0],Patch = DispatchCode;//patch表示补丁处
Interrupt->Type = InterruptObject;
Interrupt->Size = sizeof(KINTERRUPT);
if (SpinLock)//由于这个函数未导出,由系统内部调用,传的SpinLock参数很少为NULL
Interrupt->ActualLock = SpinLock;//使用头部中公共的自旋锁或者我们提供的自旋锁
else
{
KeInitializeSpinLock(&Interrupt->SpinLock);
Interrupt->ActualLock = &Interrupt->SpinLock;
}
Interrupt->ServiceRoutine = ServiceRoutine;
Interrupt->ServiceContext = ServiceContext;
Interrupt->Vector = Vector;
Interrupt->Irql = Irql;

Interrupt->SynchronizeIrql = SynchronizeIrql;
Interrupt->Mode = InterruptMode;
Interrupt->ShareVector = ShareVector;
Interrupt->Number = ProcessorNumber;
Interrupt->FloatingSave = FloatingSave;
Interrupt->TickCount = MAXULONG; Interrupt->DispatchCount = MAXULONG;

//先拷贝模板isr的字节码到中断对象内部
for (i = 0; i < DISPATCH_LENGTH; i++)
*DispatchCode++ = ((PULONG)KiInterruptTemplate)[i];
//patch 指向模板isr中的mov edx,0指令的操作数部分 Patch = (PULONG)((ULONG)Patch +
((ULONG)&KiInterruptTemplateObject -4 - (ULONG)KiInterruptTemplate));
*Patch = PtrToUlong(Interrupt);//也即将原mov edx,0 改为 mov edx,本中断对象的地址
Interrupt->Connected = FALSE;//尚未挂入
}

下面是系统的模板 isr

1
2
3
4
5
6
7
8
9
10
_KiInterruptTemplate: 
KiEnterTrap KI_PUSH_FAKE_ERROR_CODE

_KiInterruptTemplate2ndDispatch:
mov edx, 0 //这条指令的操作数0将被动态修改成具体中断对象的地址

_KiInterruptTemplateObject:
mov eax, offset @KiInterruptTemplateHandler@8 //KiInterruptTemplateHandler函数
jmp eax
_KiInterruptTemplateDispatch:

上面就是系统的模板 isr,每个中断对象的模板 isr 就是从系统的模板 isr 复制过来的,然后稍作修改。

KeConnectInterrupt

当构造好中断对象后,就需要把它挂接到目标 cpu 的目标中断号上。

下面的函数就这个用途

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
//返回值表示是否挂接成功 
BOOLEAN KeConnectInterrupt(IN PKINTERRUPT Interrupt)
{
BOOLEAN Connected, Error, Status;
KIRQL Irql, OldIrql;
UCHAR Number;
ULONG Vector;
DISPATCH_INFO Dispatch;
Number = Interrupt->Number;//目标cpu
Vector = Interrupt->Vector;//目标中断号 Irql = Interrupt->Irql;

// SynchronizeIrql 必须 >= Irql
if ((Irql > HIGH_LEVEL) || (Number >= KeNumberProcessors) ||
(Interrupt->SynchronizeIrql < Irql) || (Interrupt->FloatingSave))
{

return FALSE;
}
Connected = FALSE;
Error = FALSE;
KeSetSystemAffinityThread(1 << Number);

//改变当前线程的cpu亲缘性先,挪到目标cpu上去运行
//------------------------------------华丽的分割线--------------------------------------
//------------------------------------华丽的分割线--------------------------------------
//------------------------------------华丽的分割线--------------------------------------
//下面的这些代码已经处在目标cpu上运行了
OldIrql = KiAcquireDispatcherLock();
if (!Interrupt->Connected)//if尚未挂接
{
//查询当前cpu这个中断号上的最近一次(也即上一次)的挂接情况
KiGetVectorDispatch(Vector, &Dispatch);
if (Dispatch.Type == NoConnect)//如果这个中断号尚未挂接有任何中断对象
{
Interrupt->Connected = Connected = TRUE;
InitializeListHead(&Interrupt->InterruptListEntry);//独立

//NormalConnect表示以普通方式挂上去(非链接方式),相当于覆盖方式
KiConnectVectorToInterrupt(Interrupt, NormalConnect);
Status = HalEnableSystemInterrupt(Vector, Irql, Interrupt->Mode);//APIC相关
if (!Status) Error = TRUE;
}
else if ((Dispatch.Type != UnknownConnect) && //已挂接有中断对象
(Interrupt->ShareVector) && //本中断对象想要共享这个中断号
(Dispatch.Interrupt->ShareVector) && //并且上次挂接的那个中断对象允许共享
(Dispatch.Interrupt->Mode == Interrupt->Mode))
{
Interrupt->Connected = Connected = TRUE;
//if 上一个中断对象不是以链接方式挂上去的,就改为链接方式挂上去
if (Dispatch.Type != ChainConnect)
{
ASSERT(Dispatch.Interrupt->Mode != Latched);
KiConnectVectorToInterrupt(Dispatch.Interrupt, ChainConnect);
}
//关键。挂在上一个中断对象的后面
InsertTailList(&Dispatch.Interrupt->InterruptListEntry,
&Interrupt->InterruptListEntry);
}
}

KiReleaseDispatcherLock(OldIrql);
KeRevertToUserAffinityThread();
if ((Connected) && (Error))
{
KeDisconnectInterrupt(Interrupt);

Connected = FALSE;
}
return Connected;
}

KiGetVectorDispatch

下面的函数用于查询当前 cpu 指定中断号上的最近一次挂接情况(查询最近一次挂上去的中断对象,以及它当时是怎么挂上去的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
VOID  KiGetVectorDispatch(IN ULONG Vector,IN PDISPATCH_INFO Dispatch) 
{
PKINTERRUPT_ROUTINE Handler;
PVOID Current;
UCHAR Type;
UCHAR Entry;
Entry = HalVectorToIDTEntry(Vector);//这个宏将中断向量号转换为IDT表项索引(一般相同)

//固定为KiUnexpectedInterruptN函数的地址,N表示IRQ
Dispatch->NoDispatch = (PVOID)(((ULONG_PTR)&KiStartUnexpectedRange) +
(Entry – 0x30) *KiUnexpectedEntrySize);
Dispatch->InterruptDispatch = (PVOID)KiInterruptDispatch;//这个字段固定
Dispatch->FloatingDispatch = NULL; //尚不支持
Dispatch->ChainedDispatch = (PVOID)KiChainedDispatch;//这个字段固定
Dispatch->FlatDispatch = NULL;

Current = KeQueryInterruptHandler(Vector);//获得这个中断向量处当前存放的isr函数地址


if ((PKINTERRUPT_ROUTINE)Current == Dispatch->NoDispatch)
{
Dispatch->Interrupt = NULL;//表示尚未挂接有任何中断对象
Dispatch->Type = NoConnect; //表示尚未挂接有任何中断对象
}
else
{

//关键,可有isr(其实是个模板isr)反推出当前的中断对象(即最近一次挂上去的对象) Dispatch->Interrupt = CONTAINING_RECORD(Current,KINTERRUPT,DispatchCode);
Handler = Dispatch->Interrupt->DispatchAddress;
if (Handler == Dispatch->ChainedDispatch)
Dispatch->Type = ChainConnect;//上次的中断对象是以链接方式挂上去的
else if ((Handler == Dispatch->InterruptDispatch) ||
(Handler == Dispatch->FloatingDispatch))
{
Dispatch->Type = NormalConnect;//上次的中断对象是以普通方式挂上去的
}
else

Dispatch->Type = UnknownConnect;//不确定上次的中断对象是怎么挂上去的
}
}

KeQueryInterruptHandler

下面这个函数返回当前 cpu 上指定中断向量处的 isr。
注意:任一时刻,每个 isr 可能是个模板 isr,可能是个用户自定义的 isr,也可能没有isr(即以KiUnexpectedInterruptN函数占位)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PVOID KeQueryInterruptHandler(IN ULONG Vector) 
{
PKIPCR Pcr = (PKIPCR)KeGetPcr();
UCHAR Entry;
Entry = HalVectorToIDTEntry(Vector);
return (PVOID)(((Pcr->IDT[Entry].ExtendedOffset << 16) & 0xFFFF0000) |
(Pcr->IDT[Entry].Offset & 0xFFFF));
}

//真正的挂接操作是调用下面的函数完成的
VOID KiConnectVectorToInterrupt(IN PKINTERRUPT Interrupt,IN CONNECT_TYPE Type)//挂接类型
{
DISPATCH_INFO Dispatch;
PKINTERRUPT_ROUTINE Handler;//将要填到IDT表项中的最直接isr
KiGetVectorDispatch(Interrupt->Vector, &Dispatch);
if (Type == NoConnect)//if用户要撤销挂接
Handler = Dispatch.NoDispatch;
else
{

//填好本中断对象的dispatch isr
Interrupt->DispatchAddress = (Type == NormalConnect) ? Dispatch.InterruptDispatch:
Dispatch.ChainedDispatch;
Handler = (PVOID)&Interrupt->DispatchCode;//本中断对象的模板isr
}
//将本中断对象的模板isr或者KiUnexpectedInterruptN填写到IDT的对应表项处。
//可以看出,通过IoConnectInterrupt函数,IDT中的每个isr都是最后一次挂接的中断对象的模板isr
KeRegisterInterruptHandler(Interrupt->Vector, Handler);
}

VOID KeRegisterInterruptHandler(IN ULONG Vector,IN PVOID Handler)
{
UCHAR Entry;
ULONG_PTR Address;
PKIPCR Pcr = (PKIPCR)KeGetPcr();
Entry = HalVectorToIDTEntry(Vector);

Address = PtrToUlong(Handler);
//将isr填写到相应的表项中
Pcr->IDT[Entry].ExtendedOffset = (USHORT)(Address >> 16);
Pcr->IDT[Entry].Offset = (USHORT)Address;
}


KiInterruptTemplateHandler

通过IoConnectInterrupt函数挂接的中断对象,都是将其模板 isr 填写到 IDT 表项中,这样谁最后挂接, 谁的模板 isr 就会最后覆写到那个表项处。
如果使用了同一中断号的各个中断对象都是以链接方式挂接上去的,那么这些中断对象将组成一个链表。
这样当 cpu 收到对应的中断号时,会找到 IDT 中对应表项的 isr 给予执行。
而那个 isr 就是最后挂接的中断对象的模板 isr,这个模板 isr 的代码前面已看过,它将跳转进入下面的函数

1
2
3
4
5
6
VOID FASTCALL KiInterruptTemplateHandler(IN PKTRAP_FRAME TrapFrame, //ecx 
IN PKINTERRUPT Interrupt) //edx
{
KiEnterInterruptTrap(TrapFrame);
((PKI_INTERRUPT_DISPATCH)Interrupt->DispatchAddress)(TrapFrame, Interrupt);//关键
}

看到没每个中断对象的模板 isr,会调用它的dispatch isr
以链接方式挂上去的中断对象的dispatch isr都是KiChainedDispatch,反之则是KiInterruptDispatch

KiChainedDispatch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
VOID FASTCALL KiChainedDispatch(IN PKTRAP_FRAME TrapFrame,IN PKINTERRUPT Interrupt) 
{
KIRQL OldIrql;
BOOLEAN Handled;
PLIST_ENTRY NextEntry, ListHead;
KeGetCurrentPrcb()->InterruptCount++;//递增中断计数

//HalBeginSystemInterrupt会提升irql至指定irql,以准备执行我们的isr
if (HalBeginSystemInterrupt(Interrupt->Irql,Interrupt->Vector,&OldIrql))//APIC相关
{
ListHead = &Interrupt->InterruptListEntry;
NextEntry = ListHead;
while (TRUE) //遍历中断对象链表,直至找到一个能处理这个中断的中断对象为止
{
if (Interrupt->SynchronizeIrql > Interrupt->Irql)//再次提升irql
OldIrql = KfRaiseIrql(Interrupt->SynchronizeIrql);
KxAcquireSpinLock(Interrupt->ActualLock);//加锁,保障多cpu互斥
//执行我们的isr(即用户自己提供的isr),注意返回值
Handled = Interrupt->ServiceRoutine(Interrupt,Interrupt->ServiceContext);
KxReleaseSpinLock(Interrupt->ActualLock);
if (Interrupt->SynchronizeIrql > Interrupt->Irql)
KfLowerIrql(OldIrql);
//if本中断对象认领了,且不许执行下一个中断对象就退出查找循环。

//(LevelSensitive即FALSE,一般的中断对象都这样)
if ((Handled) && (Interrupt->Mode == LevelSensitive)) break;
NextEntry = NextEntry->Flink;
if (NextEntry == ListHead)//if链表中的最后一个中断对象
{
if (Interrupt->Mode == LevelSensitive) break;
if (!Handled) break;//if没能处理这个中断,退出循环
}
Interrupt = CONTAINING_RECORD(NextEntry, KINTERRUPT, InterruptListEntry);
}
KiExitInterrupt(TrapFrame, OldIrql, FALSE);
}
Else //清理中断Trap帧,恢复中断现场,回到原断点处继续执行
KiExitInterrupt(TrapFrame, OldIrql, TRUE);
}

用户自己的 isr 的原型是:BOOLEAN InterruptService(in struct _KINTERRUPT *Interrupt, in PVOID ServiceContex);
我们的这个 isr 应该根据ServiceContex判断这个中断是不是我们驱动中的设备发出的,若是才能处理,返回TRUE, 否则应返回FALSE
让系统继续寻找中断对象链表中的下一个中断对象去认领。

KiInterruptDispatch

而对于以普通覆写方式挂上去的中断对象,它的 dispatch isr 是KiInterruptDispatch,我们看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
VOID FASTCALL KiInterruptDispatch(IN PKTRAP_FRAME TrapFrame,IN PKINTERRUPT Interrupt) 
{
KIRQL OldIrql;
KeGetCurrentPrcb()->InterruptCount++;//递增中断计数
if (HalBeginSystemInterrupt(Interrupt->SynchronizeIrql,Interrupt->Vector,&OldIrql))
{
KxAcquireSpinLock(Interrupt->ActualLock);
//调用其自己的isr
Interrupt->ServiceRoutine(Interrupt, Interrupt->ServiceContext);
KxReleaseSpinLock(Interrupt->ActualLock);
KiExitInterrupt(TrapFrame, OldIrql, FALSE);
}
else
KiExitInterrupt(TrapFrame, OldIrql, TRUE);
}

看到没普通方式挂上去的中断对象,它独占中断号,当发生相应中断时,系统简单执行一下它自己的 isr 后就返回了,不会有在链表中查找的过程。


IoDisconnectInterrupt

底层驱动在卸载时,往往要撤销挂接的那些中断,我们看下中断对象如如何撤销挂接的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
VOID IoDisconnectInterrupt(PKINTERRUPT InterruptObject) 
{
LONG i;
PIO_INTERRUPT IoInterrupt;
PAGED_CODE();
IoInterrupt = CONTAINING_RECORD(InterruptObject,IO_INTERRUPT,FirstInterrupt);
KeDisconnectInterrupt(&IoInterrupt->FirstInterrupt);//撤销第一个中断对象的挂接
for (i = 0; i < KeNumberProcessors; i++)//撤销其它中断对象的挂接
{
if (IoInterrupt->Interrupt[i])
KeDisconnectInterrupt(&InterruptObject[i]);
}
ExFreePool(IoInterrupt);//释放整个中断对象数组加头部占用的那块内存
}

BOOLEAN KeDisconnectInterrupt(IN PKINTERRUPT Interrupt)
{
KIRQL OldIrql, Irql;
ULONG Vector;
DISPATCH_INFO Dispatch;
PKINTERRUPT NextInterrupt;
BOOLEAN State;
KeSetSystemAffinityThread(1 << Interrupt->Number);
OldIrql = KiAcquireDispatcherLock();
State = Interrupt->Connected;
if (State)
{
Irql = Interrupt->Irql;
Vector = Interrupt->Vector;
KiGetVectorDispatch(Vector, &Dispatch);//获取上次的挂接信息
if (Dispatch.Type == ChainConnect)
{
ASSERT(Irql <= SYNCH_LEVEL);
if (Interrupt == Dispatch.Interrupt)//if 要撤销挂接的中断对象就是最近挂接的那个
{
Dispatch.Interrupt = CONTAINING_RECORD(Dispatch.Interrupt-> InterruptListEntry.Flink,KINTERRUPT,InterruptListEntry);
KiConnectVectorToInterrupt(Dispatch.Interrupt, ChainConnect);
}
//关键。脱出链表
RemoveEntryList(&Interrupt->InterruptListEntry);
NextInterrupt = CONTAINING_RECORD(Dispatch.Interrupt->InterruptListEntry.Flink,
KINTERRUPT,InterruptListEntry);
if (Dispatch.Interrupt == NextInterrupt)//也即if 链表中就剩下一个中断对象了

{
KiConnectVectorToInterrupt(Dispatch.Interrupt, NormalConnect);//改为普通方式
}
}
Else //if原来本身就只挂着一个中断对象
{
HalDisableSystemInterrupt(Interrupt->Vector, Irql);
KiConnectVectorToInterrupt(Interrupt, NoConnect);//改为KiUnexpectedInterruptN
}
Interrupt->Connected = FALSE;
}
KiReleaseDispatcherLock(OldIrql);
KeRevertToUserAffinityThread();
return State;
}

通过IoConnectInterrupt函数挂接注册中断,确实为程序员减轻了大量负担。
通过这种方式注册的 isr分三层。

  • 第一层是中断对象的模板 isr
  • 第二层是中断对象的 dispatch isr
  • 第三层才是用户自己提供的 isr。

每当发生中断时系统逐层调用这三层 isr。因此也可以说,我们提供的那个 isr 被系统托管了,IDT 表项中的 isr 是系统的托管 isr。
当然程序员,也可以直接修改 IDT 中的表项,改成自己的 isr,这就是所谓的 isr hook(注意要进行 isr hook 的话,必须每个 cpu 都要 hook)


HalpClockInterruptHandler

最后我们看一下典型的系统时钟中断是怎么处理的。
系统每隔 10ms 产生一次时钟中断,时钟中断的 IRQ 固定是 0,中断号默认映射到0x30,时钟中断的 isr 最终进入下面的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
VOID FASTCALL HalpClockInterruptHandler(IN PKTRAP_FRAME TrapFrame) 
{
KIRQL Irql;
KiEnterInterruptTrap(TrapFrame);
//提升irql至CLOCK2_LEVEL
if (HalBeginSystemInterrupt(CLOCK2_LEVEL, 0x30, &Irql))//0x30就是时钟中断的中断号
{
/* Update the performance counter */
HalpPerfCounter.QuadPart += HalpCurrentRollOver;
HalpPerfCounterCutoff = KiEnableTimerWatchdog;
KeUpdateSystemTime(TrapFrame, HalpCurrentTimeIncrement, Irql);//关键函数
}
KiEoiHelper(TrapFrame);
}


VOID FASTCALL KeUpdateSystemTime(IN PKTRAP_FRAME TrapFrame,IN ULONG Increment,IN KIRQL Irql)
{
PKPRCB Prcb = KeGetCurrentPrcb();
ULARGE_INTEGER CurrentTime, InterruptTime;
ULONG Hand, OldTickCount;

//更新启动以来的运行时间计数
InterruptTime.HighPart = SharedUserData->InterruptTime.High1Time;
InterruptTime.LowPart = SharedUserData->InterruptTime.LowPart;
InterruptTime.QuadPart += Increment;//Increment一般为10ms
SharedUserData->InterruptTime.High1Time = InterruptTime.HighPart;
SharedUserData->InterruptTime.LowPart = InterruptTime.LowPart; SharedUserData->InterruptTime.High2Time = InterruptTime.HighPart;

OldTickCount = KeTickCount.LowPart;
InterlockedExchangeAdd(&KiTickOffset, -(LONG)Increment);//递减
/* Check for incomplete tick */
if (KiTickOffset <= 0)
{
//更新系统时间
CurrentTime.HighPart = SharedUserData->SystemTime.High1Time;
CurrentTime.LowPart = SharedUserData->SystemTime.LowPart;
CurrentTime.QuadPart += KeTimeAdjustment;
SharedUserData->SystemTime.High2Time = CurrentTime.HighPart;
SharedUserData->SystemTime.LowPart = CurrentTime.LowPart;
SharedUserData->SystemTime.High1Time = CurrentTime.HighPart;

//更新Tick计数
CurrentTime.HighPart = KeTickCount.High1Time;
CurrentTime.LowPart = OldTickCount;
CurrentTime.QuadPart += 1;//递增Tick计数
KeTickCount.High2Time = CurrentTime.HighPart;
KeTickCount.LowPart = CurrentTime.LowPart;
KeTickCount.High1Time = CurrentTime.HighPart;

//更新Tick计数(供应用程序GetTickCount访问)
SharedUserData->TickCount.High2Time = CurrentTime.HighPart;
SharedUserData->TickCount.LowPart = CurrentTime.LowPart;
SharedUserData->TickCount.High1Time = CurrentTime.HighPart;

Hand = OldTickCount & (TIMER_TABLE_SIZE - 1);
if (KiTimerTableListHead[Hand].Time.QuadPart <= InterruptTime.QuadPart)

{
if (!Prcb->TimerRequest)
{
Prcb->TimerRequest = (ULONG_PTR)TrapFrame;
Prcb->TimerHand = Hand;
HalRequestSoftwareInterrupt(DISPATCH_LEVEL);
}
}
OldTickCount++;
}

//下面是关键。检查是否有到点的定时器
Hand = OldTickCount & (TIMER_TABLE_SIZE - 1);

//if有到点的定时器
if (KiTimerTableListHead[Hand].Time.QuadPart <= InterruptTime.QuadPart)
{
if (!Prcb->TimerRequest)
{
Prcb->TimerRequest = (ULONG_PTR)TrapFrame;//关键。插入一个定时器dpc
Prcb->TimerHand = Hand;
HalRequestSoftwareInterrupt(DISPATCH_LEVEL);//发出一个DPC中断请求
}
}
if (KiTickOffset <= 0)
{
KiTickOffset += KeMaximumIncrement;
KeUpdateRunTime(TrapFrame, Irql);
}
else
{
Prcb->InterruptCount++;
}
KiEndInterrupt(Irql, TrapFrame);
}

上面的函数在发现有定时器到点后,就会发出一个 DPC,然后在系统定时器中断退出后,就会扫描执行 DPC 队列,调用KiTimerExpiration函数扫描所有到点的定时器,然后唤醒在那些定时器上等待的线程。
KiTimerExpiration函数的代码,有兴趣的读者自己看。




设备驱动

设备栈从上层到下层的顺序依次是:过滤设备、类设备、过滤设备、小端口设备【过、类、过滤、小端口】
驱动栈:因设备堆栈原因而建立起来的一种堆栈
老式驱动:指不提供AddDevice的驱动,又叫NT式驱动
Wdm 驱动:指提供了AddDevice的驱动

驱动初始化:指 IO 管理器加载驱动后,调用驱动的DriverEntryAddDevice函数
设备栈中上层设备与下层设备的绑定关系不是一对一,而是一对多。
一个设备可以同时绑定到 N 个下层设备上去,而一个下层设备,也可以同时被 N 个上层设备绑定,但注意形式上只可被一个上层设备绑定,因 为设备对象的AttachedDevice字段指的就是那个形式上绑定在它上面的设备。

相关结构定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct _DRIVER_OBJECT { 
CSHORT Type;//本结构的类型
CSHORT Size;//本结构的实际长度
PDEVICE_OBJECT DeviceObject;//设备对象链(第一个设备对象)
ULONG Flags;//驱动标志

//下面两个字段经常用于检测SSDT hook、DispactcRoutine hook等
PVOID DriverStart;//本驱动的sys模块在内存中的起始地址
ULONG DriverSize;//长度
PVOID DriverSection;//实际上是一个LDR_DATA_TABLE_ENTRY*指针,指向它的模块描述符结构
PEXTENDED_DRIVER_EXTENSION DriverExtension;//标准驱动扩展(所有驱动都有一个标准驱动扩准)
UNICODE_STRING DriverName;//`Driver\服务名` 或 `FileSystem\服务名`(也在对象头中保存)
PUNICODE_STRING HardwareDatabase;//注册表硬件配置键路径
struct _FAST_IO_DISPATCH *FastIoDispatch;//用于文件系统的一组快速IO例程
PDRIVER_INITIALIZE DriverInit;//sys文件的oep(一般就是DriverEntry,当然可以是其他名字)
PDRIVER_STARTIO DriverStartIo;//StartIO irp队列机制专用函数
PDRIVER_UNLOAD DriverUnload;//驱动的卸载例程
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];//各irp的派遣函数
} DRIVER_OBJECT, *PDRIVER_OBJECT;

每个驱动对象都有一个标准的驱动扩展,且紧跟在驱动对象后面。

1
2
3
4
5
6
7
8
9
typedef struct _EXTENDED_DRIVER_EXTENSION  
{
struct _DRIVER_OBJECT *DriverObject; //所属驱动对象
PDRIVER_ADD_DEVICE AddDevice; //本驱动的AddDevice例程
ULONG Count; //本驱动注册的重初始化例程的历史调用次数
UNICODE_STRING ServiceKeyName; //`Driver\服务名` 或 `FileSystem\服务名`
PIO_CLIENT_EXTENSION ClientDriverExtension;//用户提供的自定义驱动扩展
PFS_FILTER_CALLBACKS FsFilterCallbacks;
} EXTENDED_DRIVER_EXTENSION, *PEXTENDED_DRIVER_EXTENSION;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
typedef struct _DEVICE_OBJECT { 
CSHORT Type; //本结构的类型

USHORT Size; //本结构的实际长度
LONG ReferenceCount;//引用计数
struct _DRIVER_OBJECT *DriverObject;//所属驱动对象
struct _DEVICE_OBJECT *NextDevice;//在所属驱动的设备链中的下一个设备
struct _DEVICE_OBJECT *AttachedDevice;//绑定的上层设备
struct _IRP *CurrentIrp;//用于StartIO机制,表示当前正在处理中的irp
PIO_TIMER Timer;//一个内置的秒级精度定时器(定时器例程运行在DISPACH_LEVEL)
ULONG Flags;//设备标志
ULONG Characteristics;//设备特征
volatile PVPB Vpb;//卷参数块。文件卷设备、物理卷设备都有一个Vpb,指向关联的卷信息
PVOID DeviceExtension;//关键。自定义设备扩展部分(紧跟在本结构体后面)
DEVICE_TYPE DeviceType;//设备类型,与绑定的下层设备的设备类型相同
CCHAR StackSize;//设备栈中自本设备以下的栈层数(包含本层设备)
union {
LIST_ENTRY ListEntry;//用来挂入全局的‘文件系统驱动cdo链表’
WAIT_CONTEXT_BLOCK Wcb;
} Queue;
ULONG AlignmentRequirement;//对齐粒度
KDEVICE_QUEUE DeviceQueue;//本设备内置的irp队列,用于异步irp
KDPC Dpc;
ULONG ActiveThreadCount;//用于卷设备,表示当前打开线程计数
PSECURITY_DESCRIPTOR SecurityDescriptor;//SD安全描述符
KEVENT DeviceLock;//
USHORT SectorSize;//扇区大小,用于卷设备(磁盘卷设备一般为512B,关盘卷设备为2048B)
//内置的标准设备扩展部分(紧跟在自定义设备扩展后面)
struct _EXTENDED_DEVOBJ_EXTENSION *DeviceObjectExtension;
PVOID Reserved;
} DEVICE_OBJECT, *PDEVICE_OBJECT;



typedef struct _EXTENDED_DEVOBJ_EXTENSION
{
CSHORT Type;//结构类型
USHORT Size;//总设备扩展(自定义设备扩展+标准设备扩展)的长度
PDEVICE_OBJECT DeviceObject;//所属设备对象
ULONG PowerFlags;
struct DEVICE_OBJECT_POWER_EXTENSION *Dope;
ULONG ExtensionFlags;
struct _DEVICE_NODE *DeviceNode;//设备节点(只有最底层的pdo才有设备节点)
PDEVICE_OBJECT AttachedTo;//关键。最近一次绑到的下层设备
LONG StartIoCount;
LONG StartIoKey;
ULONG StartIoFlags;
struct _VPB *Vpb;//文件卷参数块
} EXTENDED_DEVOBJ_EXTENSION, *PEXTENDED_DEVOBJ_EXTENSION;

注意设备扩展由两部分组成:自定义设备扩展、标准设备扩展,共同构成设备扩展信息。

IRP

每个irp结构体后面紧跟一个数组,那就是irp的设备栈。
设备栈中的每一层叫栈层,数组中每个元素的类型为IO_SATCK_LOCATION表示的就是一个栈空间、一个栈层。

实际上我们可以把IRP结构理解为irp的头部,后面的额数组理解为irp的身体。
一个irp就是由一个irp头部和一个栈空间数组组成。
Irp头部中的CurrentLocation字段记录了该irp在各层驱动的处理进度。
该数组中第一个元素表示设备栈的栈底,最后一个元素表示栈顶。

每当将irp转发到下层设备时,irp头部中的CurrentLocation字段递减,而不是递增,就是因为这个缘故。

每个栈空间用来主要用来记录该irp的主功能码,次功能码,参数,以及各层的完成函数
因为在应用程序一次宏观的irp请求中,各层的请求可能不尽相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
typedef struct _IRP { 
CSHORT Type;//结构类型
USHORT Size;//本irp的实际分配长度(包含后面的栈空间数组)
struct _MDL *MdlAddress;//关联的MDL链表
ULONG Flags;//irp标志
union {
//(一个irp可以分成n个子irp发给下层的驱动)
struct _IRP *MasterIrp;//当该irp是子irp时表示所属的主irp
volatile LONG IrpCount;//当该irp是主irp时,表示子irp个数
PVOID SystemBuffer;//关联的系统缓冲
} AssociatedIrp;

LIST_ENTRY ThreadListEntry;//用来挂入线程的未决(指Pending)irp链表
IO_STATUS_BLOCK IoStatus;//该irp的完成结果(内置的)
KPROCESSOR_MODE RequestorMode;//表示是来自用户模式还是内核模式的irp请求
BOOLEAN PendingReturned;//表示下层设备当初处理该irp时是否是Pending异步方式处理的
CHAR StackCount;//本irp的栈层数,也即本结构体后面的数组元素个数
CHAR CurrentLocation;//从上往下数,当前的栈空间位置序号(每个栈层就是一个栈空间)
BOOLEAN Cancel;//表示用户是否发出了取消该irp的命令
KIRQL CancelIrql;//取消时的irql
CCHAR ApcEnvironment;//该irp当初分配时,线程的apc状态(挂靠态/常态)
UCHAR AllocationFlags;//用于保存当初分配该irp结构时的分配标志
PIO_STATUS_BLOCK UserIosb;//可为空。表示用户自己提供的一个结构,用来将完成结果返回给用户
PKEVENT UserEvent;//可为空。表示用户自己提供的irp完成事件

union {
struct {
_ANONYMOUS_UNION union {
PIO_APC_ROUTINE UserApcRoutine;//用户提供的APC例程
PVOID IssuingProcess;
} DUMMYUNIONNAME;
PVOID UserApcContext;//APC例程的参数
} AsynchronousParameters;
LARGE_INTEGER AllocationSize;//本结构当初分配时总的分配长度(包含后面的数组)
} Overlay;
volatile PDRIVER_CANCEL CancelRoutine;//本irp关联的取消例程(取消时将执行这个函数)
PVOID UserBuffer;//关联的用户空间缓冲区地址(直接使用可能不安全)

union {
struct {
_ANONYMOUS_UNION union {
KDEVICE_QUEUE_ENTRY DeviceQueueEntry;//用来挂入设备对象内置的irp队列
_ANONYMOUS_STRUCT struct {
PVOID DriverContext[4];
} DUMMYSTRUCTNAME;

} DUMMYUNIONNAME;
PETHREAD Thread;//该irp的发起者线程
PCHAR AuxiliaryBuffer;//关联的辅助缓冲
_ANONYMOUS_STRUCT struct {
LIST_ENTRY ListEntry;
_ANONYMOUS_UNION union {
//这个字段与CurrentLocation的作用一样,只是一个表示指针,一个表示序号
struct _IO_STACK_LOCATION *CurrentStackLocation;//当前的栈空间位置
ULONG PacketType;
} DUMMYUNIONNAME;
} DUMMYSTRUCTNAME;
//irp本来是发给设备的,但是我们也可以看做是发给文件对象(各栈层可能有变动)
struct _FILE_OBJECT *OriginalFileObject;//本irp最初发往的文件对象
} Overlay;
KAPC Apc;//与本次irp相关的APC例程
PVOID CompletionKey;
} Tail;
} IRP, *PIRP;


typedef struct _IO_STACK_LOCATION {
UCHAR MajorFunction;//当irp下发到本层时的主功能码(下发过程一般维持不变)
UCHAR MinorFunction; //当irp下发到本层时的次功能码(下发过程一般维持不变)
UCHAR Flags;//本栈层的标志
UCHAR Control;//控制标志
union {

struct {
ULONG Length;//读请求的长度
ULONG POINTER_ALIGNMENT Key;
LARGE_INTEGER ByteOffset;//读请求的文件偏移位置
} Read;
struct {
ULONG Length; //写请求的长度
ULONG POINTER_ALIGNMENT Key;
LARGE_INTEGER ByteOffset; //写请求的文件偏移位置
} Write;


} Parameters;//一个复杂的联合体,对应各种irp的参数


PDEVICE_OBJECT DeviceObject;//本栈层的设备对象
PFILE_OBJECT FileObject;//关联的文件对象

//当本栈层完成该层的irp后,会回到上层调用上层的完成例程。
// 这就是为什么在irp尚未完成的情况下贸然卸载上层驱动,会导致下层驱动调用上层的完成例程时因为完成例程无效而蓝屏崩溃。

PIO_COMPLETION_ROUTINE CompletionRoutine;//记录着上层的完成例程
PVOID Context;//完成例程的参数
} IO_STACK_LOCATION, *PIO_STACK_LOCATION;


基本函数

IoCreateDevice

下面的函数用于创建一个设备对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
NTSTATUS IoCreateDevice(IN PDRIVER_OBJECT DriverObject,//指定驱动(可以是其它第三方驱动) 
IN ULONG DeviceExtensionSize,//自定义设备扩展的大小
IN PUNICODE_STRING DeviceName,//设备对象的名字
IN DEVICE_TYPE DeviceType,//设备类型
IN ULONG DeviceCharacteristics,//设备特征
IN BOOLEAN Exclusive,//同一时刻是否只能被一个进程独占打开
OUT PDEVICE_OBJECT *DeviceObject)//返回
{
WCHAR AutoNameBuffer[20]; PAGED_CODE();//确保当前irql<=PASSIVE_LEVEL
//根据设备数量自动生成的设备名称
if (DeviceCharacteristics & FILE_AUTOGENERATED_DEVICE_NAME)
{
swprintf(AutoNameBuffer,L"\\Device\\%08lx",
InterlockedIncrementUL(&IopDeviceObjectNumber));
RtlInitUnicodeString(&AutoName, AutoNameBuffer);
DeviceName = &AutoName;
}

InitializeObjectAttributes(&ObjectAttributes,DeviceName,OBJ_KERNEL_HANDLE,NULL,NULL);
//if 同一时刻只能被一个进程独占打开
if (Exclusive) ObjectAttributes.Attributes |= OBJ_EXCLUSIVE;

//设备对象永久存于对象目录中,直至对象被完全销毁
if (DeviceName) ObjectAttributes.Attributes |= OBJ_PERMANENT;

//向上对齐8B后的自定义设备扩展大小
AlignedDeviceExtensionSize = (DeviceExtensionSize + 7) &~ 7;

//设备对象体的总长
TotalSize = sizeof(DEVICE_OBJECT) + //设备对象本身
AlignedDeviceExtensionSize + //自定义设备扩展部分
sizeof(EXTENDED_DEVOBJ_EXTENSION); //标准设备扩展部分

*DeviceObject = NULL;
//创建分配设备对象(在非分页池中)
Status = ObCreateObject(KernelMode,IoDeviceObjectType,&ObjectAttributes,KernelMode,NULL,
TotalSize,0,0, (PVOID*)&CreatedDeviceObject);
RtlZeroMemory(CreatedDeviceObject, TotalSize); CreatedDeviceObject->Type = IO_TYPE_DEVICE;//固定

//这个长度值是设备对象自身的长度加上自定义设备扩展的长度
CreatedDeviceObject->Size = sizeof(DEVICE_OBJECT) + (USHORT)DeviceExtensionSize;

//取得标准设备扩展部分的位置
DeviceObjectExtension = ((ULONG)(CreatedDeviceObject + 1) +AlignedDeviceExtensionSize);
CreatedDeviceObject->DeviceObjectExtension = DeviceObjectExtension;//记录位置
DeviceObjectExtension->Type = IO_TYPE_DEVICE_OBJECT_EXTENSION;//固定
DeviceObjectExtension->Size = 0;
PoInitializeDeviceObject(DeviceObjectExtension);//初始化电源管理器
DeviceObjectExtension->DeviceObject = CreatedDeviceObject;

CreatedDeviceObject->DeviceType = DeviceType; CreatedDeviceObject->Characteristics = DeviceCharacteristics;

//记录自定义设备扩展部分的位置
CreatedDeviceObject->DeviceExtension = DeviceExtensionSize ? CreatedDeviceObject + 1 :NU LL;
CreatedDeviceObject->StackSize = 1;//注意:设备对象初始的栈层数都为1
CreatedDeviceObject->AlignmentRequirement = 0;
CreatedDeviceObject->Flags = DO_DEVICE_INITIALIZING;//标记设备对象正在初始化过程中
if (Exclusive) CreatedDeviceObject->Flags |= DO_EXCLUSIVE;
if (DeviceName) CreatedDeviceObject->Flags |= DO_DEVICE_HAS_NAME;

//物理卷设备在创建时都会自动分配一个vpb(后面我们会看到,文件卷设备在绑定物理卷设备后,也 会记录对应的vpb)
if ((CreatedDeviceObject->DeviceType == FILE_DEVICE_DISK) ||
(CreatedDeviceObject->DeviceType == FILE_DEVICE_VIRTUAL_DISK) ||
(CreatedDeviceObject->DeviceType == FILE_DEVICE_CD_ROM) ||
(CreatedDeviceObject->DeviceType == FILE_DEVICE_TAPE))
{
Status = IopCreateVpb(CreatedDeviceObject);
KeInitializeEvent(&CreatedDeviceObject->DeviceLock,SynchronizationEvent,TRUE);
}

//磁盘卷设备的扇区大小默认512B,光盘为2048B
switch (DeviceType)
{
case FILE_DEVICE_DISK_FILE_SYSTEM:
case FILE_DEVICE_DISK:
case FILE_DEVICE_VIRTUAL_DISK:
CreatedDeviceObject->SectorSize = 512;

break;
case FILE_DEVICE_CD_ROM_FILE_SYSTEM:
CreatedDeviceObject->SectorSize = 2048;
}

//文件系统中cdo和文件卷都需要挂入相应的全局的链表中
if ((CreatedDeviceObject->DeviceType == FILE_DEVICE_DISK_FILE_SYSTEM) || //cdo
(CreatedDeviceObject->DeviceType == FILE_DEVICE_FILE_SYSTEM) || //文件卷
(CreatedDeviceObject->DeviceType == FILE_DEVICE_CD_ROM_FILE_SYSTEM) || //cdo
(CreatedDeviceObject->DeviceType == FILE_DEVICE_NETWORK_FILE_SYSTEM) || //cdo
(CreatedDeviceObject->DeviceType == FILE_DEVICE_TAPE_FILE_SYSTEM)) //cdo
{
InitializeListHead(&CreatedDeviceObject->Queue.ListEntry);
}
Else//其他的一般设备都有一个DeviceQueue表示irp队列。
{
KeInitializeDeviceQueue(&CreatedDeviceObject->DeviceQueue);
}

//插入对象目录
Status = ObInsertObject(CreatedDeviceObject,NULL,FILE_READ_DATA | FILE_WRITE_DATA,
1, (PVOID*)&CreatedDeviceObject,&TempHandle);
ObReferenceObject(DriverObject);
ASSERT((DriverObject->Flags & DRVO_UNLOAD_INVOKED) == 0);//确保驱动没在卸载当中
CreatedDeviceObject->DriverObject = DriverObject;
IopEditDeviceList(DriverObject, CreatedDeviceObject, IopAdd);//挂入驱动对象的设备链表头
if (CreatedDeviceObject->Vpb) PoVolumeDevice(CreatedDeviceObject);//电源管理相关
ObCloseHandle(TempHandle, KernelMode);//关闭临时的设备句柄(因为我们不需要这个句柄)
*DeviceObject = CreatedDeviceObject;//返回
return STATUS_SUCCESS;
}

IofCallDriver

设备对象是用来接收处理 irp 的,可以把一个 irp 发给任意设备对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//注意这个函数,是在上层驱动的上下文中调用 
#define IoCallDriver IofCallDriver
NTSTATUS FASTCALL IofCallDriver(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp)
{
PDRIVER_OBJECT DriverObject;
PIO_STACK_LOCATION StackPtr;
DriverObject = DeviceObject->DriverObject;
Irp->CurrentLocation--;//当前栈层位置向下滑动,指向下层栈空间

if (Irp->CurrentLocation <= 0)//CurrentLocation是序号,不是索引
KeBugCheckEx(NO_MORE_IRP_STACK_LOCATIONS, (ULONG_PTR)Irp, 0, 0, 0);//蓝屏

StackPtr = IoGetNextIrpStackLocation(Irp);
Irp->Tail.Overlay.CurrentStackLocation = StackPtr;//当前栈空间指向了下层
StackPtr->DeviceObject = DeviceObject;//记录好下层的设备

//关键。调用下层驱动对应irp的派遣函数
return DriverObject->MajorFunction[StackPtr->MajorFunction](DeviceObject,Irp);
}

如上可以看到,上层驱动在调用这个函数,将irp发到下层设备时,会自动在内部将当前栈空间位置向下滑动一个位置,指向下层的栈空间。
ddk提供了一个宏,用来移动irp的栈空间位

1
2
3
4
5
6
7
8
9
10
#define IoSetNextIrpStackLocation(Irp) \ 
{ \
Irp->CurrentLocation--;\ //序号向下滑动一项
Irp->Tail.Overlay.CurrentStackLocation--;\ //数组元素指针也向下滑动一项
}

// 下面的宏实际上获取的就是当前栈空间的位置
#define IoGetCurrentIrpStackLocation(irp) irp->Tail.Overlay.CurrentStackLocation
// 下面的宏实际上获取的就是下层栈空间的位置
#define IoGetNextIrpStackLocation(irp) irp->Tail.Overlay.CurrentStackLocation – 1

NtDeviceIoControlFile

应用程序常常调用DeviceIoControl这个 API 与设备驱动通信,它实际上调用NtDeviceIoControlFile这个系统服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NTSTATUS NtDeviceIoControlFile(IN HANDLE DeviceHandle,//实际上应该叫hFile 
IN HANDLE Event OPTIONAL,//用户自己提供的irp完成事件
IN PIO_APC_ROUTINE UserApcRoutine OPTIONAL,//用户提供的APC例程
IN PVOID UserApcContext OPTIONAL,//APC参数
OUT PIO_STATUS_BLOCK IoStatusBlock,//用户自己提供的Io状态块
IN ULONG IoControlCode,
IN PVOID InputBuffer,
IN ULONG InputBufferLength OPTIONAL,
OUT PVOID OutputBuffer,
IN ULONG OutputBufferLength OPTIONAL)
{
return IopDeviceFsIoControl(DeviceHandle,Event,UserApcRoutine,UserApcContext,
IoStatusBlock,IoControlCode,InputBuffer,InputBufferLength,
OutputBuffer,OutputBufferLength,
TRUE);//表示是普通的DeviceIoControl请求
}

必须注意DeviceHandle参数应该叫做文件句柄而不是设备句柄。
在 Windows 内核中,几乎从来没有设备句柄这种概念。
虽然可以为设备对象创建一个设备句柄,但很少这么做,因为设备句柄几乎没有什么作用。
相反应用程序一般都是调用CreateFile(它内部会打开设备对象,创建文件对象,返回文件对象 的句柄)
以后就可以用这个文件句柄,找到对应的文件对象及其设备对象去访问设备了。

上面函数在调用时,用户可以提供一个EventIoStatusBlock
当 irp 完成时,就会触发用户提供的这个事件,然后返回完成结果到用户提供的IoStatusBlock中。
用户还可以提供 apc 例程,irp 在完成后会自动将这个 apc 插入到线程的用户 apc 队列中,然后返回用户空间时执行这个 apc。

IopDeviceFsIoControl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
NTSTATUS IopDeviceFsIoControl(IN HANDLE DeviceHandle,IN HANDLE Event OPTIONAL, 
IN PIO_APC_ROUTINE UserApcRoutine OPTIONAL,
IN PVOID UserApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,IN ULONG IoControlCode,
IN PVOID InputBuffer,IN ULONG InputBufferLength OPTIONAL,
OUT PVOID OutputBuffer,IN ULONG OutputBufferLength OPTIONAL,
IN BOOLEAN IsDevIoCtl)//表示是否是普通的DeviceIoControl
{
PKEVENT EventObject = NULL;
BOOLEAN LockedForSynch = FALSE;
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
AccessType = IO_METHOD_FROM_CTL_CODE(IoControlCode);//提取io控制码里面的io方式
if (PreviousMode != KernelMode) 检查用户空间地址是否可读可写,略


//获得文件句柄对应的文件对象
Status = ObReferenceObjectByHandle(DeviceHandle,0,IoFileObjectType,PreviousMode,
(PVOID*)&FileObject,&HandleInformation);
//Io完成端口不支持APC
if ((FileObject->CompletionContext) && (UserApcRoutine))
{
ObDereferenceObject(FileObject);
return STATUS_INVALID_PARAMETER;
}

if (PreviousMode != KernelMode)
{
DesiredAccess = (ACCESS_MASK)((IoControlCode >> 14) & 3);//提取io控制码中的访问权限
//检查访问权限
if ((DesiredAccess != FILE_ANY_ACCESS) &&
(HandleInformation.GrantedAccess & DesiredAccess) != DesiredAccess)
{
ObDereferenceObject(FileObject);
return STATUS_ACCESS_DENIED;
}
}

if (Event)//将用户提供的irp完成事件初始化为无信号状态
{
Status = ObReferenceObjectByHandle(Event,EVENT_MODIFY_STATE,ExEventObjectType,
PreviousMode, (PVOID*)&EventObject,NULL);
KeClearEvent(EventObject);
}
if (FileObject->Flags & FO_SYNCHRONOUS_IO)
{

IopLockFileObject(FileObject);
LockedForSynch = TRUE;//标记当初打开设备时,是同步方式
}
//一般的设备都是直接打开的,上面没有绑定文件卷设备,若是这样,获取栈顶的设备
if (FileObject->Flags & FO_DIRECT_DEVICE_OPEN)
DeviceObject = IoGetAttachedDevice(FileObject->DeviceObject);
Else //物理卷设备可能不是直接打开的,上面绑定有文件卷设备,若是这样,就获取栈顶的文件卷
DeviceObject = IoGetRelatedDeviceObject(FileObject);

KeClearEvent(&FileObject->Event);//文件对象内部的那个内置事件也初始化为无信号状态

//关键。由IO管理器分配、生成一个irp(注意初始时,该irp的当前栈空间位置在栈顶的上面)
//注意irp的StackSize就是栈顶设备的StackSize
Irp = IoAllocateIrp(DeviceObject->StackSize, FALSE);


//下面是irp的初始化,这部分可由我们自行完成(前提是必须熟悉irp中各个字段的含义)
Irp->UserIosb = IoStatusBlock;//记录用户自提供的io状态块
Irp->UserEvent = EventObject;//记录用户自提供的的irp完成事件
Irp->Overlay.AsynchronousParameters.UserApcRoutine = UserApcRoutine;//记录用户提供的APC
Irp->Overlay.AsynchronousParameters.UserApcContext = UserApcContext;
Irp->Cancel = FALSE;//初始时尚未被用户取消
Irp->CancelRoutine = NULL;//初始时无取消例程
Irp->PendingReturned = FALSE;//下层尚未Pending返回
Irp->RequestorMode = PreviousMode;//来自指定模式的irp请求
Irp->MdlAddress = NULL;//由具体的io方式决定
Irp->AssociatedIrp.SystemBuffer = NULL; //由具体的io方式决定
Irp->Flags = 0;
Irp->Tail.Overlay.AuxiliaryBuffer = NULL;
Irp->Tail.Overlay.OriginalFileObject = FileObject;//最初发往的文件对象
Irp->Tail.Overlay.Thread = PsGetCurrentThread();//该irp的发起者线程

//此时的下层栈空间位置也即栈顶位置(指第一个栈空间)
//在下发irp前,先构造好下层栈空间中的功能码和参数
StackPtr = IoGetNextIrpStackLocation(Irp);
StackPtr->FileObject = FileObject;
StackPtr->MajorFunction = IsDevIoCtl ?
IRP_MJ_DEVICE_CONTROL ://普通的控制请求irp
IRP_MJ_FILE_SYSTEM_CONTROL;//专用于文件系统cdo的控制请求irp
StackPtr->MinorFunction = 0;
StackPtr->Control = 0;
StackPtr->Flags = 0;
StackPtr->Parameters.DeviceIoControl.Type3InputBuffer = NULL;

StackPtr->Parameters.DeviceIoControl.IoControlCode = IoControlCode;
StackPtr->Parameters.DeviceIoControl.InputBufferLength = InputBufferLength;
StackPtr->Parameters.DeviceIoControl.OutputBufferLength = OutputBufferLength;
switch (AccessType)//检查该控制请求irp的io方式
{
case METHOD_BUFFERED://若是缓冲方式,就由系统分配一个内核缓冲
_SEH2_TRY
{
BufferLength = max(InputBufferLength , OutputBufferLength);
if (BufferLength)
{
Irp->AssociatedIrp.SystemBuffer = ExAllocatePoolWithTag(NonPagedPool,BufferLength,TAG_SYS_BUF);
if (InputBuffer)
{
RtlCopyMemory(Irp->AssociatedIrp.SystemBuffer,InputBuffer,
InputBufferLength);
}
//标记该irp是缓冲io方式,并且完成时需要释放内核缓冲
Irp->Flags = IRP_BUFFERED_IO | IRP_DEALLOCATE_BUFFER;
if (OutputBuffer) Irp->Flags |= IRP_INPUT_OPERATION;
Irp->UserBuffer = OutputBuffer;
}
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
IopCleanupAfterException(FileObject, Irp, EventObject, NULL);
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
break;
case METHOD_IN_DIRECT:
case METHOD_OUT_DIRECT:
_SEH2_TRY
{
if ((InputBufferLength) && (InputBuffer))//输入缓冲固定不能使用直接io方式
{
Irp->AssociatedIrp.SystemBuffer = ExAllocatePoolWithTag(NonPagedPool,InputBufferLength,TAG_SYS_BUF);
RtlCopyMemory(Irp->AssociatedIrp.SystemBuffer,InputBuffer,
InputBufferLength);
Irp->Flags = IRP_BUFFERED_IO | IRP_DEALLOCATE_BUFFER;
}
if (OutputBuffer)//输出缓冲可使用直接io方式(mdl方式)

{
Irp->MdlAddress = IoAllocateMdl(OutputBuffer,OutputBufferLength,
FALSE,FALSE,Irp);
MmProbeAndLockPages(Irp->MdlAddress,PreviousMode,
(AccessType == METHOD_IN_DIRECT) ?IoReadAccess : IoWriteAccess);
}
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
IopCleanupAfterException(FileObject, Irp, EventObject, NULL);
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
break;

case METHOD_NEITHER:
Irp->UserBuffer = OutputBuffer;
StackPtr->Parameters.DeviceIoControl.Type3InputBuffer = InputBuffer;//第三类方式
}


//发给文件系统cdo设备的控制请求都会延迟完成
Irp->Flags |= (!IsDevIoCtl) ? IRP_DEFER_IO_COMPLETION : 0;
//发送该irp,异步返回或同步等待完成
return IopPerformSynchronousRequest(DeviceObject,Irp,FileObject,!IsDevIoCtl,
PreviousMode,LockedForSynch,IopOtherTransfer);
}

这儿要强调下,为什么设备栈栈顶的过滤设备能最先得到irp?

根本原因就是io管理器在将irp发给设备时,并不是直接发给当初CreateFile打开的额那个设备,而是要先调用IoGetAttachedDevice/I oGetRelatedDeviceObject这两个关键函数获得栈顶的设备,然后将irp发给它。

IoGetAttachedDevice

下面的函数用来获取设备栈栈顶的设备PDEVICE_OBJECT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
PDEVICE_OBJECT IoGetAttachedDevice(PDEVICE_OBJECT DeviceObject) 
{
while (DeviceObject->AttachedDevice)
DeviceObject = DeviceObject->AttachedDevice;
return DeviceObject;
}



// 下面的函数用来获取文件对象关联设备的设备栈栈顶的设备(用于文件系统)
PDEVICE_OBJECT IoGetRelatedDeviceObject(IN PFILE_OBJECT FileObject)
{
PDEVICE_OBJECT DeviceObject = FileObject->DeviceObject;//先获得当初打开的设备
if ((FileObject->Vpb) && (FileObject->Vpb->DeviceObject))//if打开卷设备产生的文件对象
{
ASSERT(!(FileObject->Flags & FO_DIRECT_DEVICE_OPEN));

DeviceObject = FileObject->Vpb->DeviceObject;//文件卷设备
}
//最典型,若是打开物理卷设备产生的文件对象,并且上面绑定有文件卷设备
else if (!(FileObject->Flags & FO_DIRECT_DEVICE_OPEN) &&
(FileObject->DeviceObject->Vpb) &&
(FileObject->DeviceObject->Vpb->DeviceObject))
{
DeviceObject = FileObject->DeviceObject->Vpb->DeviceObject;//文件卷设备
}
else
DeviceObject = FileObject->DeviceObject;//当初直接打开的物理卷设备或其他普通设备

if (DeviceObject->AttachedDevice)
{
DeviceObject = IoGetAttachedDevice(DeviceObject);//目标设备栈的栈顶设备
}
return DeviceObject;
}

上面这个函数一般用于文件系统。
物理卷设备往往不是直接打开的,它上面一般都会绑定着一个文件卷设备。
但是文件卷与物理卷之间的绑定关系没有形式上绑定(也即不是通过设备对象的AttachedDevice字段来维护绑定关系),而是通过一个vpb 来记录他们之间的绑定关系。
物理卷设备所在的设备栈与文件卷设备所在的设备栈是两个不同的设备栈。
这样IoGetAttachedDevice就没法获得文件卷设备栈的栈顶对象,需要通过IoGetRelatedDeviceObject这个专用的函数来获得绑定了物理卷设备的最上面的文件卷设备。


IoAllocateIrp

所有 irp 都是由 io 管理器分配生成的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
PIRP 
IoAllocateIrp(IN CCHAR StackSize,//指该irp包含的栈层数
IN BOOLEAN ChargeQuota)//指是否计较内存配额浪费,一般传FALSE
{
PIRP Irp = NULL;
USHORT Size;//该irp的实际需要分配的长度
PKPRCB Prcb;
UCHAR Flags = 0;
PNPAGED_LOOKASIDE_LIST List = NULL;

PP_NPAGED_LOOKASIDE_NUMBER ListType = LookasideSmallIrpList;//irp容器类型
Size = IoSizeOfIrp(StackSize);//获得irp的有效长度

if (ChargeQuota)
Flags |= IRP_QUOTA_CHARGED;

//如果栈层数小于等于8又不计较配额浪费,那么就从预置的irp容器分配
if ((StackSize <= 8) && (ChargeQuota == FALSE))
{
Flags = IRP_ALLOCATED_FIXED_SIZE;//标记为分配的是固定长度
if (StackSize != 1)
{
Size = IoSizeOfIrp(8);//对齐8个栈层大小

ListType = LookasideLargeIrpList;//改用8号irp容器
}
Prcb = KeGetCurrentPrcb();
//先尝试从该容器的P链表中分配出一个irp
List = (PNPAGED_LOOKASIDE_LIST)Prcb->PPLookasideList[ListType].P;
List->L.TotalAllocates++;//递增该链表总的分配请求计数
Irp = (PIRP)InterlockedPopEntrySList(&List->L.ListHead);//分配
if (!Irp)//if 分配失败
{
List->L.AllocateMisses++;//递增该链表的分配失败计数
//再尝试从该容器的L链表中分配出一个irp
List = (PNPAGED_LOOKASIDE_LIST)Prcb->PPLookasideList[ListType].L;
List->L.TotalAllocates++;//递增该链表总的分配请求计数
Irp = (PIRP)InterlockedPopEntrySList(&List->L.ListHead); //分配
}
}
if (!Irp)//如果仍然分配失败或者尚未分配
{
//if 从L链表中分配失败
if (Flags & IRP_ALLOCATED_FIXED_SIZE) List->L.AllocateMisses++;//递增分配失败计数
Irp = ExAllocatePoolWithTag(NonPagedPool, Size, TAG_IRP);//直接从非分页池中分配
}
Else
Flags &= ~IRP_QUOTA_CHARGED;

//分配完irp后,做一些基本的初始化
IoInitializeIrp(Irp, Size, StackSize);
Irp->AllocationFlags = Flags;//记录分配标志
return Irp;
}


#define IoSizeOfIrp(_StackSize) sizeof(IRP) + _StackSize * sizeof(IO_STACK_LOCATION)

如上由于 irp 频繁分配,所以内核中准备了两个 irp 容器。
一个单层的 irp 容器、一个 8 层的 irp 容器。
当 irp 栈层数小于等于 8 时,并且不计较配额浪费时,就从容器中分配 irp 结构,这样能加快分配速度。
Irp 分配后,系统内部会做一些基本的初始化

IoInitializeIrp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
VOID IoInitializeIrp(IN PIRP Irp, 
IN USHORT PacketSize,//实际分配的长度
IN CCHAR StackSize)//栈层数
{
RtlZeroMemory(Irp, PacketSize);//整个结构全部清0
Irp->Type = IO_TYPE_IRP;
Irp->Size = PacketSize;
Irp->StackCount = StackSize;
Irp->CurrentLocation = StackSize + 1;//初始栈空间位置在栈顶的上面
Irp->Tail.Overlay.CurrentStackLocation = (PIO_STACK_LOCATION)(Irp + 1) + StackSize;
Irp->ApcEnvironment = KeGetCurrentThread()->ApcStateIndex;

InitializeListHead(&Irp->ThreadListEntry);//用来挂入线程的irp链表
}

IopDeviceFsIoControl函数内部会调用下面的函数来将构造好的 irp 发到目标设备,并且异步返回或者同步等待处理完毕后再返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
NTSTATUS IopPerformSynchronousRequest(IN PDEVICE_OBJECT DeviceObject, 
IN PIRP Irp,
IN PFILE_OBJECT FileObject,
IN BOOLEAN Deferred,
IN KPROCESSOR_MODE PreviousMode,
IN BOOLEAN SynchIo,//关键参数
IN IOP_TRANSFER_TYPE TransferType)
{
NTSTATUS Status;
PKNORMAL_ROUTINE NormalRoutine;
PVOID NormalContext;
KIRQL OldIrql;
PAGED_CODE();
IopQueueIrpToThread(Irp);//将irp挂入线程的pending irp链表中
IopUpdateOperationCount(TransferType);
Status = IoCallDriver(DeviceObject, Irp);//将irp发给指定的设备
if (Deferred)//很多irp请求都需要延迟完成
{
if (Status != STATUS_PENDING)
{
ASSERT(!Irp->PendingReturned);
KeRaiseIrql(APC_LEVEL, &OldIrql);
//IoCompleteRequest的后半工作
IopCompleteRequest(&Irp->Tail.Apc,&NormalRoutine,&NormalContext,
(PVOID*)&FileObject,&NormalContext);
KeLowerIrql(OldIrql);
}
}

//if文件对象当初在CreateFile打开设备时,声明的是同步IO方式,以后的每次irp就一直等待完成后 才返回退出函数
if (SynchIo)
{
if (Status == STATUS_PENDING)//即使下层驱动是异步完成的,也要一直等到其完成后再返回
{
Status = KeWaitForSingleObject(&FileObject->Event,//等待文件对象的内置完成事件
Executive,PreviousMode,
(FileObject->Flags & FO_ALERTABLE_IO),//Alertable
NULL);

//如果是被用户apc或者强制唤醒要求给强制唤醒的,就夭折该次irp
if ((Status == STATUS_ALERTED) || (Status == STATUS_USER_APC))
IopAbortInterruptedIrp(&FileObject->Event, Irp);
Status = FileObject->FinalStatus;
}
IopUnlockFileObject(FileObject);
}
return Status;
}

如上,这个函数将 irp 发给指定设备,然后立即返回或者等到其完成后再返回
下面这个函数用来将 irp 挂入线程的pending irp链表(当线程终止时,会自动扫描它的pending irp链表,调用IoCancelIrp一一给予取消)

IopQueueIrpToThread

1
2
3
4
5
6
7
VOID  IopQueueIrpToThread(IN PIRP Irp) 
{
KIRQL OldIrql;
KeRaiseIrql(APC_LEVEL, &OldIrql);
InsertHeadList(&Irp->Tail.Overlay.Thread->IrpList, &Irp->ThreadListEntry);
KeLowerIrql(OldIrql);
}

IoCancelIrp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BOOLEAN  IoCancelIrp(IN PIRP Irp) 
{
KIRQL OldIrql;
PDRIVER_CANCEL CancelRoutine;
IoAcquireCancelSpinLock(&OldIrql);//锁定
Irp->Cancel = TRUE;//标记该irp已被用户发起了取消请求
CancelRoutine = (PVOID)IoSetCancelRoutine(Irp, NULL);//获得该irp的取消例程
if (CancelRoutine)//if 那个irp设有取消例程,就调用
{
Irp->CancelIrql = OldIrql;
CancelRoutine(IoGetCurrentIrpStackLocation(Irp)->DeviceObject, Irp);//调用取消例程
return TRUE;
}
IoReleaseCancelSpinLock(OldIrql);
return FALSE;
}


IoAttachDevice(设备的绑定)

设备的绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NTSTATUS IoAttachDevice(PDEVICE_OBJECT SourceDevice,//我们的设备 
PUNICODE_STRING TargetDeviceName,//要绑定的目标设备的名字
PDEVICE_OBJECT *AttachedDevice)//返回实际绑定的原栈顶设备
{
NTSTATUS Status;

PFILE_OBJECT FileObject = NULL;
PDEVICE_OBJECT TargetDevice = NULL;
//先根据名称获得目标设备对象
Status = IopGetDeviceObjectPointer(TargetDeviceName,FILE_READ_ATTRIBUTES,
&FileObject,&TargetDevice,IO_ATTACH_DEVICE_API);
if (!NT_SUCCESS(Status)) return Status;

Status = IoAttachDeviceToDeviceStackSafe(SourceDevice,TargetDevice,AttachedDevice);
ObDereferenceObject(FileObject);
return Status;
}

对于设备对象,除了可以使用ObReferenceObjectByName根据名称得到对象指针外,也可使用下面的函数来获得设备对象的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
NTSTATUS IopGetDeviceObjectPointer(IN PUNICODE_STRING ObjectName,IN ACCESS_MASK DesiredAccess, 
OUT PFILE_OBJECT *FileObject,OUT PDEVICE_OBJECT *DeviceObject,
IN ULONG AttachFlag)
{
OBJECT_ATTRIBUTES ObjectAttributes;
IO_STATUS_BLOCK StatusBlock;
PFILE_OBJECT LocalFileObject;
HANDLE FileHandle;
NTSTATUS Status; InitializeObjectAttributes(&ObjectAttributes,ObjectName,OBJ_KERNEL_HANDLE,NULL,NULL);

//先打开设备,给那个设备发送一个IRP_MJ_CREATE
Status = ZwOpenFile(&FileHandle,DesiredAccess,&ObjectAttributes,&StatusBlock,
0,FILE_NON_DIRECTORY_FILE | AttachFlag);
if (!NT_SUCCESS(Status)) return Status;
Status = ObReferenceObjectByHandle(FileHandle,0,IoFileObjectType,KernelMode,
(PVOID*)&LocalFileObject,NULL);
if (NT_SUCCESS(Status))
{
*DeviceObject = IoGetRelatedDeviceObject(LocalFileObject);//这个函数已看过
*FileObject = LocalFileObject;
}
ZwClose(FileHandle);//关闭文件句柄,内部会发送IRP_MJ_CLEANUP
return Status;
}

IoAttachDevice实质上调用的是IoAttachDeviceToDeviceStackSafe我们看

IoAttachDeviceToDeviceStackSafe

1
2
3
4
5
6
7
8
NTSTATUS IoAttachDeviceToDeviceStackSafe(IN PDEVICE_OBJECT SourceDevice, 
IN PDEVICE_OBJECT TargetDevice,
IN OUT PDEVICE_OBJECT *AttachedToDeviceObject)
{
if (!IopAttachDeviceToDeviceStackSafe(SourceDevice,TargetDevice,AttachedToDeviceObject))
return STATUS_NO_SUCH_DEVICE;
return STATUS_SUCCESS;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
PDEVICE_OBJECT IopAttachDeviceToDeviceStackSafe(IN PDEVICE_OBJECT SourceDevice,//我们的设备 
IN PDEVICE_OBJECT TargetDevice,//要绑定到的目标设备
OUT PDEVICE_OBJECT *AttachedToDeviceObject OPTIONAL)
{
//获得我们设备的标准设备扩展
PEXTENDED_DEVOBJ_EXTENSION SourceDeviceExtension = IoGetDevObjExtension(SourceDevice);
PDEVICE_OBJECT AttachedDevice = IoGetAttachedDevice(TargetDevice);//获取栈顶的设备
//检查栈顶设备的当前状态是否可以被绑定
if ((AttachedDevice->Flags & DO_DEVICE_INITIALIZING) ||
(IoGetDevObjExtension(AttachedDevice)->ExtensionFlags &
(DOE_UNLOAD_PENDING | DOE_DELETE_PENDING | DOE_REMOVE_PENDING | DOE_REMOVE_PROCESSED)))
{
AttachedDevice = NULL;
}
Else //绑定到目标设备所在设备栈的栈顶设备
{
AttachedDevice->AttachedDevice = SourceDevice;//关键。原栈顶设备记录绑定到它的设备
SourceDevice->StackSize = AttachedDevice->StackSize + 1;//绑定后自动确定StackSize
SourceDevice->AlignmentRequirement = AttachedDevice->AlignmentRequirement;
SourceDevice->SectorSize = AttachedDevice->SectorSize;//绑定后自动确定扇区大小
if (IoGetDevObjExtension(AttachedDevice)->ExtensionFlags & DOE_START_PENDING)
{
//传播标志
IoGetDevObjExtension(SourceDevice)->ExtensionFlags |= DOE_START_PENDING;
}


// 关键。在标准设备扩展中也会记录实际绑定到的下层设备(不过我们一般是在自定义设备扩展 中记录实际绑到的下层设备)。
// 不过由于一个设备可以同时绑定到N个下层设备,所以这个隐藏的 AttachedTo字段记录的是最近一次绑到的下层设备
SourceDeviceExtension->AttachedTo = AttachedDevice;
}
if (AttachedToDeviceObject) *AttachedToDeviceObject = AttachedDevice;//返回
return AttachedDevice;
}

如上这个函数将我们的设备绑定到目标设备(其实是绑定到目标设备所在设备栈的栈顶设备),绑定过程中,系统会自动确定我们设备的StackSize,并将实际绑到的设备记录在隐藏字段AttachedTo中。

IoAttachDeviceByPointer

如果知道了两个设备的指针,可以直接使用下面的函数进行绑定,不过这个函数有一个缺点,他不会返回实际绑到的下层设备。

1
2
3
4
5
6
7
8
NTSTATUS IoAttachDeviceByPointer(IN PDEVICE_OBJECT SourceDevice,IN PDEVICE_OBJECT TargetDevice) 
{
PDEVICE_OBJECT AttachedDevice;
NTSTATUS Status = STATUS_SUCCESS;
AttachedDevice = IoAttachDeviceToDeviceStack(SourceDevice, TargetDevice);
if (!AttachedDevice) Status = STATUS_NO_SUCH_DEVICE;
return Status;
}

IoAttachDeviceToDeviceStack

下面的函数可以解决这个缺点,返回实际绑到的设备,若返回 NULL,则表示绑定失败

1
2
3
4
PDEVICE_OBJECT IoAttachDeviceToDeviceStack(IN PDEVICE_OBJECT SourceDevice,IN PDEVICE_OBJECT TargetDevice) 
{
return IopAttachDeviceToDeviceStackSafe(SourceDevice,TargetDevice,NULL);
}


IoCompleteRequest

Irp 请求的完成处理:当一个 irp 完成时,用户会调用完成这个IoCompleteRequestirp
我们看看到底一个 irp 是如何完成的,完成时做了哪些工作呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#define IoCompleteRequest IofCompleteRequest 

VOID FASTCALL IofCompleteRequest(IN PIRP Irp,IN CCHAR PriorityBoost)
{
NTSTATUS ErrorCode = STATUS_SUCCESS;
if (Irp->CurrentLocation > Irp->StackCount + 1)
KeBugCheckEx(MULTIPLE_IRP_COMPLETE_REQUESTS, (ULONG_PTR)Irp, 0, 0, 0);
ASSERT(!Irp->CancelRoutine);//必须取消了取消例程,否则蓝屏
ASSERT(Irp->IoStatus.Status != STATUS_PENDING);//不能在阻塞态调用本函数完成irp
ASSERT(Irp->IoStatus.Status != -1);

/* Get the last stack */
LastStackPtr = (PIO_STACK_LOCATION)(Irp + 1);//最底层的栈空间
if (LastStackPtr->Control & SL_ERROR_RETURNED)
ErrorCode = PtrToUlong(LastStackPtr->Parameters.Others.Argument4);

/*
* Start the loop with the current stack and point the IRP to the next stack
* and then keep incrementing the stack as we loop through. The IRP should
* always point to the next stack location w.r.t the one currently being
* analyzed, so completion routine code will see the appropriate value.

* Because of this, we must loop until the current stack location is +1 of
* the stack count, because when StackPtr is at the end, CurrentLocation is +1.
*/
//StackPtr从最底层到最顶层
for (StackPtr = IoGetCurrentIrpStackLocation(Irp),
Irp->CurrentLocation++,
Irp->Tail.Overlay.CurrentStackLocation++;
Irp->CurrentLocation <= (Irp->StackCount + 1);
StackPtr++,
Irp->CurrentLocation++,
Irp->Tail.Overlay.CurrentStackLocation++)
{
//记录下层当初处理该irp时是否是异步返回的
Irp->PendingReturned = StackPtr->Control & SL_PENDING_RETURNED;

/* Check if we failed */
if (!NT_SUCCESS(Irp->IoStatus.Status))
{
/* Check if it was changed by a completion routine */
if (Irp->IoStatus.Status != ErrorCode)
{
/* Update the error for the current stack */
ErrorCode = Irp->IoStatus.Status;
StackPtr->Control |= SL_ERROR_RETURNED;
LastStackPtr->Parameters.Others.Argument4 = UlongToPtr(ErrorCode);
LastStackPtr->Control |= SL_ERROR_RETURNED;
}
}
IopClearStackLocation(StackPtr);//清理掉该层栈空间
//检查是否满足可以调用完成例程的条件
if ((NT_SUCCESS(Irp->IoStatus.Status) &&
(StackPtr->Control & SL_INVOKE_ON_SUCCESS)) ||
(!NT_SUCCESS(Irp->IoStatus.Status) &&
(StackPtr->Control & SL_INVOKE_ON_ERROR)) ||
(Irp->Cancel &&
(StackPtr->Control & SL_INVOKE_ON_CANCEL)))
{
if (Irp->CurrentLocation == (Irp->StackCount + 1))
DeviceObject = NULL;//回溯到最高层时DeviceObject = NULL
else
DeviceObject = IoGetCurrentIrpStackLocation(Irp)->DeviceObject;
//关键。调用上层的完成例程(注意此时的栈层位于上层栈空间)
Status = StackPtr->CompletionRoutine(DeviceObject,Irp,StackPtr->Context);


//如果该层的完成例程返回结果指示需要进一步完成的话,就中止,不再向上回溯
if (Status == STATUS_MORE_PROCESSING_REQUIRED) return;
}
Else //如果不满足调用完成例程的条件
{
if ((Irp->CurrentLocation <= Irp->StackCount) && (Irp->PendingReturned))
IoMarkIrpPending(Irp);
}
}
if (Irp->Flags & IRP_ASSOCIATED_IRP)//if 这是某个主irp关联的N个副irp之一
{
MasterIrp = Irp->AssociatedIrp.MasterIrp;
MasterCount = InterlockedDecrement(&MasterIrp->AssociatedIrp.IrpCount);
//释放该irp关联的所有MDL
for (Mdl = Irp->MdlAddress; Mdl; Mdl = NextMdl)
{
NextMdl = Mdl->Next;
IoFreeMdl(Mdl);
}
IoFreeIrp(Irp);//释放irp结构体本身
//若这是最后一个副irp,就完成掉它的主irp
if (MasterCount==0) IofCompleteRequest(MasterIrp, PriorityBoost);
return;
}

//释放该irp关联的其他辅助缓冲
if (Irp->Tail.Overlay.AuxiliaryBuffer)
{
ExFreePool(Irp->Tail.Overlay.AuxiliaryBuffer);
Irp->Tail.Overlay.AuxiliaryBuffer = NULL;
}
if (Irp->Flags & (IRP_PAGING_IO | IRP_CLOSE_OPERATION))
{
if (Irp->Flags & (IRP_SYNCHRONOUS_PAGING_IO | IRP_CLOSE_OPERATION))
{
*Irp->UserIosb = Irp->IoStatus;
KeSetEvent(Irp->UserEvent, PriorityBoost, FALSE);
Flags = Irp->Flags & (IRP_SYNCHRONOUS_PAGING_IO | IRP_PAGING_IO);
if (Flags) IoFreeIrp(Irp);//释放分页IO irp
}
return;
}
Mdl = Irp->MdlAddress;//解锁该irp关联的所有MDL页面,允许换出到外存
while (Mdl)
{

MmUnlockPages(Mdl);
Mdl = Mdl->Next;
}
if ((Irp->Flags & IRP_DEFER_IO_COMPLETION) && !(Irp->PendingReturned))
return;//直接返回,不再调用IopCompleteRequest了,由用户自己调用,去完成后半部分的工作
Thread = Irp->Tail.Overlay.Thread;
FileObject = Irp->Tail.Overlay.OriginalFileObject;
if (!Irp->Cancel) //最常见
{
KeInitializeApc(&Irp->Tail.Apc,&Thread->Tcb,Irp->ApcEnvironment,
IopCompleteRequest,//进一步的完成工作以apc的方式执行
NULL,NULL,KernelMode,NULL);
KeInsertQueueApc(&Irp->Tail.Apc,FileObject,NULL,PriorityBoost);
}
Else //该irp是被取消的(也即用户要求以取消方式完成掉这个irp)
{
if (Thread)
{
KeInitializeApc(&Irp->Tail.Apc,&Thread->Tcb,Irp->ApcEnvironment,
IopCompleteRequest,
NULL,NULL,KernelMode,NULL);
KeInsertQueueApc(&Irp->Tail.Apc,FileObject,NULL,PriorityBoost);
}
else
IopCleanupIrp(Irp, FileObject);
}
}

IopCompleteRequest

IopCompleteRequest是后半部分的完成工作,我们看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
VOID IopCompleteRequest(IN PKAPC Apc, 
IN PKNORMAL_ROUTINE* NormalRoutine,
IN PVOID* NormalContext,
IN PVOID* SystemArgument1,
IN PVOID* SystemArgument2)
{
PFILE_OBJECT FileObject;
PIRP Irp;
PMDL Mdl, NextMdl;
PVOID Port = NULL, Key = NULL;
BOOLEAN SignaledCreateRequest = FALSE;
FileObject = (PFILE_OBJECT)*SystemArgument1;
Irp = CONTAINING_RECORD(Apc, IRP, Tail.Apc);
//拷贝给用户空间,释放该irp关联的系统缓冲

if (Irp->Flags & IRP_BUFFERED_IO)
{
if ((Irp->Flags & IRP_INPUT_OPERATION) &&
(Irp->IoStatus.Status != STATUS_VERIFY_REQUIRED) &&
!(NT_ERROR(Irp->IoStatus.Status)))
{
RtlCopyMemory(Irp->UserBuffer,Irp->AssociatedIrp.SystemBuffer,
Irp->IoStatus.Information);
}
if (Irp->Flags & IRP_DEALLOCATE_BUFFER)
ExFreePool(Irp->AssociatedIrp.SystemBuffer);
}
Irp->Flags &= ~(IRP_BUFFERED_IO | IRP_DEALLOCATE_BUFFER);

//释放该irp关联的所有mdl
for (Mdl = Irp->MdlAddress; Mdl; Mdl = NextMdl)
{
NextMdl = Mdl->Next;
IoFreeMdl(Mdl);
}
Irp->MdlAddress = NULL;

if (!(NT_ERROR(Irp->IoStatus.Status)) ||
(NT_ERROR(Irp->IoStatus.Status) && (Irp->PendingReturned) &&
!(IsIrpSynchronous(Irp, FileObject))))
{
if ((FileObject) && (FileObject->CompletionContext))
{
Port = FileObject->CompletionContext->Port;
Key = FileObject->CompletionContext->Key;
}
*Irp->UserIosb = Irp->IoStatus;
if (Irp->UserEvent)
{
KeSetEvent(Irp->UserEvent, 0, FALSE);
if (FileObject)
{
if ((FileObject->Flags & FO_SYNCHRONOUS_IO) &&
!(Irp->Flags & IRP_OB_QUERY_NAME))
{
KeSetEvent(&FileObject->Event, 0, FALSE);
FileObject->FinalStatus = Irp->IoStatus.Status;
}
if (Irp->Flags & IRP_CREATE_OPERATION)

{
Irp->Overlay.AsynchronousParameters.UserApcRoutine = NULL;
SignaledCreateRequest = TRUE;
}
}
}
else if (FileObject)
{
KeSetEvent(&FileObject->Event, 0, FALSE);
FileObject->FinalStatus = Irp->IoStatus.Status;
if (Irp->Flags & IRP_CREATE_OPERATION)
{
Irp->Overlay.AsynchronousParameters.UserApcRoutine = NULL;
SignaledCreateRequest = TRUE;
}
}
//更新各自类型的历史总计转交字节数
if (!(Irp->Flags & IRP_CREATE_OPERATION))
{
if (Irp->Flags & IRP_WRITE_OPERATION)
IopUpdateTransferCount(IopWriteTransfer,Irp->IoStatus.Information);
else if (Irp->Flags & IRP_READ_OPERATION)
IopUpdateTransferCount(IopReadTransfer,Irp->IoStatus.Information);
else
IopUpdateTransferCount(IopOtherTransfer,Irp->IoStatus.Information);
}
IopUnQueueIrpFromThread(Irp);//脱出线程的阻塞irp链表
//用于用户空间的apc方式IO
if (Irp->Overlay.AsynchronousParameters.UserApcRoutine)
{
KeInitializeApc(&Irp->Tail.Apc,
KeGetCurrentThread(),
CurrentApcEnvironment,
IopFreeIrpKernelApc,
IopAbortIrpKernelApc,
(PKNORMAL_ROUTINE)Irp->
Overlay.AsynchronousParameters.UserApcRoutine,
Irp->RequestorMode,
Irp->
Overlay.AsynchronousParameters.UserApcContext);

KeInsertQueueApc(&Irp->Tail.Apc, Irp->UserIosb, NULL, 2);
}
//将一个irp完成事件插入指定完成端口的消息队列中

else if ((Port) &&
(Irp->Overlay.AsynchronousParameters.UserApcContext))
{
Irp->Tail.CompletionKey = Key;
Irp->Tail.Overlay.PacketType = IopCompletionPacketIrp;
KeInsertQueue(Port, &Irp->Tail.Overlay.ListEntry);
}
else
IoFreeIrp(Irp);//释放irp结构体本身
}
else
{
if ((Irp->PendingReturned) && (FileObject))
{
if (Irp->Flags & IRP_SYNCHRONOUS_API)
{
*Irp->UserIosb = Irp->IoStatus;
if (Irp->UserEvent)
KeSetEvent(Irp->UserEvent, 0, FALSE);
else
KeSetEvent(&FileObject->Event, 0, FALSE);
}
else
{
FileObject->FinalStatus = Irp->IoStatus.Status;
KeSetEvent(&FileObject->Event, 0, FALSE);
}
}
IopUnQueueIrpFromThread(Irp);
IoFreeIrp(Irp);
}
}

// 最后是释放 irp 结构体本身

VOID IoFreeIrp(IN PIRP Irp)
{
PNPAGED_LOOKASIDE_LIST List;
PP_NPAGED_LOOKASIDE_NUMBER ListType = LookasideSmallIrpList;
PKPRCB Prcb;
ASSERT(IsListEmpty(&Irp->ThreadListEntry));
ASSERT(Irp->CurrentLocation >= Irp->StackCount);
if (!(Irp->AllocationFlags & IRP_ALLOCATED_FIXED_SIZE))
ExFreePoolWithTag(Irp, TAG_IRP);
else

{
if (Irp->StackCount != 1) ListType = LookasideLargeIrpList;
Prcb = KeGetCurrentPrcb();
List = (PNPAGED_LOOKASIDE_LIST)Prcb->PPLookasideList[ListType].P;
List->L.TotalFrees++;
if (ExQueryDepthSList(&List->L.ListHead) >= List->L.Depth)
{
List->L.FreeMisses++;
List = (PNPAGED_LOOKASIDE_LIST)Prcb->PPLookasideList[ListType].L;
List->L.TotalFrees++;
if (ExQueryDepthSList(&List->L.ListHead) >= List->L.Depth)
{
List->L.FreeMisses++;
ExFreePoolWithTag(Irp, TAG_IRP);
Irp = NULL;
}
}
if (Irp)
{
InterlockedPushEntrySList(&List->L.ListHead,Irp);
}
}
}

Irp 完成工作可以看出主要是:向上回溯调用各层的完成例程,资源的释放,系统缓冲释放,MDL 释放,irp 结构体本身的释放,触发完成事件信号等操作。




驱动的加载过程

Windows 系统中共分 4 种类型的驱动

1
2
3
4
#define SERVICE_KERNEL_DRIVER          0x00000001 //普通内核驱动 
#define SERVICE_FILE_SYSTEM_DRIVER 0x00000002 //文件系统驱动
#define SERVICE_ADAPTER 0x00000004 //适配器驱动
#define SERVICE_RECOGNIZER_DRIVER 0x00000008 //文件系统识别器驱动每个驱动的类型记录在注册表中对应服务键下的 type 值中。

非文件系统驱动的驱动对象名都是Driver\服务名形式,文件系统驱动的驱动对象名则是FileSystem\服务名形式。这点小差别要注意。

驱动有四种加载时机:

  • 1、 系统引导时加载
  • 2、 系统初始化时加载
  • 3、 SCM 服务管理器在系统启动后的自动加载
  • 4、 运行时动态加载

NtLoadDriver

老式驱动和 WDM 驱动都支持动态加载。
老式驱动可以使用NtLoadDriver这个系统服务进行动态加载(实际上 SCM 服务管理器最终就是通过这个函数加载驱动的)
WDM 驱动则在设备热插拔时由 Pnp 管理器调用IopLoadServiceModule动态加载。

注意:凡是由NtLoadDriver加载的驱动都强制变成老式驱动,即使它提供了AddDevice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
NTSTATUS  NtLoadDriver(IN PUNICODE_STRING DriverServiceName) //参数表示服务键的全路径 
{
UNICODE_STRING CapturedDriverServiceName = { 0, 0, NULL };
KPROCESSOR_MODE PreviousMode;
LOAD_UNLOAD_PARAMS LoadParams;
NTSTATUS Status;

PAGED_CODE();
PreviousMode = KeGetPreviousMode();//一般都是用户空间的SCM在调用本函数
//检查当前令牌是否具有加载驱动的特权(加载驱动很危险,必须把好这个关口,做足权限检查)

if (!SeSinglePrivilegeCheck(SeLoadDriverPrivilege, PreviousMode))
return STATUS_PRIVILEGE_NOT_HELD;
//将DriverServiceName参数从用户空间拷贝到内核空间的CapturedDriverServiceName

Status = ProbeAndCaptureUnicodeString(&CapturedDriverServiceName,PreviousMode,
DriverServiceName);
LoadParams.ServiceName = &CapturedDriverServiceName;//服务名
LoadParams.DriverObject = NULL;//NULL表示加载,否则表示卸载
KeInitializeEvent(&LoadParams.Event, NotificationEvent, FALSE);

if (PsGetCurrentProcess() == PsInitialSystemProcess)
IopLoadUnloadDriver(&LoadParams);
//if当前进程是`system`,立即就地调用
//将这个加载任务打包成一个工作项,交由系统工作者线程去完成。
//由于系统工作者线程本身就 是`system`进程中的线程,因此,这就是为什么每个驱动的DriverEntry总是运行在`system`进程中
Else //最典型
{
ExInitializeWorkItem(&LoadParams.WorkItem,IopLoadUnloadDriver, (PVOID)&LoadParams);
ExQueueWorkItem(&LoadParams.WorkItem, DelayedWorkQueue);
KeWaitForSingleObject(&LoadParams.Event, UserRequest, KernelMode,FALSE, NULL);
}

ReleaseCapturedUnicodeString(&CapturedDriverServiceName,PreviousMode);
return LoadParams.Status;
}

如上上面的函数检查权限通过后,就打包成工作项,转入system进程中去加载驱动。

IopLoadUnloadDriver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
VOID IopLoadUnloadDriver(PLOAD_UNLOAD_PARAMS LoadParams)//既用来加载,也可用来卸载 
{
if (LoadParams->DriverObject)
{
(*LoadParams->DriverObject->DriverUnload)(LoadParams->DriverObject);//调用卸载例程
LoadParams->Status = STATUS_SUCCESS;
KeSetEvent(&LoadParams->Event, 0, FALSE);
return;
}
RtlInitUnicodeString(&ImagePath, NULL);
ServiceName = *LoadParams->ServiceName;//要加载的目标驱动的服务名
cur = LoadParams->ServiceName->Buffer + (LoadParams->ServiceName->Length / 2) - 1;
while (LoadParams->ServiceName->Buffer != cur)
{
if(*cur == L'\\')
{
ServiceName.Buffer = cur + 1;
ServiceName.Length = LoadParams->ServiceName->Length -

(USHORT)((ULONG_PTR)ServiceName.Buffer - (ULONG)LoadParams->ServiceName->Buffer);
break;
}
cur--;
}
//上面的循环提取服务键全路径末尾的服务名
IopDisplayLoadingMessage(&ServiceName);
RtlZeroMemory(&QueryTable, sizeof(QueryTable));

RtlInitUnicodeString(&ImagePath, NULL);
QueryTable[0].Name = L"Type";
QueryTable[0].Flags = RTL_QUERY_REGISTRY_DIRECT | RTL_QUERY_REGISTRY_REQUIRED;
QueryTable[0].EntryContext = &Type;//从注册表的服务键查询服务的类型,查询结果放在这个字段
QueryTable[1].Name = L"ImagePath";
QueryTable[1].Flags = RTL_QUERY_REGISTRY_DIRECT;
QueryTable[1].EntryContext = &ImagePath;//查询sys文件的路径
Status = RtlQueryRegistryValues(RTL_REGISTRY_ABSOLUTE,
LoadParams->ServiceName->Buffer, QueryTable, NULL, NULL);
//创建设备节点,注意第二个参数为NULL,因为这个函数加载的驱动都是老式驱动,老式驱动是没有硬 件pdo,但是为了统一,系统会在内部为其创建一个模拟的‘硬件pdo’。
Status = IopCreateDeviceNode(IopRootDeviceNode, NULL, &ServiceName, &DeviceNode);
if (!NT_SUCCESS(Status)) …
//检查对象目录中这个驱动是否已经加载过了,因为若已经加载了,驱动对象会进入对象目录中。
Status = IopGetDriverObject(&DriverObject,&ServiceName,
(Type == SERVICE_FILE_SYSTEM_DRIVER || Type == SERVICE_RECOGNIZER_DRIVER ));
if (!NT_SUCCESS(Status))//if 这个驱动以前没加载过(防止重复加载)
{
//关键。加载驱动的sys文件到内存,跟加载exe、dll文件相似。
Status = MmLoadSystemImage(&ImagePath, NULL, NULL, 0, &ModuleObject, &BaseAddress);
if (!NT_SUCCESS(Status) && Status != STATUS_IMAGE_ALREADY_LOADED) …
RtlCreateUnicodeString(&DeviceNode->ServiceName, ServiceName.Buffer);
if (NT_SUCCESS(Status))
{
//关键。为其创建一个DriverObject,并调用驱动的DriverEntry函数进行初始化
Status = IopInitializeDriverModule(DeviceNode,ModuleObject,
&DeviceNode->ServiceName,
(Type == SERVICE_FILE_SYSTEM_DRIVER || Type == 8 SERVICE_RECOGNIZER_DRIVER),
&DriverObject);
if (!NT_SUCCESS(Status)) //if DriverEntry返回失败
{
MmUnloadSystemImage(ModuleObject);
IopFreeDeviceNode(DeviceNode);
LoadParams->Status = Status;
KeSetEvent(&LoadParams->Event, 0, FALSE);

return;
}
}
//下面的两行实际上一点不起作用,纯属多余,因此,完全可以删去不看。
IopInitializeDevice(DeviceNode, DriverObject);//尝试调用其AddDevice函数
Status = IopStartDevice(DeviceNode);//尝试启动设备
}
Else //若驱动先前已经加载了,就失败返回。
{
ObDereferenceObject(DriverObject);
IopFreeDeviceNode(DeviceNode);
}
LoadParams->Status = Status;
KeSetEvent(&LoadParams->Event, 0, FALSE);
}

上面函数关键的工作,就是将驱动文件加载到内存,然后为为驱动创建一个对象,再调用驱动的DriverEntry函数。
不过这儿有一个小问题要注意:就是需要为老式驱动创建一个用作栈底基石的pdo,其实老式设备根本没有硬件 pdo,只不过是系统为了方便统一管理,为老式驱动也模拟创建一个硬件 pdo,然后纳入 PNP 框架统一管理。

创建设备节点的函数IopCreateDeviceNode我们后面会看。

IopGetDriverObject
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
NTSTATUS FASTCALL IopGetDriverObject(PDRIVER_OBJECT *DriverObject,PUNICODE_STRING ServiceName, 
BOOLEAN FileSystem)//表示该驱动的类型是否是文件系统
{
PDRIVER_OBJECT Object;
WCHAR NameBuffer[MAX_PATH];
UNICODE_STRING DriverName;
NTSTATUS Status;
*DriverObject = NULL;
DriverName.Buffer = NameBuffer;
DriverName.Length = 0;
DriverName.MaximumLength = sizeof(NameBuffer);
if (FileSystem)
RtlAppendUnicodeToString(&DriverName,”L\\FileSystem\\”);
else
RtlAppendUnicodeToString(&DriverName,L”\\Driver\\”);
RtlAppendUnicodeStringToString(&DriverName, ServiceName);
Status = ObReferenceObjectByName(&DriverName,
OBJ_OPENIF | OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE,
NULL,0,IoDriverObjectType,KernelMode,NULL, (PVOID*)&Object);

if (!NT_SUCCESS(Status))//检查是否有这个驱动对象
return Status;


*DriverObject = Object;//返回找到的驱动对象
return STATUS_SUCCESS;
}

如上驱动对象本身也是一种内核对象,他也是有名称的。
这个函数检查对象目录中是否有指定的驱动对象,若有,就说明,这个驱动已被加载了,否则,就还未加载。

IopInitializeDriverModule

当加载完驱动文件后,就会调用下面的函数为其创建一个驱动对象,调用DriverEntry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
NTSTATUS FASTCALL IopInitializeDriverModule( 
IN PDEVICE_NODE DeviceNode,//没使用
IN PLDR_DATA_TABLE_ENTRY ModuleObject,//该驱动模块的描述信息
IN PUNICODE_STRING ServiceName,//服务名
IN BOOLEAN FileSystemDriver,//是否为文件系统驱动
OUT PDRIVER_OBJECT *DriverObject)//返回创建的驱动对象
{
const WCHAR ServicesKeyName[] = L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\";
WCHAR NameBuffer[MAX_PATH];
UNICODE_STRING DriverName;
UNICODE_STRING RegistryKey;
PDRIVER_INITIALIZE DriverEntry;
PDRIVER_OBJECT Driver;
NTSTATUS Status;
DriverEntry = ModuleObject->EntryPoint;//关键。得到该驱动的oep,即DriverEntry
if (ServiceName != NULL && ServiceName->Length != 0)
{
RegistryKey.Length = 0;
RegistryKey.MaximumLength = sizeof(ServicesKeyName) + ServiceName->Length;
RegistryKey.Buffer = ExAllocatePool(PagedPool, RegistryKey.MaximumLength);
RtlAppendUnicodeToString(&RegistryKey, ServicesKeyName);
RtlAppendUnicodeStringToString(&RegistryKey, ServiceName);//构造好服务键全路径
}
else
RtlInitUnicodeString(&RegistryKey, NULL);
if (ServiceName && ServiceName->Length > 0)
{
if (FileSystemDriver == TRUE)
wcscpy(NameBuffer, ”L\\FileSystem\\”);
else
wcscpy(NameBuffer, ”L\\Driver\\”);
RtlInitUnicodeString(&DriverName, NameBuffer);
DriverName.MaximumLength = sizeof(NameBuffer);


RtlAppendUnicodeStringToString(&DriverName, ServiceName);//构造好这个驱动的驱动对象名
}
else
DriverName.Length = 0;
//上面构造好这个驱动的驱动对象名后,下面就开始创建驱动对象了
Status = IopCreateDriver(DriverName.Length > 0 ? &DriverName : NULL,
DriverEntry,&RegistryKey,ModuleObject,&Driver);
RtlFreeUnicodeString(&RegistryKey);
*DriverObject = Driver;//返回创建的驱动对象

if (!NT_SUCCESS(Status))
return Status; //DriverEntry可能返回失败到这里来

IopReadyDeviceObjects(Driver);

if (PnpSystemInit)
IopReinitializeDrivers();//调用所有注册了重初始化例程的驱动他们的重初始化例程
return STATUS_SUCCESS;
}

IopCreateDriver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
NTSTATUS IopCreateDriver(IN PUNICODE_STRING DriverName OPTIONAL,//驱动对象的名称,一般都会提供 
IN PDRIVER_INITIALIZE InitializationFunction,//DriverEntry
IN PUNICODE_STRING RegistryPath,//服务键的全路径
PLDR_DATA_TABLE_ENTRY ModuleObject,//驱动模块信息
OUT PDRIVER_OBJECT *pDriverObject)//返回创建的驱动对象
{
ULONG RetryCount = 0; try_again:
if (!DriverName)//如果没提供名称,根据当前TickCount随机生成一个驱动对象名称(少见)
{
NameLength = (USHORT)swprintf(NameBuffer,L"\\Driver\\%08u",KeTickCount);
LocalDriverName.Length = NameLength * sizeof(WCHAR);
LocalDriverName.MaximumLength = LocalDriverName.Length + sizeof(UNICODE_NULL);
LocalDriverName.Buffer = NameBuffer;
}
Else //最典型
LocalDriverName = *DriverName;

//LocalDriverName就表示最后的驱动对象名称
ObjectSize = sizeof(DRIVER_OBJECT) + sizeof(EXTENDED_DRIVER_EXTENSION);
InitializeObjectAttributes(&ObjectAttributes,&LocalDriverName,
OBJ_PERMANENT | OBJ_CASE_INSENSITIVE,NULL,NULL);

//驱动对象体的大小为驱动对象结构本身的大小 + 后面紧跟的标准驱动扩展结构的大小
Status = ObCreateObject(KernelMode,IoDriverObjectType,&ObjectAttributes,KernelMode,
NULL,ObjectSize,0,0, (PVOID*)&DriverObject);
RtlZeroMemory(DriverObject, ObjectSize);
DriverObject->Type = IO_TYPE_DRIVER;

DriverObject->Size = sizeof(DRIVER_OBJECT);
DriverObject->Flags = DRVO_LEGACY_DRIVER;
DriverObject->DriverExtension = (PDRIVER_EXTENSION)(DriverObject + 1);//指向尾部的扩展
DriverObject->DriverExtension->DriverObject = DriverObject;
DriverObject->DriverInit = InitializationFunction;//DriverEntry
DriverObject->DriverSection = ModuleObject;//指向驱动的模块信息
for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++)
DriverObject->MajorFunction[i] = IopInvalidDeviceRequest;//默认的irp派遣函数是这个
ServiceKeyName.Buffer = ExAllocatePoolWithTag(PagedPool,LocalDriverName.Length +
sizeof(WCHAR),TAG_IO);
ServiceKeyName.Length = LocalDriverName.Length;
ServiceKeyName.MaximumLength = LocalDriverName.MaximumLength;
RtlCopyMemory(ServiceKeyName.Buffer,LocalDriverName.Buffer,LocalDriverName.Length);
ServiceKeyName.Buffer[ServiceKeyName.Length / sizeof(WCHAR)] = UNICODE_NULL;
DriverObject->DriverExtension->ServiceKeyName = ServiceKeyName;//驱动对象的名称,\0终止

RtlCopyMemory(&DriverObject->DriverName,&ServiceKeyName,sizeof(UNICODE_STRING));
//看到没,会把驱动对象挂入对象目录中,从而可防止重复加载
Status = ObInsertObject(DriverObject,NULL,FILE_READ_DATA,0,NULL,&hDriver);

//由TickCount生成的随机驱动对象名称可能会产生冲突,因此重试。
if (!DriverName && (Status == STATUS_OBJECT_NAME_COLLISION) && (RetryCount < 100))
{
RetryCount++;
goto try_again;
}
Status = ObReferenceObjectByHandle(hDriver,0,IoDriverObjectType,KernelMode,
(PVOID*)&DriverObject,NULL);
ZwClose(hDriver);
DriverObject->HardwareDatabase = &IopHardwareDatabaseKey;//固定
DriverObject->DriverStart = ModuleObject ? ModuleObject->DllBase : 0; DriverObject->DriverSize = ModuleObject ? ModuleObject->SizeOfImage : 0;

//关键。调用我们的DriverEntry
Status = (*InitializationFunction)(DriverObject, RegistryPath);
if (!NT_SUCCESS(Status)) //我们的DriverEntry可能返回失败
{
DriverObject->DriverSection = NULL;
ObMakeTemporaryObject(DriverObject);
ObDereferenceObject(DriverObject);
}
else
*pDriverObject = DriverObject;//返回
//即使我们误将派遣函数填为NULL,也会被系统后台自动更为IopInvalidDeviceRequest
for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++)
{
if (DriverObject->MajorFunction[i] == NULL)

DriverObject->MajorFunction[i] = IopInvalidDeviceRequest;
}
return Status;
}

//下面是默认的 irp 派遣函数,它仅仅简单以失败方式完成这个 irp
NTSTATUS NTAPI IopInvalidDeviceRequest(PDEVICE_OBJECT DeviceObject,PIRP Irp)
{
Irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_INVALID_DEVICE_REQUEST;
}

每当加载了一个驱动,调用了DriverEntry函数后,就会接着调用IopReinitializeDrivers这个函数
检测系统中是否有驱动注册了重初始化例程,若有就调用之。

IopReinitializeDrivers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
VOID IopReinitializeDrivers(VOID) 
{
PDRIVER_REINIT_ITEM ReinitItem;
PLIST_ENTRY Entry;
Entry = ExInterlockedRemoveHeadList(&DriverReinitListHead,&DriverReinitListLock);//取下来
while (Entry)//遍历所有注册了`重初始化例程`的驱动
{
ReinitItem = CONTAINING_RECORD(Entry, DRIVER_REINIT_ITEM, ItemEntry);
//递增那个驱动的`重初始化例程`历史被调用次数
ReinitItem->DriverObject->DriverExtension->Count++;
ReinitItem->DriverObject->Flags &= ~DRVO_REINIT_REGISTERED;
//调用那个驱动注册的`重初始化例程`
ReinitItem->ReinitRoutine(ReinitItem->DriverObject,ReinitItem->Context,
ReinitItem->DriverObject->DriverExtension->Count);
ExFreePool(Entry);//执行后,就删除掉
Entry = ExInterlockedRemoveHeadList(&DriverReinitListHead,&DriverReinitListLock);
}
}
IoRegisterDriverReinitialization

驱动可以调用下面的函数这册一个重初始化例程,以在其DriverEntry被调用后,可以重新得到初始化。
注意重初始化例程一得到调用就立马删除。

1
2
3
4
5
6
7
8
9
10
11
12
VOID IoRegisterDriverReinitialization(IN PDRIVER_OBJECT DriverObject, 
IN PDRIVER_REINITIALIZE ReinitRoutine,IN PVOID Context)
{
PDRIVER_REINIT_ITEM ReinitItem;
ReinitItem = ExAllocatePoolWithTag(NonPagedPool,sizeof(DRIVER_REINIT_ITEM),TAG_REINIT);

ReinitItem->DriverObject = DriverObject;
ReinitItem->ReinitRoutine = ReinitRoutine;
ReinitItem->Context = Context;
DriverObject->Flags |= DRVO_REINIT_REGISTERED;
//挂入全局队列 ExInterlockedInsertTailList(&DriverReinitListHead,&ReinitItem->ItemEntry, &DriverReinitListLock);
}

至此我们看完了老式驱动通过 SCM 服务管理器加载的过程。

实际上SCM 在加载自启型(AUTO_START型)的驱动时,也是最终调用的NtLoadDriver加载的。

因此我们可以说,凡是通过 SCM 加载的驱动,都是老式驱动。
SCM 只能加载老式驱动。
下面我们看系统初始化(不是指引导)时,是如何加载各个硬件设备的驱动的。

驱动加载过程(非NtLoadDriver)

在此先介绍下背景知识:
系统在启动后,会根据总线布局层次枚举系统中所有的硬件设备,为它们创建设备节点,
加入到设备节点树中,同时加载它们的端口驱动。
内核中有一个设备节点树,树中的每个节点叫设备节点,每个设备节点就表示一个硬件设备。
系统在初始化时,PNP管理器会从根总线逐级向下枚举出系统中的所有硬件设备,为各个硬件创建一个设备节点,然后为其创建用作栈底基石的 pdo,记录在各个硬件的设备节点中,然后将设备节点按照总线位置关系挂 入节点树中。

因此可以说,节点树就是用来系统中的物理硬件排放顺序的一个缩影,通过遍历这个节点 树,我们不仅可以找出系统中的所有硬件设备,还可以得出他们之间的物理位置关系。

节点树中的每个节点表示一条总线或一个设备,总线也是一种特殊的设备,它也有自己的驱动。
比如pci.sys就是用来驱动总线本身的一个驱动。
节点树的根是一条总线,但是它实际上不存在,看不见摸不着,它是虚拟的,我们叫它根总线,也可叫它虚拟根总线

在根总线下面也即节点树的第一层,一般都是一条条具体的总线。如PCI总线、PCICMA总线等,就好像PCI总线、PCICMA总线挂在根总线的下面, PCI总线本身也是一个设备,它是根总线下面的一个设备节点。
PCI总线的下面,则可以挂接其他的总线如 ISA 总线、ATA 总线、USB 总线,PCI 总线下面也可以直接挂接真正的设备,如网卡、声卡等。

如果节点树中的某个设备节点是一个总线,那么他下面往往还可以挂接其他设备,就像文件夹一样。
如果设备节点是个真正的设备,那么它下面一般不能在挂接其他设备,这种设备节点我们可以叫他叶设备

一般来说,非总线设备都是叶设备,不能再挂接其他设备,但是也有例外的时候,比如 USB 根 HUB,它本身是挂在 USB 总线上的,但是一个 HUB 可以提供很多插口,外接其他 USB 设备或其他 HUB。

我们可以说:设备节点即是硬件,硬件即是设备节点。 根总线是虚拟的,系统在初始化时,会创建一个全局的虚拟设备对象,来表示那条根总线,这个全局变量是PnpRootDeviceObject,然后会创建一个全局的根设备节点来表示那条总线,这个全局变量名是IopRootDeviceNode,同时还会创建一个根驱动对象IopRootDriverObject

这三者都是系统初始化时创建的,他们之间的关系是:PnpRootDeviceObject->DriverObject等于IopRootDriverObject,IopRootDeviceNode->PhysicalDeviceObject等于PnpRootDeviceObject

简而言之:在根总线驱动中创建的根总线设备对象,根设备节点则代表根总线。 另外节点树中的每个节点都表示真实的硬件设备对象,因此设备节点中的PhysicalDeviceObject总是指那个用作堆栈基石的栈底 pdo,也即硬件 pdo
各个硬件设备的栈底 pdo 都是由其所在的总线驱动为其创建的。

比如显卡是挂在 pci 总线上的,因此显卡的硬件 pdo 就是由 PCI 总线驱动(pci.sys)在内部 创建的。
因此我们可以说某个设备,你挂在哪条总线旗下,你的硬件 pdo 就是由那条总线的驱动自动为你创建的。

设备节点树反映的是物理位置布局层次关系,而设备栈反映的则是逻辑堆栈层次关系(设计设备栈的目的 就是用来使驱动开发变得模块化以减轻驱动开发工作,仅此而已),
设备树与设备栈之间没有必然的关系。
如果说设备树是立体的(最上面的根,最下面的是叶),那么设备栈就是水平的(离我们最近的是栈顶,最远的是栈底)。

换言之,有下面的说法

    1. 每个设备节点都有自己的设备堆栈,并且位于设备栈底。
    1. 设备树中有多少个设备节点,系统中就有多少个设备栈。(各个设备栈之间是独立的,互不关联)

关于设备节点的图形展示,推荐阅读:http://technet.microsoft.com/zh-cn/library/ff554721
(注:由于本人对硬件也不是太了解,上面的大段话可能有误,望各位同行纠正)

基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct _DEVICE_NODE //设备节点 
{
struct _DEVICE_NODE *Sibling;//下一个兄弟节点
struct _DEVICE_NODE *Child;//第一个子节点
struct _DEVICE_NODE *Parent;//父节点
struct _DEVICE_NODE *LastChild;//最后一个子节点

ULONG Level;//在节点数的层号(从上往下数,基于0的索引)
PDEVICE_OBJECT PhysicalDeviceObject;//关键字段。所属的硬件pdo(指栈底的基石pdo)
PCM_RESOURCE_LIST ResourceList;//分配到的资源列表
PCM_RESOURCE_LIST ResourceListTranslated;//转换资源列表

//每个硬件设备(包括总线)都记录在注册表中,对应这册表中的某个键。每个设备由设备ID与实例 ID的组合唯一标识。(设备ID其实是设备类ID),
//常见的InstancePath如:Root\Legacy_设备类名\0000, Root\Legacy_设备类名\0001, Root\Legacy_设备类名\0003
UNICODE_STRING InstancePath;//本硬件设备在注册表中的的实例键路径
UNICODE_STRING ServiceName;//本硬件匹配的端口驱动
PIO_RESOURCE_REQUIREMENTS_LIST ResourceRequirements;//资源需求
ULONG BusNumber;//挂在总线位置 LIST_ENTRY DeviceTranslatorList;
……
} DEVICE_NODE, *PDEVICE_NODE;

IopCreateDeviceNode

下面的函数用来为指定的硬件设备创建一个硬件设备节点,加入到设备树,同时记录到注册表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
NTSTATUS IopCreateDeviceNode(PDEVICE_NODE ParentNode,//父节点 
PDEVICE_OBJECT PhysicalDeviceObject,//硬件设备(老式驱动为NULL)
PUNICODE_STRING ServiceName,//匹配的驱动
PDEVICE_NODE *DeviceNode)//返回
{
UNICODE_STRING LegacyPrefix = RTL_CONSTANT_STRING(L"LEGACY_");
UNICODE_STRING UnknownDeviceName = RTL_CONSTANT_STRING(L"UNKNOWN");

Node = (PDEVICE_NODE)ExAllocatePool(NonPagedPool, sizeof(DEVICE_NODE));//分配一个节点

RtlZeroMemory(Node, sizeof(DEVICE_NODE));
if (!ServiceName)
ServiceName1 = &UnknownDeviceName;
else
ServiceName1 = ServiceName;
if (!PhysicalDeviceObject)//老式驱动是没有硬件pdo支撑的,模拟为其创建一个
{
FullServiceName.MaximumLength = LegacyPrefix.Length + ServiceName1->Length;
FullServiceName.Length = 0;
FullServiceName.Buffer = ExAllocatePool(PagedPool, FullServiceName.MaximumLength);
RtlAppendUnicodeStringToString(&FullServiceName, &LegacyPrefix);
RtlAppendUnicodeStringToString(&FullServiceName, ServiceName1);
//FullServiceName此时等于“LEGACY_服务名”
//在根总线驱动中为其创建一个模拟的硬件pdo(用作栈底基石)
Status = PnpRootCreateDevice(&FullServiceName, &PhysicalDeviceObject, &Node->InstancePath);//返回生成的实例键路径


//将该硬件设备记录到注册表中(即创建它的实例键),这就是为什么这次表的ROOT键下面有一大
//堆‘Legacy_设备类名’和‘Legacy_设备类名\XXXX’的原因,就是这个函数创建写入注册表的。
//注意是REG_OPTION_VOLATILE类型,即不会保存到注册表磁盘中,机器一重启就会丢失。
Status = IopCreateDeviceKeyPath(&Node->InstancePath, REG_OPTION_VOLATILE, &InstanceHandle);//返回实例键的hKey
Node->ServiceName.Buffer = ExAllocatePool(PagedPool, ServiceName1->Length);
Node->ServiceName.MaximumLength = ServiceName1->Length;
Node->ServiceName.Length = 0;
RtlAppendUnicodeStringToString(&Node->ServiceName, ServiceName1);//匹配的端口驱动
if (ServiceName)
{
RtlInitUnicodeString(&KeyName, L"Service");
//在注册表登记这个硬件设备的匹配端口驱动
Status = ZwSetValueKey(InstanceHandle, &KeyName, 0, REG_SZ, ServiceName->Buffer, ServiceName->Length);
}

if (NT_SUCCESS(Status))
{
//在注册表中登记这个设备是一个老式设备
RtlInitUnicodeString(&KeyName, L"Legacy");
LegacyValue = 1;
Status = ZwSetValueKey(InstanceHandle, &KeyName, 0, REG_DWORD, &LegacyValue, sizeof(LegacyValue));
if (NT_SUCCESS(Status))
{
RtlInitUnicodeString(&KeyName, L"Class");
RtlInitUnicodeString(&ClassName, L"LegacyDriver");
Status = ZwSetValueKey(InstanceHandle, &KeyName, 0, REG_SZ, ClassName.Buffer, ClassName.Length);
}
}
ZwClose(InstanceHandle);

ExFreePool(FullServiceName.Buffer);
IopDeviceNodeSetFlag(Node, DNF_LEGACY_DRIVER);//标记这个硬件是一个老式的设备
IopDeviceNodeSetFlag(Node, DNF_ADDED);//标记已经被绑定(用来模拟pnp设备)
IopDeviceNodeSetFlag(Node, DNF_STARTED);//标记已启动(用来模拟pnp设备)
}

Node->PhysicalDeviceObject = PhysicalDeviceObject;//关键。记录指定的硬件pdo
(PhysicalDeviceObject->DeviceObjectExtension)->DeviceNode = Node;//互相维护

if (ParentNode)
{
KeAcquireSpinLock(&IopDeviceTreeLock, &OldIrql);
Node->Parent = ParentNode;
Node->Sibling = ParentNode->Child;
ParentNode->Child = Node;//挂在父节点的子节点列表开头
if (ParentNode->LastChild == NULL)
ParentNode->LastChild = Node;
KeReleaseSpinLock(&IopDeviceTreeLock, OldIrql);
Node->Level = ParentNode->Level + 1;
}
PhysicalDeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;
*DeviceNode = Node;//返回创建的设备节点
return STATUS_SUCCESS;
}

下面这个函数专用于在根总线驱动中创建一个硬件 pdo

所有老式设备,以及最上层的总线,都挂在根总线下面因此他们的硬件 pdo 都是靠这个函数创建的

PnpRootCreateDevice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
NTSTATUS PnpRootCreateDevice( 
IN PUNICODE_STRING ServiceName,//服务名
OUT PDEVICE_OBJECT *PhysicalDeviceObject,//返回创建的设备对象
OUT OPTIONAL PUNICODE_STRING FullInstancePath)//返回实例键路径(‘Root\类型\实例ID’形式)
{
UNICODE_STRING PathSep = RTL_CONSTANT_STRING(L"\\");
//#define REGSTR_PATH_SYSTEMENUM TEXT("System\\CurrentControlSet\\Enum")

UNICODE_STRING EnumKeyName = RTL_CONSTANT_STRING(L"\\Registry\\Machine\\"REGSTR_PATH_SYSTEMENUM);
HANDLE EnumHandle, DeviceKeyHandle = INVALID_HANDLE_VALUE;
DeviceExtension = PnpRootDeviceObject->DeviceExtension;//根总线设备的扩展 KeAcquireGuardedMutex(&DeviceExtension->DeviceListLock);

//#define REGSTR_KEY_ROOTENUM TEXT("Root")
_snwprintf(DevicePath, sizeof(DevicePath) / sizeof(WCHAR), L"%s\\%wZ", REGSTR_KEY_ROOTENUM, ServiceName);
//最终DevicePath等于‘Root\服务名’形式

//PNPROOT_DEVICE为根总线驱动中定义的‘扩展中的扩展’结构
Device = ExAllocatePoolWithTag(PagedPool, sizeof(PNPROOT_DEVICE), TAG_PNP_ROOT);
RtlZeroMemory(Device, sizeof(PNPROOT_DEVICE));
RtlCreateUnicodeString(&Device->DeviceID, DevicePath);//记录该硬件设备的DeviceID
//打开Enum键
Status = IopOpenRegistryKeyEx(&EnumHandle, NULL, &EnumKeyName, KEY_READ);
if (NT_SUCCESS(Status))
{
InitializeObjectAttributes(&ObjectAttributes, &Device->DeviceID, OBJ_CASE_INSENSITIVE, EnumHandle, NULL);
//创建‘Root\服务名’键,注意是REG_OPTION_VOLATILE类型,即不会保存到注册表磁盘中
//机器一重启就会丢失。
Status = ZwCreateKey(&DeviceKeyHandle, KEY_SET_VALUE, &ObjectAttributes, 0, NULL, REG_OPTION_VOLATILE, NULL);
ZwClose(EnumHandle);
}
RtlZeroMemory(QueryTable, sizeof(QueryTable));
QueryTable[0].Name = L"NextInstance";
QueryTable[0].EntryContext = &NextInstance;
QueryTable[0].Flags = RTL_QUERY_REGISTRY_DIRECT | RTL_QUERY_REGISTRY_REQUIRED;

//在注册表中查询该类设备的下一个可分配的空闲实例ID号
Status = RtlQueryRegistryValues(RTL_REGISTRY_HANDLE, (PWSTR)DeviceKeyHandle,
QueryTable,NULL,NULL);
if (!NT_SUCCESS(Status))//若没有NextInstance值(这种情况少见)
{
//搜索一个空闲的尚未分配出去的实例ID号
for (NextInstance = 0; NextInstance <= 9999; NextInstance++)
{
_snwprintf(InstancePath, sizeof(InstancePath) / sizeof(WCHAR), L"%04lu", NextInstance);
Status = LocateChildDevice(DeviceExtension, DevicePath, InstancePath, &Device);
if (Status == STATUS_NO_SUCH_DEVICE) //if 找到了
break;
}
}

//经过上面的折腾后,NextInstance就是本次可用分配到的实例ID号
_snwprintf(InstancePath, sizeof(InstancePath) / sizeof(WCHAR), L"%04lu", NextInstance);
Status = LocateChildDevice(DeviceExtension, DevicePath, InstancePath, &Device);
NextInstance++;

Status = RtlWriteRegistryValue(RTL_REGISTRY_HANDLE,DeviceKeyHandle,L"NextInstance", REG_DWORD,&NextInstance,sizeof(NextInstance));

RtlCreateUnicodeString(&Device->InstanceID, InstancePath);//‘XXXX’形式


if (FullInstancePath)
{
FullInstancePath->MaximumLength = Device->DeviceID.Length + PathSep.Length + Device->InstanceID.Length;
FullInstancePath->Length = 0;
FullInstancePath->Buffer = ExAllocatePool(PagedPool,FullInstancePath->MaximumLength);
RtlAppendUnicodeStringToString(FullInstancePath, &Device->DeviceID);
RtlAppendUnicodeStringToString(FullInstancePath, &PathSep);
RtlAppendUnicodeStringToString(FullInstancePath, &Device->InstanceID);
//最终为FullInstancePath等于‘Root\服务名\XXXX’形式
}

//关键,创建硬件pdo。(注意:除了wdm驱动,系统也会为老式驱动模拟创建一个硬件pdo)
Status = IoCreateDevice(
PnpRootDeviceObject->DriverObject,//关键。根总线驱动
sizeof(PNPROOT_PDO_DEVICE_EXTENSION),//根总线驱动中定义的设备扩展
NULL,FILE_DEVICE_CONTROLLER,
FILE_AUTOGENERATED_DEVICE_NAME,//硬件pdo的对象名都是自动生成的,\Device\XXXXXXXX形式
FALSE,&Device->Pdo);
PdoDeviceExtension = (PPNPROOT_PDO_DEVICE_EXTENSION)Device->Pdo->DeviceExtension;
RtlZeroMemory(PdoDeviceExtension, sizeof(PNPROOT_PDO_DEVICE_EXTENSION));
PdoDeviceExtension->Common.IsFDO = FALSE;//硬件pdo当然不是fdo了 PdoDeviceExtension->DeviceInfo = Device;//扩展中的扩展
//标记这个设备对象是在其所属的总线驱动枚举到后,自动在内部给创建的
Device->Pdo->Flags |= DO_BUS_ENUMERATED_DEVICE;
Device->Pdo->Flags &= ~DO_DEVICE_INITIALIZING;
//关键。根总线下面挂接的所有老式设备、其它总线,都记录在根总线设备扩展的链表中。
InsertTailList(&DeviceExtension->DeviceListHead, &Device->ListEntry);
DeviceExtension->DeviceListCount++;
*PhysicalDeviceObject = Device->Pdo;//返回创建的硬件pdo
Status = STATUS_SUCCESS; cleanup: //清理工作,略
return Status;
}

如上上面这个函数的主要用途就是为指定服务类型的设备分配一个实例 ID,在根总线驱动中创建一个硬件 pdo。

下面的函数用来将硬件设备记录到注册表中(即创建一个对应它的实例键)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
NTSTATUS IopCreateDeviceKeyPath(IN PCUNICODE_STRING RegistryPath,//`Root\服务名\XXXX`形式 
IN ULONG CreateOptions,OUT PHANDLE Handle)

{
// #define ENUM_ROOT L"\\Registry\\Machine\\System\\CurrentControlSet\\Enum"
UNICODE_STRING EnumU = RTL_CONSTANT_STRING(ENUM_ROOT);
HANDLE hParent = NULL, hKey;
*Handle = NULL;
//打开Enum键
Status = IopOpenRegistryKeyEx(&hParent, NULL, &EnumU, KEY_CREATE_SUB_KEY);
Current = KeyName.Buffer = RegistryPath->Buffer;
Last = &RegistryPath->Buffer[RegistryPath->Length / sizeof(WCHAR)];

//下面的循环为RegistryPath路径参数中的各节创建一个键
while (Current <= Last)
{
if (Current != Last && *Current != '\\')
{
Current++;
continue;
}
dwLength = (ULONG_PTR)Current - (ULONG_PTR)KeyName.Buffer;
KeyName.MaximumLength = KeyName.Length = dwLength;
InitializeObjectAttributes(&ObjectAttributes,&KeyName,OBJ_CASE_INSENSITIVE,
hParent,NULL);
Status = ZwCreateKey(&hKey,Current == Last ? KEY_ALL_ACCESS : KEY_CREATE_SUB_KEY,
&ObjectAttributes,0,NULL,CreateOptions,NULL);
if (hParent) ZwClose(hParent);
if (Current == Last)
{
*Handle = hKey;
return STATUS_SUCCESS;
}
hParent = hKey;
Current++;
KeyName.Buffer = (LPWSTR)Current;
}
return STATUS_UNSUCCESSFUL;
}


Pnp 驱动的加载过程

看完了老式驱动通过 NtLoadDriver 的加载过程,下面看 Pnp 驱动的加载过程
Pnp 驱动最初是在启动机器,上电自检后进行加载的,这是系统初始化时的静态加载。Pnp 驱动本意为即插 即用,所以 pnp 驱动最重要的特征便是还可以在设备插入电脑时动态加载设备驱动。下面先看一下系统初 始化时静态加载 Pnp 驱动的过程

IoInitSystem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
BOOLEAN  IoInitSystem(IN PLOADER_PARAMETER_BLOCK LoaderBlock) 
{

IopEnumerateDevice(IopRootDeviceNode->PhysicalDeviceObject);//枚举出根总线下面的所有设备

}

//下面这个函数枚举出指定总线下面挂接的所有子设备,加载它们的驱动

NTSTATUS IopEnumerateDevice(IN PDEVICE_OBJECT DeviceObject)
{
PDEVICE_NODE DeviceNode = IopGetDeviceNode(DeviceObject);//对应的设备节点
//先将根设备报告给pnp管理器的用户模式部分
IopQueueTargetDeviceEvent(&GUID_DEVICE_ARRIVAL,&DeviceNode->InstancePath);
Stack.Parameters.QueryDeviceRelations.Type = BusRelations;//查询总线关系
//向指定的总线发出一个查询子设备请求
Status = IopInitiatePnpIrp(DeviceObject,&IoStatusBlock,
IRP_MN_QUERY_DEVICE_RELATIONS,&Stack);
//一般总线驱动会处理这种irp,返回枚举出的所有子设备
DeviceRelations = (PDEVICE_RELATIONS)IoStatusBlock.Information;
for (i = 0; i < DeviceRelations->Count; i++)
{
ChildDeviceObject = DeviceRelations->Objects[i];
ChildDeviceNode = IopGetDeviceNode(ChildDeviceObject);
if (!ChildDeviceNode)//if尚未为其创建设备节点
{
Status = IopCreateDeviceNode(DeviceNode,ChildDeviceObject,NULL,&ChildDeviceNode);
if (NT_SUCCESS(Status))
{
ChildDeviceNode->Flags |= DNF_ENUMERATED;
ChildDeviceObject->Flags |= DO_BUS_ENUMERATED_DEVICE;
}
Else 。。。
}
else
{
ChildDeviceNode->Flags |= DNF_ENUMERATED;
ObDereferenceObject(ChildDeviceObject);
}
}
ExFreePool(DeviceRelations);
//通过上面,指定总线中的所有子设备都登记到设备节点树中去了,下面开始处理每个子设备
IopInitDeviceTreeTraverseContext(&Context,DeviceNode, IopActionInterrogateDeviceStack, DeviceNode);

//对指定总线下面的每个子设备执行IopActionInterrogateDeviceStack函数
Status = IopTraverseDeviceTree(&Context);


IopInitDeviceTreeTraverseContext(&Context,DeviceNode,IopActionConfigureChildServices,DeviceNode);

//对指定总线下面的每个子设备执行IopActionConfigureChildServices函数
Status = IopTraverseDeviceTree(&Context);
Status = IopInitializePnpServices(DeviceNode);//加载该设备的驱动程序
return STATUS_SUCCESS;
}

//下面这个函数用来为指定总线下面的每个子设备执行指定的函数
NTSTATUS IopTraverseDeviceTree(PDEVICETREE_TRAVERSE_CONTEXT Context)
{
NTSTATUS Status;
Context->DeviceNode = Context->FirstDeviceNode;
Status = IopTraverseDeviceTreeNode(Context);//实质函数
if (Status == STATUS_UNSUCCESSFUL)
Status = STATUS_SUCCESS;
return Status;
}

//下面这个函数对指定总线及其下面的所有子孙设备调用指定函数,递归
NTSTATUS IopTraverseDeviceTreeNode(PDEVICETREE_TRAVERSE_CONTEXT Context)
{
PDEVICE_NODE ParentDeviceNode;
PDEVICE_NODE ChildDeviceNode;
NTSTATUS Status;
ParentDeviceNode = Context->DeviceNode;
Status = (Context->Action)(ParentDeviceNode, Context->Context);
if (!NT_SUCCESS(Status))
return Status;
for (ChildDeviceNode = ParentDeviceNode->Child;
ChildDeviceNode != NULL;
ChildDeviceNode = ChildDeviceNode->Sibling)
{
Context->DeviceNode = ChildDeviceNode;
Status = IopTraverseDeviceTreeNode(Context);
if (!NT_SUCCESS(Status))
return Status;
}
return Status;
}

对每个子设备都会先后执行IopActionInterrogateDeviceStackIopActionConfigureChildServices
这两个函数所做的工作如下:IopActionInterrogateDeviceStack函数会查询指定总线下面挂接的所有直接子设备的设备 ID、实例 ID,

记录到注册表中,然后查询每个直接子设备的所有硬件ID、所有兼容ID、设备描述、设备能力、总线信息。
然后记录到注册表中,并查询它们的资源需求。
然后将每个直接子设备报告给 PNP 管理器的用户模式部分。

至于IopActionConfigureChildServices函数的工作不多,作用不大忽略

IopInitializePnpServices
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
NTSTATUS IopInitializePnpServices(IN PDEVICE_NODE DeviceNode) 
{
DEVICETREE_TRAVERSE_CONTEXT Context;
IopInitDeviceTreeTraverseContext(&Context,DeviceNode,
IopActionInitChildServices,DeviceNode);
return IopTraverseDeviceTree(&Context);//加载指定总线下面每个子设备的驱动程序
}

//下面的函数加载对应的驱动
NTSTATUS IopActionInitChildServices(PDEVICE_NODE DeviceNode,
PVOID Context)
{
PDEVICE_NODE ParentDeviceNode;
NTSTATUS Status;
BOOLEAN BootDrivers = !PnpSystemInit;
ParentDeviceNode = (PDEVICE_NODE)Context;
if (DeviceNode == ParentDeviceNode)
return STATUS_SUCCESS;
if (IopDeviceNodeHasFlag(DeviceNode, DNF_STARTED) ||
IopDeviceNodeHasFlag(DeviceNode, DNF_ADDED) ||
IopDeviceNodeHasFlag(DeviceNode, DNF_DISABLED))
return STATUS_SUCCESS;
if (DeviceNode->ServiceName.Buffer == NULL) …
else
{
PLDR_DATA_TABLE_ENTRY ModuleObject;
PDRIVER_OBJECT DriverObject;
Status = IopGetDriverObject(&DriverObject,&DeviceNode->ServiceName,FALSE);
if (!NT_SUCCESS(Status))
{
//加载相应的驱动
Status = IopLoadServiceModule(&DeviceNode->ServiceName, &ModuleObject);
if (NT_SUCCESS(Status) || Status == STATUS_IMAGE_ALREADY_LOADED)
{
if ((Status != STATUS_IMAGE_ALREADY_LOADED) ||
(Status == STATUS_IMAGE_ALREADY_LOADED && !DriverObject))

{
Status = IopInitializeDriverModule(DeviceNode, ModuleObject,
&DeviceNode->ServiceName, FALSE, &DriverObject);
}
Else Status = STATUS_SUCCESS;
}
}
if (NT_SUCCESS(Status))
Status = PipCallDriverAddDevice(DeviceNode, FALSE, DriverObject);
Else …
}
return STATUS_SUCCESS;
}



// 下面的函数在加载完驱动后进行调用,加载注册的所有上下层过滤驱动,然后调用 AddDevice 函数
NTSTATUS PipCallDriverAddDevice(IN PDEVICE_NODE DeviceNode,IN BOOLEAN LoadDriver,IN PDRIVER_OBJECT DriverObject)
{
UNICODE_STRING EnumRoot = RTL_CONSTANT_STRING(ENUM_ROOT);
UNICODE_STRING ControlClass =
RTL_CONSTANT_STRING(L"\\Registry\\Machine\\System\\CurrentControlSet\\Control\\Class");
PKEY_VALUE_FULL_INFORMATION KeyValueInformation = NULL;
Status = IopOpenRegistryKeyEx(&EnumRootKey,NULL,&EnumRoot,KEY_READ);

//打开对应的实例键
Status = IopOpenRegistryKeyEx(&SubKey,EnumRootKey,&DeviceNode->InstancePath,KEY_READ);
Status = IopGetRegistryValue(SubKey,REGSTR_VAL_CLASSGUID,&KeyValueInformation);
if (NT_SUCCESS(Status))
{
Buffer = (PVOID)((ULONG_PTR)KeyValueInformation + KeyValueInformation->DataOffset);
PnpRegSzToString(Buffer, KeyValueInformation->DataLength, &ClassGuid.Length);
ClassGuid.MaximumLength = KeyValueInformation->DataLength;
ClassGuid.Buffer = Buffer;
Status = IopOpenRegistryKeyEx(&ControlKey,NULL,&ControlClass,KEY_READ);
if (!NT_SUCCESS(Status)) ClassKey = NULL;
else
{
//打开对应的设备类键
Status = IopOpenRegistryKeyEx(&ClassKey,ControlKey,&ClassGuid,KEY_READ);
if (!NT_SUCCESS(Status)) ClassKey = NULL;
}
if (ClassKey)
{
RtlInitUnicodeString(&Properties, REGSTR_KEY_DEVICE_PROPERTIES);


Status = IopOpenRegistryKeyEx(&PropertiesKey,ClassKey,&Properties,KEY_READ);
if (!NT_SUCCESS(Status)) PropertiesKey = NULL;
}
}

//先加载该设备实例注册的所有下层过滤驱动以及所属设备类注册的所有下层过滤驱动
IopAttachFilterDrivers(DeviceNode, TRUE);
Status = IopInitializeDevice(DeviceNode, DriverObject);//调用AddDevice
if (NT_SUCCESS(Status))
{

//再加载该设备实例注册的所有上层过滤驱动以及所属设备类注册的所有上层过滤驱动
IopAttachFilterDrivers(DeviceNode, FALSE);
Status = IopStartDevice(DeviceNode);
}
return Status;
}

NTSTATUS IopInitializeDevice(PDEVICE_NODE DeviceNode,PDRIVER_OBJECT DriverObject)
{
PDEVICE_OBJECT Fdo;
NTSTATUS Status;
if (!DriverObject)
{
DeviceNode->Flags |= DNF_ADDED;
return STATUS_SUCCESS;
}
if (!DriverObject->DriverExtension->AddDevice)
DeviceNode->Flags |= DNF_LEGACY_DRIVER;
if (DeviceNode->Flags & DNF_LEGACY_DRIVER)
{
DeviceNode->Flags |= DNF_ADDED + DNF_STARTED;
return STATUS_SUCCESS;
}


//关键。调用AddDevice函数
Status = DriverObject->DriverExtension->AddDevice(DriverObject, DeviceNode->PhysicalDeviceObject);
if (!NT_SUCCESS(Status))
{
IopDeviceNodeSetFlag(DeviceNode, DNF_DISABLED);
return Status;
}
Fdo = IoGetAttachedDeviceReference(DeviceNode->PhysicalDeviceObject);
//少见,因为一般AddDevice函数中,用户都会创建一个fdo绑定在pdo上

if (Fdo == DeviceNode->PhysicalDeviceObject) …
if (Fdo->DeviceType == FILE_DEVICE_ACPI) …
IopDeviceNodeSetFlag(DeviceNode, DNF_ADDED);//标志已调用了AddDevice函数
return STATUS_SUCCESS;
}
IopStartDevice

下面的函数过滤、分配、转换资源,最后启动设备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
NTSTATUS  IopStartDevice(PDEVICE_NODE DeviceNode) 
{
NTSTATUS Status;
HANDLE InstanceHandle = INVALID_HANDLE_VALUE, ControlHandle = INVALID_HANDLE_VALUE;
UNICODE_STRING KeyName;
OBJECT_ATTRIBUTES ObjectAttributes;
if (DeviceNode->Flags & (DNF_STARTED | DNF_START_REQUEST_PENDING))
return STATUS_SUCCESS;
Status = IopAssignDeviceResources(DeviceNode);//IO管理器自动为其分配需要的资源
if (!NT_SUCCESS(Status))
goto ByeBye;
IopStartAndEnumerateDevice(DeviceNode);//分配资源后启动设备
Status = IopCreateDeviceKeyPath(&DeviceNode->InstancePath, 0, &InstanceHandle);
if (!NT_SUCCESS(Status))
goto ByeBye;

RtlInitUnicodeString(&KeyName, L"Control");
InitializeObjectAttributes(&ObjectAttributes,&KeyName,OBJ_CASE_INSENSITIVE,
InstanceHandle,NULL);

Status = ZwCreateKey(&ControlHandle, KEY_SET_VALUE, &ObjectAttributes, 0, NULL, REG_OPTION_VOLATILE, NULL);
if (!NT_SUCCESS(Status))
goto ByeBye;
RtlInitUnicodeString(&KeyName, L"ActiveService");
Status = ZwSetValueKey(ControlHandle, &KeyName, 0, REG_SZ, DeviceNode->ServiceName.Buffer, DeviceNode->ServiceName.Length);

ByeBye:
if (ControlHandle != INVALID_HANDLE_VALUE)
ZwClose(ControlHandle);
if (InstanceHandle != INVALID_HANDLE_VALUE)
ZwClose(InstanceHandle);
return Status;
}


NTSTATUS IopStartAndEnumerateDevice(IN PDEVICE_NODE DeviceNode)
{
PDEVICE_OBJECT DeviceObject;

NTSTATUS Status;
DeviceObject = DeviceNode->PhysicalDeviceObject;
if (!(DeviceNode->Flags & DNF_STARTED))//if 尚未启动
IopStartDevice2(DeviceObject);//启动设备

//当一个pnp设备启动后,需要继续往下枚举出它下面的各个设备,加入设备节点树,记录到注册表中, 加载各自的驱动。
// 由此而形成递归关系,使系统在初始化时沿着各条总线的物理布局层次,周而复始逐级 向下枚举出所有设备。
// 这样最终枚举出所有pnp设备,建立好整个设备节点树,完成初始化。
if ((DeviceNode->Flags & DNF_STARTED) && (DeviceNode->Flags & DNF_NEED_ENUMERATION_ONLY))
{
IoSynchronousInvalidateDeviceRelations(DeviceObject, BusRelations);
IopDeviceNodeClearFlag(DeviceNode, DNF_NEED_ENUMERATION_ONLY);
Status = STATUS_SUCCESS;
}
else
Status = STATUS_SUCCESS;
return Status;
}

VOID IopStartDevice2(IN PDEVICE_OBJECT DeviceObject)
{
IO_STACK_LOCATION Stack;
PDEVICE_NODE DeviceNode;
NTSTATUS Status;
PVOID Dummy;
DeviceNode = IopGetDeviceNode(DeviceObject);
RtlZeroMemory(&Stack, sizeof(IO_STACK_LOCATION));
Stack.MajorFunction = IRP_MJ_PNP;
Stack.MinorFunction = IRP_MN_START_DEVICE;//这种irp
if (!(DeviceNode->Flags & DNF_RESOURCE_REPORTED))
{
Stack.Parameters.StartDevice.AllocatedResources =
DeviceNode->ResourceList;
Stack.Parameters.StartDevice.AllocatedResourcesTranslated =
DeviceNode->ResourceListTranslated;
}

//向栈顶设备发送这种irp,请求启动设备。
Status = IopSynchronousCall(DeviceObject, &Stack, &Dummy);
if (!NT_SUCCESS(Status))
{
IopSendRemoveDevice(DeviceObject);
DeviceNode->Flags |= DNF_START_FAILED;
return;
}
DeviceNode->Flags |= DNF_STARTED;

DeviceNode->Flags |= DNF_NEED_ENUMERATION_ONLY;//标志需要继续枚举下面设备
}



NTSTATUS IoSynchronousInvalidateDeviceRelations(IN PDEVICE_OBJECT DeviceObject,
IN DEVICE_RELATION_TYPE Type)
{
switch (Type)
{
case BusRelations:
return IopEnumerateDevice(DeviceObject);//枚举下面设备
case PowerRelations:
return STATUS_NOT_IMPLEMENTED;
case TargetDeviceRelation:
return STATUS_SUCCESS;
default:
return STATUS_NOT_SUPPORTED;
}
}

至此pnp 驱动的静态加载过程讲解完毕,pnp 设备可以在动态插入时动态加载。

IoInvalidateDeviceRelations

当系统发现一个新的 pnp 设备插入机器时,会调用下面的函数完成驱动加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
VOID IoInvalidateDeviceRelations( 
IN PDEVICE_OBJECT DeviceObject,
IN DEVICE_RELATION_TYPE Type)
{
PIO_WORKITEM WorkItem;
PINVALIDATE_DEVICE_RELATION_DATA Data;
Data = ExAllocatePool(NonPagedPool, sizeof(INVALIDATE_DEVICE_RELATION_DATA));
WorkItem = IoAllocateWorkItem(DeviceObject);
ObReferenceObject(DeviceObject);
Data->DeviceObject = DeviceObject;
Data->Type = Type;
Data->WorkItem = WorkItem;
IoQueueWorkItem(
WorkItem,
IopAsynchronousInvalidateDeviceRelations, //关键函数
DelayedWorkQueue,
Data);
}

VOID IopAsynchronousInvalidateDeviceRelations(
IN PDEVICE_OBJECT DeviceObject,
IN PVOID InvalidateContext)
{
PINVALIDATE_DEVICE_RELATION_DATA Data = InvalidateContext;
//重新枚举,建立设备节点树,加载驱动
IoSynchronousInvalidateDeviceRelations(Data->DeviceObject,Data->Type);
ObDereferenceObject(Data->DeviceObject);
IoFreeWorkItem(Data->WorkItem);
ExFreePool(Data);
}

我们说总线本身也是一种设备。我们可以向一条总线发出一个请求,查询它下面挂接的所有设备。
根总线驱动就会处理这种 irp,返回查询结果给客户。

PnpRootQueryDeviceRelations

具体的处理这种 irp 的派遣函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
NTSTATUS PnpRootQueryDeviceRelations(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp) 
{
PDEVICE_RELATIONS Relations = NULL, OtherRelations = Irp->IoStatus.Information;
PPNPROOT_DEVICE Device = NULL;
//从这册表中枚举出根总线下面的所有设备(因为根总线是虚拟的,所以借助注册表)
Status = EnumerateDevices(DeviceObject);
DeviceExtension = (PPNPROOT_FDO_DEVICE_EXTENSION)DeviceObject->DeviceExtension;
Size = FIELD_OFFSET(DEVICE_RELATIONS, Objects) + sizeof(PDEVICE_OBJECT) * DeviceExtension->DeviceListCount;

if (OtherRelations)
Size += sizeof(PDEVICE_OBJECT) * OtherRelations->Count;
Relations = (PDEVICE_RELATIONS)ExAllocatePool(PagedPool, Size);
RtlZeroMemory(Relations, Size);
if (OtherRelations)
{
Relations->Count = OtherRelations->Count;
RtlCopyMemory(Relations->Objects, OtherRelations->Objects, sizeof(PDEVICE_OBJECT) * OtherRelations->Count);
}
for (NextEntry = DeviceExtension->DeviceListHead.Flink;
NextEntry != &DeviceExtension->DeviceListHead;
NextEntry = NextEntry->Flink)
{
Device = CONTAINING_RECORD(NextEntry, PNPROOT_DEVICE, ListEntry);
if (!Device->Pdo)
{
Status = IoCreateDevice(
DeviceObject->DriverObject,
sizeof(PNPROOT_PDO_DEVICE_EXTENSION),

NULL,
FILE_DEVICE_CONTROLLER,
FILE_AUTOGENERATED_DEVICE_NAME,//硬件pdo的对象名称都是自动生成的
FALSE,
&Device->Pdo);
PdoDeviceExtension = (PPNPROOT_PDO_DEVICE_EXTENSION)Device->Pdo->DeviceExtension;
RtlZeroMemory(PdoDeviceExtension, sizeof(PNPROOT_PDO_DEVICE_EXTENSION));
PdoDeviceExtension->Common.IsFDO = FALSE;
PdoDeviceExtension->DeviceInfo = Device;
Device->Pdo->Flags |= DO_BUS_ENUMERATED_DEVICE;
Device->Pdo->Flags &= ~DO_DEVICE_INITIALIZING;
}
Relations->Objects[Relations->Count++] = Device->Pdo;
}
Irp->IoStatus.Information = (ULONG_PTR)Relations;//将枚举结果返回给客户 cleanup:…
return Status;
}




Mdl

Mdl 意为内存映射描述符缓冲描述符,一个 mdl 就代表一个缓冲。
(任意一块物理内存,可以同时 映射到用户地址空间和系统地址空间的)
设备 IO 方式分为三种:缓冲方式、直接 IO 方式、直接方式

缓冲方式:将用户空间中的数据拷贝到内核缓冲,将内核缓冲中的数据拷贝到用户空间,效率低,适合少量数据交换构体本身的释放,触发完成事件信号等操作。
直接 IO 方式:将用户空间中的内存通过 MDL 机制映射到系统地址空间,效率高,适合大数据交换
直接方式:直接使用用户空间地址,效率最高,但不安全。

核心Api

向设备写数据的操作通过下面的内核 API 执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
NTSTATUS NtWriteFile(IN HANDLE FileHandle, 
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PVOID Buffer,
IN ULONG Length,
IN PLARGE_INTEGER ByteOffset OPTIONAL,
IN PULONG Key OPTIONAL)
{
...
if (DeviceObject->Flags & DO_BUFFERED_IO)
{
if (Length)
{
_SEH2_TRY
{
Irp->AssociatedIrp.SystemBuffer = ExAllocatePoolWithTag(NonPagedPool,Length,TAG_SYSB);
//看到没,系统内部自动分配一个内核缓冲,再将用户空间数据拷贝到内核缓冲
RtlCopyMemory(Irp->AssociatedIrp.SystemBuffer, Buffer, Length);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
IopCleanupAfterException(FileObject, Irp, EventObject, NULL);
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
Irp->Flags = (IRP_BUFFERED_IO | IRP_DEALLOCATE_BUFFER);
}
}
else if (DeviceObject->Flags & DO_DIRECT_IO)
{
if (Length)

{
_SEH2_TRY
{
//看到没,分配一个MDL
Mdl = IoAllocateMdl(Buffer, Length, FALSE, TRUE, Irp);
MmProbeAndLockPages(Mdl, PreviousMode, IoReadAccess);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
IopCleanupAfterException(FileObject, Irp, EventObject, NULL);
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
Irp->Flags = 0;
...
}
...
}



typedef struct _MDL { //缓冲描述符
struct _MDL *Next;//下一个MDL,用来构成mdl链表
CSHORT Size;//整个mdl结构体的长度(包含后面的数组)
CSHORT MdlFlags;
struct _EPROCESS *Process;//所属进程
PVOID MappedSystemVa;//该段缓冲在映射在系统空间中的地址
PVOID StartVa;//虚拟地址(对齐4kb)
ULONG ByteCount;//该段缓冲的长度
ULONG ByteOffset;// StartVa+ByteOffset就是该段缓冲在Process进程地址空间中的虚拟地址
} MDL, *PMDL;

MDL结构体后面紧跟一个物理页号数组。用来记录这段缓冲的物理页面。(物理页面不一定连续)
注意:MDL结构体本身必须位于非分页内存中

IoAllocateMdl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
PMDL IoAllocateMdl(IN PVOID VirtualAddress,// 目标虚拟内存的其实地址 
IN ULONG Length,//目标虚拟内存的长度
IN BOOLEAN SecondaryBuffer,//指新的mdl是插入到指定irp的关联mdl链表后面还是替换
IN BOOLEAN ChargeQuota,
IN PIRP Irp)//将新建的mdl插入或替换到这个irp的mdl链表中
{

PMDL Mdl = NULL, p;
ULONG Flags = 0;
ULONG Size;
if (Length >= 2GB) return NULL;
//计算这段虚拟内存跨越了多少个虚拟页面(包括左右两端两个部分占据的页面)
Size = ADDRESS_AND_SIZE_TO_SPAN_PAGES(VirtualAddress, Length);
if (Size > 23)//若超过了23个虚拟页面,就采用实际占用的虚拟页面数
{
Size *= sizeof(PFN_NUMBER);
Size += sizeof(MDL);
if (Size > MAXUSHORT) return NULL;
}
Else //否则,使用固定大小的23个虚拟页面,这样分配内存比较快。
{
Size = (23 * sizeof(PFN_NUMBER)) + sizeof(MDL);
Flags |= MDL_ALLOCATED_FIXED_SIZE;
Mdl = IopAllocateMdlFromLookaside(LookasideMdlList);
}
if (!Mdl)
{
//分配一个mdl结构体(包含后面的物理页号数组)
Mdl = ExAllocatePoolWithTag(NonPagedPool, Size, TAG_MDL);
if (!Mdl) return NULL;
}
MmInitializeMdl(Mdl, VirtualAddress, Length);
Mdl->MdlFlags |= Flags;
if (Irp)
{
if (SecondaryBuffer) //插在原mdl链表末尾
{
p = Irp->MdlAddress;
while (p->Next) p = p->Next;
p->Next = Mdl;
}
Else //替换原mdl为新的mdl
{
Irp->MdlAddress = Mdl;
}
}
return Mdl;
}


#define MmInitializeMdl(_MemoryDescriptorList, \

_BaseVa, \
_Length) \
{ \
(_MemoryDescriptorList)->Next = (PMDL) NULL; \ //单个mdl
(_MemoryDescriptorList)->Size = (CSHORT) (sizeof(MDL) + \
(sizeof(PFN_NUMBER) * ADDRESS_AND_SIZE_TO_SPAN_PAGES(_BaseVa, _Length))); \
(_MemoryDescriptorList)->MdlFlags = 0; \
(_MemoryDescriptorList)->StartVa = (PVOID) PAGE_ALIGN(_BaseVa); \
(_MemoryDescriptorList)->ByteOffset = BYTE_OFFSET(_BaseVa); \
(_MemoryDescriptorList)->ByteCount = (ULONG) _Length; \
}

上文在NtWriteFile函数体内中,分配一个对应大小的mdl内存映射描述符后,又马上调用了MmProbeAndLockPages函数,这是为什么呢?

这个函数会获取这段虚拟内存映射着的物理页面,记录到mdl 结构体后面紧跟的数组中 ,并将这些物理页面锁定在内存,防止被置换出去(注意:如果当时那些虚拟页 面尚未映射到物理内存,这个函数内部还会自动将那些虚拟页面换入物理内存的)

通过IoAllocateMdlMmProbeAndLockPages这两步操作后,指定虚拟内存就被锁定在物理内存了,就完成 了映射的准备工作。接下来用户想要在什么时候把这段虚拟内存对应的那些物理内存映射到系统地址空 间时,就可以使用MmGetSystemAddressForMdl宏达到目的。

1
2
3
4
5
#define  MmGetSystemAddressForMdl(Mdl) \ 
Mdl->MdlFlags & (MDL_MAPPED_TO_SYSTEM_VA | MDL_SOURCE_IS_NONPAGED_POOL) ? \
Mdl->MappedSystemVa : MmMapLockedPages (Mdl,KernelMode)

// 这个宏的意思是如果该段虚拟内存尚未映射到系统空间,就映射MmMapLockedPages

这个函数用于映射用户空间中锁定的页面到内核地址空间中(实际上内核地址空间中有 一块专用区段叫mdl区段,专用于mdl映射,这个函数就是将用户空间内存映射到内核中的那个区段中的)


IoFreeMdl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
VOID IoFreeMdl(PMDL Mdl) 
{
MmPrepareMdlForReuse(Mdl);
if (!(Mdl->MdlFlags & MDL_ALLOCATED_FIXED_SIZE))
ExFreePoolWithTag(Mdl, TAG_MDL);
else
IopFreeMdlFromLookaside(Mdl, LookasideMdlList);
}

// 另外:系统空间的非分页内存本身就是锁定在内存的,也可以使用mdl
VOID MmBuildMdlForNonPagedPool(IN PMDL Mdl)
{
PPFN_NUMBER MdlPages, EndPage;
PFN_NUMBER Pfn, PageCount;
PVOID Base;
PMMPTE PointerPte;

Mdl->Process = NULL;

MdlPages = (PPFN_NUMBER)(Mdl + 1); Base = Mdl->StartVa;
//非分页内存本身就位于系统空间,不用重新映射,直接使用
Mdl->MappedSystemVa = (PVOID)((ULONG_PTR)Base + Mdl->ByteOffset);
PageCount = ADDRESS_AND_SIZE_TO_SPAN_PAGES(Mdl->MappedSystemVa,Mdl->ByteCount);
EndPage = MdlPages + PageCount;
PointerPte = MiAddressToPte(Base);
do
{
Pfn = PFN_FROM_PTE(PointerPte++);
*MdlPages++ = Pfn;//关键。填充mdl结构体后面的物理页号数组
} while (MdlPages < EndPage);
Mdl->MdlFlags |= MDL_SOURCE_IS_NONPAGED_POOL;//标记来源本身就是非分页内存。
if (!MiGetPfnEntry(Pfn)) Mdl->MdlFlags |= MDL_IO_SPACE;
}

通过MmBuildMdlForNonPagedPool后,也可以使用MmGetSystemAddressForMdl宏获得系统空间地址。




网络通信篇

典型的基于 tcpip 协议套接字方式的网络通信模块层次

应用程序调用WS2_32.dll中的socket apisocket api在内部生成socket irp发给 afd.sys 这个中间辅助驱动层,
afd.sys 将socket irp转换成tdi irp发给tcpip协议驱动,协议驱动通过注册的回调函数与小端口驱动(中间可能穿插 N 个中间层过滤驱动),小端口驱动最终通过中断与网卡交互,操作硬件。

其中,协议驱动、中间层驱动、小端口驱动三者之间的交互是通过 ndis.sys 这个库函数模块实现的,或者说 ndis.sys 提供了 ndis 框架,协议驱动、中间层驱动、小端口驱动三者都得遵循这个框架。

为什么网络通信需要这么复杂的分层?

答案是为了减轻开发维护管理工作的需要,分层能够提供最大的灵 活性。
各层的设计人员只需专注自身模块的设计工作,无需担心其他模块是怎么实现的,只需保持接口一致即可。

如应用程序可以调用socket api就可以实现网络通信,而不管底层是如何实现的。使用socket api还可以使得 windows 上能兼容运行 Unix 系统上的网络通信程序,ws2_32.dll 这个模块中实现了socket接口。
Afd.sys 实际上是一个适配层,他可以适配 N 种协议驱动。
Tcpip.sys 是一种协议驱动(其实是一个协议栈驱动),它内部实现了一套协议栈,决定了如何解析从网卡 接收到的包,以及以什么格式将应用程序数据发到网卡。
只不过 tcpip.sys 将收到的包按链路层、网络层、 传输层分层三层逐层解析。
事实上我们可以完全可以自定义、自编写一个协议驱动,按照我们自己的协议来发包、收包(我们的这个自定义协议驱动可以采用分层机制,也可以采用简单的单层机制),这样在发送方电脑和接收方电脑都安装我们的自定义协议驱动后,发送方就可以按照自定义协议发包,接收方就按照约定的格式解包。

如果不考虑中间驱动,协议驱动是直接与小端口驱动交互的。
协议驱动从小端口驱动收包,协议驱动发包给小端口驱动,这就是二者之间的交互。他们之间的交互通过 ndis 框架预约的一套回调函数接口来实现。

下面我们看各层驱动的实现:
一个协议驱动需要在DriverEntry中将自己注册为一个协议驱动,向 ndis 框架登记、声明自己的协议特征。
一个协议特征记录了协议的名称以及它提供的各个回调函数




后面的还是看书为准吧……不想写了… 记在脑子里才是真的记住了, 写这么多到时候也不想看了
后面的还是看书为准吧……不想写了… 记在脑子里才是真的记住了, 写这么多到时候也不想看了
后面的还是看书为准吧……不想写了… 记在脑子里才是真的记住了, 写这么多到时候也不想看了