进程包含以下内容:
每个进程都有自己的私有虚拟且线性的地址空间
地址空间一开始是空的,然后可执行映像和ntdll.dll首先被映射进内存中,继而是更多的子系统dll
地址空间从0开始(但是第一个64KB大小的地址是不可以被使用的),一直增长最大值(依赖于进程的位数和操作系统的位数)
进程用户地址空间最大值规则如下:
LARGEADDRESSAWARE
标志增加地址空间,最大可扩展到3GBLARGEADDRESSAWARE
标志增加地址空间,最大可扩展到4GB虚拟地址可能实际位于物理内存中,也有可能位于页面文件中。
如果是位于物理内存中,则直接访问数据;
如果位于页面文件中,则cpu或产生一个page fault
异常,内存管理器的页错误异常处理程序将会从页面文件中读取数据并复制到物理内存中,然后让cpu重新访问
内存以页面为单位进行管理,页面的大小由cpu的类型决定。windows系统中默认为4KB(小页内存),也支持2MB(x86/x64/ARM64)和4MB(ARM),又叫大页内存
大页内存的优点:
直接使用页目录入口(Page DIrectory Entry, PDE)进行映射,不适用页表,因此转换速度更快,能更好的利用地址转换缓冲区(Translation Lookaside Buffer,TLB)一个由CPU维护的近期转换页面的缓存。单个TLB入口能比使用小页面映射更多的内存
大页内存的缺点:
大页内存需要在RAM中是连续的,因此当内存紧张或非常碎片化时可能会分配失败。
大页内存始终是非分页的并且只能设置读/写保护
虚拟内存的那每个页面处于如下的三种状态之一:
地址空间的较低部分由进程使用,较高部分由操作系统使用
操作系统地址空间是进程无关的,而用户进程的地址空间是进程相关的,每个进程都有独立的地址空间,其他进程无法访问
操作系统地址都是绝对地址,无论从任何进程中看同一个地址,内容都是一样的。
从用户模式直接访问系统空间地址会触发访问违规Access violation异常
线程位于进程中,使用进程提供的资源来工作(如虚拟内存和内核对象的句柄)
线程包含一下内容:
线程的状态包括:
每个线程都有栈,用于存放局部变量,传递函数参数,以及在调用函数之前存放返回地址
每个线程至少有一个栈位于系统内核空间内,这个栈很小(32位系统默认12KB,64位系统默认24KB)
每个线程还有一个栈位于用户空间中,这个栈比较大(默认最大1MB)
当线程位于运行或就绪状态时,内核栈一直驻留在RAM中。用户模式下的栈可能被页换出,和所有用户模式的内存一样。
用户模式的栈在起始时,只提交一小部分内存(可能只有一个页面),栈地址空间的其余部分作为保留内存,使用一个保护属性PAGE_GUARD
标记已提交页面的下一页(有时会多于一页),指明这是一个警戒页面(guard page)。当线程需要更多的栈时,它会写到警戒页面,此时会产生一个异常被内存管理器处理,此时内存管理器会移除该页上的警戒保护,并提交该页,然后设置下一页的警戒保护
线程用户模式的栈大小按如下方式确定:
CreateThread
及类似函数创建线程时,可以在函数参数中指定线程用户栈的大小应用程序需要进行一些非纯粹计算的操作,如分配内存、打开文件、创建线程等,最终只能由内核中的代码来完成
打开文件系统服务的执行流程:
CreateFile
(位于kernel32.dll中实现)NtCreateFile
(NTDLL.DLL中的一个导出函数),NTDLL.DLL是一个基础DLL,实现了原生API,是位于用户模式的最底层代码。EAX
),然后执行一个特殊的CPU指令(x64下是syscall
,x86下是sysenter
)来实际转换到内核模式,并跳转到一个预定义的被称为系统服务分发器(system service dispatcher)的例程中NtCreateFile
函数,且参数相同用户进程
,基于映像文件的普通进程,在系统中执行子系统DLL
,实现子系统API的动态链接库,子系统包含众所周知的文件,kernel32.dll、user32.dll、gdi32.dll、advapi32.dll等NTDLL.DLL
,系统范围的DLL,实现了Windows的原生API,这是用户模式的底层,主要提供为系统调用提供内核模式转换,还有堆管理、映像加载、部分用户模式线程池功能等服务进程
,普通的Windows进程,它和服务控制管理器(SCM,在services.exe中实现)通信,对它的生命周期进行管理执行体
,执行体位于NtOskrnl.dll(内核本体)的高层,包含了绝大部分的内核代码,其中大部分是管理器:对象管理器、内存管理器、IO管理器、即插即用管理器、电源管理器、配置管理器等内核
,内核层实现了最基础和最时间敏感的内核模式操作系统代码,包括线程调度、中断、异常分发、互斥量、信号量等设备驱动程序
,可装载的内核模块,具备完全的内核能力Win32k.sys
,Windows子系统的内核模式组件,本质上是一个内核模块(驱动程序),处理所有Windows用户界面相关的操作硬件抽象层HAL
,最接近CPU的硬件之上的一个抽象层,使得设备驱动可以通过调用API来工作,而不需要知道硬件细节系统进程
,用于描述那些通常就在那干自己事情的进程,如smss.exe、lsass.exe、winlogon.exe、services.exe等子系统进程
,windows子系统进程运行的映像文件是csrss.exe,视为一个助手进程,帮助内核对windows系统中运行的进程进行管理。每个会话都会有一个csrss.exe的实例在运行Hyper-V虚拟机管理器
,位于windows10及以后的版本里,如果cpu支持vbs的话,vbs提供一个额外的安全层,让实际的机器只是一个Hyper-V控制的虚拟机Windows内核提供多种类型的对象以供用户模式进程、内核本身、驱动程序使用。这些对象由对象管理器(执行体的一部分)在用户模式或内核模式代码请求时创建在内核空间中
对象时引用计数的,只有当对象的最后一个引用被释放之后,对象才会被销毁并从内存中释放
由于这些对象位于内核空间中,用户模式无法直接访问,因此通过一种间接的机制来从用户模式访问操作这些对象,即句柄
句柄是一个表格的入口索引,该表格在进程的基础上维护,每个进程都有一个表格,表格中的每一项都指向内核中的一个对象。通过Create或Open等函数创建和打开内核中的对象,并返回内核对象对应的表格中项的索引
内核代码可以使用句柄或对象的直接指针,可以通过ObReferenceObjectByHandle
函数将句柄转化为对象的直接指针,此时对象的索引将+1,用完后需要使用ObDerefenceObject
将索引-1
句柄的值始终的4的倍数,第一个有效的句柄值是4,0并不是有效的句柄值
ERROR_ALREADY_EXISTS
提供给Create函数的名称并非对象的最终名称,名称的前面会被添加\Sessions\x\BaseNamedObjects\
,其中x是调用者的会话标识符;
如果是0号会话,名称的前面会加上\BaseNamedObjects\
如果调用者在应用容器内,一般是通用windows平台uwp进程,那么加到前面的字符串将更加复杂,包含了唯一的应用容器SID,如\Sessions\x\AppContainerNamedObjects\{AppContainerSID}
对象的名称是相对于会话的,如果一个对象需要在多个会话间共享,可以通过加上前缀Global\
在0号会话中创建它,应用容器没有使用0号会话名字空间的能力
整个名称空间保留在内存中由对象管理器进行管理
查看对象引用技术的正确方法是使用内核调试器的
!trueref
命令
打开测试签名:bcdedit /set testsigning on
,重启生效
安装驱动程序:sc create simdrv binpath= c:\users\admin\desktop\simdrv.sys type= kernel start= demand
运行驱动程序:sc start simdrv
暂停驱动程序:sc pause simdrv
停止驱动程序:sc stop simdrv
卸载驱动程序:sc delete simdrv
#pragma once
#include
#define _LogMsg(lvl, lvlname, frmt, ...) { \
DbgPrintEx( \
DPFLTR_IHVDRIVER_ID, \
lvl, \
"[" lvlname "]" "[irql:%d pid:%-6Iu tid:%-6Iu %s::%-4d] " frmt "\n", \
KeGetCurrentIrql(), \
PsGetCurrentProcessId(), \
PsGetCurrentThreadId(), \
__FILE__, \
__LINE__, \
__VA_ARGS__ \
); \
}
#define DbgError(frmt, ...) _LogMsg(DPFLTR_ERROR_LEVEL, "erro", frmt, __VA_ARGS__)
#define DbgWarning(frmt, ...) _LogMsg(DPFLTR_WARNING_LEVEL, "warn", frmt, __VA_ARGS__)
#define DbgInfo(frmt, ...) _LogMsg(DPFLTR_INFO_LEVEL, "info", frmt, __VA_ARGS__)
#define DbgTrace(frmt, ...) _LogMsg(DPFLTR_TRACE_LEVEL, "trac", frmt, __VA_ARGS__)
#define DbgLog(frmt, ...) _LogMsg(DPFLTR_ERROR_LEVEL, "****", frmt, __VA_ARGS__)
用户模式开发和内核模式开发区别:
内核中常用的C++特性有:
内核中Zw开头的函数是NTDLL.DLL中的原生API的镜像,是从原生API到执行体实现之间的网关
当调用来自于用户模式时,内核中的Nt系列函数会进行合法性检查,调用者的信息会以线程为基础保存在每个线程对应的KTHREAD结构中未公开的PreviousMode
字段里
当调用来自于内核中时,无需进行合法性检查,因此可以调用Zw系列函数,Zw系列函数会将PreviousMode
设置为KernelMode(0)
,然后调用内核中的Nt系列函数,此时Nt系列函数检查PreviousMode
发现本次调用来自于内核,将跳过安全检查
多数内核API会返回一个状态码来指示操作成功或失败,类型是NTSTATUS
,一个32位的整数
可以通过NT_SUCCESS
宏来检查状态码的最高位,其代表着成功与否
某些情况下,从函数返回的NTSTATUS值最终会返回到用户模式,用户模式通过GetLastError得到转换后的值,内核中的状态码和用户层的错误值并非对应的
内核中常用两种字符串类型:wchar_t *
或UNICODE_STRING
通常使用一组Rtl系列的函数来操作UNICODE_STRING
内核中也实现了常用的C运行时库的处理函数,wcscpy、wcscat、wcslen、wcscpy_s、wcschr、strcpy、strcpy_s等
内核中的栈非常小,任何大块的内存都必须动态分配
内核提供了两个通用的内存池以供使用
驱动程序应当尽可能少使用非分页池,除非必须,其他情况都应使用分页池
只有三种类型的内存可以被驱动程序分配使用:
常用内存分配函数:
系统中所有进程使用EPROCESS结构进行管理,这些结构使用一个环形双向链表连接在一起,其中链表的头部存储在内核变量PsActiveProcessHead
中,每个进程的EPROCESS.ActiveProcessLinks
字段就是一个LIST_ENTRY
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY;
通过CONTAINING_RECORD
宏可以正确计算出包含LIST_ENTRY字段的结构
常用链表处理函数:
DRIVER_OBJECT结构在驱动加载时,由内核分配,并进行部分初始化,然后传递给DriverEntry函数进一步初始化
DRIVER_OBJECT的MajorFunction
字段是一个指针数组,指明了驱动程序支持那些操作
起初MajorFunction数组会被内核初始化成指向内核的内部例程IopInvalidDeviceRequest
,它会给调用者返回一个错误的状态,表明不支持该操作
驱动程序无法直接和应用程序通信,只能通过驱动程序创建出的设备对象来进行通信
通过CreateFile
可以打开一个符号链接,也即内核对象,并返回一个对象的句柄或指针
所有能够在用户模式直接打开的符号链接都位于对象管理器中名为??
的目录下
常用的如C:、Aux、Con等都是合法的符号链接,可以被直接打开
其他一些名字很长很复杂的符号链接,是基于硬件的驱动程序调用IoRegisterDeviceInterface
后由IO系统自动生成的
??
目录中的多数符号链接都指向Device目录下的内部设备名称,用户模式不能直接访问这些设备,但内核中可以通过IoGetDeviceObjectPointer
来访问,用户模式只能通过符号链接来访问这些设备
举例:
Process Explorer会生成一个\Device\PROCEXP152
的设备,符号链接名为PROCEXP152
。当用户程序想要打开时,必须添加\\.\
前缀,否则会被当作当前目录下的文件来对待。
HANDLE hDevice = CreateFile(L"\\\\.\\PROCEXP152", GENERIC_WRITE| GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
创建设备:
驱动程序使用IoCreateDevice
函数创建设备对象,该函数会分配并初始化一个DEVICE_OBJECT结构,并返回指针
一个驱动所包含的设备对象的实例位于DRIVER_OBJECT结构的DeviceObject字段中,DEVICE_OBJECT的NextDevice字段指向下一个设备对象,形成了一个链表
新创建的设备对象是从头部插入链表的,因此第一个创建的设备对象保存在链表的最后,它的NextDevice为NULL
用户模式线程的最终优先级 = 线程所在进程的优先级类别(SetPriorityClass
) + 该线程的优先级偏移值(SetThreadPriority
)
内核模式下可以调用KeSetPriorityThread
直接设置线程的优先级,而没有优先级类别的限制
在应用层只有一小部分的优先级能够被直接设置,但是在内核层中可以绕开这些限制,直接将线程设置为任意的优先级
DeviceIoControl用于和内核设备通信
控制码必须使用CTL_CODE宏来定义
#define CTL_CODE( DeviceType, Function, Method, Access ) ( \
((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \
)
DeviceType
,标识设备的类型,FILE_DEVICE_XXX的常数,微软规定第三方的值必须从0x8000开始Function
,一个数字码,用于区分不同的操作,微软规定第三方驱动需要从0x800开始method
,输入输出缓冲区的传递方式Access
,指明这个操作是读还是写,FILE_WRITE_ACCESS、FILE_READ_ACCESS、FILE_ANY_ACCESS创建设备需要调用IoCreateDevice
这个API
如果IoCreateDeive调用成功,那么将从非分页内存池中分配DEVICE_OBJECT结构
IRP是一个半文档化的结构,用来表示一个请求
IRP通常来自于执行体中的管理器:IO管理器、即插即用管理器、电源管理器(对于软件驱动,基本上都来自于IO管理器)
IRP通常会包括一个或多个IO_STACK_LOCATION
结构,用于代表设备栈中的每一个设备实例,可通过IoGetCurrentIrpStackLocation
获得
IoCompleteRequest函数会完成IRP,将IRP传送会它的创建者,第二个参数是驱动程序提供给客户程序的优先级临时提升数值,一般为0
IRP例程中需要返回一个值,该值和放到IRP中的值一样
IRQL是处理器的一个属性
示例:由磁盘启动器执行的IO操作,在操作执行完成后,磁盘驱动器会通过请求中断来通知操作已经完成。此中断连接到中断控制器硬件,然后将请求发送到处理器进行处理。哪个线程该执行相关的中断服务例程
每个硬件中断都与一个优先级相联系,即IRQL,由HAL确定
每个处理器的上下文都有自己的IRQL,可以看作寄存器对待
IRQL的执行规则是,cpu始终执行最高级IRQL代码,比如原本cpu的IRQL级别是0,此时一个IRQL是5的中断进来,那么cpu就在当前线程的内核栈里保存其目标状态(上下文),然后将自己的IRQL上升到5并执行与中断相关的中断服务例程ISR。在ISR完成之后,将自己的IRQL降低到原来的级别,从栈中恢复之前的上下文,并执行
重点:ISR由中断发生时,处理器上正在运行的任一线程处理;因此当IRQL>=2时,cpu无法进行上下文切换,即无法切换线程
重要IRQL描述如下:
windbg调试时,可以通过
!irql
命令,查看当前IRQL,可以指定一个CPU号码,显示该CPU的IRQL
windbg调试时,可以通过!idt
调试命令,查看系统中已注册的中断和其相关联的ISR
内核模式下,KeRaiseIrql提升,KeLowerIrql降低
KIRQL oldIrql;
KeRaiseIrql(DISPATCH_LEVEL, &oldIrql);
NT_ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL);
//do something
KeLowerIrql(oldIrql);
如果提升了IRQL,请确保在同一个函数中将它降低
DPC是一个对象,内部封装了一个函数,该函数会在IRQL在DISPATCH_LEVEL上调用
DPC由来:举例ReadFile
由于IRS此时运行在设备IRQL上,无法执行IoCompleteRequest(IRQL<= DISPATCH_LEVEL),因此引入延迟过程调用DPC机制来完成IRP
如果直接在ISR中将当前IRQL降低到DISPATCH_LEVEL,然后调用IoCompleteRequest,再将IRQL提升到原先的值,这样会引起死锁
由于DPC执行在DISPATCH_LEVEL上,因此不会进行线程切换、访问分页内存等
DPC可以通过某些方式控制,参阅KeSetImportanceDpc和KeSetTargetProcessorDpc
内核时钟(KTIMER)允许被设置成未来的某个时间到期,可以通过KeWaitForSingleObject来等待,或者通过DPC作为回调在始终到期时执行
APC和DPC一样,都是封装了回调函数的数据结构
DPC和调用的线程无关,而APC和调用的线程相关,只有那个线程才能调用APC关联的回调函数
每个线程都有一个相关联的APC队列
APC共有三种类型:
APC的API在内核模式下是未公开的,因此驱动程序一般不会直接使用APC
进入关键区:KeEnterCriticalRegion
离开关键区:KeLeaveCriticalRegion
关键区会阻止执行用户模式和普通内核APC(特殊内核APC除外),内核中有些函数需要位于关键区中执行,特别是执行体资源
进入警戒区:KeEnterGuardedRegion
离开警戒区:KeLeaveGuardedRegion
将IRQL提升到APC_LEVEL将禁止所有APC的发送
异常是一种事件和中断类似,区别是异常是同步的,中断异步的
异常包括:除零、断点、页错误、栈溢出、非法指令等
内核中的异常处理程序是基于中断分配表(IDT)进行调用的,IDT中还保存了从中断向量到ISR的映射关系
合法的关键字组合是:__try__/__except__
和__try__/__finally__
将EXCEPTION_EXECUTE_HANDLER
放到__except__中表示任何异常都会被处理,还可以调用GetExceptionCode
来检查实际发生的异常,进而有选择的处理,遇到不想处理的异常,可以让内核继续在调用栈上寻找别的处理程序
//使用__try和_except组合
__try
{
// do something
}
_except (GetException() == STATUS_ACCESS_VIOLATION
? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
}
//使用__try和__finally组合
void foo()
{
void *p = ExAllocatePool(PagedPool, 1024);
__try {
//do something
}
__finally {
ExFreePool(p);
}
}
SEH还提供了ExRaiseStatus
和ExRaiseAccessViolation
这样的函数来抛出一个异常
template<typename T = void>
struct kunique_ptr {
kunique_ptr(T* p = nullptr) : _p(p) {}
//remove copy ctor and copy = (single owner)
kunique_ptr(const kunique_ptr&) = delete;
kunique_ptr& operator=(const kunique_ptr&) = delete;
//allow ownership transfer
kunique_ptr(kunique_ptr&& other) : _p(other._p)
{
other._p = nullptr;
}
kunique_ptr& operator=(kunique_ptr&& other)
{
if (&other != this)
{
Release();
_p = other._p;
other._p = nullptr;
}
return *this;
}
~kunique_ptr()
{
Release();
}
operator bool() const
{
return _p != nullptr;
}
T* operator->() const
{
return _p;
}
T& operator*() const
{
return *_p;
}
void Release() {
if (_p)
{
ExFreePool(_p);
}
}
private:
T* _p;
};
函数 | 描述 |
---|---|
InterlockedIncrement InterlockedIncrement16 InterlockedIncrement64 | 对32/16/64位的整数原子化加一 |
InterlockedDecrement InterlockedDecrement16 InterlockedDecrement64 | 对32/16/64位的整数原子化减一 |
InterlockedAdd InterlockedAdd64 | 原子化将一个32/64位整数加到一个变量上 |
InterlockedExchange InterlockedExchange8 InterlockedExchange16 InterlockedExchange64 | 原子化的交换两个32/8/16/64位整数 |
InterlockedCompareExchange InterlockedCompareExchange64 InterlockedCompareExchange128 | 原子化比较一个变量和一个值 |
InterlockedCompareExchange
函数家族再无锁编程中使用
NTSTATUS KeWaitForSingleObject
(
PVOID Object,
KWAIT_REASON WaitReason,
KPROCESSOR_MODE WaitMode,
BOOLEAN Alertable,
PLARGE_INTEGER Timeout
);
NTSTATUS KeWaitForMultipleObjects
(
ULONG Count,
PVOID Object[],
WaitType,
KWAIT_REASON WaitReason,
KPROCESSOR_MODE WaitMode,
BOOLEAN Alertable,
PLARGE_INTEGER Timeout,
PKWAIT_BLOCK WaitBlockArray
);
object
,要等待的对象WaitReason
,等待的原因,驱动程序通常设置为Executive,如果此等待将由用户模式触发,设为UserRequestWaitMode
,UserMode或KernelMode,驱动程序一般设置为KernelModeAlertable
,等待过程中,线程是否处于警戒状态。警戒状态下允许传递APC,如果WaitMode是UserMode还允许传递用户模式APC,驱动程序一般设置为FALSETimeout
,等待超时时间,参数单位是100纳秒,负值表示相对时间值,正值表示从1601年1月1日午夜起计算的绝对时间值。如果为NULL,则一直等待下去Count
,等待对象的数目Object[]
,要等待的对象指针数组WaitType
,指明要等待所有对象编程有信号(waitAll),还是只要一个对象有信号就行(waitAny)WaitBlockArray
,结构数组,用于等待操作的内部管理,如果等待对象的数目<=THREAD_WAIT_OBJECTS(目前是3),那么这个参数是可选的,内核会使用每个线程里内建的数组,如果等待对象数目多于这个数,驱动程序就必须从非分页池中分配正确大小的结构,并在等待结束之后释放它们KeWaitForSingleObject返回值:
返回值 | 描述 |
---|---|
STATUS_SUCCESS | 等待完成,等待对象变为有信号 |
STATUS_TIMEOUT | 等待完成,超时时间已到 |
KeWaitForMultipleObject返回值:
如果指定WaitAll,
返回值 | 描述 |
---|---|
STATUS_SUCCESS | 等待完成,所有等待对象都变为有信号 |
STATUS_TIMEOUT | 等待完成,超时时间已到 |
如果指定WaitAny,
返回值 | 描述 |
---|---|
索引值 | 其中一个对象变为有信号了,该对象在对象数组中的索引值 |
STATUS_TIMEOUT | 等待完成,超时时间已到 |
常见可等待对象,以及有信号和无信号的含义
互斥量在自由时是有信号态,一旦某个线程调用等待函数等待互斥量成功,互斥量变为无信号状态,该线程成为互斥量的拥有者
使用方法:
KeInitializeMutex
初始化互斥量KeWaitForSingleObject
或KeWaitForMultipleObjects
去等待这个互斥量KeReleaseMutex
释放该互斥量如果一个线程没有释放互斥量就死亡了,此时内核会显式的释放这个互斥量,然后下一个企图获取该互斥量的线程会在等待函数中接收到
STATUS_ABANDONED
返回值
快速互斥量是传统互斥量的一个替代,提供了更好的性能
快速互斥量并不是一个可等待对象,它有自己的获取和释放的API
多数需要使用互斥量的程序都应当使用快速互斥量,除非有必要的理由去使用正式互斥量
使用方法:
ExInitializeFastMutex
初始化快速互斥量ExAcquireFastMutex
或ExAcquireFastMutexUnsafe
来获取快速互斥量ExReleaseFastMutex
或ExReleaseFastMutexUnsafe
释放该快速互斥量使用方法:
KeInitializeSemaphore
初始化信号量,设置一个最大值和初始值(通常是最大值),内部值大于0时,信号量处于有信号KeWaitForSingleObject
来等待信号量,当有信号时,将结束等待,同时信号量值减一一个最大值为1的信号量是否相当于互斥量?区别在于信号量没有所有权,一个线程获得信号量可以被另一个线程释放
事件封装了一个布尔值的标志,要么真(有信号)要么假(无信号)
事件有两种类型:
使用方法:
KeInitializeEvent
进行初始化KeSetEvent
触发事件KeResetEvent
或KeClearEvent
(速度更快,因为无需返回之前的状态)重置该事件为无信号执行体资源用于单写多读的场景,是一种特殊对象,不属于可等待对象
使用方法:
ERESOURCE
结构ExInitializeResourceLite
初始化该执行体资源ExAcquireResourceExclusiveLite
获取排他锁(用于写)ExAcquireResourceSharedLite
获取共享锁(用于读)ExReleaseResourceList
释放锁ExDeleteResourceLite
释放执行体资源调用获取和释放锁的先决条件是,必须禁止通常的内核APC,可以在获取锁前调用
KeEnterCriticalRegion
,以及释放锁后调用KeLeaveCriticalRegion
ERESOURCE resource;
void WriteData()
{
KeEnterCriticalRegion();
ExAcquireResourceExclusiveLite(&resource, TRUE);
//do something
ExReleaseResourceLite(&resource);
KeLeaveCriticalRegion();
}
为了简化上述代码,
对于排他锁提供ExEnterCriticalRegionAndAcquireResourceExclusive
函数,进入关键段然后获得排他锁
对于共享锁提供ExEnterCriticalRegionAndAcquireResourceShared
函数,进入关键段然后获得共享锁
提供 ExReleaseResourceAndLeaveCriticalRegion
释放排他锁或共享锁,然后离开关键段
在IRQL>=DISPATCH_LEVEL时,线程不可进行等待,无法调用等待函数
由于此时线程调度器无法工作,因此不存在线程之间的资源冲突,而是存在处理器当前正在运行的线程之间的资源冲突
自旋锁是内存中的一个简单位,用于处理器之间的同步
当CPU想要获取自旋锁而它当前并不自由时,CPU会一直在自旋锁上自旋,即一直进行while空循环,直到自旋锁被另一个CPU释放变成自由状态
自旋锁必须只能在IRQL >= DISPATCH_LEVEL上获取和释放
使用方法:
KeInitializeSpinLock
初始化自旋锁,将自旋锁置于自由状态KeAcquireSpinLock
:获取自旋锁并将IRQL提升至DISPATCH_LEVEL取消自旋锁:内核在调用驱动程序注册的取消例程之前,会先获得这个自旋锁,这是驱动程序释放一个不是由它主动显式申请的自旋锁的唯一情形
分离耗时较长业务到单独的线程中可用:PsCreateSystemThread
和IoCreateSystemThread
(windows8及以上可用)
分离耗时较短业务到内核提供的线程池:使用工作项目
工作项目:在系统线程池中排队的函数。驱动程序可以分配和初始化工作项目,使其指向驱动程序希望执行的函数,然后让工作项目在线程池中排队
工作项目类似于DPC,但工作项目总是在IRQL_PASSIVE_LEVEL上执行,这常被用来在IRQL==DISPATCH_LEVEL上执行PASSIVE_LEVEL的操作
创建工作项目两种方式:
IoAllocateWorkItem
分配和初始化工作项目。返回一个指向IO_WORKITEM
的指针,用完后调用IoFreeWOrkItem
释放它IoSizeofWorkItem
提供的大小动态分配一个IO_WORKITEM,然后调用IoInitializeWorkItem
初始化,在用完工作项目之后,调用IoUninitializeWorkItem
这两个函数都接收设备对象作为参数,所以需要确保工作项目在排队或执行时,驱动程序没有卸载
将工作项目进行排队:IoQueueWorkItem
void IoQueueWorkItem(
[in] __drv_aliasesMem PIO_WORKITEM IoWorkItem, //the work item
[in] PIO_WORKITEM_ROUTINE WorkerRoutine, //the function to be called
[in] WORK_QUEUE_TYPE QueueType, //queue type
[in, optional] __drv_aliasesMem PVOID Context //driver-defined value
);
//驱动程序提供的回调函数WorkerRoutine原型如下:
IO_WORKITEM_ROUTINE IoWorkitemRoutine;
void IoWorkitemRoutine(
[in] PDEVICE_OBJECT DeviceObject,
[in, optional] PVOID Context
)
{...}
系统线程池由多个队列,基于服务这些工作项目的线程优先级进行区分,级别如下:
CriticalWorkQueue
,线程优先级 13
DelayedWorkQueue
,线程优先级 12
HyperCriticalWorkQueue
,线程优先级 15
NormalWorkQueue
,线程优先级 8
BackgroundWorkQueue
,线程优先级 7
RealTimeWorkQueue
,线程优先级 18
SuperCriticalWorkQueue
,线程优先级 14
MaximumWorkQueue
CustomPriorityWorkQueue
虽然文档中指明必须使用
DelayedWorkQueue
,但是实际上任何支持的级别都可以使用
另一个用于将工作项目排队的函数
IoQueueWorkItemEx
,使用一个不同的回调函数,多了一个参数即工作项本身。用于在工作项目函数需要退出之前释放自身
IRP由执行体中IO管理器、即插即用管理器、电源管理器之一从非分页池中分配,也可由驱动程序分配
IO_STACK_LOCATION
一起分配IO_STACK_LOCATION
需要跟IRP一起分配IO_STACK_LOCATION
在内存中紧跟IRP后面,且每一个IO_STACK_LOCATION
对应设备栈中的一个设备对象IoGetCurrentIrpStackLocation
函数获得属于当前驱动使用的IO_STACK_LOCATION
Window的IO系统以设备为中心,而非以驱动为中心,一个驱动中可以创建多个设备
设备对象被分为三类:
举例PCI网卡驱动安装事件序列如下:
HKLM\System\CurrentControlSet\Enum\PCI\(硬件ID)
,如果该PDO的驱动程序已经装载过了,就会在这个位置注册,pnp管理器就直接装载已经在这个注册表里注册好的驱动程序IoAttachDeviceToDeviceStack
的调用,将自己置于PDO之上如果过滤驱动程序在注册表中注册表了,那么也会被一起装载
过滤器会在两个地方搜索:
HKLM\System\CurrentControlSet\Enum\PCI\(硬件ID)
HKLM\System\CurrentControlSet\ControlClasses\{ClassGuid值}
,硬件ID对应的类别,值名称为LowerFilters
和UpperFilters
IRP由执行体中的管理器创建,管理器只初始化主IRP结构和第一个IO栈的位置,然后就把IRP传递给设备栈的最上层设备
包含设备栈最上层设备的驱动程序在相应的分发例程中收到这个IRP,调用分发例程,分发例程中处理IRP
由于IO管理器只初始化第一个IO栈位置,因此每个接收到IRP的设备都需要给下一层设备初始IO栈位置
处理IRP的方式:
IoCopyIrpStackLocationNext
来复制当前的IO栈位置到下一层IoSkipCurrentIrpStackLocation
将IRP内部指向当前IO栈位置的指针减一,后续调用IoCallDriver
时这个指针又会加一,从而下一层设备就能看到和当前设备看到一样的IO栈位置了。
IoSkipCurrentIrpStackLocation
以确保下一个设备能看到和这个设备一样的信息,必须能看到同一个IO栈位置IoCallDriver
传递IRP给下层设备对象(驱动调用IoAttachDeviceToDeviceStack
时获得)IoCompleteRequest
来结束此IRP,任何下层的设备都不会看到这个请求IoSetCompletion
设置一个IO完成例程IoMarkIrpPending
将此IRP标记挂起STATUS_PENDING
IoCompleteRequest
来完成此IRP一旦某层调用IoCompleteRequest
,该IRP被完成,IRP将朝着发起IRP的方向进行依次传送。如果注册了完成例程,也将会按照先注册的后调用的顺序被逐个调用。
IRP结构解析
IoStatus
:包含IRP的Status(NT_STATUS)和一个Information字段(意义取决于IRP类型,对于读和写是操作中的字节数)UserBuffer
:包含原始的缓冲区指针,指向相关IRP的用户缓冲区UserEvent
:一个事件对象,如果客户程序调用是异步的,那么客户程序会提供这个事件对象AssociatedIrp
:联合体
Cancel Routine
:取消例程,如果操作被要求取消,如用户模式调用CancelIo
和CancelIoEx
,取消例程会被调用MdlAddress
:指向一个可选的内存描述符列表(MDL),一种内核数据结构,用于描述RAM中的缓冲区,主要用于直接IOIRP中的IO_STACK_LOCATION结构解析
MajorFunction
:IRP的主功能代码(如IRP_MJ_CREATE、IRP_MJ_READ等)MinorFunction
:部分IRP有次功能代码(如IRP_MJ_PNP、IRP_MJ_POWER、IRP_MJ_SYSTEM_CONTROL等)FileObject
:此IRP相关联的FILE_OBJECTDeviceObject
:此IRP相关联的设备对象,分发例程会收到一个指向此设备对象的指针,因此一般无需访问这个字段CompletionRoutine
:完成例程,由上一层设备对象调用IoSetCompletionRoutine
所设置Context
:传递给完成例程的参数Parameters
:联合体,不同的IRP使用特定的某个操作(如IRP_MJ_READ中Parameters.Read结构表示读操作的更多信息)使用
IoGetCurrentIrpStackLocation
取得当前的IO_STACK_LOCATION
调试IRP:可以使用!irpfind
命令检索当前非分页池中的IRP,使用!irp
解析单个irp
分发例程就是通过IRP主功能号连接起来的函数,DRVIER_OBJECT结构中的majorFunction字段
分发例程的原型如下:
typedef NTSTATUS DRIVER_DISPATCH
{
_In_ PDEVICE_OBJECT DeviceObject,
_Inout_ PIRP Irp
};
一旦驱动程序决定处理IRP,意味着必须最终完成它
完成请求指在填写完请求状态和其他信息后调用IoCompleteRequest
示例代码:
NTSTATUS MyDispatchRoutine(PDEVICE_OBJECT devObj, PIRP Irp)
{
Irp->IoStatus.Status = STATUS_XXX;
Irp->IoStatus.Information = NumberOfBytesTransfered;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_XXX;
}
IoCompleteRequest
后,Irp可能就已经被释放掉了,所以在完成Irp之后,不要再去使用Irp了(如return Irp->Iostatus.Status
)通常分发例程在IRQL==0和请求线程的上下文中被调用,这意味可以轻松直接访问用户提供的缓冲区,因为IRQL==0所以可以忽略页错误,并且由于时请求线程上下文,因此用户模式传递的指针在此处理过程中有效。
但是在一些不便利的情况下,将无法直接访问用户缓冲区,需要用到缓冲IO和直接IO
为了支持缓冲IO的读写操作,设备对象必须设置一个标志:DeviceObject->Flags |= DO_BUFFER_IO;
缓冲IO举例,当一个读或写操作到达IO管理器和驱动程序的步骤:
IoCompleteRequest
),(对于读请求)IO管理器就把系统缓冲区复制回用户缓冲区中,复制的大小由Irp的IoStatus.Information字段决定,由驱动程序设置IO管理器是怎样在IoCompleteRequest中将系统缓冲区复制到用户缓冲区中的?
答:通过将一个特殊内核APC排队到最初发出请求的线程中来实现,一旦此线程获得CPU的执行权,第一件事就是执行这个APC,APC中执行复制操作
缓冲IO的特点:
直接IO的目的是允许在任何IRQL和线程中访问用户缓冲区,但是不需要在前后进行复制
为了支持直接IO的操作,设备对象必须设置一个标志:DeviceObject->Flags |= DO_DIRECT_IO
直接IO举例,当一个读或写操作到达IO管理器和驱动程序的步骤:
MmProbeAndLockPages
将缓冲区锁定在内存中,因此在另行通知之前它不会被换出。这就解决了缓冲区访问的问题之一,不能发生页错误MmGetSystemAddressForMdlSafe
将MDL映射到系统空间内,映射完毕后就能直接读取系统空间地址所描述的内存了MmUnlockPages
解锁,因此它就能和其他用户模式内存一样被正常的换出了。可以多次调用MmGetSystemAddressForMdlSafe。MDL保存有一个标志,用来指示系统映射是否已经被执行过了。如果是,它仅仅返回已经存在的指针
在设备对象的标志中既没有设置DO_BUFFERED_IO
也没有设置DO_DIRECT_IO
的驱动程序使用无I/O(Neither I/O)方式,这单纯表示驱动程序不会从I/O管理器得到任何帮助,怎么处理用户缓冲区完全取决于驱动程序自身。
用户模式下DeviceIoControl函数原型:
BOOL DeviceIoControl(
[in] HANDLE hDevice, //设备或文件句柄
[in] DWORD dwIoControlCode, //IOCTL code
[in, optional] LPVOID lpInBuffer, //input Buffer
[in] DWORD nInBufferSize, //size of input buffer
[out, optional] LPVOID lpOutBuffer, //output Buffer
[in] DWORD nOutBufferSize, //size of output Buffer
[out, optional] LPDWORD lpBytesReturned, //count of bytes actually returned
[in, out, optional] LPOVERLAPPED lpOverlapped //for async, operation
);
访问输入输出缓冲区的方式取决于控制码
#define CTL_CODE( DeviceType, Function, Method, Access ) ( \
((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \
)
DeviceType
,标识设备的类型,FILE_DEVICE_XXX的常数,微软规定第三方的值必须从0x8000开始Function
,一个数字码,用于区分不同的操作,微软规定第三方驱动需要从0x800开始method
,输入输出缓冲区的传递方式Access
,指明这个操作是读还是写,FILE_WRITE_ACCESS、FILE_READ_ACCESS、FILE_ANY_ACCESS其中method描述了如何访问输入输出缓冲区:
Parameters.DeviceIoControl.Type3InputBuffer
字段中UserBuffer
字段中IoStatus.Information
字段指出AssociatedIrp.SystemBuffer
Method | 输入缓冲区 | 输出缓冲区 |
---|---|---|
METHOD_NEITHER | 无 | 无 |
METHOD_BUFFERED | 缓冲 | 缓冲 |
METHOD_IN_DIRECT | 缓冲 | 直接 |
METHOD_OUT_DIRECT | 缓冲 | 直接 |