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() { NTDLL.NtReadFile(); } NTDLL.NtReadFile() { Mov eax,152 If(cpu 不支持sysenter 指令) { Lea edx,[esp+4 ] Int 2 e } Else { Lea edx,[esp +4 ] Sysenter } Ret 36 }
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 2 e { Cli Mov esp, TSS.内核栈地址 Push SS Push esp Push eflags Push cs Push eip Jmp IDT[中断号] }
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 Push ebp Push ebx Push esi Push edi Push fs Push kpcr.ExceptionList Push kthread.PreviousMode Sub esp,0x48 -----------至此,上面的这些语句连同int 2 e 中的语句在栈上构造了一个trap 帧----------------- Mov CurTrapFrame,esp Mov CurTrapFrame.edx, kthread.TrapFrame Mov kthread.TrapFrame, CurTrapFrame, Mov kthread.PreviousMode,GetMode(进入内核前的CS) Mov kpcr.ExceptionList,-1 Mov fs,kpcr If(当前线程处于调试状态) 保存 DR0-DR7 到 trap 帧中 } FindTableCall() { Mov edi,eax Mov eax, edi.低 12 位 If(edi.第 13 位=1 ) { If(当前线程.服务描述符表!=shadow) 当前线程.服务描述符表=shadow } 服务表描述符=当前线程.服务描述符表[edi.第 13 位] Mod edi=服务表描述符.base Mov ecx=服务表描述符.Number[eax] Mov esi,edx Rep movsb Call ebx } KiSystemService() { SaveTrap(); Sti FindTableCall(); Move esp,kthread.TrapFrame; Cli if (上次模式==UserMode) { Call KiDeliverApc 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 现场帧 { 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; ULONG Ecx; ULONG Eax; ULONG PreviousPreviousMode; struct _EXCEPTION_REGISTRATION_RECORD FAR *ExceptionList ; ULONG SegFs; ULONG Edi; ULONG Esi; ULONG Ebx; ULONG Ebp; ULONG ErrCode; ULONG Eip; ULONG SegCs; ULONG EFlags; ULONG HardwareEsp; ULONG HardwareSegSs; 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; KPCR* self; KPRCB* kprcb; KIRQL irql; USHORT* IDT; USHORT* GDT; KTSS* TSS; „„ } Struct KPRCB { KTHREAD* CurrentThread; KTHREAD* NextThread; BYTE CpuID; ULONG KernelTime,UserTime; „„ }
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 Mov esp,TSS.ESP Push ds Push edx Push eflags Push cs Push sysenter 指令的后面那条指令的地址 Cli Mov eflags,0x2 SaveTrap(); Sti FindTableCall(); Move esp,kthread.TrapFrame; Call KiDeliverApc Sti Mov ecx,保存的用户空间栈顶地址 Mov edx,保存的返回地址,也即 sysenter 指令的后面那条指令的地址 sysexit }
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 }
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() { Mov eax,系统服务号 Lea edx,[esp+4 ] Push eflags Push cs Call KiSystemService Ret } 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; Int idx; Int rpl: }
简单的讲可以将他们视为 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; 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 ; 总页表=CR3; PDE=总页表[va.页表号]; 二级页表=PDE.PageAddr; PTE=二级页表[va.页号]; If(PTE 空白) 触发 0x0e 号页面访问异常(具体为缺页异常) Else If(PTE.bPresent==false ) 触发 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; VOID* LowestAddress; EPROCESS* Process; 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; Void* EndAddress; MEMORY_AREA* Parent; MEMORY_AREA* LeftChild; MEMORY_AREA* RightChild; ULONG type; ULONG protect; ULONG flags; BOOLEAN DeleteInProgress; ULONG PageOpCount; Union{ Struct { ROS_SECTION_OBJECT* section; ULONG ViewOffest; MM_SECTION_SEGMENT* Segment; BOOLEAN WriteCopyView; }SectionData; LIST_ENTRY RegionListHead; }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; 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,4 kb); UINT BaseAlign; If(type==普通区段) BaseAlign=64 KB; Else BaseAlign =4 KB; If(*BaseAddr ==NULL && !bFixedAddr) { *BaseAddr=MmFindGap(as,Len, BaseAlign, AllocFlags 要求 TopDown?); } Else { *BaseAddr=Align(*BaseAddr, BaseAlign); } If(要分配的区域没有完全落在指定地址空间内部) Return fail; If(MmLocateMemoryAreaByRegion(as,*BaseAddr,Len)!=0 ) 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); *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 ; Consumer; Zero; ListEntry; ReferenceCount; SWAPENTRY SavedSwapEntry; LockCount; 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 ; PageDescriptor->LockCount=0 ; PageDescriptor->MapCount=0 ; InserTailList(&UsedPageListHeads[ConsumerType], PageDescriptor); if (NeedClear) MiZeroPage(PfnOffset); Pfn = PageDescriptor-MmPageArray; 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; UINT pfn; }Mem; Struct { 文件中的页面号; 页文件号; }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); Void* CurPageAddr = FirstVirtualPageAddr; PTE* Pt; For(int i=0 ; i< VirtualPageCount;i++) { Pt = MmGetPageTableForProcess(process, CurPageAddr); OldPte = *Pt; If(OldPte 映射到了页文件) return fail; If(OldPte 映射到了物理内存) NewPTE.pfn = PfnArray[i]; *pt = NewPTE; Process.地址空间.PageTableRefCountTable[ ADDR_TO_PAGE_TABLE(CurPageAddr) ]++; If(OldPte 映射到了某物理内存页面) MiFlushTlb(pt, CurPageAddr); CurPageAddr+=4 KB; } }
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 OldPte=*pt; *pt=0 ; If(bFreePhysicalPage) MmReleasePageMemoryConsumer(pfn); 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++) { If(*pte == 空白) { *pte.pfn=pfn; Break; } } If(i==1024 ) { PTE* Pte=临时映射保留区的映射描述符们所在的二级页表; For(i=0 ; i<pfn%1024 ;i++,Pte++) { If(*pte == 空白) { *pte.pfn=pfn; Break; } } } Return HYPERSPACE + i*4 kb; }
既然叫临时映射,那用完后就得撤销映射
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); If(process!=当前进程 && PageAddr<2 GB) { PFN pfn=process.pcb.DirectoryTableBase; PageDir=MmCreateHyperspaceMapping(pfn); If(PageDir[PDE_IDX]==空白) Return NULL ; Pfn= PageDir[PDE_IDX].pfn; MmDeleteHyperspaceMapping(PageDir); PTE* pte= MmCreateHyperspaceMapping(Pfn); Return pte+ADDR_TO_PTE_OFFSET(PageAddr); } Else { PageDir=ADDR_TO_PDE(PageAddr); If(PageDir[PDE_IDX]==空白) Return 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(参数不合法) Return fail; *BaseAddr=Align(*BaseAddr,64 kb); *Len=Align(*Len,4 kb); EPROCESS* process; ObReferenceObjectByHandle(hProcess,PROCESS_VM_OPERATION,UserMode,&process,„) Type=(AllocType & MEM_COMMIT)?MEM_COMMIT:MEM_RESERVE; MADDRESS_SPACE* As = process->VadRoot; If(*BaseAddr!=NULL ) { MEMORY_AREA* Area=MmLocateMemoryAreaByAddress(As,*BaseAddr); If(Area!=NULL ) { AreaLen=Area->EndAddress – Area->StartingAddress; If(AreaLen >= *Len) { MmAlterRegion(As,Area->StratingAddr, Area->区块链表, *BaseAddr,*Len,Type,protect AlterFunc=MmModifyAttributes); Return succ; } Else Return fail; } } 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); 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; 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)) { MmDeletePageFileMapping(Address,&SwapEntry); MmReadFromSwapPage(); Pfn.SavedSwapEntry=SwapEntry; } MmCreateVirtualMapping(AddressSpace->process, Address, Region->Protect, &pfn 数组,1 个元素) MmInsertRmap(pfn, AddressSpace->process,Align(Address,4 kb)); Return succ; } NTSTATUS MmReadFromSwapEntry (SwapEntry,pfn) { MDL mdl; … MmBuildMdlFromPages(mdl,pfn); FileNo=SwapEntry.FileNo; FileOffset=SwapEntry.PageNo * 4 kb; Status=IoPageRead(PagingFileList[FileNo]->FileObject, FileOffset,mdl,…); if (Status == STATUS_PENDING) { KeWaitForSingleObject(&Event, Executive, KernelMode, FALSE, 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=*pt; PFN pfn=pte.pfn; SavedSwapEntry = pfn.SavedSwapEntry; If(pte.bDirty == false ) { MmDeleteVirtualMapping(as.process, PageAddr, „); If(SavedSwapEntry != 0 ) { MmCreatePageFileMapping(as.process, PageAddr, SavedSwapEntry); Pfn.SavedSwapEntry = 0 ; } MmReleasePageMemoryConsumer(USER, pfn); Return succ; } Else { If(SavedSwapEntry == 0 ) NewSwapEntry= MmAllocSwapPage(); Else NewSwapEntry= SavedSwapEntry; MmWriteToSwapPage(pfn ---> NewSwapEntry); MmDeleteVirtualMapping(as.process, PageAddr, „); 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(); 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>2 GB) AddressSpace=内核地址空间; Else AddressSpace=process->VadRoot; MemoryArea = MmLocateMemoryAreaByAddress(AddressSpace, PageAddr); If(MemoryArea->Type == 视图型区段) { 遍历 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; ULONG protect; ULONGLONG MaxSize; ULONG AllocationAttributes; FILE_OBJECT* FileObject; Union { MM_SECTION_SEGMENT* Segment; MM_IMAGE_SECTION_OBJECT* ImageSegments; } 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; ULONG ImageSize; PMM_SECTION_SEGMENT Segments; };
参考《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 ]; DWORD VirtualSize; DWORD VirtualAddress; SizeOfRawData; DWORD PointerToRawData; PointerToRelocations; PointerToLinenumbers; WORD NumberOfRelocations; 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; ULONG VirtualAddress; ULONG RawLength; ULONG Length; ULONG protect; ULONG ReferenceCount; SECTION_PAGE_DIRECTORY PageDir; ULONG Characteristics; BOOL WriteCopy; }
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; MmCreateSection(hFile, &SectionObject , DesiredAccess, ObjectAttribute, MaxSize, protect, AllocAttr); ObInsertObject(SectionObject, …, hSection); } NTSTATUS MmCreateSection (hFile, ROS_SECTION_OBJEC** SectionObject , DesiredAccess, ObjectAttribute, MaxSize, protect, AllocAttr,) { If(AllocAttr & SEC_IMAGE) Return MmCreateImageSection (hFile,SectionObject, DesiredAccess, ObjectAttribute, MaxSize, … ) If (hFile!=NULL ) 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; 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); 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->WriteCopy=FALSE; Segment->RawLength=MaxSize; Segment->Length=Align4kb(MaxSize); Segment->VirtualAddress=0 ; Section->segment=segment; Section->MaxSize=MaxSize; Section->Fileobject=Fileobject; 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; 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; ImageBase = (ULONG_PTR)*BaseAddress; if (ImageBase == 0 ) ImageBase = ImageSectionObject->ImageBase; ImageSize = 0 ; for (i = 0 ; i < NrSegments; i++) { if (!(SectionSegments[i].Characteristics & IMAGE_SCN_TYPE_NOLOAD)) { ULONG_PTR MaxExtent; MaxExtent=SectionSegments[i].VirtualAddress + SectionSegments[i].Length; ImageSize = max(ImageSize, MaxExtent); } } ImageSectionObject->ImageSize = ImageSize; if (MmLocateMemoryAreaByRegion(AddressSpace, ImageBase,PAGE_ROUND_UP(ImageSize))) { if ((*BaseAddress) != NULL ) return (STATUS_UNSUCCESSFUL); ImageBase = MmFindGap(AddressSpace, ImageSize, PAGE_SIZE, FALSE); } for (i = 0 ; i < NrSegments; i++) { if (!(SectionSegments[i].Characteristics & IMAGE_SCN_TYPE_NOLOAD)) { PVOID SBaseAddress = ((char *)ImageBase + (SectionSegments[i].VirtualAddress); MmMapViewOfSegment(AddressSpace, Section, &SectionSegments[i], &SBaseAddress, SectionSegments[i].Length, SectionSegments[i].Protection, 0 , 0 ); } } *BaseAddress = (PVOID)ImageBase; } Else { 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); Area->Data.SectionData.Section=Section; Area->Data.SectionData.Segment=segment; Area->Data.SectionData.ViewOffset=ViewOffset; Area->Data.SectionData..WriteCopyView=FALSE; }
如上将文件中的的某个 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); Offset = PageAddr - MemoryArea->StartingAddress + MemoryArea->Data.SectionData.ViewOffset; Segment=MemoryArea->Data.SectionData.Segment; If(该页面的 PTE 映射到了 页文件) 按普通页文件异常处理方式处理; Else { Entry = MmGetPageEntrySectionSegment(Segment, Offset); PFN Pfn; If(*Entry==0 ) { FileOffset=ConvertTo(Segment, Offset); 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
等信息。
堆管理器则在底层调用VirtualAlloc
API分配,增长虚拟页面,提供底层服务。
详细的用户空间中的堆分配原理请参考: 张银奎-《软件调试》一书
内核对象 写过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; UCHAR HandleInfoOffset; UCHAR QuotaInfoOffset; UCHAR Flags; union { OBJECT_CREATE_INFORMATION* ObjectCreateInfo; PVOID QuotaBlockCharged; }; PSECURITY_DESCRIPTOR SecurityDescriptor; 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; 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;
对象头中记录了NameInfo
、HandleInfo
、QuotaInfo
、CreatorInfo
这 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 打开这个对象,获得句柄,没有名字的对象无法记录到
对象目录中,应用层看不到,只能由内核自己使用。
对象目录 内核中各种类型的对象在对象目录中的位置:
目录对象: 最常见就是对象目录中的目录节点(可以作为叶节点)
普通对象: 只能作为叶节点
符号链接对象: 只能作为叶节点
注意文件对象和注册表中的键对象看似有文件名、键名,但此名非对象名。因此文件对象与键对象是无名的,无法进入对象目录中
根目录也是一种目录对象,符号链接对象可以链接到对象目录中的任何节点,包括又链向另一个符号链接对象。 对象目录中,每个目录节点下面的子节点可以是
该目录中的所有子节点对象都保存在该目录内部的目录项列表中。不过这个列表不是一个简单的数组,而是一个开式hash
表用来方便查找。 根据该目录中各个子对象名的hash
值,将对应的子对象挂入对应的hash
链表中,用hash
方式存储这些子对象以提高查找效率 目录本身也是一种内核对象,其类型就叫"目录类型"
现在就可以看一下这种对象的结构体定义1 2 3 4 5 6 7 typedef struct _OBJECT_DIRECTORY { struct _OBJECT_DIRECTORY_ENTRY * HashBuckets [37]; 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; } 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); HashIndex = HashValue % 37 ; Context->HashValue = HashValue; Context->HashIndex = (USHORT)HashIndex; if (!Context->DirectoryLocked) ObpAcquireDirectoryLockShared(Directory, Context); 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; 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; 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); 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 , &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 ; 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; } 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) InsertTailList(&ObpTypeObjectType->TypeList,&CreatorInfo->TypeList); LocalObjectType->Index = ObpTypeObjectType->TotalNumberOfObjects; if (LocalObjectType->Index < 32 ) ObpObjectTypes[LocalObjectType->Index - 1 ] = LocalObjectType; 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; … 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; }; 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; 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, 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; if (PsGetCurrentProcess() != PsInitialSystemProcess) { KeStackAttachProcess(&PsInitialSystemProcess->Pcb, &ApcState); AttachedToProcess = TRUE; } } else HandleTable = PsGetCurrentProcess()->ObjectTable; 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 (KernelHandle) Handle = ObMarkHandleAsKernelHandle(Handle); *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; ULONG Attributes; PVOID SecurityDescriptor; 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); 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) { 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) 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()) { if ((ObjectType == PsProcessType) || !(ObjectType)) { CurrentProcess = PsGetCurrentProcess(); GrantedAccess = CurrentProcess->GrantedAccess; 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()) { 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); 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, IN PUNICODE_STRING ObjectName, IN ULONG Attributes, IN POBJECT_TYPE ObjectType, IN OUT PVOID ParseContext, IN PVOID InsertObject OPTIONAL, IN OUT PACCESS_STATE AccessState, OUT POBP_LOOKUP_CONTEXT LookupContext, OUT PVOID *FoundObject) { *FoundObject = 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 ; 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) { 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 ((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; } else if ( (ObjectName->Buffer[0 ] == L”\\”) ) { RootDirectory = ObpRootDirectoryObject; RootHandle = NULL ; goto ParseFromRoot; } else if (--MaxReparse) continue ; else { *FoundObject = Object; if (!Object) Status = STATUS_OBJECT_NAME_NOT_FOUND; return Status; } } } } Else { RootDirectory = ObpRootDirectoryObject; if ( (ObjectName->Buffer[0 ] != L”\\”)) return STATUS_OBJECT_PATH_SYNTAX_BAD; } ParseFromRoot: if (!SymLink) { Reparse = TRUE; MaxReparse = 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; 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 (!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 ) { Status = STATUS_OBJECT_NAME_NOT_FOUND; break ; } if (!ObCheckCreateObjectAccess(Directory, ObjectType == ObDirectoryType ? DIRECTORY_CREATE_SUBDIRECTORY : DIRECTORY_CREATE_OBJECT, AccessState, &ComponentName)) { break ; } ObpInsertEntryDirectory(Directory,LookupContext,ObjectHeader); 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 ((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, IN KPROCESSOR_MODE AccessMode, IN OUT PVOID ParseContext, OUT PVOID* ObjectPtr) { PVOID Object = NULL ; Status = ObpCaptureObjectName(&ObjectName, ObjectPath, AccessMode, TRUE); if (!PassedAccessState) { PassedAccessState = &AccessState; Status = SeCreateAccessState(&AccessState,&AuxData,DesiredAccess, &ObjectType->TypeInfo.GenericMapping); } *ObjectPtr = NULL ; Status = ObpLookupObjectName(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, KernelMode, NULL ,
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, 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 ) { 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); }
以上两个函数说明:对象的引用计数一旦减到0,就释放对象及其相关其它结构从系统中消失
句柄的遗传和复制 说到句柄请自觉建立起这样一个完整概念:"具有指定权限和属性的访问句柄"
。 一个句柄代表一次对对象的打开操作(句柄最终的目的是用来访问对象的,因此叫"访问句柄"
) 句柄的权限指本次打开指定对象时,当初申请得到的访问权限,以后凡是使用基于这个句柄的操作都不能超出当初打开时申请得到的访问权限(如打开文件时申请的权限是读) 那么使用那个hFile
调用WriteFile
,将拒绝访问,即使当前用户拥有对那个文件的写权限。
句柄的属性则有重要的一条:是否可继承给子进程。(句柄在打开时通过OBJ_INHERIT
标志指示可否继承)
CreateProcess Win32 API1 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)) Failed = TRUE; else { 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 CloseHandle
、closesocket
会在内部调用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)) { HandleTable = ObpKernelHandleTable; Handle = ObKernelHandleToHandle(Handle); if (Process != PsInitialSystemProcess) { KeStackAttachProcess(&PsInitialSystemProcess->Pcb, &ApcState); 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); if (ObjectType->TypeInfo.MaintainHandleCount) { 递减对应进程对该对象的句柄计数; } 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, { 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); Status = IoCallDriver(DeviceObject, 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_CLEANUP
irp。 另外文件对象在引用计数递减到0后,在最后销毁前,会生成IRP_MJ_CLOSE
irp。
简单一句话【柄完清理、引完关闭】,这两个irp 的产生时机是非常重要的。
进程线程 本篇主要讲述进程的启动过程、线程的调度与切换、进程挂靠
进程的启动过程 1 2 3 4 5 6 7 8 9 10 11 12 BOOL CreateProcess ( LPCTSTR lpApplicationName, LPTSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation ) ;
这个 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); 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 ’; Status = MmMapViewOfSection(Section,Process,&ImageBase,0 ,0 ,NULL ,&ViewSize,0 , MEM_COMMIT,…); Process->SectionBaseAddress = ImageBase; } 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 ; 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); Status = MiCreatePebOrTeb(Process, sizeof (PEB), (PULONG_PTR)&Peb); RtlZeroMemory(Peb, sizeof (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 ; 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 ); NtHeaders = RtlImageNtHeader(Peb->ImageBaseAddress); 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)); 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, 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(); 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) { 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, SectionImageInfo->CommittedStackSize, &InitialTeb); BasepInitializeContext(&Context, NtCurrentPeb(), SectionImageInfo->TransferAddress, InitialTeb.StackBase, 0 ); ObjectAttributes = BasepConvertObjectAttributes(&LocalObjectAttributes, lpThreadAttributes,NULL ); Status = NtCreateThread(&hThread,THREAD_ALL_ACCESS,ObjectAttributes,ProcessHandle, ClientId,&Context,&InitialTeb,TRUE); Status = BasepNotifyCsrOfThread(hThread, ClientId); 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, SIZE_T StackCommit, OUT PINITIAL_TEB InitialTeb) { 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; } StackReserve = ROUND_UP(StackReserve, SystemBasicInfo.AllocationGranularity); StackCommit = ROUND_UP(StackCommit, SystemBasicInfo.PageSize); 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; if (UseGuard) { SIZE_T GuardPageSize = SystemBasicInfo.PageSize; Status = ZwProtectVirtualMemory(hProcess, (PVOID*)&Stack,&GuardPageSize, PAGE_GUARD | PAGE_READWRITE); 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; Context->Ebx = (ULONG)Parameter; Context->Esp = (ULONG)StackAddress; Context->SegFs = KGDT_R3_TEB | RPL_MASK; 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 ; 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, IN PINITIAL_TEB InitialTeb, OUT PTEB *BaseTeb) { NTSTATUS Status = STATUS_SUCCESS; *BaseTeb = NULL ; KeAttachProcess(&Process->Pcb); Status = MiCreatePebOrTeb(Process, sizeof (TEB), (PULONG_PTR)&Teb); _SEH2_TRY { RtlZeroMemory(Teb, sizeof (TEB)); Teb->NtTib.ExceptionList = -1 ; Teb->NtTib.Self = (PNT_TIB)Teb; Teb->NtTib.Version = 30 << 8 ; Teb->ClientId = *ClientId; Teb->RealClientId = *ClientId; Teb->ProcessEnvironmentBlock = Process->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, LPSECURITY_ATTRIBUTES lpThreadAttributes, 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(); SIZE_T EnvSize = 0 ; 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; lpApplicationName = NameBuffer; } else if (!lpCommandLine || *lpCommandLine == UNICODE_NULL) { CmdLineIsAppName = TRUE; lpCommandLine = (LPWSTR)lpApplicationName; } Status = BasepMapFile(lpApplicationName, &hSection, &ApplicationName); if (!NT_SUCCESS(Status)) { If(是一个bat批脚本文件) Else … } if (!StartupInfo.lpDesktop) StartupInfo.lpDesktop = OurPeb->ProcessParameters->DesktopInfo.Buffer; 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; dwCreationFlags |= DETACHED_PROCESS; } ObjectAttributes = BasepConvertObjectAttributes(&LocalObjectAttributes, lpProcessAttributes,NULL ); if (dwCreationFlags & (DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS)) { Status = DbgUiConnectToDbg(); hDebug = DbgUiGetThreadDebugObject(); } 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; Status = BasepInitializeEnvironment(hProcess,RemotePeb,lpApplicationName, CurrentDirectory,lpCommandLine, lpEnvironment,EnvSize, &StartupInfo,dwCreationFlags,bInheritHandles); 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); } Status = BasepNotifyCsrOfCreation(dwCreationFlags,ProcessBasicInfo.UniqueProcessId, bInheritHandles); hThread = BasepCreateFirstThread(hProcess,lpThreadAttributes,&SectionImageInfo, &ClientId); 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 ; 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); 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 下面的NtCreateProcess
、PspCreateProces
只是创建了一个进程(它的内核对象、地址空间等) 进程本身是不能运行的,所以CreateProcess
API 最终还会调用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, 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; if (ParentProcess) { Status = ObReferenceObjectByHandle(ParentProcess, PROCESS_CREATE_PROCESS, PsProcessType,PreviousMode, (PVOID*)&Parent); Affinity = Parent->Pcb.Affinity; } 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; if (SectionHandle) { Status = ObReferenceObjectByHandle(SectionHandle,SECTION_MAP_EXECUTE, MmSectionObjectType,PreviousMode, (PVOID*)&SectionObject); } Else … Process->SectionObject = SectionObject; 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) { Status = MmInitializeProcessAddressSpace(Process,SectionObject,&Flags,ImageFileNam e); NeedsPeb = TRUE; } Else … if (SectionObject) PspMapSystemDll(Process, NULL , FALSE); CidEntry.Object = Process; CidEntry.GrantedAccess = 0 ; Process->UniqueProcessId = ExCreateHandle(PspCidTable, &CidEntry); Process->ObjectTable->UniqueProcessId = Process->UniqueProcessId; if ((Parent) && (NeedsPeb)) { RtlZeroMemory(&InitialPeb, sizeof (INITIAL_PEB)); InitialPeb.Mutant = (HANDLE)-1 ; if (SectionHandle) Status = MmCreatePeb(Process, &InitialPeb, &Process->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
模块中的BaseProcessStrartThunk
或BaseThreadStrartThunk
)继续执行)
并调用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, IN PKSTART_ROUTINE StartRoutine, IN PVOID StartContext, IN PCONTEXT Context, IN PVOID 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; 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; KeInitializeApc(&Thread->SuspendApc,Thread, OriginalApcEnvironment, KiSuspendNop, KiSuspendRundown, KiSuspendThread, 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; KernelStack = MmCreateKernelStack(FALSE, 0 ); AllocatedStack = TRUE; Thread->InitialStack = KernelStack; Thread->StackBase = KernelStack; Thread->StackLimit = KernelStack -12 kb; Thread->KernelStackResident = TRUE; Status = STATUS_SUCCESS; KiInitializeContextThread(Thread, SystemRoutine, StartRoutine, 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, IN PKSTART_ROUTINE StartRoutine, IN PVOID StartContext, IN PCONTEXT ContextPointer) { PFX_SAVE_AREA FxSaveArea; PFXSAVE_FORMAT FxSaveFormat; PKSTART_FRAME StartFrame; PKSWITCHFRAME CtxSwitchFrame; PKTRAP_FRAME TrapFrame; CONTEXT LocalContext; PCONTEXT Context = NULL ; ULONG ContextFlags; PKUINIT_FRAME InitFrame; InitFrame = (PKUINIT_FRAME)( Thread->InitialStack - sizeof (KUINIT_FRAME)); FxSaveArea = &InitFrame->FxSaveArea; RtlZeroMemory(FxSaveArea,KTRAP_FRAME_LENGTH + sizeof (FX_SAVE_AREA)); TrapFrame = &InitFrame->TrapFrame; StartFrame = &InitFrame->StartFrame; 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); 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; } else { {内核线程则会初始化成不同的浮点寄存器,略} Thread->PreviousMode = KernelMode; StartFrame->UserThread = FALSE; } StartFrame->StartContext = StartContext; StartFrame->StartRoutine = StartRoutine; StartFrame->SystemRoutine = SystemRoutine; CtxSwitchFrame->RetAddr = KiThreadStartup; CtxSwitchFrame->ApcBypassDisable = TRUE; CtxSwitchFrame->ExceptionList = -1 ; Thread->KernelStack = CtxSwitchFrame; }
为了弄懂线程初始时的内核栈布局,必须理解下面几个结构体定义和函数。
_KUINIT_FRAME
1 2 3 4 5 6 7 typedef struct _KUINIT_FRAME //每个线程的初始内核栈帧 { KSWITCHFRAME CtxSwitchFrame; KSTART_FRAME StartFrame; KTRAP_FRAME TrapFrame; 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; PKSTART_ROUTINE StartRoutine; PVOID StartContext; BOOLEAN UserThread; } KSTART_FRAME, *PKSTART_FRAME;
_KSWITCHFRAME
1 2 3 4 5 6 7 8 9 typedef struct _KSWITCHFRAME //切换帧 { PVOID ExceptionList; Union { BOOLEAN ApcBypassDisable; }; 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 PKSTART_ROUTINE StartRoutine Void* StartContext BOOL UserThread) { Xor ebx,ebx Xor esi,esi Xor edi,edi Xor ebp,ebp Mov ecx,APC_LEVEL Call KfLowerIrql Pop eax Call eax ----------------------------------华丽的分割线------------------------------------------ Pop ecx Or ecx,ecx Jz BadThread Mov ebp,esp Jmp KiServiceExit2 }
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); KiInitializeUserApc(KeGetExceptionFrame(&Thread->Tcb), KeGetTrapFrame(&Thread->Tcb), PspSystemDllEntryPoint, NULL ,PspSystemDllBase,NULL ); KeLowerIrql(PASSIVE_LEVEL); } Else … 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 () { Lea eax,[esp+16 ] Mov [esp+4 ],eax Xor ebp,ebp Jmp LdrpInit } VOID LdrpInit (PCONTEXT Context,PVOID SystemArgument1,PVOID SystemArgument2) { if (!LdrpInitialized) { LdrpInit2(Context, SystemArgument1, SystemArgument2); LdrpInitialized = TRUE; } LdrpAttachThread(); } VOID LdrpInit2 (PCONTEXT Context,PVOID SystemArgument1,PVOID SystemArgument2) { PPEB Peb = NtCurrentPeb(); PVOID BaseAddress = SystemArgument1; 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) { 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, NTHeaders->OptionalHeader.SizeOfHeapCommit, NULL ,NULL ); RtlpInitializeVectoredExceptionHandling(); RtlInitializeCriticalSection(&PebLock); Peb->FastPebLock = &PebLock; Peb->TlsBitmap = &TlsBitMap; Peb->TlsExpansionBitmap = &TlsExpansionBitMap; Peb->TlsExpansionCounter = 64 ; RtlInitializeBitMap(&TlsBitMap, Peb->TlsBitmapBits,64 ); 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; Peb->Ldr->SsHandle = NULL ; InitializeListHead(&Peb->Ldr->InLoadOrderModuleList); InitializeListHead(&Peb->Ldr->InMemoryOrderModuleList); 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; 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; InsertTailList(&Peb->Ldr->InLoadOrderModuleList,&ExeModule->InLoadOrderLinks); wcscpy(FullNtDllPath, SharedUserData->NtSystemRoot); 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 ; NtModule->TlsIndex = -1 ; NtModule->SectionPointer = NULL ; NtModule->CheckSum = 0 ; NTHeaders = RtlImageNtHeader(NtModule->DllBase); NtModule->SizeOfImage = LdrpGetResidentSize(NTHeaders); NtModule->TimeDateStamp = NTHeaders->FileHeader.TimeDateStamp; InsertTailList(&Peb->Ldr->InLoadOrderModuleList,&NtModule->InLoadOrderLinks); InsertTailList(&Peb->Ldr->InInitializationOrderModuleList, &NtModule->InInitializationOrderModuleList); LdrpInitLoader(); ExeModule->EntryPoint = LdrPEStartup(ImageBase, NULL , NULL , NULL ); Peb->Ldr->Initialized = TRUE; if (Peb->BeingDebugged) DbgBreakPoint(); }
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, 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 (ImageBase != (PVOID) NTHeaders->OptionalHeader.ImageBase) Status = LdrPerformRelocations(NTHeaders, ImageBase); if (Module != NULL ) { *Module = LdrAddModuleEntry(ImageBase, NTHeaders, FullDosName); (*Module)->SectionPointer = SectionHandle; } Else { 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); Status = LdrpInitializeTlsForProccess(); if (NT_SUCCESS(Status)) { LdrpAttachProcess(); LdrpTlsCallback(*Module, DLL_PROCESS_ATTACH); } if (NTHeaders->OptionalHeader.AddressOfEntryPoint != 0 ) EntryPoint = (ULONG_PTR)ImageBase+ NTHeaders->OptionalHeader.AddressOfEntryPoint; return EntryPoint; }
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, IN PLDR_DATA_TABLE_ENTRY Module) { ULONG TlsSize = 0 ; NTSTATUS Status = STATUS_SUCCESS; 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) TlsDirectory = NULL ; } 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) { ImportedName = Module->DllBase + ImportModuleDirectoryCurrent->Name; if (SearchPath == NULL ) { 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: Status = LdrpProcessImportDirectoryEntry(Module, ImportedModule, ImportModuleDirectoryCurrent); ImportModuleDirectoryCurrent++; } } if (TlsDirectory && TlsSize > 0 ) LdrpAcquireTlsSlot(Module, TlsSize, FALSE); return STATUS_SUCCESS; } NTSTATUS LdrpGetOrLoadModule (PWCHAR SearchPath, PCHAR Name, 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 { Status = LdrpMapKnownDll(&AdjustedName, &FullDosName, &SectionHandle); if (!NT_SUCCESS(Status)) { MappedAsDataFile = (0 != (LoadFlags & LOAD_LIBRARY_AS_DATAFILE)); Status = LdrpMapDllImageFile(SearchPath, &AdjustedName, &FullDosName, MappedAsDataFile, &SectionHandle); } ViewSize = 0 ; 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) { if (NULL != BaseAddress) *BaseAddress = (PVOID) ((char *) *BaseAddress + 1 ); *Module = NULL ; return STATUS_SUCCESS; } 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; Status = LdrFixupImports(SearchPath, *Module); 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 : { xor ebp, ebp push eax push 0 } declspec(noreturn) VOID BaseProcessStartup (PPROCESS_START_ROUTINE lpStartAddress) { UINT uExitCode = 0 ; _SEH2_TRY { NtSetInformationThread(NtCurrentThread(),ThreadQuerySetWin32StartAddress, &lpStartAddress,sizeof (PPROCESS_START_ROUTINE)); uExitCode = (lpStartAddress)(); } _SEH2_EXCEPT(BaseExceptionFilter(_SEH2_GetExceptionInformation())) { uExitCode = _SEH2_GetExceptionCode(); } _SEH2_END; ExitProcess(uExitCode); }
普通线程的用户空间总入口 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 : { xor ebp, ebp push ebx push eax push 0 } declspec(noreturn) VOID BaseThreadStartup (LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter) { volatile UINT uExitCode = 0 ; _SEH2_TRY { uExitCode = (lpStartAddress)((PVOID)lpParameter); } _SEH2_EXCEPT(BaseThreadExceptionFilter(_SEH2_GetExceptionInformation())) { uExitCode = _SEH2_GetExceptionCode(); } _SEH2_END; ExitThread(uExitCode); }
LoadLibraryW Dll可以在进程启动初期,被PE加载器静态加载外,程序员也可以调用LoadLibrary
API显式的动态加载。 看一下这个函数的原理,实际上这个函数不是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); RtlInitUnicodeString(&DllName, (LPWSTR)lpLibFileName); if (dwFlags & LOAD_LIBRARY_AS_DATAFILE) { Status = LdrGetDllHandle(SearchPath, NULL , &DllName, (PVOID*)&hInst); if (!NT_SUCCESS(Status)) { Status = LoadLibraryAsDatafile(SearchPath, DllName.Buffer, &hInst); Return Status; } } if (InWindows) Status = LdrLoadDll(SearchPath,&DllCharacteristics,&DllName, (PVOID*)&hInst); Else 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 ; } 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) { 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(); } *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)) { LdrpCallDllEntry(Module, DLL_THREAD_ATTACH, NULL ); } Entry = Entry->Flink; } Entry = NtCurrentPeb()->Ldr->InLoadOrderModuleList.Flink; 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; 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 { KPCR_TIB Tib; KPCR* self; KPRCB* Prcb; KIRQL irql; USHORT* IDT; USHORT* GDT; KTSS* TSS; … }
KPCR_TIB 1 2 3 4 5 6 7 8 9 Struct KPCR_TIB { Void* ExceptionList; 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; KTHREAD* NextThread; KTHREAD* IdleThread; BOOL QuantumEnd; LIST_ENTRY WaitListHead; ULONG ReadSummary; ULONG SelectNextLast; LIST_ENTRY DispatcherReadyListHead[32 ]; FX_SAVE_AREA NpxSaveArea; … }
_KSWITCHFRAME
1 2 3 4 5 6 7 8 9 10 11 12 13 typedef struct _KSWITCHFRAME //切换帧(用来保存切换线程) { PVOID ExceptionList; { BOOLEAN ApcBypassDisable; WaitIrql; }; 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 { 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; ULONG Ecx; ULONG Eax; ULONG PreviousPreviousMode; struct _EXCEPTION_REGISTRATION_RECORD FAR *ExceptionList ; ULONG SegFs; ULONG Edi; ULONG Esi; ULONG Ebx; ULONG Ebp; ULONG ErrCode; ULONG Eip; ULONG SegCs; ULONG EFlags; ULONG HardwareEsp; ULONG HardwareSegSs; 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 : { sub esp, 4 * 4 mov [esp+12 ], ebx mov [esp+8 ], esi mov [esp+4 ], edi mov [esp+0 ], ebp mov ebx, fs:[KPCR_SELF] mov edi, ecx mov esi, edx movzx ecx, byte ptr [edi+KTHREAD_WAIT_IRQL] call @KiSwapContextInternal@0 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 : { inc dword ptr es:[ebx+KPCR_CONTEXT_SWITCHES] push ecx push [ebx+KPCR_EXCEPTION_LIST] AfterTrace: mov ebp, cr0 mov edx, ebp SetStack: mov [edi+KTHREAD_KERNEL_STACK], esp mov eax, [esi+KTHREAD_INITIAL_STACK] -------------------------------------------------------------------------------- cli movzx ecx, byte ptr [esi+KTHREAD_NPX_STATE] and edx, ~(CR0_MP + CR0_EM + CR0_TS) or ecx, edx or ecx, [eax - (NPX_FRAME_LENGTH - FN_CR0_NPX_STATE)] cmp ebp, ecx jnz NewCr0 Sti -------------------------------------------------------------------------------- mov esp, [esi+KTHREAD_KERNEL_STACK] mov ebp, [esi+KTHREAD_APCSTATE_PROCESS] mov eax, [edi+KTHREAD_APCSTATE_PROCESS] cmp ebp, eax jz SameProcess mov ecx, [ebp+KPROCESS_LDT_DESCRIPTOR0] or ecx, [eax+KPROCESS_LDT_DESCRIPTOR0] jnz LdtReload UpdateCr3: mov eax, [ebp+KPROCESS_DIRECTORY_TABLE_BASE] mov cr3, eax SameProcess: xor eax, eax mov gs, ax mov eax, [esi+KTHREAD_TEB] mov [ebx+KPCR_TEB], eax mov ecx, [ebx+KPCR_GDT] mov [ecx+0x3A ], ax shr eax, 16 mov [ecx+0x3C ], al mov [ecx+0x3F ], ah mov eax, [esi+KTHREAD_INITIAL_STACK] sub eax, NPX_FRAME_LENGTH test dword ptr [eax - KTRAP_FRAME_SIZE + KTRAP_FRAME_EFLAGS], EFLAGS_V86_MASK jnz NoAdjust sub eax, KTRAP_FRAME_V86_GS - KTRAP_FRAME_SS NoAdjust: mov ecx, [ebx+KPCR_TSS] mov [ecx+KTSS_ESP0], eax mov ax, [ebp+KPROCESS_IOPM_OFFSET] mov [ecx+KTSS_IOMAPBASE], ax inc dword ptr [esi+KTHREAD_CONTEXT_SWITCHES] pop [ebx+KPCR_EXCEPTION_LIST] pop ecx cmp byte ptr [ebx+KPCR_PRCB_DPC_ROUTINE_ACTIVE], 0 jnz BugCheckDpc --------------------------------新线程环境--------------------------------------- --------------------------------新线程环境--------------------------------------- --------------------------------新线程环境--------------------------------------- cmp byte ptr [esi+KTHREAD_PENDING_KERNEL_APC], 0 jnz CheckApc xor eax, eax ret CheckApc: cmp word ptr [esi+KTHREAD_SPECIAL_APC_DISABLE], 0 jnz ApcReturn test cl, cl jz ApcReturn mov cl, APC_LEVEL call @HalRequestSoftwareInterrupt@4 or eax, esp ApcReturn: setz al ret LdtReload: mov eax, [ebp+KPROCESS_LDT_DESCRIPTOR0] test eax, eax jz LoadLdt mov ecx, [ebx+KPCR_GDT] mov [ecx+KGDT_LDT], eax mov eax, [ebp+KPROCESS_LDT_DESCRIPTOR1] mov [ecx+KGDT_LDT+4 ], eax 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 }
如上:线程从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(); PKTHREAD Thread = KeGetCurrentThread(), NextThread; if (Prcb->ReadySummary==0 ) return Status; OldIrql = KeRaiseIrqlToSynchLevel(); if (Prcb->ReadySummary!=0 ) { 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; MiSyncForContextSwitch(NextThread); KiSwapContext(Thread, NextThread); ---------------------------华丽的分割线--------------------------------------- Status = STATUS_SUCCESS; } else { KiReleasePrcbLock(Prcb); KiReleaseThreadLock(Thread); } } KeLowerIrql(OldIrql); 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) { 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)); ASSERT(Thread->NextProcessor == Prcb->Number); if (RemoveEntryList(&Thread->WaitListEntry)) Prcb->ReadySummary ^= PRIORITY_MASK(HighPriority); 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 )) KeSetEvent(&Prcb->DpcEvent, 0 , 0 ); KeRaiseIrqlToSynchLevel(); 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 ); 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; 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); KiAcquireApcLock(Thread, &ApcLock); PreviousCount = Thread->SuspendCount; if (PreviousCount) { Thread->SuspendCount--; if ((!Thread->SuspendCount) && (!Thread->FreezeCount)) { KiAcquireDispatcherLockAtDpcLevel(); Thread->SuspendSemaphore.Header.SignalState++; KiWaitTest(&Thread->SuspendSemaphore.Header, IO_NO_INCREMENT); KiReleaseDispatcherLockFromDpcLevel(); } } KiReleaseApcLockFromDpcLevel(&ApcLock); 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 VOID FASTCALL KiExitDispatcher (IN KIRQL OldIrql) { PKPRCB Prcb = KeGetCurrentPrcb(); PKTHREAD Thread, NextThread; BOOLEAN PendingApc; ASSERT(KeGetCurrentIrql() >= DISPATCH_LEVEL); KiCheckDeferredReadyList(Prcb); if (OldIrql >= DISPATCH_LEVEL) { if ((Prcb->NextThread) && !(Prcb->DpcRoutineActive)) HalRequestSoftwareInterrupt(DISPATCH_LEVEL); goto Quickie; } if (!Prcb->NextThread) 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; PendingApc = KiSwapContext(Thread, NextThread); -------------------------------------华丽的分割线--------------------------------------- -------------------------------------华丽的分割线--------------------------------------- -------------------------------------华丽的分割线--------------------------------------- if (PendingApc) { ASSERT(OldIrql == PASSIVE_LEVEL); KeLowerIrql(APC_LEVEL); KiDeliverApc(KernelMode, NULL , NULL ); } Quickie: KeLowerIrql(OldIrql); }
上面的函数在降低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 ; 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 ; PKTHREAD NextThread; if (Thread->AdjustReason == AdjustBoost) else if (Thread->AdjustReason == AdjustUnwait) Preempted = Thread->Preempted; OldPriority = Thread->Priority; Thread->Preempted = FALSE; Thread->NextProcessor = 0 ; Prcb = KiProcessorBlock[0 ]; KiAcquirePrcbLock(Prcb); if (KiIdleSummary) { KiIdleSummary = 0 ; Thread->State = Standby; Prcb->NextThread = Thread; KiReleasePrcbLock(Prcb); return ; } Thread->NextProcessor = (UCHAR)Processor; NextThread = Prcb->NextThread; 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; KiReleasePrcbLock(Prcb); KiDeferredReadyThread(NextThread); 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); return ; } } Thread->State = Ready; Thread->WaitTime = KeTickCount.LowPart; 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 ; KiAcquireProcessLock(Process, &ProcessLock); if (Quantum) Process->QuantumReset = Quantum; OldPriority = Process->BasePriority; Process->BasePriority = (SCHAR)Priority; Delta = Priority - OldPriority; 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); 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) { for (;;) { if (Thread->State == Ready) { if (!Thread->ProcessReadyQueue) { Processor = Thread->NextProcessor; Prcb = KiProcessorBlock[Processor]; KiAcquirePrcbLock(Prcb); 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) { 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) { 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 … } } 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; 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; DWORD TlsBitmapBits[2 ]; … }
RTL_BITMAP 1 2 3 4 5 Struct RTL_BITMAP { ULONG SizeOfBitmap; BYTE* Buffer; }
TEB 1 2 3 4 5 6 7 8 Struct TEB { … Void* ThreadLocalStoragePointer; Void* TlsSlots[64 ]; Void* TlsExpansionSlots; … }
动态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(); Index = RtlFindClearBitsAndSet(NtCurrentPeb()->TlsBitmap, 1 , 0 ); if (Index == -1 ) { 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 ; Index += 64 ; } else SetLastError(ERROR_NO_MORE_ITEMS); } else NtCurrentTeb()->TlsSlots[Index] = 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 ) { 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 ) { 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) { 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); Status = LdrpInitializeTlsForProccess(); if (NT_SUCCESS(Status)) { LdrpAttachProcess(); LdrpTlsCallback(*Module, DLL_PROCESS_ATTACH); } … }
钻进各个函数里面去看一下
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) TlsDirectory = NULL ; } … if (TlsDirectory && TlsSize > 0 ) 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; LdrpTlsCount++; LdrpTlsSize += Size; 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 ) { LdrpTlsArray = RtlAllocateHeap(RtlGetProcessHeap(),0 , LdrpTlsCount * sizeof (TLS_DATA)); ModuleListHead = &NtCurrentPeb()->Ldr->InLoadOrderModuleList; Entry = ModuleListHead->Flink; while (Entry != ModuleListHead) { Module = CONTAINING_RECORD(Entry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks); if (Module->LoadCount ==-1 && Module->TlsIndex != -1 ) { TlsDirectory = RtlImageDirectoryEntryToData(Module->DllBase, TRUE,IMAGE_DIRECTORY_ENTRY_TLS,&Size); TlsData = &LdrpTlsArray[Module->TlsIndex]; TlsData->StartAddressOfRawData = TlsDirectory->StartAddressOfRawData; TlsData->TlsDataSize = TlsDirectory->EndAddressOfRawData - TlsDirectory->StartAddressOfRawData; TlsData->TlsZeroSize = TlsDirectory->SizeOfZeroFill; if (TlsDirectory->AddressOfCallBacks) TlsData->TlsAddressOfCallBacks = TlsDirectory->AddressOfCallBacks; else TlsData->TlsAddressOfCallBacks = NULL ; TlsData->Module = Module; *(PULONG)TlsDirectory->AddressOfIndex = Module->TlsIndex; } Entry = Entry->Flink; } } return STATUS_SUCCESS; }
如上这个函数为进程建立起一个tls描述符数组。
1 2 3 4 5 6 7 8 9 typedef struct _TLS_DATA { PVOID StartAddressOfRawData; DWORD TlsDataSize; DWORD TlsZeroSize; 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(); 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 ) { TlsPointers = RtlAllocateHeap(RtlGetProcessHeap(),0 , LdrpTlsCount * sizeof (PVOID) + LdrpTlsSize); TlsData = (PVOID)((ULONG_PTR)TlsPointers + LdrpTlsCount * sizeof (PVOID)); Teb->ThreadLocalStoragePointer = TlsPointers; TlsInfo = LdrpTlsArray; for (i = 0 ; i < LdrpTlsCount; i++, TlsInfo++) { TlsPointers[i] = TlsData; if (TlsInfo->TlsDataSize) { memcpy (TlsData, TlsInfo->StartAddressOfRawData, TlsInfo->TlsDataSize); TlsData = (PVOID)((ULONG_PTR)TlsData + TlsInfo->TlsDataSize); } if (TlsInfo->TlsZeroSize) { memset (TlsData, 0 , TlsInfo->TlsZeroSize); TlsData = (PVOID)((ULONG_PTR)TlsData + TlsInfo->TlsZeroSize); } } } 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) { Process->StackCount++; KiMoveApcState(&Thread->ApcState, SavedApcState); 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) { Thread->ApcStatePointer[OriginalApcEnvironment] = &Thread->SavedApcState; Thread->ApcStatePointer[AttachedApcEnvironment] = &Thread->ApcState; Thread->ApcStateIndex = AttachedApcEnvironment; } if (Process->State == ProcessInMemory) { KiReleaseDispatcherLockFromDpcLevel(); KiReleaseApcLockFromDpcLevel(ApcLock); KiSwapProcess(Process, SavedApcState->Process); KiExitDispatcher(ApcLock->OldIrql); } Else … }
KiSwapProcess 实质性的函数是KiSwapProcess
继续看
1 2 3 4 5 6 7 8 VOID KiSwapProcess (IN PKPROCESS NewProcess,IN PKPROCESS OldProcess) { PKIPCR Pcr = (PKIPCR)KeGetPcr(); writecr3(NewProcess->DirectoryTableBase[0 ]); Ke386SetGs(0 ); Pcr->TSS->IoMapBase = NewProcess->IopmOffset; }
看到没进程挂靠的实质工作,就是将cr3寄存器改为目标寄存器的地址空间 这样线程的所有有关内存的操作,操作的都是目标进程的地址空间。 明白了进程挂靠后,理解跨进程操作就很容易了。
一个进程可以调用OpenProcess
打开另一个进程,取得目标进程的句柄后,就可调用VirtualAllocEx
、Wri teProcessMemory
、ReadProcessMemory
、CreateRemoteThread
等函数操作那个进程的地址空间。 这些跨进程操作的函数功能强大,而且带有破坏性,以至于往往被杀毒软件重点封杀,特别是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) { 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) { 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)) return STATUS_INVALID_PARAMETER_MIX; 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) Status = PsLookupProcessThreadByCid(ClientId, &Process, &Thread); Else 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 ) { 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; 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) { Thread->SuspendApc.Inserted = TRUE; KiInsertQueueApc(&Thread->SuspendApc, IO_NO_INCREMENT); } 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) { 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++; 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 ) ; 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 INT DllMain ( IN PVOID hInstanceDll, IN ULONG dwReason, IN PVOID reserved) { switch (dwReason) { case DLL_PROCESS_ATTACH: Init(); … … } } BOOL Init (VOID) { … LoadAppInitDlls(); … }
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()) { 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); } } } }
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; Status = NtQueryValueKey(hKey,&szLoadName,KeyValuePartialInformation, kvpInfo,dwSize,&dwSize); RtlMoveMemory(&bLoad,kvpInfo->Data,kvpInfo->DataLength); HeapFree(GetProcessHeap(), 0 , kvpInfo); kvpInfo = NULL ; if (bLoad) { 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 ) { 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) { LARGE_INTEGER Offset; NTSTATUS Status; Offset.u.LowPart = lpOverlapped->Offset; Offset.u.HighPart = lpOverlapped->OffsetHigh; lpOverlapped->Internal = STATUS_PENDING; Status = NtReadFile(hFile, NULL , ApcRoutine, lpCompletionRoutine, (PIO_STATUS_BLOCK)lpOverlapped, lpBuffer, nNumberOfBytesToRead, &Offset, 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, 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->Overlay.AsynchronousParameters.UserApcRoutine = ApcRoutine; Irp->Overlay.AsynchronousParameters.UserApcContext = ApcContext; … Status = IoCallDriver(DeviceObject, 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) { 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 ); } … }
如上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; KAPC_STATE SavedApcState; KAPC_STATE* ApcStatePointer[2 ]; UCHAR ApcStateIndex; UCHAR ApcQueueable; ULONG KernelApcDisable; KAPC SuspendApc; … } KTHREAD; Typedef struct _KAPC_STATE { LIST_EBTRY ApcListHead[2 ]; PKPROCESS Process; BOOL KernelApcInProgress; BOOL KernelApcPending; BOOL UserApcPending; }
_KAPC_ENVIRONMENT
1 2 3 4 5 6 7 Typedef enum _KAPC_ENVIRONMENT { OriginalApcEnvironment, AttachedApcEnvironment; CurrentApc Environment; CurrentApc Environment; }
一个线程可以挂靠到其他进程的地址空间中,因此,一个线程的状态分两种:常态、挂靠态。 常态下状态数组中 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*
,SystemArgument1
与SystemArgument2
表示插入队列时的附加参数
若这个 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; PKKERNEL_ROUTINE KernelRoutine; PKRUNDOWN_ROUTINE RundownRoutine; PKNORMAL_ROUTINE NormalRoutine; PVOID NormalContext; PVOID SystemArgument1; PVOID SystemArgument2; CCHAR ApcStateIndex; KPROCESSOR_MODE ApcMode; BOOLEAN Inserted; } 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, pfnAPC, 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, IN PVOID NormalContext, IN PVOID SystemArgument1, IN PVOID SystemArgument2) { PKAPC Apc; PETHREAD Thread; NTSTATUS Status = STATUS_SUCCESS; Status = ObReferenceObjectByHandle(ThreadHandle,THREAD_SET_CONTEXT,PsThreadType, ExGetPreviousMode(), (PVOID)&Thread,NULL ); Apc = ExAllocatePoolWithTag(NonPagedPool |POOL_QUOTA_FAIL_INSTEAD_OF_RAISE, sizeof (KAPC),TAG_PS_APC); KeInitializeApc(Apc, &Thread->Tcb, OriginalApcEnvironment, PspQueueApcSpecialApc, NULL , ApcRoutine, UserMode, NormalContext); KeInsertQueueApc(Apc, SystemArgument1, SystemArgument2, 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, IN PKKERNEL_ROUTINE KernelRoutine, IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL, IN PKNORMAL_ROUTINE NormalRoutine, IN KPROCESSOR_MODE Mode, IN PVOID Context) { Apc->Type = ApcObject; Apc->Size = sizeof (KAPC); if (TargetEnvironment == CurrentApcEnvironment) Apc->ApcStateIndex = Thread->ApcStateIndex; else Apc->ApcStateIndex = TargetEnvironment; Apc->Thread = Thread; Apc->KernelRoutine = KernelRoutine; Apc->RundownRoutine = RundownRoutine; Apc->NormalRoutine = NormalRoutine; if (NormalRoutine) { Apc->ApcMode = Mode; Apc->NormalContext = Context; } Else { Apc->ApcMode = KernelMode; Apc->NormalContext = NULL ; } Apc->Inserted = FALSE; }
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)) State = FALSE; else { Apc->SystemArgument1 = SystemArgument1; Apc->SystemArgument2 = SystemArgument2; Apc->Inserted = TRUE; KiInsertQueueApc(Apc, PriorityBoost); } KiReleaseApcLockFromDpcLevel(&ApcLock); KiExitDispatcher(ApcLock.OldIrql); 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) Apc->ApcStateIndex = Thread->ApcStateIndex; ApcState = Thread->ApcStatePointer[(UCHAR)Apc->ApcStateIndex]; ApcMode = Apc->ApcMode; if (Apc->NormalRoutine) { if ((ApcMode == UserMode) && (Apc->KernelRoutine == PsExitSpecialApc)) { Thread->ApcState.UserApcPending = TRUE; InsertHeadList(&ApcState->ApcListHead[ApcMode],&Apc->ApcListEntry); } else InsertTailList(&ApcState->ApcListHead[ApcMode],&Apc->ApcListEntry); } Else { 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); } if (Thread->ApcStateIndex == Apc->ApcStateIndex) { if (Thread == KeGetCurrentThread()) { ASSERT(Thread->State == Running); if (ApcMode == KernelMode) { Thread->ApcState.KernelApcPending = TRUE; if (!Thread->SpecialApcDisable) HalRequestSoftwareInterrupt(APC_LEVEL); } } Else { KiAcquireDispatcherLock(); if (ApcMode == KernelMode) { 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 == 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() { SaveTrap(); ---------------上面保存完寄存器等现场后,开始查 SST 表调用系统服务------------------ FindTableCall(); ---------------------------------调用完系统服务函数后------------------------------ Move esp,kthread.TrapFrame; If(上次模式==UserMode) { Call KiDeliverApc 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 Jnz 1 Test byte ptr[ebp+KTRAP_FRAME_CS],1 Je 2 1 : Mov ebx,PCR[KPCR_CURRENT_THREAD] Mov byte ptr[ebx+KTHREAD_ALERTED],0 Cmp byte ptr[ebx+KTHREAD_PENDING_USER_APC],0 Je 2 Mov ebx,ebp Mov [ebx,KTRAP_FRAME_EAX],eax Mov ecx,APC_LEVEL Call KfRaiseIrql Push eax Sti Push ebx Push UserMode Call KiDeliverApc Pop ecx Call KfLowerIrql Move eax, [ebx,KTRAP_FRAME_EAX] Cli Jmp 1 …
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, IN PKEXCEPTION_FRAME ExceptionFrame, IN PKTRAP_FRAME TrapFrame) { PKTHREAD Thread = KeGetCurrentThread(); PKPROCESS Process = Thread->ApcState.Process; OldTrapFrame = Thread->TrapFrame; Thread->TrapFrame = TrapFrame; Thread->ApcState.KernelApcPending = FALSE; if (Thread->SpecialApcDisable) goto Quickie; while (!IsListEmpty(&Thread->ApcState.ApcListHead[KernelMode])) { KiAcquireApcLockAtApcLevel(Thread, &ApcLock); ApcListEntry = Thread->ApcState.ApcListHead[KernelMode].Flink; Apc = CONTAINING_RECORD(ApcListEntry, KAPC, ApcListEntry); KernelRoutine = Apc->KernelRoutine; NormalRoutine = Apc->NormalRoutine; NormalContext = Apc->NormalContext; SystemArgument1 = Apc->SystemArgument1; SystemArgument2 = Apc->SystemArgument2; if (NormalRoutine==NULL ) { RemoveEntryList(ApcListEntry); Apc->Inserted = FALSE; KiReleaseApcLock(&ApcLock); KernelRoutine(Apc,&NormalRoutine,&NormalContext, &SystemArgument1,&SystemArgument2); } Else { if ((Thread->ApcState.KernelApcInProgress) || (Thread->KernelApcDisable)) { KiReleaseApcLock(&ApcLock); goto Quickie; } RemoveEntryList(ApcListEntry); Apc->Inserted = FALSE; KiReleaseApcLock(&ApcLock); KernelRoutine(Apc, &NormalRoutine, &NormalContext, &SystemArgument1, &SystemArgument2); if (NormalRoutine) { Thread->ApcState.KernelApcInProgress = TRUE; KeLowerIrql(PASSIVE_LEVEL); NormalRoutine(NormalContext, SystemArgument1, SystemArgument2); KeRaiseIrql(APC_LEVEL, &ApcLock.OldIrql); } Thread->ApcState.KernelApcInProgress = FALSE; } } if ((DeliveryMode == UserMode) && !(IsListEmpty(&Thread->ApcState.ApcListHead[UserMode])) && (Thread->ApcState.UserApcPending)) { KiAcquireApcLockAtApcLevel(Thread, &ApcLock); Thread->ApcState.UserApcPending = FALSE; ApcListEntry = Thread->ApcState.ApcListHead[UserMode].Flink; Apc = CONTAINING_RECORD(ApcListEntry, KAPC, ApcListEntry); KernelRoutine = Apc->KernelRoutine; NormalRoutine = Apc->NormalRoutine; NormalContext = Apc->NormalContext; SystemArgument1 = Apc->SystemArgument1; SystemArgument2 = Apc->SystemArgument2; RemoveEntryList(ApcListEntry); Apc->Inserted = FALSE; KiReleaseApcLock(&ApcLock); KernelRoutine(Apc, &NormalRoutine, &NormalContext, &SystemArgument1, &SystemArgument2); if (!NormalRoutine) KeTestAlertThread(UserMode); Else { KiInitializeUserApc(ExceptionFrame, TrapFrame, NormalRoutine, NormalContext, SystemArgument1, 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; KeTrapFrameToContext(TrapFrame, ExceptionFrame, &Context); _SEH2_TRY { AlignedEsp = Context.Esp & ~3 ; ContextLength = CONTEXT_ALIGNED_SIZE + (4 * sizeof (ULONG_PTR)); Stack = ((AlignedEsp - 8 ) & ~3 ) - ContextLength; *(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; RtlCopyMemory( (Stack + (4 * sizeof (ULONG_PTR))),&Context,sizeof (CONTEXT)); 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 ] Mov ecx,fs:[TEB_EXCEPTION_LIST] Mov edx,offset KiUserApcExceptionHandler -------------------------------------------------------------------------------------- Mov [eax],ecx Mov [eax+4 ],edx Mov fs:[TEB_EXCEPTION_LIST],eax --------------------上面三条指令在栈中构造一个 8B 的标准 seh 节点----------------------- Pop eax Lea edi,[esp+12 ] Call eax Mov ecx,[edi+ CONTEXT_ALIGNED_SIZE] Mov fs:[ TEB_EXCEPTION_LIST],ecx Push TRUE Push edi Call NtContinue ----------------------------------华丽的分割线------------------------------------------- Mov esi,eax Push esi Call RtlRaiseStatus 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, BOOL TestAlert ) { Push ebp Mov ebx,PCR[KPCR_CURRENT_THREAD] Mov edx,[ebp+KTRAP_FRAME_EDX] Mov [ebx+KTHREAD_TRAP_FRAME],edx Mov ebp,esp Mob eax,[ebp] Mov ecx,[ebp+8 ] /本函数的第一个参数,即 Context Push eax Push NULL Push ecx Call KiContinue Or eax,eax Jnz error Cmp dword ptr[ebp+12 ],0 Je DontTest Mov al,[ebx+KTHREAD_PREVIOUS_MODE] Push eax Call KeTestAlertThread DontTest: Pop ebp Mov esp,ebp Jmp KiServiceExit2 } NTSTATUS KiContinue (IN PCONTEXT Context, IN PKEXCEPTION_FRAME ExceptionFrame, IN PKTRAP_FRAME 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); else { KeContextToTrapFrame(Context,ExceptionFrame,TrapFrame,Context->ContextFlags, KernelMode); } } _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) { CONTEXT LocalContext; ProbeForRead(Context, sizeof (CONTEXT), sizeof (ULONG)); RtlCopyMemory(&LocalContext, Context, sizeof (CONTEXT)); Context = &LocalContext; KeContextToTrapFrame(&LocalContext,ExceptionFrame,TrapFrame, LocalContext.ContextFlags,UserMode); }
上面的函数,就把NtContinue
的TrapFrame
强制还原成原来的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; } 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(); _disable(); Pcr->Irql = OldIrql; 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); }
这个函数在从当前 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) { ULONG EFlags; PKPCR Pcr = KeGetPcr(); KIRQL PendingIrql; EFlags = readeflags(); _disable(); Pcr->IRR |= (1 << Irql); PendingIrql = SWInterruptLookUpTable[Pcr->IRR & 3 ]; if (PendingIrql > Pcr->Irql) SWInterruptHandlerTable[PendingIrql](); writeeflags(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); 扫描执行当前线程的内核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)) { WaitableObject = OBJECT_TO_OBJECT_HEADER(Object)->Type->DefaultObject; if (IsPointerOffset(WaitableObject)) { 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; PKWAIT_BLOCK WaitBlock = &Thread->WaitBlock[0 ]; 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 ; if (!Thread->WaitNext) goto WaitStart; Thread->WaitNext = FALSE; KxSingleThreadWait(); for (;;) { Thread->Preempted = FALSE; if ((Thread->ApcState.KernelApcPending) && !(Thread->SpecialApcDisable) && (Thread->WaitIrql == PASSIVE_LEVEL)) { KiReleaseDispatcherLock(Thread->WaitIrql); } Else { 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; goto DontWait; } Else 抛出异常… } } else if (CurrentObject->Header.SignalState > 0 ) { KiSatisfyNonMutantWait(CurrentObject); WaitStatus = STATUS_WAIT_0; 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; } InsertTailList(&CurrentObject->Header.WaitListHead,&WaitBlock->WaitListEntry); if (Thread->Queue) KiActivateWaiterQueue(Thread->Queue); Thread->State = Waiting; KiAddThreadToWaitList(Thread, Swappable); if (Timeout) KxInsertTimer(Timer, Hand); else KiReleaseDispatcherLockFromDpcLevel(); KiSetThreadSwapBusy(Thread); WaitStatus = KiSwapThread(Thread, KeGetCurrentPrcb()); -------------------------------华丽的分割线-------------------------------------- -------------------------------华丽的分割线-------------------------------------- -------------------------------华丽的分割线-------------------------------------- if (WaitStatus != STATUS_KERNEL_APC) return WaitStatus; if (Timeout) Timeout = KiRecalculateDueTime(OriginalDueTime,&DueTime,&NewDueTime); } WaitStart: Thread->WaitIrql = KeRaiseIrqlToSynchLevel(); KxSingleThreadWait(); KiAcquireDispatcherLockAtDpcLevel(); } KiReleaseDispatcherLock(Thread->WaitIrql); 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; 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; Thread->WaitStatus = 0 ; if (Timeout) { 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 (Thread->Alerted[WaitMode]) { Thread->Alerted[WaitMode] = FALSE; return STATUS_ALERTED; } else if ((WaitMode != KernelMode) && (!IsListEmpty(&Thread->ApcState.ApcListHead[UserMode]))) { Thread->ApcState.UserApcPending = TRUE; return STATUS_USER_APC; } else if (Thread->Alerted[KernelMode]) { Thread->Alerted[KernelMode] = FALSE; return STATUS_ALERTED; } } else if ((WaitMode != KernelMode) && (Thread->ApcState.UserApcPending)) { return STATUS_USER_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; if (WaitBlock->WaitType == WaitAny) { 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 ) { (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) { 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 的时候可能会强制唤醒目标线程。
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; Thread->WaitStatus |= WaitStatus; WaitBlock = Thread->WaitBlockList; do { RemoveEntryList(&WaitBlock->WaitListEntry); WaitBlock = WaitBlock->NextWaitBlock; } while (WaitBlock != Thread->WaitBlockList); 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; 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; MiSyncForContextSwitch(NextThread); ApcState = KiSwapContext(CurrentThread, NextThread); ----------------------------------------华丽的分割线--------------------------------------- ----------------------------------------华丽的分割线--------------------------------------- ----------------------------------------华丽的分割线--------------------------------------- if (ApcState) { KeLowerIrql(APC_LEVEL); KiDeliverApc(KernelMode, NULL , NULL ); ASSERT(WaitIrql == PASSIVE_LEVEL); } KeLowerIrql(WaitIrql); WaitStatus = CurrentThread->WaitStatus; 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 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, 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) WaitBlockArray = &Thread->WaitBlock[0 ]; if (!Thread->WaitNext) goto 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 { 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) { 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 (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; 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; } 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 ); 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 ; 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 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 ); if (Thread->Tcb.ServiceTable != KeServiceDescriptorTable) return STATUS_ALREADY_WIN32; if (!Thread->Tcb.LargeStack) { NewStack = (ULONG_PTR)MmCreateKernelStack(TRUE, 0 ); OldStack = KeSwitchKernelStack(NewStack, (NewStack - KERNEL_STACK_SIZE)); MmDeleteKernelStack(OldStack, FALSE); } if (!Process->Win32Process) Status = PspW32ProcessCallout(Process, TRUE); Thread->Tcb.ServiceTable = KeServiceDescriptorTableShadow; Status = PspW32ThreadCallout(Thread, PsW32ThreadCalloutInitialize); if (!NT_SUCCESS(Status)) Thread->Tcb.ServiceTable = KeServiceDescriptorTable; return Status; }
如上每个线程在转换为 GUI 线程时,必须换用64KB
的大内核栈,因为普通的内核栈只有12KB
大小,不能支持开销大的图形任务。 然后分配一个W32PROCESS
结构,将进程转换为 GUI 进程,然后分配W32THREAD
结构,更改系统服务表描述符表
上面的PspW32ProcessCallout
和PspW32ThreadCallout
函数都是回调函数 分别指向win32k.sys
模块中的Win32kProcessCallback
、Win32kThreadCallback
函数。
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); if (!Win32Process) { 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 ) { 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; RTL_AVL_TABLE GDIEngUserMemAllocTable; 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) { Win32Thread = ExAllocatePoolWithTag(NonPagedPool,sizeof(THREADINFO),'t23W'); RtlZeroMemory(Win32Thread, sizeof (THREADINFO)); PsSetThreadWin32Thread(Thread, Win32Thread); } if (Type == PsW32ThreadCalloutInitialize) { 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; 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; UINT cTimersReady; 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; 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 ; UINT InternalPosInitialized : 1 ; UINT HideFocus : 1 ; UINT HideAccel : 1 ; } WND, *PWND;
消息循环机制重点函数 下面重点讲述消息循环机制中的几个重点函数
GetMessage
DispatchMessage
SendMessage
PostMessage
PeekMessage
1 2 3 4 5 while (GetMessage(&Msg, NULL , 0 , 0 ) > 0 ){ TranslateMessage(&Msg); DispatchMessage(&Msg); }
当运行程序->事件操作引发消息->消息先存在系统消息队列->再存入到应用程序消息队列->用消息循环提取消息->处理消息->再返回消息队列....
上面代码的执行过程为
消息循环调用GetMessage()
从消息队列中查找消息进行处理,如果消息队列为空,程序将停止执行并等待(程序阻塞)。
事件发生时导致一个消息加入到消息队列(例如系统注册了一个鼠标点击事件)GetMessage()
将返回一个正值,这表明有消息需要被处理,并且消息已经填充到传入的MSG参数中; 当传入WM_QUIT
消息时返回0, 如果返回值为负表明发生了错误。
取出消息(在Msg变量中)并将其传递给TranslateMessage()
函数这个函数做一些额外的处理:将虚拟键值信息转换为字符信息。这一步实际上是可选的但有些地方需要用到这一步。
上面的步骤执行完后,将消息传递给DispatchMessage()
函数。DispatchMessage()
函数将消息分发到消息的目标窗口,并且查找目标窗口过程函数,给窗口过程函数
传递窗口句柄、消息、wParam、lParam
等参数然后调用该函数。
在窗口过程函数中检查消息和其他参数,你可以用它来实现你想要的操作。 如果不想处理某些特殊的消息,你应该总是调用DefWindowProc()
函数,系统将按按默认的方式处理这些消息(通常认为是不做任何操作)。
一旦一个消息处理完成,窗口过程函数返回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; PUSER32_THREAD_DATA ThreadData = User32GetThreadData(); 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; } NTUSERGETMESSAGEINFO, *PNTUSERGETMESSAGEINFO; typedef struct tagMSG { HWND hwnd; UINT message; WPARAM wParam; 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; MsgMemoryEntry = FindMsgMemory(Info.Msg.message); if (NULL == MsgMemoryEntry) Info.LParamSize = 0 ; else { Size = MsgMemorySize(MsgMemoryEntry, Info.Msg.wParam,Info.Msg.lParam); Info.LParamSize = Size; UserMem = NULL ; Status = ZwAllocateVirtualMemory(NtCurrentProcess(), &UserMem, 0 , &Info.LParamSize, MEM_COMMIT, PAGE_READWRITE); 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); 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) { ti = GetW32ThreadInfo(); if (ti == NULL ) { SetLastWin32Error(ERROR_ACCESS_DENIED); return NULL ; } } 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; 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; UINT Size; 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; LIST_ENTRY PostedMessagesListHead; LIST_ENTRY NotifyMessagesListHead; LIST_ENTRY HardwareMessagesListHead; KMUTEX HardwareLock; PUSER_MESSAGE MouseMoveMsg; BOOLEAN QuitPosted; ULONG QuitExitCode; PKEVENT NewMessages; HANDLE NewMessagesHandle; ULONG LastMsgRead; HWND FocusWindow; ULONG PaintCount; HWND ActiveWindow; HWND CaptureWindow; HWND MoveSize; HWND MenuOwner; BYTE MenuState; PTHRDCARETINFO CaretInfo; PHOOKTABLE Hooks; WORD WakeMask; WORD QueueBits; WORD ChangedBits; LPARAM ExtraInfo; LIST_ENTRY DispatchingMessagesHead; LIST_ENTRY LocalDispatchingMessagesHead; struct _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; MSG Msg; } USER_MESSAGE, *PUSER_MESSAGE;
PostMessage
与SendMessage
的区别。 前者是异步的发生方直接把消息发送到接收方的消息队列中就返 回,不管消息有没有被处理完成。 而后者是同步的,要一直等到消息被接收方处理后才会返回。
回到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; while (co_MsqDispatchOneSentMessage(ThreadQueue)); 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; } 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; } while (co_MsqDispatchOneSentMessage(ThreadQueue)); if ( IntGetPaintMessage( Window,MsgFilterMin,MsgFilterMax,pti,&Msg->Msg,RemoveMessages)) { Msg->FreeLParam = FALSE; goto MsgExit; } if (PostTimerMessages(Window)) 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; } 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; MSG Msg; PKEVENT CompletionEvent; LRESULT* Result; struct _USER_MESSAGE_QUEUE * SenderQueue ; SENDASYNCPROC CompletionCallback; ULONG_PTR CompletionCallbackContext; LIST_ENTRY DispatchingListEntry; INT HookMessage; BOOL HasPackedLParam; } 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); InsertTailList(&MessageQueue->LocalDispatchingMessagesHead,&Message->ListEntry); 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); if (!(Message->HookMessage & MSQ_SENTNOWAIT)) { if (Message->DispatchingListEntry.Flink != NULL ) RemoveEntryList(&Message->DispatchingListEntry); } 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); 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, 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); }
上面的代码我想不用解释了。
由此可以看到在GetMessage
、PeekMessage
函数内部,会一直等待到消息队列中出现符合指定条件的消息,这就是为什么GUI
线程绝大多数时刻都处于睡眠状态的原因, 因为它在等待消息。 另外在GetMessage
、PeekMessage
内部,即使因为队列中一直没有符合指定条件的消息而等待,也为在内部不断的处理别的线程通过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); 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) { 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) { if (MessageQueue->MouseMoveMsg) { RemoveEntryList(&MessageQueue->MouseMoveMsg->ListEntry); ExFreePool(MessageQueue->MouseMoveMsg); } MessageQueue->MouseMoveMsg = Current; } InsertTailList(&MessageQueue->HardwareMessagesListHead,&Current->ListEntry); IntUnLockHardwareMessageQueue(MessageQueue); } IntUnLockSystemHardwareMessageQueueLock(FALSE); *Message = Current; RETURN(TRUE); } 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 ( 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, BOOL Remove, PBOOL Freed, PWINDOW_OBJECT ScopeWin, PPOINT ScreenPoint, BOOL FromGlobalQueue) { USHORT Msg = Message->Msg.message; PWINDOW_OBJECT CaptureWindow = NULL ; hCaptureWin = IntGetCaptureWindow(); if (hCaptureWin == 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) { 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 ((Window != NULL && PtrToInt(Window) != 1 && CaptureWindow->hSelf != Window->hSelf) || ((FilterLow != 0 || FilterHigh != 0 ) && (Msg < FilterLow || Msg > FilterHigh))) { if (FromGlobalQueue) { 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); } Message->Msg.hwnd = CaptureWindow->hSelf; Message->Msg.lParam = MAKELONG(Message->Msg.pt.x, Message->Msg.pt.y); if (Message->Msg.message == WM_MOUSEMOVE || Message->Msg.message == WM_NCMOUSEMOVE) { if (FromGlobalQueue) { 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、 一个线程调用SendMessage
、PostMessage
等函数给自己或别的线程发消息,它就是一个发送方线程
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); 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) { MsgiUMToKMCleanup(&UMMsg, &KMMsg); Result = IntCallWindowProcW( Info.Ansi, Info.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, 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); 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; 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); InsertTailList(&ThreadQueue->DispatchingMessagesHead, &Message->DispatchingListEntry); 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) { 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; } 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)); } 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) { 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, int HookId, HOOKPROC HookProc, BOOL Ansi) { BOOLEAN ThreadReferenced = FALSE; ClientInfo = GetWin32ClientInfo(); if (ThreadId!=0 ) { 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); ThreadReferenced = TRUE; if (Thread->ThreadsProcess != PsGetCurrentProcess()) { ObDereferenceObject(Thread); SetLastWin32Error(ERROR_INVALID_PARAMETER); RETURN( NULL ); } } else { 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)) { SetLastNtError(Status); RETURN( (HANDLE) NULL ); } ThreadReferenced = TRUE; } else if (NULL == Mod) { SetLastWin32Error(ERROR_HOOK_NEEDS_HMOD); RETURN( NULL ); } else Thread = 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) { 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); } Hook = UserCreateObject(gHandleTable, NULL , &Handle, otHook, sizeof (HOOK)); Hook->Thread = Thread; Hook->HookId = HookId; if (Thread) { W32Thread = ((PTHREADINFO)Thread->Tcb.Win32Thread); 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 ]; 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(); 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; Result = co_IntCallHookProc(HookId,Code,wParam,lParam,Hook->Proc,Hook->Ansi, &Hook->ModuleName); ClientInfo->phkCurrent = SaveHook; Status = IntValidateWindowStationHandle(PsGetCurrentProcess()->Win32WindowStation, KernelMode,0 ,&WinStaObj); 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, 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)]); 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,并检查该钩子是否正在被调用,若是就立即返回。否则销毁钩子后再返回。
键盘消息的产生、处理 熟悉了消息机制的原理后,我们看看具体键盘鼠标消息的处理流程。 系统在初始化时会创建两个内核守护线程,分别用来监视、处理键盘输入和鼠标输入
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 ); Status = PsCreateSystemThread(&KeyboardThreadHandle,THREAD_ALL_ACCESS,NULL ,NULL , &KeyboardThreadId,KeyboardThreadMain,NULL ); 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 ; UINT ModifierState = 0 ; 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 ); 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 ); IntKeyboardGetIndicatorTrans(KeyboardDeviceHandle,&IndicatorTrans); for (;;) { Status = KeWaitForSingleObject(&InputThreadsStart,0 ,KernelMode,TRUE,NULL ); while (InputThreadsRunning) { 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); fsModifiers = IntKeyboardGetModifiers(&KeyInput); if (fsModifiers) { if (KeyInput.Flags & KEY_BREAK) { ModifierState &= ~fsModifiers; if (fsModifiers == MOD_ALT) { if (KeyInput.Flags & KEY_E0) gQueueKeyStateTable[VK_RMENU] = 0 ; else gQueueKeyStateTable[VK_LMENU] = 0 ; if (gQueueKeyStateTable[VK_RMENU] == 0 &&gQueueKeyStateTable[VK_LMENU] == 0 ) gQueueKeyStateTable[VK_MENU] = 0 ; } } Else { ModifierState |= fsModifiers; if (ModifierState == fsModifiers && (fsModifiers == MOD_ALT || fsModifiers == MOD_WIN)) { bLeftAlt = FALSE; if (fsModifiers == MOD_ALT) { if (KeyInput.Flags & KEY_E0) gQueueKeyStateTable[VK_RMENU] = 0x80 ; else { gQueueKeyStateTable[VK_LMENU] = 0x80 ; bLeftAlt = TRUE; } gQueueKeyStateTable[VK_MENU] = 0x80 ; } 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); fsNextModifiers = IntKeyboardGetModifiers(&NextKeyInput); if (fsNextModifiers) ModifierState ^= fsNextModifiers; if (ModifierState == 0 ) { if (fsModifiers == MOD_WIN) IntKeyboardSendWinKeyMsg(); else if (fsModifiers == MOD_ALT) { gQueueKeyStateTable[VK_MENU] = 0 ; if (bLeftAlt) gQueueKeyStateTable[VK_LMENU] = 0 ; else gQueueKeyStateTable[VK_RMENU] = 0 ; co_IntKeyboardSendAltKeyMsg(); } continue ; } NumKeys = 2 ; } } } for (;NumKeys;memcpy (&KeyInput, &NextKeyInput, sizeof (KeyInput)),NumKeys--) { PKBL keyboardLayout = NULL ; lParam = 0 ; IntKeyboardUpdateLeds(KeyboardDeviceHandle,&KeyInput,IndicatorTrans); * 0 -15 : 重复计数 * 16 -23 : 扫描码 * 24 : 修正键标志 * 29 : alt键是否按着标志 * 30 : 标志是否与上次的按键状态相同 * 31 : 标志当前按键状态是按下还是弹起 if (!(KeyInput.Flags & KEY_BREAK)) { if (((KeyInput.Flags & (KEY_E0 | KEY_E1)) == LastFlags) && (KeyInput.MakeCode == LastMakeCode)) { RepeatCount++; lParam |= (1 << 30 ); } Else { RepeatCount = 1 ; LastFlags = KeyInput.Flags & (KEY_E0 | KEY_E1); LastMakeCode = KeyInput.MakeCode; } } Else { 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) { 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(); 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; USHORT MakeCode; USHORT Flags; 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; } } }
用户可以调用RegisterHotkey
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 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 ) 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; 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 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 ; 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); mi.time = 0 ; mi.dwExtraInfo = 0 ; for (i = 0 ; i < InputCount; i++) { mid = (Data + i); mi.dx += mid->LastX; mi.dy += mid->LastY; if (mid->Flags == MOUSE_MOVE_ABSOLUTE) 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); } } } SendMouseEvent(mi); } #define SendMouseEvent(mi) \ if (mi.dx != 0 || mi.dy != 0 ) \ mi.dwFlags |= MOUSEEVENTF_MOVE; \ if (mi.dwFlags) \ IntMouseInput(&mi); \ ClearMouseInput(mi);
可以看出,第一个满足的 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 (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)) { return FALSE; } 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 ; if (co_HOOK_CallHooks(WH_MOUSE_LL, HC_ACTION, Msg->message, (LPARAM) &MouseHookData)) return ; IntLockSystemMessageQueue(OldIrql); if (SystemMessageQueueCount == SYSTEM_MESSAGE_QUEUE_SIZE) { IntUnLockSystemMessageQueue(OldIrql); return ; } if (Msg->message == WM_MOUSEMOVE && SystemMessageQueueCount !=0 ) { if (SystemMessageQueueTail == 0 ) Prev = SYSTEM_MESSAGE_QUEUE_SIZE - 1 ; else Prev = SystemMessageQueueTail - 1 ; 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); }
当键盘按键消息插入到系统环形缓冲队列中后,会触发事件HardwareMessageEvent
。GetMessage
、PeekMessage
内部会等待这个事件。
模拟键盘鼠标动作 除了由真实的硬件(键盘、鼠标)产生键盘消息外,用户也可以模拟键盘鼠标动作。keybd_event
、mouse_event
、SendInput
这几个 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)); }
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: 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; UCHAR Importance; volatile USHORT Number; LIST_ENTRY DpcListEntry; PKDEFERRED_ROUTINE DeferredRoutine; PVOID DeferredContext; PVOID SystemArgument1; PVOID SystemArgument2; volatile PVOID DpcData; } 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, IN PKDEFERRED_ROUTINE DeferredRoutine, IN PVOID DeferredContext) { 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 ; Dpc->Importance= MediumImportance; Dpc->DeferredRoutine = DeferredRoutine; Dpc->DeferredContext = DeferredContext; Dpc->DpcData = NULL ; } 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(); if (Dpc->Number >= 32 ) { Cpu = Dpc->Number - 32 ; Prcb = KiProcessorBlock[Cpu]; } Else { Cpu = Prcb->Number; Prcb = CurrentPrcb; } if ((Dpc->Type == ThreadedDpcObject) && (Prcb->ThreadDpcEnable)) DpcData = &Prcb->DpcData[DPC_THREADED]; else DpcData = &Prcb->DpcData[DPC_NORMAL]; if (!InterlockedCompareExchangePointer(&Dpc->DpcData, DpcData, NULL )) { Dpc->SystemArgument1 = SystemArgument1; Dpc->SystemArgument2 = SystemArgument2; DpcData->DpcQueueDepth++; DpcData->DpcCount++; DpcConfigured = TRUE; 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 { if (!(Prcb->DpcRoutineActive) && !(Prcb->DpcInterruptRequested)) { if (Prcb != CurrentPrcb) { if (((Dpc->Importance == HighImportance) || (DpcData->DpcQueueDepth >= Prcb->MaximumDpcQueueDepth)) && (!(AFFINITY_MASK(Cpu) & KiIdleSummary) || (Prcb->Sleeping))) { Prcb->DpcInterruptRequested = TRUE; DpcInserted = TRUE; } } Else { if ((Dpc->Importance != LowImportance) || (DpcData->DpcQueueDepth >= Prcb->MaximumDpcQueueDepth) || (Prcb->DpcRequestRate < Prcb->MinimumDpcRate)) { Prcb->DpcInterruptRequested = TRUE; DpcInserted = TRUE; } } } } } KiReleaseSpinLock(&DpcData->DpcLock); if (DpcInserted) { if (Prcb != CurrentPrcb) KiIpiSend(AFFINITY_MASK(Cpu), IPI_DPC); else HalRequestSoftwareInterrupt(DISPATCH_LEVEL); } KeLowerIrql(OldIrql); 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 VOID FASTCALL HalRequestSoftwareInterrupt (IN KIRQL Irql) { ULONG EFlags; PKPCR Pcr = KeGetPcr(); KIRQL PendingIrql; EFlags = readeflags(); _disable(); Pcr->IRR |= (1 << Irql); PendingIrql = SWInterruptLookUpTable[Pcr->IRR & 3 ]; if (PendingIrql > Pcr->Irql) SWInterruptHandlerTable[PendingIrql](); writeeflags(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) { ULONG EFlags; ULONG PendingIrql, PendingIrqlMask; PKPCR Pcr = KeGetPcr(); PIC_MASK Mask; EFlags = readeflags(); _disable(); Pcr->Irql = OldIrql; PendingIrqlMask = Pcr->IRR & FindHigherIrqlMask[OldIrql]; if (PendingIrqlMask) { BitScanReverse(&PendingIrql, PendingIrqlMask); if (PendingIrql > DISPATCH_LEVEL) … SWInterruptHandlerTable[PendingIrql](); SWInterruptHandlerTable[31 -PendingIrql](); } writeeflags(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, HalpApcInterrupt, HalpDispatchInterrupt2, 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(); OldIrql = _HalpDispatchInterruptHandler(); PendingIrqlMask = Pcr->IRR & FindHigherIrqlMask[OldIrql]; if (PendingIrqlMask) { BitScanReverse(&PendingIrql, PendingIrqlMask); if (PendingIrql > DISPATCH_LEVEL) … SWInterruptHandlerTable[PendingIrql](); } } KIRQL _HalpDispatchInterruptHandler(VOID) { KIRQL CurrentIrql; PKPCR Pcr = KeGetPcr(); CurrentIrql = Pcr->Irql; Pcr->Irql = DISPATCH_LEVEL; Pcr->IRR &= ~(1 << DISPATCH_LEVEL); _enable(); KiDispatchInterrupt(); _disable(); return CurrentIrql; }
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] cli 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] mov dword ptr [ebx+KPCR_EXCEPTION_LIST], -1 mov edx, esp mov esp, [ebx+KPCR_PRCB_DPC_STACK] push edx mov ecx, [ebx+KPCR_PRCB] call @KiRetireDpcList@4 pop esp pop dword ptr [ebx+KPCR_EXCEPTION_LIST] pop ebp CheckQuantum: Sti cmp byte ptr [ebx+KPCR_PRCB_QUANTUM_END], 0 jnz QuantumEnd cmp byte ptr [ebx+KPCR_PRCB_NEXT_THREAD], 0 je Return 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 call _KeRaiseIrqlToSynchLevel@0 mov byte ptr [edi+KTHREAD_SWAP_BUSY], 1 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]; ListHead = &DpcData->DpcListHead; do { Prcb->DpcRoutineActive = TRUE; if (Prcb->TimerRequest) { TimerHand = Prcb->TimerHand; Prcb->TimerRequest = 0 ; _enable(); KiTimerExpiration(NULL , NULL , (PVOID)TimerHand, NULL ); _disable(); } while (DpcData->DpcQueueDepth != 0 ) { 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(); DeferredRoutine(Dpc,DeferredContext,SystemArgument1,SystemArgument2); ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL); _disable(); } else { ASSERT(DpcData->DpcQueueDepth == 0 ); KeReleaseSpinLockFromDpcLevel(&DpcData->DpcLock); } } Prcb->DpcRoutineActive = FALSE; 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 都不能处在这个中断级别 如ZwCreateFie
ddk文档规定了,这个内核函数必须运行在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) { PLARGE_INTEGER TimeoutPointer = NULL ; PETHREAD Thread = PsGetCurrentThread(); if ((ULONG_PTR)Context & EX_DYNAMIC_WORK_THREAD) { Timeout.QuadPart = Int32x32To64(10 , -10000000 * 60 ); 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; for (;;) { QueueEntry = KeRemoveQueue(&WorkQueue->WorkerQueue,WaitMode,TimeoutPointer); if ((NTSTATUS)(ULONG_PTR)QueueEntry == STATUS_TIMEOUT) break ; InterlockedIncrement((PLONG)&WorkQueue->WorkItemsProcessed); WorkItem = CONTAINING_RECORD(QueueEntry, WORK_QUEUE_ITEM, List); WorkItem->WorkerRoutine(WorkItem->Parameter); if (Thread->Tcb.SpecialApcDisable) Thread->Tcb.SpecialApcDisable = FALSE; 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, IN PVOID ServiceContext, IN PKSPIN_LOCK SpinLock, IN ULONG Vector, IN KIRQL Irql, IN KIRQL SynchronizeIrql, IN KINTERRUPT_MODE InterruptMode, IN BOOLEAN ShareVector, IN KAFFINITY ProcessorEnableMask, IN BOOLEAN FloatingSave) { PKINTERRUPT Interrupt; PKINTERRUPT InterruptUsed; PIO_INTERRUPT IoInterrupt; PKSPIN_LOCK SpinLockUsed; BOOLEAN FirstRun; CCHAR Count = 0 ; KAFFINITY Affinity; PAGED_CODE(); *InterruptObject = NULL ; Affinity = ProcessorEnableMask & KeActiveProcessors; while (Affinity) { if (Affinity & 1 ) Count++; Affinity >>= 1 ; } IoInterrupt = ExAllocatePoolWithTag(NonPagedPool, (Count - 1 ) * sizeof (KINTERRUPT) + sizeof (IO_INTERRUPT),TAG_KINTERRUPT); if (!IoInterrupt) return STATUS_INSUFFICIENT_RESOURCES; *InterruptObject = &IoInterrupt->FirstInterrupt; 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 ) { if (Affinity & 1 ) { InterruptUsed = FirstRun ? &IoInterrupt->FirstInterrupt : Interrupt; KeInitializeInterrupt(InterruptUsed,ServiceRoutine,ServiceContext, SpinLockUsed,Vector,Irql,SynchronizeIrql, InterruptMode,ShareVector,Count,FloatingSave); if (!KeConnectInterrupt(InterruptUsed)) { if (FirstRun) ExFreePool(IoInterrupt); else IoDisconnectInterrupt(&IoInterrupt->FirstInterrupt); return STATUS_INVALID_PARAMETER; } if (FirstRun) FirstRun = FALSE; Else 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]; KSPIN_LOCK SpinLock; } IO_INTERRUPT, *PIO_INTERRUPT; typedef struct _KINTERRUPT //中断对象 { CSHORT Type; CSHORT Size; LIST_ENTRY InterruptListEntry; PKSERVICE_ROUTINE ServiceRoutine; PVOID ServiceContext; KSPIN_LOCK SpinLock; ULONG TickCount; PKSPIN_LOCK ActualLock; ULONG Vector; KIRQL Irql; KIRQL SynchronizeIrql; BOOLEAN FloatingSave; BOOLEAN Connected; BOOLEAN ShareVector; KINTERRUPT_MODE Mode; ULONG ServiceCount; ULONG DispatchCount; ULONG DispatchCode[DISPATCH_LENGTH]; } 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; Interrupt->Type = InterruptObject; Interrupt->Size = sizeof (KINTERRUPT); if (SpinLock) 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; for (i = 0 ; i < DISPATCH_LENGTH; i++) *DispatchCode++ = ((PULONG)KiInterruptTemplate)[i]; ((ULONG)&KiInterruptTemplateObject -4 - (ULONG)KiInterruptTemplate)); *Patch = PtrToUlong(Interrupt); 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 _KiInterruptTemplateObject: mov eax, offset @KiInterruptTemplateHandler@8 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; Vector = Interrupt->Vector; if ((Irql > HIGH_LEVEL) || (Number >= KeNumberProcessors) || (Interrupt->SynchronizeIrql < Irql) || (Interrupt->FloatingSave)) { return FALSE; } Connected = FALSE; Error = FALSE; KeSetSystemAffinityThread(1 << Number); OldIrql = KiAcquireDispatcherLock(); if (!Interrupt->Connected) { KiGetVectorDispatch(Vector, &Dispatch); if (Dispatch.Type == NoConnect) { Interrupt->Connected = Connected = TRUE; InitializeListHead(&Interrupt->InterruptListEntry); KiConnectVectorToInterrupt(Interrupt, NormalConnect); Status = HalEnableSystemInterrupt(Vector, Irql, Interrupt->Mode); if (!Status) Error = TRUE; } else if ((Dispatch.Type != UnknownConnect) && (Interrupt->ShareVector) && (Dispatch.Interrupt->ShareVector) && (Dispatch.Interrupt->Mode == Interrupt->Mode)) { Interrupt->Connected = Connected = TRUE; 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); 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); if ((PKINTERRUPT_ROUTINE)Current == Dispatch->NoDispatch) { Dispatch->Interrupt = NULL ; Dispatch->Type = NoConnect; } else { 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; KiGetVectorDispatch(Interrupt->Vector, &Dispatch); if (Type == NoConnect) Handler = Dispatch.NoDispatch; else { Interrupt->DispatchAddress = (Type == NormalConnect) ? Dispatch.InterruptDispatch: Dispatch.ChainedDispatch; Handler = (PVOID)&Interrupt->DispatchCode; } 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); 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, IN PKINTERRUPT Interrupt) { 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++; if (HalBeginSystemInterrupt(Interrupt->Irql,Interrupt->Vector,&OldIrql)) { ListHead = &Interrupt->InterruptListEntry; NextEntry = ListHead; while (TRUE) { if (Interrupt->SynchronizeIrql > Interrupt->Irql) OldIrql = KfRaiseIrql(Interrupt->SynchronizeIrql); KxAcquireSpinLock(Interrupt->ActualLock); Handled = Interrupt->ServiceRoutine(Interrupt,Interrupt->ServiceContext); KxReleaseSpinLock(Interrupt->ActualLock); if (Interrupt->SynchronizeIrql > Interrupt->Irql) KfLowerIrql(OldIrql); if ((Handled) && (Interrupt->Mode == LevelSensitive)) break ; NextEntry = NextEntry->Flink; if (NextEntry == ListHead) { if (Interrupt->Mode == LevelSensitive) break ; if (!Handled) break ; } Interrupt = CONTAINING_RECORD(NextEntry, KINTERRUPT, InterruptListEntry); } KiExitInterrupt(TrapFrame, OldIrql, FALSE); } Else 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); 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) { 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) { KiConnectVectorToInterrupt(Dispatch.Interrupt, NormalConnect); } } Else { HalDisableSystemInterrupt(Interrupt->Vector, Irql); KiConnectVectorToInterrupt(Interrupt, NoConnect); } 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); if (HalBeginSystemInterrupt(CLOCK2_LEVEL, 0x30 , &Irql)) { 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; SharedUserData->InterruptTime.High1Time = InterruptTime.HighPart; SharedUserData->InterruptTime.LowPart = InterruptTime.LowPart; SharedUserData->InterruptTime.High2Time = InterruptTime.HighPart; OldTickCount = KeTickCount.LowPart; InterlockedExchangeAdd(&KiTickOffset, -(LONG)Increment); 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; CurrentTime.HighPart = KeTickCount.High1Time; CurrentTime.LowPart = OldTickCount; CurrentTime.QuadPart += 1 ; KeTickCount.High2Time = CurrentTime.HighPart; KeTickCount.LowPart = CurrentTime.LowPart; KeTickCount.High1Time = CurrentTime.HighPart; 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 (KiTimerTableListHead[Hand].Time.QuadPart <= InterruptTime.QuadPart) { if (!Prcb->TimerRequest) { Prcb->TimerRequest = (ULONG_PTR)TrapFrame; Prcb->TimerHand = Hand; HalRequestSoftwareInterrupt(DISPATCH_LEVEL); } } if (KiTickOffset <= 0 ) { KiTickOffset += KeMaximumIncrement; KeUpdateRunTime(TrapFrame, Irql); } else { Prcb->InterruptCount++; } KiEndInterrupt(Irql, TrapFrame); }
上面的函数在发现有定时器到点后,就会发出一个 DPC,然后在系统定时器中断退出后,就会扫描执行 DPC 队列,调用KiTimerExpiration
函数扫描所有到点的定时器,然后唤醒在那些定时器上等待的线程。KiTimerExpiration
函数的代码,有兴趣的读者自己看。
设备驱动 设备栈从上层到下层的顺序依次是:过滤设备、类设备、过滤设备、小端口设备【过、类、过滤、小端口】 驱动栈:因设备堆栈原因而建立起来的一种堆栈 老式驱动:指不提供AddDevice
的驱动,又叫NT
式驱动 Wdm 驱动:指提供了AddDevice
的驱动
驱动初始化:指 IO 管理器加载驱动后,调用驱动的DriverEntry
、AddDevice
函数 设备栈中上层设备与下层设备的绑定关系不是一对一,而是一对多。 一个设备可以同时绑定到 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; PVOID DriverStart; ULONG DriverSize; PVOID DriverSection; PEXTENDED_DRIVER_EXTENSION DriverExtension; UNICODE_STRING DriverName; PUNICODE_STRING HardwareDatabase; struct _FAST_IO_DISPATCH *FastIoDispatch ; PDRIVER_INITIALIZE DriverInit; PDRIVER_STARTIO DriverStartIo; PDRIVER_UNLOAD DriverUnload; PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1 ]; } 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; ULONG Count; UNICODE_STRING ServiceKeyName; 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 ; PIO_TIMER Timer; ULONG Flags; ULONG Characteristics; volatile PVPB Vpb; PVOID DeviceExtension; DEVICE_TYPE DeviceType; CCHAR StackSize; union { LIST_ENTRY ListEntry; WAIT_CONTEXT_BLOCK Wcb; } Queue; ULONG AlignmentRequirement; KDEVICE_QUEUE DeviceQueue; KDPC Dpc; ULONG ActiveThreadCount; PSECURITY_DESCRIPTOR SecurityDescriptor; KEVENT DeviceLock; USHORT SectorSize; 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 ; 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; struct _MDL *MdlAddress ; ULONG Flags; union { struct _IRP *MasterIrp ; volatile LONG IrpCount; PVOID SystemBuffer; } AssociatedIrp; LIST_ENTRY ThreadListEntry; IO_STATUS_BLOCK IoStatus; KPROCESSOR_MODE RequestorMode; BOOLEAN PendingReturned; CHAR StackCount; CHAR CurrentLocation; BOOLEAN Cancel; KIRQL CancelIrql; CCHAR ApcEnvironment; UCHAR AllocationFlags; PIO_STATUS_BLOCK UserIosb; PKEVENT UserEvent; union { struct { _ANONYMOUS_UNION union { PIO_APC_ROUTINE UserApcRoutine; PVOID IssuingProcess; } DUMMYUNIONNAME; PVOID UserApcContext; } AsynchronousParameters; LARGE_INTEGER AllocationSize; } Overlay; volatile PDRIVER_CANCEL CancelRoutine; PVOID UserBuffer; union { struct { _ANONYMOUS_UNION union { KDEVICE_QUEUE_ENTRY DeviceQueueEntry; _ANONYMOUS_STRUCT struct { PVOID DriverContext[4 ]; } DUMMYSTRUCTNAME; } DUMMYUNIONNAME; PETHREAD Thread; PCHAR AuxiliaryBuffer; _ANONYMOUS_STRUCT struct { LIST_ENTRY ListEntry; _ANONYMOUS_UNION union { struct _IO_STACK_LOCATION *CurrentStackLocation ; ULONG PacketType; } DUMMYUNIONNAME; } DUMMYSTRUCTNAME; struct _FILE_OBJECT *OriginalFileObject ; } Overlay; KAPC Apc; PVOID CompletionKey; } Tail; } IRP, *PIRP; typedef struct _IO_STACK_LOCATION { UCHAR MajorFunction; UCHAR MinorFunction; 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; PDEVICE_OBJECT DeviceObject; PFILE_OBJECT FileObject; 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(); 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 (Exclusive) ObjectAttributes.Attributes |= OBJ_EXCLUSIVE; if (DeviceName) ObjectAttributes.Attributes |= OBJ_PERMANENT; 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 ; CreatedDeviceObject->AlignmentRequirement = 0 ; CreatedDeviceObject->Flags = DO_DEVICE_INITIALIZING; if (Exclusive) CreatedDeviceObject->Flags |= DO_EXCLUSIVE; if (DeviceName) CreatedDeviceObject->Flags |= DO_DEVICE_HAS_NAME; 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); } 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 ; } if ((CreatedDeviceObject->DeviceType == FILE_DEVICE_DISK_FILE_SYSTEM) || (CreatedDeviceObject->DeviceType == FILE_DEVICE_FILE_SYSTEM) || (CreatedDeviceObject->DeviceType == FILE_DEVICE_CD_ROM_FILE_SYSTEM) || (CreatedDeviceObject->DeviceType == FILE_DEVICE_NETWORK_FILE_SYSTEM) || (CreatedDeviceObject->DeviceType == FILE_DEVICE_TAPE_FILE_SYSTEM)) { InitializeListHead(&CreatedDeviceObject->Queue.ListEntry); } Else { 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 ) KeBugCheckEx(NO_MORE_IRP_STACK_LOCATIONS, (ULONG_PTR)Irp, 0 , 0 , 0 ); StackPtr = IoGetNextIrpStackLocation(Irp); Irp->Tail.Overlay.CurrentStackLocation = StackPtr; StackPtr->DeviceObject = DeviceObject; 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, 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) { return IopDeviceFsIoControl(DeviceHandle,Event,UserApcRoutine,UserApcContext, IoStatusBlock,IoControlCode,InputBuffer,InputBufferLength, OutputBuffer,OutputBufferLength, TRUE); }
必须注意DeviceHandle
参数应该叫做文件句柄而不是设备句柄。 在 Windows 内核中,几乎从来没有设备句柄
这种概念。 虽然可以为设备对象创建一个设备句柄,但很少这么做,因为设备句柄几乎没有什么作用。 相反应用程序一般都是调用CreateFile
(它内部会打开设备对象,创建文件对象,返回文件对象 的句柄) 以后就可以用这个文件句柄,找到对应的文件对象及其设备对象去访问设备了。
上面函数在调用时,用户可以提供一个Event
和IoStatusBlock
当 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) { PKEVENT EventObject = NULL ; BOOLEAN LockedForSynch = FALSE; KPROCESSOR_MODE PreviousMode = ExGetPreviousMode(); AccessType = IO_METHOD_FROM_CTL_CODE(IoControlCode); if (PreviousMode != KernelMode) 检查用户空间地址是否可读可写,略 Status = ObReferenceObjectByHandle(DeviceHandle,0 ,IoFileObjectType,PreviousMode, (PVOID*)&FileObject,&HandleInformation); if ((FileObject->CompletionContext) && (UserApcRoutine)) { ObDereferenceObject(FileObject); return STATUS_INVALID_PARAMETER; } if (PreviousMode != KernelMode) { DesiredAccess = (ACCESS_MASK)((IoControlCode >> 14 ) & 3 ); if ((DesiredAccess != FILE_ANY_ACCESS) && (HandleInformation.GrantedAccess & DesiredAccess) != DesiredAccess) { ObDereferenceObject(FileObject); return STATUS_ACCESS_DENIED; } } if (Event) { 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); Irp = IoAllocateIrp(DeviceObject->StackSize, FALSE); Irp->UserIosb = IoStatusBlock; Irp->UserEvent = EventObject; Irp->Overlay.AsynchronousParameters.UserApcRoutine = UserApcRoutine; Irp->Overlay.AsynchronousParameters.UserApcContext = UserApcContext; Irp->Cancel = FALSE; Irp->CancelRoutine = NULL ; Irp->PendingReturned = FALSE; Irp->RequestorMode = PreviousMode; Irp->MdlAddress = NULL ; Irp->AssociatedIrp.SystemBuffer = NULL ; Irp->Flags = 0 ; Irp->Tail.Overlay.AuxiliaryBuffer = NULL ; Irp->Tail.Overlay.OriginalFileObject = FileObject; Irp->Tail.Overlay.Thread = PsGetCurrentThread(); StackPtr = IoGetNextIrpStackLocation(Irp); StackPtr->FileObject = FileObject; StackPtr->MajorFunction = IsDevIoCtl ? IRP_MJ_DEVICE_CONTROL : IRP_MJ_FILE_SYSTEM_CONTROL; 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) { 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->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)) { 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) { 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; } Irp->Flags |= (!IsDevIoCtl) ? IRP_DEFER_IO_COMPLETION : 0 ; 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)) { 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, IN BOOLEAN ChargeQuota) { PIRP Irp = NULL ; USHORT Size; PKPRCB Prcb; UCHAR Flags = 0 ; PNPAGED_LOOKASIDE_LIST List = NULL ; PP_NPAGED_LOOKASIDE_NUMBER ListType = LookasideSmallIrpList; Size = IoSizeOfIrp(StackSize); if (ChargeQuota) Flags |= IRP_QUOTA_CHARGED; if ((StackSize <= 8 ) && (ChargeQuota == FALSE)) { Flags = IRP_ALLOCATED_FIXED_SIZE; if (StackSize != 1 ) { Size = IoSizeOfIrp(8 ); ListType = LookasideLargeIrpList; } Prcb = KeGetCurrentPrcb(); List = (PNPAGED_LOOKASIDE_LIST)Prcb->PPLookasideList[ListType].P; List->L.TotalAllocates++; Irp = (PIRP)InterlockedPopEntrySList(&List->L.ListHead); if (!Irp) { List->L.AllocateMisses++; List = (PNPAGED_LOOKASIDE_LIST)Prcb->PPLookasideList[ListType].L; List->L.TotalAllocates++; Irp = (PIRP)InterlockedPopEntrySList(&List->L.ListHead); } } if (!Irp) { if (Flags & IRP_ALLOCATED_FIXED_SIZE) List->L.AllocateMisses++; Irp = ExAllocatePoolWithTag(NonPagedPool, Size, TAG_IRP); } Else Flags &= ~IRP_QUOTA_CHARGED; 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); 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); }
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); IopUpdateOperationCount(TransferType); Status = IoCallDriver(DeviceObject, Irp); if (Deferred) { if (Status != STATUS_PENDING) { ASSERT(!Irp->PendingReturned); KeRaiseIrql(APC_LEVEL, &OldIrql); IopCompleteRequest(&Irp->Tail.Apc,&NormalRoutine,&NormalContext, (PVOID*)&FileObject,&NormalContext); KeLowerIrql(OldIrql); } } if (SynchIo) { if (Status == STATUS_PENDING) { Status = KeWaitForSingleObject(&FileObject->Event, Executive,PreviousMode, (FileObject->Flags & FO_ALERTABLE_IO), NULL ); 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; CancelRoutine = (PVOID)IoSetCancelRoutine(Irp, NULL ); if (CancelRoutine) { 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 ); 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); 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 ; SourceDevice->AlignmentRequirement = AttachedDevice->AlignmentRequirement; SourceDevice->SectorSize = AttachedDevice->SectorSize; if (IoGetDevObjExtension(AttachedDevice)->ExtensionFlags & DOE_START_PENDING) { IoGetDevObjExtension(SourceDevice)->ExtensionFlags |= DOE_START_PENDING; } 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 完成时,用户会调用完成这个IoCompleteRequest
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 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); ASSERT(Irp->IoStatus.Status != -1 ); LastStackPtr = (PIO_STACK_LOCATION)(Irp + 1 ); if (LastStackPtr->Control & SL_ERROR_RETURNED) ErrorCode = PtrToUlong(LastStackPtr->Parameters.Others.Argument4); for (StackPtr = IoGetCurrentIrpStackLocation(Irp), Irp->CurrentLocation++, Irp->Tail.Overlay.CurrentStackLocation++; Irp->CurrentLocation <= (Irp->StackCount + 1 ); StackPtr++, Irp->CurrentLocation++, Irp->Tail.Overlay.CurrentStackLocation++) { Irp->PendingReturned = StackPtr->Control & SL_PENDING_RETURNED; if (!NT_SUCCESS(Irp->IoStatus.Status)) { if (Irp->IoStatus.Status != ErrorCode) { 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 ; 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) { MasterIrp = Irp->AssociatedIrp.MasterIrp; MasterCount = InterlockedDecrement(&MasterIrp->AssociatedIrp.IrpCount); for (Mdl = Irp->MdlAddress; Mdl; Mdl = NextMdl) { NextMdl = Mdl->Next; IoFreeMdl(Mdl); } IoFreeIrp(Irp); if (MasterCount==0 ) IofCompleteRequest(MasterIrp, PriorityBoost); return ; } 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); } return ; } Mdl = Irp->MdlAddress; while (Mdl) { MmUnlockPages(Mdl); Mdl = Mdl->Next; } if ((Irp->Flags & IRP_DEFER_IO_COMPLETION) && !(Irp->PendingReturned)) return ; Thread = Irp->Tail.Overlay.Thread; FileObject = Irp->Tail.Overlay.OriginalFileObject; if (!Irp->Cancel) { KeInitializeApc(&Irp->Tail.Apc,&Thread->Tcb,Irp->ApcEnvironment, IopCompleteRequest, NULL ,NULL ,KernelMode,NULL ); KeInsertQueueApc(&Irp->Tail.Apc,FileObject,NULL ,PriorityBoost); } Else { 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); 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); 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); 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 ); } 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); } 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); } } 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(); if (!SeSinglePrivilegeCheck(SeLoadDriverPrivilege, PreviousMode)) return STATUS_PRIVILEGE_NOT_HELD; Status = ProbeAndCaptureUnicodeString(&CapturedDriverServiceName,PreviousMode, DriverServiceName); LoadParams.ServiceName = &CapturedDriverServiceName; LoadParams.DriverObject = NULL ; KeInitializeEvent(&LoadParams.Event, NotificationEvent, FALSE); if (PsGetCurrentProcess() == PsInitialSystemProcess) IopLoadUnloadDriver(&LoadParams); 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; Status = RtlQueryRegistryValues(RTL_REGISTRY_ABSOLUTE, LoadParams->ServiceName->Buffer, QueryTable, NULL , NULL ); 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)) { 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)) { Status = IopInitializeDriverModule(DeviceNode,ModuleObject, &DeviceNode->ServiceName, (Type == SERVICE_FILE_SYSTEM_DRIVER || Type == 8 SERVICE_RECOGNIZER_DRIVER), &DriverObject); if (!NT_SUCCESS(Status)) { MmUnloadSystemImage(ModuleObject); IopFreeDeviceNode(DeviceNode); LoadParams->Status = Status; KeSetEvent(&LoadParams->Event, 0 , FALSE); return ; } } IopInitializeDevice(DeviceNode, DriverObject); 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; 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; 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, IN PUNICODE_STRING RegistryPath, PLDR_DATA_TABLE_ENTRY ModuleObject, OUT PDRIVER_OBJECT *pDriverObject) { ULONG RetryCount = 0 ; try_again: if (!DriverName) { 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; 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; DriverObject->DriverSection = ModuleObject; for (i = 0 ; i <= IRP_MJ_MAXIMUM_FUNCTION; i++) DriverObject->MajorFunction[i] = IopInvalidDeviceRequest; 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; RtlCopyMemory(&DriverObject->DriverName,&ServiceKeyName,sizeof (UNICODE_STRING)); Status = ObInsertObject(DriverObject,NULL ,FILE_READ_DATA,0 ,NULL ,&hDriver); 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 ; Status = (*InitializationFunction)(DriverObject, RegistryPath); if (!NT_SUCCESS(Status)) { DriverObject->DriverSection = NULL ; ObMakeTemporaryObject(DriverObject); ObDereferenceObject(DriverObject); } else *pDriverObject = DriverObject; for (i = 0 ; i <= IRP_MJ_MAXIMUM_FUNCTION; i++) { if (DriverObject->MajorFunction[i] == NULL ) DriverObject->MajorFunction[i] = IopInvalidDeviceRequest; } return Status; } 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; }
至此我们看完了老式驱动通过 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 就是由那条总线的驱动自动为你创建的。
设备节点树反映的是物理位置布局层次关系,而设备栈反映的则是逻辑堆栈层次关系(设计设备栈的目的 就是用来使驱动开发变得模块化以减轻驱动开发工作,仅此而已), 设备树与设备栈之间没有必然的关系。 如果说设备树是立体的(最上面的根,最下面的是叶),那么设备栈就是水平的(离我们最近的是栈顶,最远的是栈底)。
换言之,有下面的说法
每个设备节点都有自己的设备堆栈,并且位于设备栈底。
设备树中有多少个设备节点,系统中就有多少个设备栈。(各个设备栈之间是独立的,互不关联)
关于设备节点的图形展示,推荐阅读: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; PDEVICE_OBJECT PhysicalDeviceObject; PCM_RESOURCE_LIST ResourceList; PCM_RESOURCE_LIST ResourceListTranslated; UNICODE_STRING InstancePath; UNICODE_STRING ServiceName; PIO_RESOURCE_REQUIREMENTS_LIST ResourceRequirements; ULONG BusNumber; …… } 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, 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) { FullServiceName.MaximumLength = LegacyPrefix.Length + ServiceName1->Length; FullServiceName.Length = 0 ; FullServiceName.Buffer = ExAllocatePool(PagedPool, FullServiceName.MaximumLength); RtlAppendUnicodeStringToString(&FullServiceName, &LegacyPrefix); RtlAppendUnicodeStringToString(&FullServiceName, ServiceName1); Status = PnpRootCreateDevice(&FullServiceName, &PhysicalDeviceObject, &Node->InstancePath); Status = IopCreateDeviceKeyPath(&Node->InstancePath, REG_OPTION_VOLATILE, &InstanceHandle); 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); IopDeviceNodeSetFlag(Node, DNF_STARTED); } Node->PhysicalDeviceObject = PhysicalDeviceObject; (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) { UNICODE_STRING PathSep = RTL_CONSTANT_STRING(L"\\" ); UNICODE_STRING EnumKeyName = RTL_CONSTANT_STRING(L"\\Registry\\Machine\\" REGSTR_PATH_SYSTEMENUM); HANDLE EnumHandle, DeviceKeyHandle = INVALID_HANDLE_VALUE; DeviceExtension = PnpRootDeviceObject->DeviceExtension; _snwprintf(DevicePath, sizeof (DevicePath) / sizeof (WCHAR), L"%s\\%wZ" , REGSTR_KEY_ROOTENUM, ServiceName); Device = ExAllocatePoolWithTag(PagedPool, sizeof (PNPROOT_DEVICE), TAG_PNP_ROOT); RtlZeroMemory(Device, sizeof (PNPROOT_DEVICE)); RtlCreateUnicodeString(&Device->DeviceID, DevicePath); Status = IopOpenRegistryKeyEx(&EnumHandle, NULL , &EnumKeyName, KEY_READ); if (NT_SUCCESS(Status)) { InitializeObjectAttributes(&ObjectAttributes, &Device->DeviceID, OBJ_CASE_INSENSITIVE, EnumHandle, NULL ); 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; Status = RtlQueryRegistryValues(RTL_REGISTRY_HANDLE, (PWSTR)DeviceKeyHandle, QueryTable,NULL ,NULL ); if (!NT_SUCCESS(Status)) { 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) break ; } } _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); 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); } Status = IoCreateDevice( PnpRootDeviceObject->DriverObject, sizeof (PNPROOT_PDO_DEVICE_EXTENSION), NULL ,FILE_DEVICE_CONTROLLER, FILE_AUTOGENERATED_DEVICE_NAME, FALSE,&Device->Pdo); PdoDeviceExtension = (PPNPROOT_PDO_DEVICE_EXTENSION)Device->Pdo->DeviceExtension; RtlZeroMemory(PdoDeviceExtension, sizeof (PNPROOT_PDO_DEVICE_EXTENSION)); PdoDeviceExtension->Common.IsFDO = FALSE; Device->Pdo->Flags |= DO_BUS_ENUMERATED_DEVICE; Device->Pdo->Flags &= ~DO_DEVICE_INITIALIZING; InsertTailList(&DeviceExtension->DeviceListHead, &Device->ListEntry); DeviceExtension->DeviceListCount++; *PhysicalDeviceObject = Device->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, IN ULONG CreateOptions,OUT PHANDLE Handle) { UNICODE_STRING EnumU = RTL_CONSTANT_STRING(ENUM_ROOT); HANDLE hParent = NULL , hKey; *Handle = NULL ; Status = IopOpenRegistryKeyEx(&hParent, NULL , &EnumU, KEY_CREATE_SUB_KEY); Current = KeyName.Buffer = RegistryPath->Buffer; Last = &RegistryPath->Buffer[RegistryPath->Length / sizeof (WCHAR)]; 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); IopQueueTargetDeviceEvent(&GUID_DEVICE_ARRIVAL,&DeviceNode->InstancePath); Stack.Parameters.QueryDeviceRelations.Type = BusRelations; Status = IopInitiatePnpIrp(DeviceObject,&IoStatusBlock, IRP_MN_QUERY_DEVICE_RELATIONS,&Stack); DeviceRelations = (PDEVICE_RELATIONS)IoStatusBlock.Information; for (i = 0 ; i < DeviceRelations->Count; i++) { ChildDeviceObject = DeviceRelations->Objects[i]; ChildDeviceNode = IopGetDeviceNode(ChildDeviceObject); if (!ChildDeviceNode) { 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); Status = IopTraverseDeviceTree(&Context); IopInitDeviceTreeTraverseContext(&Context,DeviceNode,IopActionConfigureChildServices,DeviceNode); 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; }
对每个子设备都会先后执行IopActionInterrogateDeviceStack
、IopActionConfigureChildServices
这两个函数所做的工作如下: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; } 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); 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; } Status = DriverObject->DriverExtension->AddDevice(DriverObject, DeviceNode->PhysicalDeviceObject); if (!NT_SUCCESS(Status)) { IopDeviceNodeSetFlag(DeviceNode, DNF_DISABLED); return Status; } Fdo = IoGetAttachedDeviceReference(DeviceNode->PhysicalDeviceObject); if (Fdo == DeviceNode->PhysicalDeviceObject) … if (Fdo->DeviceType == FILE_DEVICE_ACPI) … IopDeviceNodeSetFlag(DeviceNode, DNF_ADDED); 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); 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)) IopStartDevice2(DeviceObject); 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; if (!(DeviceNode->Flags & DNF_RESOURCE_REPORTED)) { Stack.Parameters.StartDevice.AllocatedResources = DeviceNode->ResourceList; Stack.Parameters.StartDevice.AllocatedResourcesTranslated = DeviceNode->ResourceListTranslated; } 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, 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; 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 = 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 ; CSHORT Size; CSHORT MdlFlags; struct _EPROCESS *Process ; PVOID MappedSystemVa; PVOID StartVa; ULONG ByteCount; ULONG ByteOffset; } 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, IN BOOLEAN ChargeQuota, IN PIRP Irp) { PMDL Mdl = NULL , p; ULONG Flags = 0 ; ULONG Size; if (Length >= 2 GB) return NULL ; Size = ADDRESS_AND_SIZE_TO_SPAN_PAGES(VirtualAddress, Length); if (Size > 23 ) { Size *= sizeof (PFN_NUMBER); Size += sizeof (MDL); if (Size > MAXUSHORT) return NULL ; } Else { Size = (23 * sizeof (PFN_NUMBER)) + sizeof (MDL); Flags |= MDL_ALLOCATED_FIXED_SIZE; Mdl = IopAllocateMdlFromLookaside(LookasideMdlList); } if (!Mdl) { Mdl = ExAllocatePoolWithTag(NonPagedPool, Size, TAG_MDL); if (!Mdl) return NULL ; } MmInitializeMdl(Mdl, VirtualAddress, Length); Mdl->MdlFlags |= Flags; if (Irp) { if (SecondaryBuffer) { p = Irp->MdlAddress; while (p->Next) p = p->Next; p->Next = Mdl; } Else { Irp->MdlAddress = Mdl; } } return Mdl; } #define MmInitializeMdl(_MemoryDescriptorList, \ _BaseVa, \ _Length) \ { \ (_MemoryDescriptorList)->Next = (PMDL) NULL ; \ (_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 结构体后面紧跟的数组中 ,并将这些物理页面锁定在内存,防止被置换出去(注意:如果当时那些虚拟页 面尚未映射到物理内存,这个函数内部还会自动将那些虚拟页面换入物理内存的)
通过IoAllocateMdl
、MmProbeAndLockPages
这两步操作后,指定虚拟内存就被锁定在物理内存了,就完成 了映射的准备工作。接下来用户想要在什么时候把这段虚拟内存对应的那些物理内存映射到系统地址空 间时,就可以使用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); } 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; } 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 api
,socket 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 框架登记、声明自己的协议特征。 一个协议特征记录了协议的名称以及它提供的各个回调函数
后面的还是看书为准吧……不想写了… 记在脑子里才是真的记住了, 写这么多到时候也不想看了 后面的还是看书为准吧……不想写了… 记在脑子里才是真的记住了, 写这么多到时候也不想看了 后面的还是看书为准吧……不想写了… 记在脑子里才是真的记住了, 写这么多到时候也不想看了
国内查看评论需要代理~