【问题标题】:How do i properly implement threads in Windows Kernel Driver?如何在 Windows 内核驱动程序中正确实现线程?
【发布时间】:2025-11-28 13:25:07
【问题描述】:

我正在尝试学习如何编写 Windows 内核驱动程序。 在我的驱动程序中,我有 2 个线程在某个时候使用 PsCreateSystemThread

创建

我有一个名为 Kill 的全局变量,它表示线程像这样终止。

VOID AThread(IN PVOID Context)
{
    for (;;)
    {
        if(Kill == True)
            break;

        KmWriteProcessMemory(rProcess, &NewValue, dwAAddr, sizeof(NewValue));
    }


    PsTerminateSystemThread(STATUS_SUCCESS);
}

在我的卸载功能中,我设置Kill = TRUE

VOID f_DriverUnload(PDRIVER_OBJECT pDriverObject)
{
    Kill = TRUE;
    IoDeleteSymbolicLink(&SymLinkName);
    IoDeleteDevice(pDeviceObject);
    DbgPrint("Driver Unloaded successfully..\r\n");
}

大多数时候没有问题,但有时当我尝试卸载驱动程序时机器会崩溃。当我在线程中使用某种睡眠功能时,它会更频繁地发生,所以我假设它正在崩溃,因为在驱动程序尝试卸载之前线程尚未终止。

我不太清楚如何使用同步等,而且我找不到很多明确的信息。那么如何正确实现线程并确保在卸载驱动程序之前终止它们?

