去年學習了之後就給忘了,現在又花了半天時間熟悉了這塊的知識,防止自己再忘記先記錄下來警醒自己。
本文主題在於指出在驅動中調用I/O函數時存在的問題,輕者卡死,重者BSOD
1. 前言
驅動中我們經常使用一些I/O函數來查詢文件、設備的信息,比如IoQueryFileDosDeviceName獲取進程的DOS路徑,IoVolumeDeviceToDosName獲取卷的DOS名稱等等,一般使用這些函數的場景無外乎在LoadImage回調,CreateProcess回調,微文件過濾器/傳統文件過濾器註冊的callback,甚至會在一些內核Hook點中調用。可能不做線上產品用戶不多的時候很多問題不會被察覺,自己嘗試的時候基本上也無法出現問題。但是如果用戶一多可能會遇到各種奇奇怪怪的反饋,那麼到底哪裡容易出問題呢?
2. I/O API的特殊性
Windows中的I/O管理器提供的API大多數都是異步完成的,而其內部氾濫地使用APC,導致很多I/O函數對使用場景有很高的要求,例如:
IoVolumeDeviceToDosName
Starting with Windows Vista, you must ensure that APCs are not
disabled before calling this routine. The KeAreAllApcsDisabled routine
can be used to verify that APCs are not disabled.
意思很明確,就是說這個API內部需要用到KernelApc,在調用時得確保當前線程的APCs可以執行,可以用KeAreAllApcsDisabled這個API做判斷。
你無法預知當前的代碼執行時的環境是怎麼樣的,比如說在LoadImage回調,你無法確保當前的IRQL一定是PASSIVE_LEVEL,或者沒有在一些內核的臨界區範圍內,說到內核的臨界區,現在常用的兩種:
KeEnterCriticalRegion與KeEnterGuardedRegion。
//
// Enters a Guarded Region
//
#define KeEnterGuardedRegionThread(_Thread) \
{ \
/* Sanity checks */ \
ASSERT(KeGetCurrentIrql() <= APC_LEVEL); \
ASSERT(_Thread == KeGetCurrentThread()); \
ASSERT((_Thread->SpecialApcDisable <= 0) && \
(_Thread->SpecialApcDisable != -32768)); \
\
/* Disable Special APCs */ \
_Thread->SpecialApcDisable--; \
}
#define KeEnterGuardedRegion() \
{ \
PKTHREAD _Thread = KeGetCurrentThread(); \
KeEnterGuardedRegionThread(_Thread); \
}
//
// Leaves a Guarded Region
//
#define KeLeaveGuardedRegionThread(_Thread) \
{ \
/* Sanity checks */ \
ASSERT(KeGetCurrentIrql() <= APC_LEVEL); \
ASSERT(_Thread == KeGetCurrentThread()); \
ASSERT(_Thread->SpecialApcDisable < 0); \
\
/* Leave region and check if APCs are OK now */ \
if (!(++_Thread->SpecialApcDisable)) \
{ \
/* Check for Kernel APCs on the list */ \
if (!IsListEmpty(&_Thread->ApcState. \
ApcListHead[KernelMode])) \
{ \
/* Check for APC Delivery */ \
KiCheckForKernelApcDelivery(); \
} \
} \
}
#define KeLeaveGuardedRegion() \
{ \
PKTHREAD _Thread = KeGetCurrentThread(); \
KeLeaveGuardedRegionThread(_Thread); \
}
//
// Enters a Critical Region
//
#define KeEnterCriticalRegionThread(_Thread) \
{ \
/* Sanity checks */ \
ASSERT(_Thread == KeGetCurrentThread()); \
ASSERT((_Thread->KernelApcDisable <= 0) && \
(_Thread->KernelApcDisable != -32768)); \
\
/* Disable Kernel APCs */ \
_Thread->KernelApcDisable--; \
}
#define KeEnterCriticalRegion() \
{ \
PKTHREAD _Thread = KeGetCurrentThread(); \
KeEnterCriticalRegionThread(_Thread); \
}
//
// Leaves a Critical Region
//
#define KeLeaveCriticalRegionThread(_Thread) \
{ \
/* Sanity checks */ \
ASSERT(_Thread == KeGetCurrentThread()); \
ASSERT(_Thread->KernelApcDisable < 0); \
\
/* Enable Kernel APCs */ \
_Thread->KernelApcDisable++; \
\
/* Check if Kernel APCs are now enabled */ \
if (!(_Thread->KernelApcDisable)) \
{ \
/* Check if we need to request an APC Delivery */ \
if (!(IsListEmpty(&_Thread->ApcState.ApcListHead[KernelMode])) && \
!(_Thread->SpecialApcDisable)) \
{ \
/* Check for the right environment */ \
KiCheckForKernelApcDelivery(); \
} \
} \
}
#define KeLeaveCriticalRegion() \
{ \
PKTHREAD _Thread = KeGetCurrentThread(); \
KeLeaveCriticalRegionThread(_Thread); \
}
我們看到在調用KeEnterCriticalRegion後當前線程的KernelApcDisable是非零值,而
調用KeEnterGuardedRegion之後,當前線程的SpecialApcDisable是非零值
這兩個是什麼玩意呢??
我們接著來看Apc分發的時候如何使用這兩個值的
VOID
NTAPI
KiDeliverApc(IN KPROCESSOR_MODE DeliveryMode,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame)
{
PKTHREAD Thread = KeGetCurrentThread();
PKPROCESS Process = Thread->ApcState.Process;
PKTRAP_FRAME OldTrapFrame;
PLIST_ENTRY ApcListEntry;
PKAPC Apc;
KLOCK_QUEUE_HANDLE ApcLock;
PKKERNEL_ROUTINE KernelRoutine;
PVOID NormalContext;
PKNORMAL_ROUTINE NormalRoutine;
PVOID SystemArgument1;
PVOID SystemArgument2;
ASSERT_IRQL_EQUAL(APC_LEVEL);
/* Save the old trap frame and set current one */
OldTrapFrame = Thread->TrapFrame;
Thread->TrapFrame = TrapFrame;
/* Clear Kernel APC Pending */
Thread->ApcState.KernelApcPending = FALSE;
/* Check if Special APCs are disabled */
if (Thread->SpecialApcDisable) goto Quickie; // 總開關,如果SpecialApcDisable那麼整個線程的APC都不會被執行
/* Do the Kernel APCs first */
while (!IsListEmpty(&Thread->ApcState.ApcListHead[KernelMode]))
{
/* Lock the APC Queue */
KiAcquireApcLockAtApcLevel(Thread, &ApcLock);
/* Check if the list became empty now */
if (IsListEmpty(&Thread->ApcState.ApcListHead[KernelMode]))
{
/* It is, release the lock and break out */
KiReleaseApcLock(&ApcLock);
break;
}
/* Kernel APC is not pending anymore */
Thread->ApcState.KernelApcPending = FALSE;
/* Get the next Entry */
ApcListEntry = Thread->ApcState.ApcListHead[KernelMode].Flink;
Apc = CONTAINING_RECORD(ApcListEntry, KAPC, ApcListEntry);
/* Save Parameters so that it's safe to free the Object in the Kernel Routine*/
NormalRoutine = Apc->NormalRoutine;
KernelRoutine = Apc->KernelRoutine;
NormalContext = Apc->NormalContext;
SystemArgument1 = Apc->SystemArgument1;
SystemArgument2 = Apc->SystemArgument2;
/* Special APC */
if (!NormalRoutine)
{
/* Remove the APC from the list */
RemoveEntryList(ApcListEntry);
Apc->Inserted = FALSE;
/* Release the APC lock */
KiReleaseApcLock(&ApcLock);
/* Call the Special APC */
KernelRoutine(Apc,
&NormalRoutine,
&NormalContext,
&SystemArgument1,
&SystemArgument2);
/* Make sure it returned correctly */
if (KeGetCurrentIrql() != ApcLock.OldIrql)
{
KeBugCheckEx(IRQL_UNEXPECTED_VALUE,
(KeGetCurrentIrql() << 16) |
(ApcLock.OldIrql << 8),
(ULONG_PTR)KernelRoutine,
(ULONG_PTR)Apc,
(ULONG_PTR)NormalRoutine);
}
}
else
{
/* Normal Kernel APC, make sure it's safe to deliver */
if ((Thread->ApcState.KernelApcInProgress) ||
(Thread->KernelApcDisable)) // 子開關,控制著Normal KernelApc的執行與否
{
/* Release lock and return */
KiReleaseApcLock(&ApcLock);
goto Quickie;
}
/* Dequeue the APC */
RemoveEntryList(ApcListEntry);
Apc->Inserted = FALSE;
/* Go back to APC_LEVEL */
KiReleaseApcLock(&ApcLock);
/* Call the Kernel APC */
KernelRoutine(Apc,
&NormalRoutine,
&NormalContext,
&SystemArgument1,
&SystemArgument2);
/* Make sure it returned correctly */
if (KeGetCurrentIrql() != ApcLock.OldIrql)
{
KeBugCheckEx(IRQL_UNEXPECTED_VALUE,
(KeGetCurrentIrql() << 16) |
(ApcLock.OldIrql << 8),
(ULONG_PTR)KernelRoutine,
(ULONG_PTR)Apc,
(ULONG_PTR)NormalRoutine);
}
/* Check if there still is a Normal Routine */
if (NormalRoutine)
{
/* At Passive Level, an APC can be prempted by a Special APC */
Thread->ApcState.KernelApcInProgress = TRUE;
KeLowerIrql(PASSIVE_LEVEL);
/* Call and Raise IRQL back to APC_LEVEL */
NormalRoutine(NormalContext, SystemArgument1, SystemArgument2);
KeRaiseIrql(APC_LEVEL, &ApcLock.OldIrql);
}
/* Set Kernel APC in progress to false and loop again */
Thread->ApcState.KernelApcInProgress = FALSE;
}
}
/* Now we do the User APCs */
if ((DeliveryMode == UserMode) &&
!(IsListEmpty(&Thread->ApcState.ApcListHead[UserMode])) &&
(Thread->ApcState.UserApcPending))
{
/* Lock the APC Queue */
KiAcquireApcLockAtApcLevel(Thread, &ApcLock);
/* It's not pending anymore */
Thread->ApcState.UserApcPending = FALSE;
/* Check if the list became empty now */
if (IsListEmpty(&Thread->ApcState.ApcListHead[UserMode]))
{
/* It is, release the lock and break out */
KiReleaseApcLock(&ApcLock);
goto Quickie;
}
/* Get the actual APC object */
ApcListEntry = Thread->ApcState.ApcListHead[UserMode].Flink;
Apc = CONTAINING_RECORD(ApcListEntry, KAPC, ApcListEntry);
/* Save Parameters so that it's safe to free the Object in the Kernel Routine*/
NormalRoutine = Apc->NormalRoutine;
KernelRoutine = Apc->KernelRoutine;
NormalContext = Apc->NormalContext;
SystemArgument1 = Apc->SystemArgument1;
SystemArgument2 = Apc->SystemArgument2;
/* Remove the APC from Queue, and release the lock */
RemoveEntryList(ApcListEntry);
Apc->Inserted = FALSE;
KiReleaseApcLock(&ApcLock);
/* Call the kernel routine */
KernelRoutine(Apc,
&NormalRoutine,
&NormalContext,
&SystemArgument1,
&SystemArgument2);
/* Check if there's no normal routine */
if (!NormalRoutine)
{
/* Check if more User APCs are Pending */
KeTestAlertThread(UserMode);
}
else
{
/* Set up the Trap Frame and prepare for Execution in NTDLL.DLL */
KiInitializeUserApc(ExceptionFrame,
TrapFrame,
NormalRoutine,
NormalContext,
SystemArgument1,
SystemArgument2);
}
}
Quickie:
/* Make sure we're still in the same process */
if (Process != Thread->ApcState.Process)
{
/* Erm, we got attached or something! BAD! */
KeBugCheckEx(INVALID_PROCESS_ATTACH_ATTEMPT,
(ULONG_PTR)Process,
(ULONG_PTR)Thread->ApcState.Process,
Thread->ApcStateIndex,
KeGetCurrentPrcb()->DpcRoutineActive);
}
/* Restore the trap frame */
Thread->TrapFrame = OldTrapFrame;
}
An
asynchronous procedure call (APC) is a function that executes
asynchronously. APCs are similar to deferred procedure calls (DPCs), but
unlike DPCs, APCs execute within the context of a particular thread.
Drivers (other than file systems and file-system filter drivers) do not
use APCs directly, but other parts of the operating system do, so you
need to be aware of how APCs work.
The Windows operating system uses three kinds of APCs:
1.
User APCs run strictly in user mode and only when the current thread is
in an alertable wait state. The operating system uses user APCs to
implement mechanisms such as overlapped I/O and the QueueUserApc Win32
routine.
2. Normal kernel APCs run in kernel
mode at IRQL = PASSIVE_LEVEL. A normal kernel APC preempts all user-mode
code, including user APCs. Normal kernel APCs are generally used by
file systems and file-system filter drivers.
3.
Special kernel APCs run in kernel mode at IRQL = APC_LEVEL. A special
kernel APC preempts user-mode code and kernel-mode code that executes at
IRQL = PASSIVE_LEVEL, including both user APCs and normal kernel APCs.
The operating system uses special kernel APCs to handle operations such
as I/O request completion.
其實總結一下很簡單,一個用戶模式的Apc,一個內核模式的Apc(分為NormalRoutine為NULL的SpecialKernelApc和不為NULL的NormalKernelApc)
區別在於,
SpecialKernelApc只執行KernelRoutine,
IRQL為APC_LEVEL,而
NormalKernelApc不僅僅執行 KernelRoutine還執行
NormalRoutine,在PASSIVE_LEVEL下執行NormalRoutine。
但是I/O API也沒有說明他內部用的是哪個類型的Kernel APC,要穩一點就判斷總開關
SpecialApcDisable。不過一般MSDN都會說明。
3. KeAreApcsDisabled/KeAreAllApcsDisabled
BOOLEAN
NTAPI
KeAreApcsDisabled(VOID)
{
/* Return the Kernel APC State */
return KeGetCurrentThread()->CombinedApcDisable ? TRUE : FALSE;
}
BOOLEAN
NTAPI
KeAreAllApcsDisabled(VOID)
{
/* Return the Special APC State */
return ((KeGetCurrentThread()->SpecialApcDisable) ||
(KeGetCurrentIrql() >= APC_LEVEL)) ? TRUE : FALSE;
}
typedef struct _KTHREAD
{
......
union
{
struct
{
SHORT KernelApcDisable;
SHORT SpecialApcDisable;
};
ULONG CombinedApcDisable;
};
......
};
主要看這兩個API有什麼區別,可能很多人看不出來什麼區別。。。
我也理解了很久,
KeAreApcsDisabled是說只要當前在內核臨界區內就是Disable狀態,這個可以是子開關KernelApcDisable或者是總開關SpecialApcDisable至少一個有值,要是
SpecialApcDisable則就是所有Apc都是無效狀態(與KeAreAllApcsDisabled判斷相同),要是KernelApcDisable就是Normal KernelApc失效;要是用的 KeEnterCriticalRegion就只能用這個函數檢查,一般用這個就可以了,當然最好if ( KeAreApcsDisabled() || __readcr8() == APC_LEVEL )
而KeAreAllApcsDisabled則是真正意義上的所以APC都無效,但是對於KeEnterCriticalRegion的臨界區這個API是無法判斷的。
4. 解決辦法
當出現無法調用I/O API的時候,建議使用勞務線程,這個線程的執行環境還是比較穩定的,而且就算是做同步響應也不會耗時很久。例如:
typedef struct tag_FyWorkQueueItem
{
WORK_QUEUE_ITEM WorkQueueItem;
PVOID lpParameter1;
PVOID lpParameter2;
PVOID lpParameter3;
KEVENT CompleteEvent;
BOOL bStatus;
} FyWorkQueueItem, *PFyWorkQueueItem;
PUNICODE_STRING QueryProcessObjectName(IN HANDLE ProcessId)
{
NTSTATUS Status = STATUS_SUCCESS;
PEPROCESS EProcess = NULL;
HANDLE hProcess = NULL;
ULONG ulRealSize = 0;
PUNICODE_STRING lpuniImageFileName = NULL;
BOOL bSuccess = FALSE;
if (KeGetCurrentIrql() <= APC_LEVEL)
{
Status = PsLookupProcessByProcessId(ProcessId, &EProcess);
if (NT_SUCCESS(Status) && EProcess)
{
Status = ObOpenObjectByPointer((PVOID)EProcess, OBJ_KERNEL_HANDLE, NULL,
PROCESS_ALL_ACCESS, NULL, KernelMode, &hProcess);
if (NT_SUCCESS(Status))
{
Status = ZwQueryInformationProcess(hProcess, ProcessImageFileName, NULL, 0, &ulRealSize);
if (Status == STATUS_INFO_LENGTH_MISMATCH)
{
lpuniImageFileName = (PUNICODE_STRING)ExAllocatePoolWithTag(NonPagedPool,
ulRealSize + sizeof(UNICODE_STRING), 'hiti');
if (lpuniImageFileName)
{
memset(lpuniImageFileName, 0, ulRealSize + sizeof(UNICODE_STRING));
Status = ZwQueryInformationProcess(hProcess, ProcessImageFileName,
lpuniImageFileName, ulRealSize + sizeof(UNICODE_STRING), &ulRealSize);
if (NT_SUCCESS(Status))
{
bSuccess = TRUE;
}
}
}
ZwClose(hProcess);
}
ObDereferenceObject(EProcess);
}
}
if (!bSuccess)
{
ExFreePool(lpuniImageFileName);
lpuniImageFileName = NULL;
}
return lpuniImageFileName;
}
BOOL GetProcessImageFileName(
IN HANDLE ProcessId,
OUT WCHAR* lpwzImageFileName,
IN ULONG uMaxSize)
{
NTSTATUS Status = STATUS_SUCCESS;
HANDLE FileHandle = NULL;
IO_STATUS_BLOCK IoStatusBlock = { 0 };
PUNICODE_STRING lpuniProcessObjectName = NULL;
OBJECT_ATTRIBUTES oa = { 0 };
PFILE_OBJECT FileObject = NULL;
POBJECT_NAME_INFORMATION ObjectNameInformation = NULL;
BOOL bStatus = FALSE;
if (KeGetCurrentIrql() > PASSIVE_LEVEL) {
return FALSE;
}
lpuniProcessObjectName = QueryProcessObjectName(ProcessId);
if (!lpuniProcessObjectName) {
return FALSE;
}
InitializeObjectAttributes(&oa, lpuniProcessObjectName, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, NULL);
Status = IoCreateFile(
&FileHandle,
FILE_READ_ATTRIBUTES,
&oa,
&IoStatusBlock,
NULL,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_WRITE,
FILE_OPEN,
FILE_NON_DIRECTORY_FILE,
NULL,
0,
CreateFileTypeNone,
NULL,
IO_NO_PARAMETER_CHECKING);
if (!NT_SUCCESS(Status))
{
ExFreePool(lpuniProcessObjectName);
return FALSE;
}
Status = ObReferenceObjectByHandle(FileHandle, FILE_ANY_ACCESS, *IoFileObjectType,
KernelMode, (PVOID*)&FileObject, NULL);
if (NT_SUCCESS(Status) && FileObject)
{
Status = IoQueryFileDosDeviceName(FileObject, &ObjectNameInformation);
if (NT_SUCCESS(Status))
{
if (ObjectNameInformation)
{
if (ObjectNameInformation->Name.Length <= sizeof(WCHAR) * uMaxSize)
{
memset(lpwzImageFileName, 0, 2 * uMaxSize);
memcpy(lpwzImageFileName, ObjectNameInformation->Name.Buffer,
ObjectNameInformation->Name.Length);
bStatus = TRUE;
}
ExFreePool(ObjectNameInformation);
ObjectNameInformation = NULL;
}
}
ObDereferenceObject(FileObject);
}
ObCloseHandle(FileHandle, KernelMode);
FileHandle = NULL;
ExFreePool(lpuniProcessObjectName);
return bStatus;
}
VOID QueryProcessFileNameWorkItem(IN PFyWorkQueueItem lpFyWorkQueueItem)
{
lpFyWorkQueueItem->bStatus = GetProcessImageFileName(
(HANDLE)lpFyWorkQueueItem->lpParameter1,
(WCHAR*)lpFyWorkQueueItem->lpParameter2,
(ULONG)lpFyWorkQueueItem->lpParameter3);
KeSetEvent(&lpFyWorkQueueItem->CompleteEvent, IO_NO_INCREMENT, FALSE);
}
BOOL GetProcessImageFileNameSafeIrql(
IN HANDLE ProcessId,
OUT WCHAR* lpwzImageFileName,
IN ULONG uMaxSize)
{
BOOL bStatus;
FyWorkQueueItem WorkItem;
if (KeGetCurrentIrql() <= APC_LEVEL)
{
if (KeAreApcsDisabled() || KeGetCurrentIrql() == APC_LEVEL)
{
memset(&WorkItem, 0, sizeof(WorkItem));
KeInitializeEvent(&WorkItem.CompleteEvent, NotificationEvent, FALSE);
WorkItem.bStatus = FALSE;
WorkItem.WorkQueueItem.List.Flink = NULL;
WorkItem.WorkQueueItem.WorkerRoutine = (PWORKER_THREAD_ROUTINE)QueryProcessFileNameWorkItem;
WorkItem.lpParameter1 = (PVOID)ProcessId;
WorkItem.lpParameter2 = (PVOID)lpwzImageFileName;
WorkItem.lpParameter3 = (PVOID)uMaxSize;
WorkItem.WorkQueueItem.Parameter = &WorkItem;
ExQueueWorkItem(&WorkItem.WorkQueueItem, DelayedWorkQueue);
KeWaitForSingleObject(&WorkItem.CompleteEvent, Executive, KernelMode, FALSE, NULL);
bStatus = WorkItem.bStatus;
}
else
{
bStatus = GetProcessImageFileName(ProcessId, lpwzImageFileName, uMaxSize);
}
}
else
{
bStatus = FALSE;
}
return bStatus;
}
閱讀更多 看雪學院 的文章