在 APC机制分析中是主要分析了APC的执行流程,初始化、插入和派发,这里主要分析一下DPC的相关机制。
在此之前总结一下APC主要的应用点,由于初学不久,很多不全的请补充分享一下,毕竟对大神可能很简单的,对我们小白其实根本看不到路,甚至不知道有这条路,谢谢。
在用户层没有可以对DPC进行相关操作的函数,直接从0环开始分析。
KiInitializeDpc
用于初始化DPC对象,
该函数只被KeInitializeDpc
和KeInitializeThreadedDpc
所调用,这个和DPC的类型有关,下面会分析到。
以上就是初始的全部内容,十分简单。
插入DPC在KeInsertQueueDpc
中实现。
DpcData = KiSelectDpcData(TargetPrcb, Dpc);
KiSelectDpcData
内部的获取逻辑也很简单
如果Dpc.Type==ThreadedDpcObject并且CPU的环境块的TargetPrcb->ThreadDpcEnable==TRUE,那么返回 Prcb->DpcData[DPC_THREADED]
一般的Dpc就返回 Prcb->DpcData[DPC_NORMAL]
那么经过上面的步骤,已经获取了当前核心的_KPDC_DATA数据对象DpcData。
如果 Dpc->DpcData是NULL,说明DPC object不在Dpc Queue中,
那么增加当前核心DpcData的DpcCount+=1,增加DpcQueueDepth+=1,
设置Dpc对象的参数1和参数2。
如果Dpc->DpcData不是NULL,就什么都不做,因为它已经在DPC队列中了。
上面DPC已经插入到了当前CPU的DPC队列或者指定CPU的DPC队列中。
这部分在Wrk源码和ReactOS源码上查找到的引用有些出入,错误之处希望指正。
最后注意,DPC不会受线程切换影响,因为线程切换也是在DISPATCH_LEVEL,所以无法中断当前DPC的执行。
APC是线程相关的,每个线程有一个APC队列;
而DPC是CPU核心相关的,每个CPU核心有一个DPC队列。
APC级别可以访问分页内存,DPC级别是不能访问分页内存的
(在同一处理器上,线程只能被更高级别IRQL的线程所中断。每个处理器都有自己的中断IRQL。)
PspTerminateThreadByPointer;
KeInitializeApc (ExitApc,
PsGetKernelThread (Thread),
OriginalApcEnvironment,
PsExitSpecialApc,//里面调用了PspExitThread用于结束线程
PspExitApcRundown,
PspExitNormalApc,
KernelMode,
ULongToPtr (ExitStatus));
typedef struct _KDPC_DATA {
LIST_ENTRY DpcListHead;
KSPIN_LOCK DpcLock;
volatile ULONG DpcQueueDepth; //深度
ULONG DpcCount;
} KDPC_DATA, *PKDPC_DATA;
typedef struct _KDPC {
UCHAR Type; //DPC类型(分为普通DPC和线程化DPC)
UCHAR Importance; //该DPC的重要性,将决定挂在队列头还是尾
UCHAR Number; //第5位为0就表示当前cpu,否则,最低4位表示目标cpu号
UCHAR Expedite;
LIST_ENTRY DpcListEntry; //用来挂入dpc链表,和_KDPC_DATA中的DpcListHead对应,
//内核中很多这样的结构安排
PKDEFERRED_ROUTINE DeferredRoutine; //dpc函数
PVOID DeferredContext; //dpc函数的参数
PVOID SystemArgument1;
PVOID SystemArgument2;
PVOID DpcData;
} KDPC, *PKDPC, *PRKDPC;
Dpc->Type = (UCHAR)DpcType;//和ApcObject一样,属于_KOBJECTS枚举,分为DpcObject和ThreadedDpcObject(线程化的DPC对象)
Dpc->Importance = MediumImportance; //重要性决定了DPC挂在队列的顺序
Dpc->Number = 0; //初始的目标cpu为当前cpu
Dpc->Expedite = 0;
Dpc->DeferredRoutine = DeferredRoutine; //函数地址
Dpc->DeferredContext = DeferredContext; //函数参数
Dpc->DpcData = NULL; //表示该DPC尚未挂入任何DPC队列
//首先进行参数检查
if (Dpc->Number >= MAXIMUM_PROCESSORS/*32*/) {
//修正Number
Number = Dpc->Number - MAXIMUM_PROCESSORS;
//KiProcessorBlock是PKPRCB的数组,
//这里取出DPC->Number对应的_KPRCB是因为DPC是CPU核心相关
TargetPrcb = KiProcessorBlock[Number];
} else {
//参数不需要修正的分支,就是当前CPU的PRCB
Number = CurrentPrcb->Number;
TargetPrcb = CurrentPrcb;
}
1. 如果Dpc->Importance == HighImportance,那么把DPC挂载在Dpc队列的头部,`InsertHeadList(&DpcData->DpcListHead, &Dpc->DpcListEntry);`,DPC队列就是由的CPU环境块中的dpcData进行维护。
2. 否则把DPC插入核心DPC队列的尾部
----------------------------------------
1. 如果DPC是线程化的DPC对象 DpcData == &TargetPrcb->DpcData[DPC_THREADED],且
`TargetPrcb->DpcThreadActive == FALSE&&
TargetPrcb->DpcThreadRequested == FALSE`,也就是说Dpc线程不是活动状态,因为执行Dpc的时候会设置TargetPrcb->DpcThreadActive == TRUE。这种情况下设置如下PRCB的标志:
TargetPrcb->DpcSetEventRequest=TRUE;//设置Prcb->DpcEvent有信号
TargetPrcb->DpcThreadRequested = TRUE;
TargetPrcb->QuantumEnd = TRUE;
也就是要把线程唤醒。
2. 如果DPC指定的不是当前CPU的PRCB,那么做一些检测以判断是否需要线程唤醒。
-------------------------------------------------------
1.如果DPC是一般的DPC对象,且
`TargetPrcb->DpcRoutineActive == FALSE&&
TargetPrcb->DpcInterruptRequested == FALSE`的情况下,
然后判断只要Dpc的Importance不是LowImportance或者
DpcData->DpcQueueDepth已经超过了PRCB的MaximumDpcQueueDepth(最大Queue深度)
获取TargetPrcb->DpcRequestRate小于TargetPrcb->MinimumDpcRate(最小请求率),
那么就会请求当前CPU的dispatch 中断,
且设置 TargetPrcb->DpcInterruptRequested=TRUE。
---------------------------------------------
那么经过上面的判断检验,需要dispatch 中断的都会执行
` KiRequestSoftwareInterrupt(DISPATCH_LEVEL);`,
这个函数在APC分析中也看到了,只不过那里的参数是APC_LEVEL。
这是由于APC是运行在APC_LEVEL,而DPC是运行在DISPATCH_LEVEL的。
if (KeThreadDpcEnable != FALSE);//threaded DPCs是启用的
KeInitializeEvent(&Prcb->DpcEvent, SynchronizationEvent, FALSE);
InitializeListHead(&Prcb->DpcData[DPC_THREADED].DpcListHead);
KeInitializeSpinLock(&Prcb->DpcData[DPC_THREADED].DpcLock);
Prcb->DpcData[DPC_THREADED].DpcQueueDepth = 0;
Status = PsCreateSystemThread(&Handle,
THREAD_ALL_ACCESS,
&ObjectAttributes,
NULL,
NULL,
KiExecuteDpc,//注意这里
Prcb);//参数
//线程优先级最高
KeSetPriorityThread(Thread, HIGH_PRIORITY);
//启用线程化Dpc,前面KiSelectDpcData获取线程化的KDPC_DATA
//就会检查该字段是否为TRUE
Prcb->ThreadDpcEnable = TRUE;
//取出链表头,注意是取得DPC_THREADED中的
ListHead = &Prcb->DpcData[DPC_THREADED].DpcListHead;
do{
//设置为TRUE,和前面对应
Prcb->DpcThreadActive = TRUE;
//调用DPC回调函数(此时的IRQL==PASSIVE_LEVEL)
(DeferredRoutine)(Dpc,
DeferredContext,
SystemArgument1,
SystemArgument2);
...........
//深度来判断DPC是否为空
}while(Prcb->DpcData[DPC_THREADED].DpcQueueDepth != 0);
//设置标志,和前面相对应
Prcb->DpcThreadActive = FALSE;
Prcb->DpcThreadRequested = FALSE;
//最后判断如果线程华DPC list是空的,那么
KeWaitForSingleObject(&Prcb->DpcEvent, //等待该事件,和Prcb->DpcSetEventRequest标志有关,在KiQuantumEnd中让事件有信号
Suspended,
KernelMode,
FALSE,
NULL);
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)