【问题讨论】:

    标签: c windows multithreading kernel-mode


    【解决方案1】:

    创建线程后,您将获得HANDLE threadHandle 结果。然后你需要把这个句柄转换成PETHREAD ThreadObject;

    ObReferenceObjectByHandle(threadHandle,
                              THREAD_ALL_ACCESS,
                              NULL,
                              KernelMode,
                              &ThreadObject,
                              NULL );
    

    并关闭threadHandle:

    ZwClose(threadHandle);
    

    当你想停止线程时,设置标志并等待线程完成:

    Kill = TRUE;
    
    KeWaitForSingleObject(ThreadObject,
                        Executive,
                        KernelMode,
                        FALSE,
                        NULL );
    
    ObDereferenceObject(ThreadObject);
    

    那么f_DriverUnload函数可能会退出。

    你可以在这里看到所有这些东西:https://github.com/Microsoft/Windows-driver-samples/tree/master/general/cancel/sys

    查看 cancel.h 和 cancel.c 文件。此外,此代码使用信号量而不是全局标志来停止线程。

    【讨论】:

    • @MichaelStrobel - 存在更好的解决方案
    • 需要详细说明吗?
    【解决方案2】:

    当您创建使用您的驱动程序的线程时,当然不能卸载驱动程序,直到线程不退出。为此需要在创建线程之前为您的驱动程序对象调用ObfReferenceObject。如果创建线程失败 - 调用 ObfDereferenceObject。当线程退出时 - 需要调用ObfDereferenceObject。但这是一个问题 - 如何/从哪里调用这个?从线程例程结束调用ObfDereferenceObject 没有意义 - 驱动程序可以在ObfDereferenceObject 内卸载,我们从调用返回到不存在的内存位置。理想情况下,如果外部代码(Windows 本身)在线程返回之后调用它。

    寻找IoAllocateWorkItem 的好例子。工作项——比如线程,并且驱动程序不能被卸载,直到WorkerRoutine 不返回。在这里系统关心这个 - 为此我们将DeviceObject传递给IoAllocateWorkItem指向调用者的驱动程序对象或调用者的设备对象之一的指针。 - 系统引用这个对象(设备或driver) 当我们调用IoQueueWorkItem 时,这是保证在WorkerRoutine 执行期间不会卸载驱动程序。当它返回时 - Windows 调用 ObfDereferenceObject 以获取传递的设备或驱动程序对象。在这里一切正常,因为我们在此之后返回系统内核代码(而不是驱动程序)。但不幸的是PsCreateSystemThread 没有指向驱动程序对象,也没有实现这样的功能。

    另一个很好的例子FreeLibraryAndExitThread - 驱动程序实际上是内核模式dll,可以加载和卸载。和FreeLibraryAndExitThread 完全实现了我们需要的功能,但仅适用于用户模式dll。在内核模式下再次没有这样的 api。

    但无论如何解决方案都是可能的。可能在线程执行结束时跳转(不调用)到ObfDereferenceObject,但为此需要使用汇编代码。在 c/c++ 中不可能做到这一点。

    首先让我们在全局变量中声明指向驱动程序对象的指针——我们将它初始化为驱动程序入口点中的有效值。

    extern "C" PVOID g_DriverObject;
    

    比起一些宏来获得混乱的c++名称,这需要在asm文件中使用它:

    #if 0
    #define __ASM_FUNCTION __pragma(message(__FUNCDNAME__" proc\r\n" __FUNCDNAME__ " endp"))
    #define _ASM_FUNCTION {__ASM_FUNCTION;}
    #define ASM_FUNCTION {__ASM_FUNCTION;return 0;}
    #define CPP_FUNCTION __pragma(message("extern " __FUNCDNAME__ " : PROC ; "  __FUNCSIG__))
    #else
    #define _ASM_FUNCTION
    #define ASM_FUNCTION
    #define CPP_FUNCTION
    #endif
    

    c++中我们为线程声明了2个函数:

    VOID _AThread(IN PVOID Context)_ASM_FUNCTION;
    
    VOID __fastcall AThread(IN PVOID Context)
    {
        CPP_FUNCTION;
        // some code here
        // but not call PsTerminateSystemThread !!
    }
    

    不要忘记 AThread 上的 __fastcall - 对于 x86 这需要

    现在我们用下一个代码创建线程:

        ObfReferenceObject(g_DriverObject);
        HANDLE hThread;
        if (0 > PsCreateSystemThread(&hThread, 0, 0, 0, 0, _AThread, ctx)) 
        {
            ObfDereferenceObject(g_DriverObject);
        }
        else
        {
            NtClose(hThread);
        }
    

    所以你将线程入口点设置为_AThread,这将在asm 文件中实现。一开始你打电话给ObfReferenceObject(g_DriverObject);_AThread 会在 c++ 中调用你实际的线程实现 AThread。最后它返回到_AThread(因为这个你不能调用PsTerminateSystemThread。无论如何调用这个api是可选的——当线程例程将控制权返回给系统时——这将被自动调用)。和最后的_AThread 取消引用g_DriverObject 并返回系统。

    asm 文件中的主要技巧。这里有 2 个用于 x86 和 x64 的 asm:

    x86:

    .686p
    
    extern _g_DriverObject:DWORD
    extern __imp_@ObfDereferenceObject@4:DWORD
    extern ?AThread@@YIXPAX@Z : PROC ; void __fastcall AThread(void *)
    
    _TEXT segment
    
    ?_AThread@@YGXPAX@Z proc
            pop ecx
            xchg ecx,[esp]
            call ?AThread@@YIXPAX@Z
            mov ecx,_g_DriverObject
            jmp __imp_@ObfDereferenceObject@4
    ?_AThread@@YGXPAX@Z endp
    
    _TEXT ends
    
    END
    

    x64:

    extern g_DriverObject:QWORD
    extern __imp_ObfDereferenceObject:QWORD
    extern ?AThread@@YAXPEAX@Z : PROC ; void __cdecl AThread(void *)
    
    _TEXT segment 'CODE'
    
    ?_AThread@@YAXPEAX@Z proc
        sub rsp,28h
        call ?AThread@@YAXPEAX@Z
        add rsp,28h
        mov rcx,g_DriverObject
        jmp __imp_ObfDereferenceObject
    ?_AThread@@YAXPEAX@Z endp
    
    _TEXT ENDS
    
    END
    

    【讨论】: