一、中断级IRQL
高级别可以打断低级别的调用,同级别不能打断同级别的调用。
中断级在软件层面分为三级,再高的级别是硬件发送的中断。
- - 0 pass_level
- - 1 apc_level
- - 2 dpc_level 只有硬件中断能打断
1.获取中断级
DbgPrint("当前执行中断级为 %d\n", KeGetCurrentIrql());
2.提高中断级
DPC这个级别是软件层面能提升到的最高权限,ISR(延迟过程调用)当某个硬件设备引发一个高中断的时候,可以把不那么紧急的任务放在DPC里边来执行,同时又把中断级降低。
DPC是一个队列,当CPU处理完高于DPC中断级的任务后就来找这个表,然后挨个拿出来执行,也可以不插队的形式短暂的把我们当前任务请求级提高到DPC。
KIRQL originIrql = KeGetCurrentIrql();
DbgPrint("当前执行中断级为 %d\n", KeGetCurrentIrql());
//提升中断级
originIrql = KeRaiseIrqlToDpcLevel();
DbgPrint("提升后请求中断级为 %d\n", KeGetCurrentIrql());
在使用API函数的时候关注一下中断级别。
当我们在一个DPC例程里边访问一个换页内存的时候,页面在物理内存中时,一切OK。但是当页面被换到磁盘上的pagefile.sys,缺页中断无法打断DPC过程(弹幕大佬:“准确说缺页中断会产生,但页面错误异常处理函数无法回应中断请求”),结果是访问一个无效内存,导致BSOD
DPC编程的时候不要使用换页内存(动态申请的时候有标志)
二、自旋锁
防止重入,保证资源使用时的唯一性,比如在尝试重入全局变量。自旋锁的本质是一种忙等待,一直在尝试获取,只有在上锁部分动作执行完毕,解锁完成后,才能被其他请求再次获取。(弹幕大佬:“也就是说获取自旋锁的线程中断级更高,被抢占的概率更小,从而能尽可能快的释放锁,避免其他线程长时间忙等待”)。
应用场景:
设置一个全局变量BYTE PhyBuffer[]设置读写函数来试图并发的处理这个变量
//定义全局变量作为读写操作的目标
BYTE PhyBuffer[] = { 0x11,0x11,0x22,0x33 };
NTSTATUS MyRead(PDEVICE_OBJECT pdevice, PIRP pirp){
...
//
// 读取全局变量
//
RtlCopyMemory(readbuffer,
PhyBuffer,
strlen(PhyBuffer));
...
}
NTSTATUS MyWrite(PDEVICE_OBJECT pdevice, PIRP pirp){
...
//
// 写入全局变量 如果不加锁便会产生异常
//
RtlZeroMemory(PhyBuffer, strlen(PhyBuffer));
RtlCopyMemory(PhyBuffer, writebuffer, strlen(PhyBuffer));
...
}
那么此时就会出现问题,当读还未完成的时候,对全局变量进行写操作那么会产生异常。需要加一些控制那么确定读或写操作的唯一性,只有当读完成后才进行写操作。
自旋锁使用
使用自旋锁来进行保护:
NTSTATUS MyRead(PDEVICE_OBJECT pdevice, PIRP pirp){
...
//
//初始化自旋锁
//
KIRQL oldirql = 0;
KeAcquireSpinLock(&spinlock, &oldirql);
DbgPrint("自旋锁内获取当前中断级 %d\n", KeGetCurrentIrql());
//
// 读取全局变量
//
RtlCopyMemory(readbuffer,
PhyBuffer,
sizeof(PhyBuffer));
pirp->IoStatus.Status = status;
pirp->IoStatus.Information = sizeof(PhyBuffer);
//
// 释放自旋锁
//
KeReleaseSpinLock(&spinlock, oldirql);
...
}
来自微软官方文档:
在持有旋转锁时,切勿实现执行下列任何一项的例程:
导致硬件异常或引发软件异常。
尝试访问可分页内存。
进行会导致死锁或可能导致旋转锁持有时间超过 25 微秒的递归调用。
如果尝试获取另一个旋转锁,则可能导致死锁。
调用违反上述任何规则的外部例程。
一个视频里使用但是不清楚在什么情况下使用的小技巧😣😣😣,据说NDIS源码里边有使用:
block是布尔类型设置初值为FALSE,这个逻辑是:如果没有上锁那么,我们尝试获取自旋锁,中断级提高,这个时候没办法访问分页内存。不过接下来设置了block为TRUE,假设其他获取自旋锁的地方也通过布尔值先做判别,那么其他地方实际上是没有办法获取自旋锁和处理的,相当于该判断中的其他操作还是具有唯一性。然后再释放自旋锁,降低了中断级,这个时候可以访问分页内存,执行完毕后设置布尔量为FALSE,让其他部分代码获取自旋锁。
这个技巧我觉得很容易出错,因为对同一个全局变量或者其他临界资源进行操作的地方,需要使用同样的逻辑来处理,否则就是单纯的上锁解锁。而且这个本质上是逻辑操作,通过布尔量来选择是否执行一些代码,而不是加锁解锁。🤣🤣🤣
让代码跑在DPC级别中
除了提高自身中断级别外,让代码跑在DPC中的方法:
- 插入DPC队列
涉及API:
- KeInitializeDpc
- KeInsertQueueDpc
KDPC dpcobj = { 0 }; //DPC对象
VOID DpcRoutie(PVOID context)
{
UNREFERENCED_PARAMETER(context);
DbgPrint("DpcRoutie DPC is %d\n", KeGetCurrentIrql());
return;
}
NTSTATUS DriverEntry(){
···
KeInitializeDpc(&dpcobj, DpcRoutie, NULL); // 初始化DPC对象
KeInsertQueueDpc(&dpcobj, NULL, NULL); // 插入DPC队列
···
}
三、蓝屏分析
WIN7可以设置蓝屏的时候转存dump文件
这样蓝屏后可以获取到dump文件进行分析,拖到WinDbg里边可以通过命令来查看具体信息,可以看到堆栈和出错的函数等。
具体使用呢,还得深入学习...
小结
其实看了半天也只是了解到了自旋锁和具体的API,应用场景还不知道。