Windows内核开发-6-内核机制 Kernel Mechanisms
一部分Windows的内核机制对于驱动开发很有帮助,还有一部分对于内核理解和调试也很有帮助。
Interrupt Request Level | 中断请求级别 |
---|---|
Deferred Procedure Calls(DPC) | 延迟调用 |
Asynchronous Procedure Calls(APC) | 异步调用 |
Structured Exception Handling | 异常处理 |
System Crash | 系统崩溃 |
Thread Synchronization | 线程同步 |
High IRQL Synchronization | 高级IRQL(中断请求级别)同步 |
Work Items |
Interrupt Request Level(IRQL) 中断请求级别
ISR:当要处理的线程多于了可用处理器的数量时,会考虑到线程的优先级。同时硬件设备需要去通知系统来让系统进程调度。比如:由磁盘驱动器执行的I/O操作,操作完后,磁盘驱动器会通过请求中断来通知系统操作已经完成。然后该请求中断连接到中断控制器硬件设备,然后把请求发送到处理器进行处理。有一个问题是,哪一共线程来执行中断服务程序(ISR Interrupt Service Routine)呢?
每个物理硬件中断都有一个优先级,叫做IRQL(Interrupt Request Level)中断请求等级。由HAL(硬件抽象层,Windows内核中的一个东西)来决定IRQL为多少。每个处理器以及处理器的上下文都有自己的IRQL对应就像寄存器一样,每条指令都有寄存器的值对应。可以像对待CPU的寄存器一样来对待IRQL。
对于IRQL来说基本规则就是:处理器会执行IRQL级别高的对应的代码。例如:当前处理器的IRQL为0,这时有一个IRQL为5的中断关联进来,处理器就会在当前线程的内核堆栈中保存状态,然后将处理器的IRQL提升为5,然后执行中断服务程序(ISR Interrupt Service Routine)和执行中断相关的代码。一旦执行结束,IRQL就会回到原来的环境。另一方面如果在中断的IRQL==5的时候又有新中断来了也是一样的,先判断IRQL的大小,如果大就调用新中断如果小就等待。
发生中断时的操作
中断嵌套
对于以上两张图,有一个很明显的情况,就是所有的ISR(Interrupt Service Routine中断服务程序)都在首先被中断的线程中完成的。Windows没有专门的线程来处理中断而是由当前在中断处理器上运行的线程来处理。
当User态的代码执行时,IRQL总是等于0,所以在用户态开发的时候经常也没文档记录这个IRQL这个东西。大部分的内核代码也是伴随IRQL==0来运行但是在内核kernel态时可以提高IRQL。
一些比较重要的IRQL:
IRQL | 作用 |
---|---|
PASSIVE_LEVEL in WDK (0) | 这是最常用的IRQL,用户态的代码的IRQL就一直是一个值,由线程调度来进行工作。 |
APC_LEVEL (1) | 专门用于内核的APC,线程调度正常。 |
DISPATCH_LEVEL (2) | 分发派遣层。DPC和更低的中断被屏蔽,不能访问分页内存,因为缺页中断也是在这个层。线程调度器也在此层,调度时只考虑优先级。因此APC_LEVEL上的线程被阻塞后,可以调度执行PASSIVE_LEVEL线程。 |
Device IRQL | 用于硬件中断的一系列级别(x64/ARM/ARM64 上为 3 到 11,x86 上为 3 到 26).来自 IRQL 2 的所有规则也适用于此。 |
Highest level (HIGH_LEVEL) | 这是最高的 IRQL,屏蔽所有中断。 被一些人使用 处理链表操作的 API。 实际值15(x64/ARM/ARM64) 和 31 (x86)。 |
当处理器的IRQL提升到二及二以上时,执行代码就会有很多限制:
1:访问不存在物理内存的内存会导致系统崩溃,这意味着从非分页池访问数据总是安全的,而从分页池或用户提供的缓冲区访问数据是不安全的,应该避免。
2: 等待任何调度程序内核对象(例如互斥锁或事件)会导致系统崩溃,除非等待超时为零,这仍然是允许的。
产生限制的原因:由于调度程序是在IRQL(2)上运行,因此如果处理器的IRQL大于等于2那么就无法在处理器上运行因此就不会发生上下线程环境切换(用该CPU上的另一个线程替换该线程)。只有更高级别的中断才能临时将代码转移到关联的ISR,但是它仍然是同一个线程。
TIPS:在WinDbg中使用!irql可以查看当前处理器的IRQL,也可以查看指定处理器的IRQL。还可以在WinDbg中使用 !idt debugger 命令查看系统上注册的中断。
Raising and Lowering IRQL 提高和降低IRQL
在用户态是不能修改IRQL的,只有内核态可以。IRQL可以被KeRaiseIrql函数提升和被KeLowerIrql函数降低。这里提供一个代码片段来方便理解:
//假设当前IRQL <=DISPATCH_LEVEL 也就是IRQL(2)
KIRQL oldIrql;//KIRQL是对UCHAR的一种typedef重命名
KeRaiseIrql(DISPATCH_LEVEL,&oldIrql);
?
NT_ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL);
?
KeLowerIrql(oldIrql);
如果提高了IRQL,请确保在相同的函数中降低它,从函数中只提升了原来的却不降低是非常危险的。用了KeRaiseIrql来提高请务必用KeLowerIrql来降低
Thread Priorities vs. IRQLs 线程优先级和IRQL的异同
IRQL是处理器的一个属性,线程优先级是线程的一个属性,线程优先级只有在IRQL<2时才有意义。但是也不能一直在IRQL>=2的状态下,不然用户态的代码无法运行。
任务管理器用一个叫做System interrupt的伪进程来描述CPU在IRQL>=2的情况下花费的时候,而Process Explorer用interrupt来描述:
Deferred Procedure Calls(DPC)延迟调用
该图显示了客户端调用I/O操作时的经典表达:User下的线程打开文件句柄,然后调用ReadFile来读内容。由于线程可以异步调用,它几乎马上就可以重新获得控制权并可以做其他工作。收到ReadFile的读取请求的驱动程序会调用文件系统驱动程序(例如 NTFS),它可能会一直往下调用知道磁盘驱动程序,最后磁盘驱动程序对磁盘进行操作。
当硬件完成读操作时,会发出一个中断。该中断会引起与之关联的中断服务程序(ISR Interrupt Service Routine)在设备的IRQL处执行。在该设备IRQL处执行。一个经典的ISR访问设备硬件的获取的结果应该是初始请求想要的结构。
完成一个请求通常是通过调用IoCompleteRequest函数来完成的,但是该函数的文档说只能在IRQL<=DISPATCH_LEVEL(2)时才能有用。
允许ISR调用IoCompleteRequest(和类似的函数)的机制被称为DPC(Derferred Procedure Call)
注册ISR的驱动程序通过从非分页池内存中分配KDPC结构体,并用KeInitializeDpc来初始化给后面DPC做调用准备。当ISR被调用时,就在快要退出函数时,ISR通过KeInsertQueueDpc函数来对其进行排队来请求DPC尽快执行,当DPC函数执行时,就会调用IoCompleteRequest函数了。这是一种调用DPC的折中方案。它在IRQL=DISPATCH_LEVEL状态上运行,这表示它也不能进行调度和访问分页内存。但是也不英雄。
每一个处理器都有自己的DPC队列,在默认的情况下KeInsertQueueDpc函数将DPC插入当前处理器的DPC队列里。当ISR(interrupt service routine中断服务程序)调用完成即将返回前,在降低IRQL等级为之前的等级时,会检测处理器的队列里面是否还有PDC,如果有处理器降低IRQL等级为DISPATH_LEVEL(2)然后以先进先出(队列的方式)来处理队列里的DPC直到队列为空。处理器的IRQL等级才降为0,并回复中断时的环境。
也可以自己定制DPC:通过这两个函数KeSetImportantceDpc KeSetTargetProcessorDpc.
Using DPC with a Timer 使用带定时器的DPC
DPC最初是为了给ISR使用而创建的,但是也有别的机制。DPC可以和计时器绑定一起使用。
KTIMER结构体表示内核定时器(kernel timer)允许通过相对或者绝对时间来设置一个定时器。定时器(timer)是一个调度对象,可以用KeWaitForSingleObject等函数来等待,但是不太方便。更简单常用的办法是在计时器(kernel timer)中使用回调函数。
用一个例子程序来方便理解:
KTIMER Timer;
KDPC TimerDpc;
void OnTimerExpired(KDPC* Dpc, PVOID context, PVOID,PVOID)
{
UNREFERENCED_PARAMETER(Dpc);
UNREFERENCED_PARAMETER(context);
?
NT_ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL);//处理计时器到了的情况
?
}
?
void InitializeAndStartTimer(ULONG msec)
{
KeInitializeTimer(&Timer);
KeInitializeDpc(&TimerDpc,
OnTimerExpired,
nullptr
); //把函数作为一种参数传进去,就是回调函数
?
LARGE_INTEGER interval;
?
//相对间隔以 100 纳秒为单位(并且必须为负)通过乘以 10000 转换为毫秒
interval.QuadPart = -10000LL * msec;
KeSetTimer(&Timer, interval, &TimerDpc);
}
这段代码表示当计时器到期时,DPC会被插入到CPU中的DPC队列中并尽快执行。使用DPC比普通基于IRQL(0)的回调更厉害,因为它级别比较高,保证在User太代码和大多数内核代码之前执行。
Asynchronouts Procedure Calls(APC)异步调用
DPC被封装成函数在IRQL==DISPATCH_LEVEL的时候被调用。
异步调用APC也是被封装成函数来调用。但是和DPC不同,APC是专门给特定线程使用,而DPC和线程无关。这意味着每个线程都有一个APC队列,每个处理器有DPC队列。
APC有三种:
类型 | 详情 |
---|---|
User mode APCs | 仅当线程进入警报状态时,它们才会在IRQL==PASSIVE_LEVEL的用户模式下执行。通常通过API调用来实现,比如:SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectEx等类型API。这些API的最后一个参数可以设置为true来讲线程处于警报状态(alertable state)。在警报状态下,线程会查看它的APC队列,执行APC队列里的内容,直到APC队列为空。 |
Normal kernel mode APCs | 在内核模式下以IRQL==PASSIVE_LEVEL执行并抢占用户模式下的代码和DPC时间。 |
Special kernel APCs | 在内核模式下以IRQL==APC_LEVEL(1)执行并抢占User下的代码和APC以及普通内核里的APC时间。这些APC被I/O系统来完成I/O操作。 |
APC的API在内核模式下没有文档记录,所以驱动程序一般不用APC。
用户模式可以通过调用某些 API 来使用(用户模式)APC。例如,调用 ReadFileEx 或 WriteFileEx 启动异步 I/O 操作。操作完成后,用户模式 APC 会附加到调用线程。如前所述,当线程进入可警报状态时,该 APC 将执行。在用户模式下显式生成 APC 的另一个有用函数是QueueUserAPC。查看 Windows API 文档以获取更多信息。
Critical Regions and Guarded Regions 关键区域和保护区域
关键区域禁止用户态和普通内核APC执行。线程使用KeEnterCriticalRegion函数来进入临界区,使用KeLeaveCriticalRegion来离开临界区。内核编程中的某些功能需要位于关键区(Critical Regions)内。尤其是在使用执行资源(executive resources)时。保护区域阻止所有APC执行。KeEnterGuardedRegion和KeleaveGuardedRegion必须成套出现不然很危险。
Structured Exception Handling 异常处理
异常是由于某条指令执行某些导致处理器引发错误的操作而发生的事件。异常的例子包括:除数为0,断点,页错误,堆栈溢出和无效指令等。如果发生异常内核会捕获它并在可能的情况下运行代码来处理异常,这种机制称为异常处理结构(Structured Exception Handling SEH),可以用于User和Kernel。异常也是断点实现的原理。
异常:异常和中断概念类似,当程序中出现了某种异常,操作系统会寻找该异常的处理函数,如果没有处理函数,就会由操作系统的默认错误处理函数处理,内核模式下就是直接蓝屏。
回卷:程序执行到某个地方出现异常错误时,系统会寻找出错点是否处于一个try{}块中,并进入try块中提供的异常处理函数,如果当前try中没有就会向更外一层的try中找直到最外层try也没有就交给操作系统处理。
内核异常处理程序由IDT(Interrupt Dispatch Table 中断调度列表)来调用,IDT与中断和ISR之间的映射相同。一一对应。对于Windbg来说,可以使用!idt命令来查看所有的映射。编号较低的中断向量实际上就是异常处理程序,比如:
lkd> !idt
Dumping IDT: fffff8011d941000
00: fffff8011dd6c100 nt!KiDivideErrorFaultShadow
01: fffff8011dd6c180 nt!KiDebugTrapOrFaultShadow Stack = 0xFFFFF8011D9459D0
02: fffff8011dd6c200 nt!KiNmiInterruptShadow Stack = 0xFFFFF8011D9457D0
03: fffff8011dd6c280 nt!KiBreakpointTrapShadow
04: fffff8011dd6c300 nt!KiOverflowTrapShadow
05: fffff8011dd6c380 nt!KiBoundFaultShadow
06: fffff8011dd6c400 nt!KiInvalidOpcodeFaultShadow
07: fffff8011dd6c480 nt!KiNpxNotAvailableFaultShadow
08: fffff8011dd6c500 nt!KiDoubleFaultAbortShadow Stack = 0xFFFFF8011D9453D0
09: fffff8011dd6c580 nt!KiNpxSegmentOverrunAbortShadow
0a: fffff8011dd6c600 nt!KiInvalidTssFaultShadow
0b: fffff8011dd6c680 nt!KiSegmentNotPresentFaultShadow
0c: fffff8011dd6c700 nt!KiStackFaultShadow
0d: fffff8011dd6c780 nt!KiGeneralProtectionFaultShadow
0e: fffff8011dd6c800 nt!KiPageFaultShadow
10: fffff8011dd6c880 nt!KiFloatingErrorFaultShadow
11: fffff8011dd6c900 nt!KiAlignmentFaultShadow
?
一些常见的异常:
exception | description |
---|---|
Division by zero(0) 除0 | 就是除0了 |
BreakPoint(3) 断点 | 内核将控制传送给调试器 |
Invalid opcode(6) 无效 | CPU遇到了未知指令 |
page fault(14) 段错误 | 如果用于将虚拟地址转换为物理地址的页表条目的 Valid 位设置为零,则 CPU 会引发此错误,这表明(就 CPU 而言)该页面未驻留在物理内存中。简单来说就是访问了不可访问的地址。 |
一旦引发了异常,内核会在发生异常的函数中搜索处理程序(除了一些透明处理的异常,例如断点BreakPoint(3)),如果没有找到就会向上搜索调用堆栈,直到找到异常处理程序,如果堆栈耗尽,那么系统崩溃。
Windows在C语言中提供了四个关键字来让开发者完成异常处理:
关键字 | 描述 |
---|---|
_try | 一段可能出现问题的代码 |
_except | 如果代码出现了问题的解决办法 |
_finally | 和异常无关,提供无论_try代码块是正常退出还是异常退出都可以保证执行的代码。 |
_leave | 提供一种优化的机制来从 __try 块内的某处跳转到 _finally 块。 |
关键字的有效组合是 _try/except和 _try/finally.这些关键字在User下和Kernel下一样。
_try/except
这一章里,实现了一个驱动程序,其中有代码访问用户模式缓冲区的内容,这个是非常危险的。因为如果用户模式的代码开辟了一个线程在驱动程序访问缓冲区前就释放掉了这个缓冲区。这个时候就会导致系统崩溃。所有永远不能信任User下的数据,包含user下的数据就应该在_try/except块中来处理,以确保错误的缓冲区不会使得驱动崩溃。
__try
{}
__except(filter_value)
{}
try里面的函数如果出现了异常就会根据filter_value中的数值来判断是否需要再__except里面处理。
System Crash 系统崩溃
也就是之前经常看到网上吐槽的 Windows又又又又蓝屏啦。蓝屏就是Windows系统的一种系统崩溃,也叫BSOD。
这里将会讨论系统崩溃时会发生什么以及如何应对它:
大家身为Windows内核的学习人员,千万不要把系统蓝屏当作坏东西来处理。系统蓝屏其实是一种保护机制,如果再往下执行就有可能有毁灭性打击,就直接蓝屏不让系统继续执行了。
如果崩溃的系统连接到了一个内核的调试器的话,会在调试器中产生一个中断,可以让你在调试器里面对系统的状态进行检查。
可以在Windows里面进行配置使得当出现蓝屏时保存一个dump文件,这个dump文件会保存系统蓝屏的环境。
这里的设置就很明显了,是要设置成什么样子的,保存的目标文件地址。
转存的类型决定了会写入多少信息:
类型 | 描述 |
---|---|
小内存转储 | 非常小没啥用 |
内核内存转储 | 捕获所有的内核内存,一般来说这个是够的,因为用户代码也不会搞出蓝屏。 |
完整内存转储 | 提供了所有的信息,包括User和Kernel的,可以获得完整信息。但是这个文件太大了。 |
自动内存转储(Windows8+) | 等同与内核内存转储,在启动时自动调整页面文件大小,来保证有一个合适的大小来存储内核内存转储文件。 |
活跃内存转储(Windows10+) | 类似与完整内存转储,除了崩溃的系统有文件,否则是不会有的。有助于减小服务器系统的转储文件大小。 |
崩溃转储信息
一旦有了dump文件,就可以直接在WinDbg中选择文件/打开转储文件来指向这个dump文件了,比如:
Microsoft (R) Windows Debugger Version 10.0.18317.1001 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.
Loading Dump File [C:\Temp\MEMORY.DMP]
Kernel Bitmap Dump File: Kernel address space is available, User address space may n\
ot be available.
************* Path validation summary **************
Response Time (ms) Location
Deferred SRV*c:\Symbols*http://msdl.microsoft.\
com/download/symbols
Symbol search path is: SRV*c:\Symbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
Windows 10 Kernel Version 18362 MP (4 procs) Free x64
Product: WinNt, suite: TerminalServer SingleUserTS
Built by: 18362.1.amd64fre.19h1_release.190318-1202
Machine Name:
Kernel base = 0xfffff803`70abc000 PsLoadedModuleList = 0xfffff803`70eff2d0
Debug session time: Wed Apr 24 15:36:55.613 2019 (UTC + 3:00)
System Uptime: 0 days 0:05:38.923
Loading Kernel Symbols
Microsoft (R) Windows Debugger Version 10.0.18317.1001 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.
Loading Dump File [C:\Temp\MEMORY.DMP]
Kernel Bitmap Dump File: Kernel address space is available, User address space may n\
ot be available.
************* Path validation summary **************
Response Time (ms) Location
Deferred SRV*c:\Symbols*http://msdl.microsoft.\
com/download/symbols
Symbol search path is: SRV*c:\Symbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
Windows 10 Kernel Version 18362 MP (4 procs) Free x64
Product: WinNt, suite: TerminalServer SingleUserTS
Built by: 18362.1.amd64fre.19h1_release.190318-1202
Machine Name:
Kernel base = 0xfffff803`70abc000 PsLoadedModuleList = 0xfffff803`70eff2d0
Debug session time: Wed Apr 24 15:36:55.613 2019 (UTC + 3:00)
System Uptime: 0 days 0:05:38.923
Loading Kernel Symbols
WinDbg调试器建议执行 !analyze -v命令来初步分析dump文件:
2: kd> !analyze -v
*******************************************************************************
* *
* Bugcheck Analysis *
* *
*******************************************************************************
DRIVER_IRQL_NOT_LESS_OR_EQUAL (d1)
An attempt was made to access a pageable (or completely invalid) address at an
interrupt request level (IRQL) that is too high. This is usually
caused by drivers using improper addresses.
If kernel debugger is available get stack backtrace.
Arguments:
Arg1: ffffd907b0dc7660, memory referenced
Arg2: 0000000000000002, IRQL
Arg3: 0000000000000000, value 0 = read operation, 1 = write operation
Arg4: fffff80375261530, address which referenced memory
Debugging Details:
------------------
(truncated)
DUMP_TYPE: 1
BUGCHECK_P1: ffffd907b0dc7660
BUGCHECK_P2: 2
BUGCHECK_P3: 0
BUGCHECK_P4: fffff80375261530
READ_ADDRESS: Unable to get offset of nt!_MI_VISIBLE_STATE.SpecialPool
Unable to get value of nt!_MI_VISIBLE_STATE.SessionSpecialPool
ffffd907b0dc7660 Paged pool
CURRENT_IRQL: 2
FAULTING_IP:
myfault+1530
fffff803`75261530 8b03 mov eax,dword ptr [rbx]
(truncated)
ANALYSIS_VERSION: 10.0.18317.1001 amd64fre
TRAP_FRAME: fffff98853b0f7f0 -- (.trap 0xfffff98853b0f7f0)
NOTE: The trap frame does not contain all registers.
Some register values may be zeroed or incorrect.
rax=0000000000000000 rbx=0000000000000000 rcx=ffffd90797400340
rdx=0000000000000880 rsi=0000000000000000 rdi=0000000000000000
rip=fffff80375261530 rsp=fffff98853b0f980 rbp=0000000000000002
r8=ffffd9079c5cec10 r9=0000000000000000 r10=ffffd907974002c0
r11=ffffd907b0dc1650 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei ng nz na po nc
myfault+0x1530:
fffff803`75261530 8b03 mov eax,dword ptr [rbx] ds:00000000`00000000=?\
???????
Resetting default scope
LAST_CONTROL_TRANSFER: from fffff80370c8a469 to fffff80370c78810
STACK_TEXT:
fffff988`53b0f6a8 fffff803`70c8a469 : 00000000`0000000a ffffd907`b0dc7660 00000000`0\
0000002 00000000`00000000 : nt!KeBugCheckEx
fffff988`53b0f6b0 fffff803`70c867a5 : ffff8788`e4604080 ffffff4c`c66c7010 00000000`0\
0000003 00000000`00000880 : nt!KiBugCheckDispatch+0x69
fffff988`53b0f7f0 fffff803`75261530 : ffffff4c`c66c7000 00000000`00000000 fffff988`5\
3b0f9e0 00000000`00000000 : nt!KiPageFault+0x465
fffff988`53b0f980 fffff803`75261e2d : fffff988`00000000 00000000`00000000 ffff8788`e\
c7cf520 00000000`00000000 : myfault+0x1530
fffff988`53b0f9b0 fffff803`75261f88 : ffffff4c`c66c7010 00000000`000000f0 00000000`0\
0000001 ffffff30`21ea80aa : myfault+0x1e2d
fffff988`53b0fb00 fffff803`70ae3da9 : ffff8788`e6d8e400 00000000`00000001 00000000`8\
3360018 00000000`00000001 : myfault+0x1f88
fffff988`53b0fb40 fffff803`710d1dd5 : fffff988`53b0fec0 ffff8788`e6d8e400 00000000`0\
0000001 ffff8788`ecdb6690 : nt!IofCallDriver+0x59
fffff988`53b0fb80 fffff803`710d172a : ffff8788`00000000 00000000`83360018 00000000`0\
0000000 fffff988`53b0fec0 : nt!IopSynchronousServiceTail+0x1a5
fffff988`53b0fc20 fffff803`710d1146 : 00000054`344feb28 00000000`00000000 00000000`0\
0000000 00000000`00000000 : nt!IopXxxControlFile+0x5ca
fffff988`53b0fd60 fffff803`70c89e95 : ffff8788`e4604080 fffff988`53b0fec0 00000054`3\
44feb28 fffff988`569fd630 : nt!NtDeviceIoControlFile+0x56
fffff988`53b0fdd0 00007ff8`ba39c147 : 00000000`00000000 00000000`00000000 00000000`0\
0000000 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x25
00000054`344feb48 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`0\
0000000 00000000`00000000 : 0x00007ff8`ba39c147
(truncated)
FOLLOWUP_IP:
myfault+1530
fffff803`75261530 8b03 mov eax,dword ptr [rbx]
FAULT_INSTR_CODE: 8d48038b
SYMBOL_STACK_INDEX: 3
SYMBOL_NAME: myfault+1530
FOLLOWUP_NAME: MachineOwner
MODULE_NAME: myfault
IMAGE_NAME: myfault.sys
(truncated)
每个dump文件有四个关键线索可以分析:
Arguments:
Arg1: ffffd907b0dc7660, memory referenced
Arg2: 0000000000000002, IRQL
Arg3: 0000000000000000, value 0 = read operation, 1 = write operation
Arg4: fffff80375261530, address which referenced memory
Arg1:代表被引用的内存地址,2:IRQL等级,3:读操作还是写操作,4:正在访问的地址。
一旦获得dump文件,可以查看WinDbg文档:“错误检查代码参考”(Bugcheck Code Reference)来分析。
分析转储文件
转储文件(dump文件)就是对系统的一个快照,除了无法设置断点和执行指令以外其他的和内核调试是一样的,比如:
指令 | 作用 |
---|---|
~ns | 切换cpu索引值,类似于切换线程 |
!running -t | !running命令列出崩溃时所有在处理器上运行的线程,-t更是显示每个线程的栈 |
!stacks | 列出所有线程的栈 |
系统挂起
不仅仅只有系统崩溃需要导出转储文件来分析,还有情况也需要,比如系统卡死,死锁了,无法响应了这种情况也需要生成dump文件来分析。
如果说系统卡死了,但是还可以响应一小部分,可以采用一个工具来将系统崩溃生成一个dump文件来分析:
https://docs.microsoft.com/en-us/sysinternals/downloads/(下载地址),里面的NotMyFault程序:
这里选择你要生成系统崩溃的原因,然后选择Crash就可以了。
如果说系统完全无法响应,可以将WinDbg连接上去,来正常调试或者使用.dump指令来生成dumo文件。
如果说系统无法响应有连接不了就可以采用注册表来手动生成dump文件。参考:
Thread Synchronization 线程同步
一个驱动程序可以被多个user程序调用,所以就难免会出现线程调度的问题,比如说一个正在改一个正在访问,这样就很不安全了,这也被称为数据竞争。这种情况下,最简单安全的办法就是当一个线程访问某个内容时,其他线程都不能访问,只能等待。这样就不会导致不安全的情况了。
Windows提供了一些原语办法来实现线程同步。
互锁Interlocked
互锁函数提供了执行原子操作的方便方式,利用硬件特征。
一个简单的例子:如果有两个线程同时访问一个地址执行加1操作,没有使用一些操作的话是有可能会导致最终结果只增加了1,而不是加2:
一些驱动程序能够使用的互锁函数:
函数 | 描述 |
---|---|
InterlockedIncrement/InterlockedIncrement16/InterlockedIncrement64 | 对32/16/64位的整数原子化加1 |
InterlockedDecrement/16/64 | 对32/16/64位的整数原子化减1 |
InterlockedAdd/InterlockedAdd64 | 原子化的将32/64位数加到一个变量上 |
InterlockedExchange8/16/64 | 原子化地交换32/8/16/64位整数 |
InterlockedCompareExchange/64/128 | 原子化地比较一个变量与一个值,如果相等则将提供的值交换到变量中并返回TRUE;否则,将当前的值放入变量中并返回FALSE |
这些函数在User下也可以用的。因为他们其实并不是函数,而是CPU的内联函数--CPU的一种特殊指令。
分发器对象 Dispatcher Objects
分发器对象也叫可等待对象。这些对象有着有信号和无信号两种状态,之所以被称为可等待对象是因为线程可以等待该对象从无信号到有信号然后再使用。这个在User态下被称为信号对象。
用于等待的主要函数是KeWaitForSingleObject和KeWaitForMultipleObject函数:
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 | 等待的对象而不是句柄,如果是句柄可以采用ObReferenceObjectByHandle来获取对象指针 |
WaitReason | 等待的原因,这个列表很长,不过驱动程序通常设置为Executive,如果是用户请求就应该设置为UserRequest |
WaitMode | 等待模式,可以是KernelMode也可以是UserMode,多数内核驱动设置为KernelMode |
Altertable | 等待过程中线程是否该处于警报状态,警报状态允许传递用户模式的异步过程调用(APC),用户模式的APC在等待模式WaitMode设置为UserMode时可以传递。多数驱动程序将其设置为FALSE |
Timeout | 等待超时时间,如果为NULL表示一直等待。单位是100纳秒 |
Count | 等待对象数目 |
Object[] | 等待对象的指针数组 |
WaitType | 指明要等待所有对象有信号(WaitAll)还是只要有一个对象有信号(WaitAny) |
WaitBlockArray | 结构数组,用于等待操作的内部管理。 |
返回值有两种:STATUS_SUCCESS 等待完成有信号了,STATUS_TIMEOUT:等待完成,超时。
注意返回值都是真,不能直接用返回值为真来判断是否等待成功。
KeWaitForSingleObject和KeWaitForMultipleObject都一样,如果指定了WaitAll只有所有都等待了才返回真。对于WaitAny如果有一个有信号了,就会返回该有信号的对象在对象数组中的索引。
互斥量Mutex
很经典的一个东西,用来解决多线程中的某个线程在任何时刻访问共享资源的标准问题。
互斥量Mutex在*的时候是有信号的,一旦被调用这个互斥量就变成没信号的了,别的线程就无法调用它了。调用的线程就被称为拥有者。对于Mutex来说,拥有关系很重要。因为:
1 如果某个线程拥有了它,该线程就是唯一可以释放该互斥量的线程
2 一个互斥量能多次被统一线程获取,需要注意的是使用完之后必须释放掉,不然别的线程无法获取。
要使用互斥量Mutex,需要从非分页池(notPaged)中分配一个KMUTEX结构。互斥量的API包含了如下与KMUTEX一起工作的函数:
KeInitializeMutex:必须被调用一次来初始化互斥量。
某一个等待函数需要将分配的KMUTEX结构体的地址作为参数传递给它
在某个线程是互斥量的拥有者时需要调用KeReleaseMutex释放互斥量
示例代码:
#include<ntddk.h>
?
?
KMUTEX MyMutex;
?
void Init()
{
KeInitializeMutex(&MyMutex, 0);
}
void DoWork()
{
//等待互斥量有信号
KeWaitForSingleObject(&MyMutex, Executive, KernelMode, FALSE, NULL);
?
//*处理
?
?
//使用完之后释放互斥量
KeReleaseMutex(&MyMutex, FALSE);
}
这里有一个优化,就是不管怎么样都需要释放掉互斥量,所有这里可以采用_try/__finally来使用:
#include<ntddk.h>
?
?
KMUTEX MyMutex;
?
void Init()
{
KeInitializeMutex(&MyMutex, 0);
}
void DoWork()
{
//等待互斥量有信号
KeWaitForSingleObject(&MyMutex, Executive, KernelMode, FALSE, NULL);
?
__try
{
//*处理
?
}
?
?
__finally
{
//使用完之后释放互斥量
KeReleaseMutex(&MyMutex, FALSE);
}
}
再用C++来给它包装成一个容器来更方便使用:
#include<ntddk.h>
?
?
?
class Mutex
{
public:
void Init()
{
KeInitializeMutex(&_mutex, 0);
}
void Lock()
{
KeWaitForSingleObject(&_mutex, Executive, KernelMode, FALSE, NULL);
?
}
void Unlock()
{
KeReleaseMutex(&_mutex, FALSE);
}
private:
KMUTEX _mutex;
};
?
?
template<typename Tlock>
class AutoLock
{
public:
AutoLock(Tlock& lock) :_lock(lock)
{
lock.Lock();
}
~AutoLock
{
_lock.Unlock();
}
private:
Tlock& _lock;
};
?
?
?
Mutex MyMutex;
?
void Init()
{
MyMutex.Init();
}
?
void DoWork()
{
AutoLock<Mutex> lock(MyMutex);
?
//*使用
}
快速互斥量
快速互斥量是传统互斥量的升级版。有自己的API。
特性:
1 不能递归获取,不然会死锁
2 被获取后,CPU的IRQL会提高到APC_LEVEL(1),会阻止线程上的APC传递
3 只能无限等待,不能限制等待时间
4 User下没有
使用流程:
从非分页池中分配FAST_MUTEX结构并调用ExInitializeFastMutex初始化。使用ExAcquireFastMutex或ExAcquireFastMutexUnsafe来获取,使用ExReleaseFastMutex或ExReleaseFastMutexUnsafe来释放。
class FastMutex
{
public:
void Init();
void Lock();
void Unlock();
private:
FAST_MUTEX _mutex;
};
?
void FastMutex::Init()
{
ExInitializeFastMutex(&_mutex);
}
void FastMutex::Lock()
{
ExAcquireFastMutex(&_mutex);
}
void FastMutex::Unlock()
{
ExReleaseFastMutex(&_mutex);
}
信号量
信号量一般来限制某些东西,比如队列的长度。信号量的最大值和初始值(一般初始值==最大值)又KeInitalizeSemaphore来确定,当信号量内部值大于1,此时为有信号,等于0为无信号。调用KeWaitForSingleObject使用时当信号值大于一会表示等待成功然后信号值减1。KeReleaseSemaphore会释放信号让信号值加1.
事件(event)
是一个BOOL值,真为有信号,假为无信号。主要目的是在某事发生时释放信号。来提供同步。
事件有两种类型:
1 通知事件N(手动重置):该事件被触发后会释放所有正在等待的线程,并且状态一直保持为有信号,除非被显示重置。
2: 同步事件(自动重置):被触发后最后释放一个线程。触发后回到无信号状态。
创建方法:从非分页池里创建一个KEVENT结构,指明类型和初始状态,然后调用KeInitalizeEvent初始化,调用KeResetEvent或KeClearEvent重置。
执行体资源(Executive Resource)
内核提供了一种单写多读的线程同步原语,就是执行体资源。
流程:非分页池中创建ERESOURCE结构调用ExInitializeResourceLite初始化。线程就可以调用ExAcquireResourceExclusiveLite来获取写操作,调用ExAcquireResourceSharedLite来获取共享锁(读),调用完之后不管什么操作都得用ExReleaseResouceList释放。但是必须要禁用APC。可以通过临界区来实现:
ERESOURCE resource;
void WriteData()
{
KeEnterCriticalRegion();
ExAcquireResourceExclusiveLite(&resource, TRUE);
//随意
?
ExReleaseResource(&resource);
KeLeaveCriticalRegion();
}
void DeleteResource()
{
ExDeleteResource(&resource);
}
?