/ Windows / 198浏览

线程挂起和恢复

SuspendThread 挂起线程

调用链

SuspendThread KERNEL32!SuspendThreadStubKERNELBASE!SuspendThreadntdll!NtSuspendThreadPsSuspendThreadKeSuspendThreadKiSuspendThreadKiInsertQueueApc

过程分析

未挂起前:

  • 查看线程的 apc 列表:(空的,表示目前没有 apc)
  • 查看线程的结构,看看 SuspendCount,通过 dt _KTHREAD ffffd3071fedd080

通过 SuspendThread 挂起线程后,再查看一下状态:

  • 线程的 apc 列表,增加了一个 ffffd3071fedd308 的 APC 对象
  • KTHREAD 中的 SuspendCount 也增加了
  • 再详细的看下增加的 APC 中包含什么内容

基本发现,就是塞进去了一个 nt!KiSchedulerApc 的方法。猜测这个方法是一直卡住,执行不完的。(Win2003中KiSuspendThread 逻辑就是等待 SuspendCount 变成0,KiSchedulerApc 中应该也是类似的逻辑)

💡 KiSchedulerApc 是在 Win10 22H2 上看到的,在 KiInsertQueueApc 中通过 Thread→SchedulerApc 塞进去的KiSchedulerApc (结合UAC的 token 校验不过,直接终止,猜测其中包含 token 的判断逻辑);查看 Win2003 的代码(reactos也是)是通过 Thread→SuspendApc,塞进去的 KiSuspendThread 。代码如下:

VOID
KiSuspendThread (
    IN PVOID NormalContext,
    IN PVOID SystemArgument1,
    IN PVOID SystemArgument2
    )

/*++

Routine Description:

    This function is the kernel routine for the builtin suspend APC of a
    thread. It is executed as the result of queuing the builtin suspend
    APC and suspends thread execution by waiting nonalerable on the thread's
    builtin suspend semaphore. When the thread is resumed, execution of
    thread is continued by simply returning.

Arguments:

    NormalContext - Not used.

    SystemArgument1 - Not used.

    SystemArgument2 - Not used.

Return Value:

    None.

--*/

{

    PKTHREAD Thread;

    UNREFERENCED_PARAMETER(NormalContext);
    UNREFERENCED_PARAMETER(SystemArgument1);
    UNREFERENCED_PARAMETER(SystemArgument2);

    //
    // Get the address of the current thread object and Wait nonalertable on
    // the thread's builtin suspend semaphore.
    //

    Thread = KeGetCurrentThread();
    KeWaitForSingleObject(&Thread->SuspendSemaphore,
                          Suspended,
                          KernelMode,
                          FALSE,
                          NULL);

    return;
}

总结

可以看到其实挂起线程相对比较简单,只是插入了一个 APC,让此线程一直无法执行而已。

关于 APC 可以看这里:

ResumeThread 恢复线程

调用链

ResumeThread KERNEL32!ResumeThreadStubKERNELBASE!ResumeThreadntdll!NtResumeThreadPsResumeThread KeResumeThread KiWaitTest KiUnwaitThreadKiReadyThreadKiDeferredReadyThread

详细过程

KeResumeThread 代码如下:

ULONG
NTAPI
KeResumeThread(IN PKTHREAD Thread)
{
    KLOCK_QUEUE_HANDLE ApcLock;
    ULONG PreviousCount;
    ASSERT_THREAD(Thread);
    ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);

    /* Lock the APC Queue */
    KiAcquireApcLockRaiseToSynch(Thread, &ApcLock);

    /* Save the Old Count */
    PreviousCount = Thread->SuspendCount;

    /* Check if it existed */
    if (PreviousCount)
    {
        /* Decrease the suspend count */
        Thread->SuspendCount--;

        /* Check if the thrad is still suspended or not */
        if ((!Thread->SuspendCount) && (!Thread->FreezeCount))
        {
            /* Acquire the dispatcher lock */
            KiAcquireDispatcherLockAtSynchLevel();

            /* Signal the Suspend Semaphore */
            Thread->SuspendSemaphore.Header.SignalState++;
            KiWaitTest(&Thread->SuspendSemaphore.Header, IO_NO_INCREMENT);

            /* Release the dispatcher lock */
            KiReleaseDispatcherLockFromSynchLevel();
        }
    }

    /* Release APC Queue lock and return the Old State */
    KiReleaseApcLockFromSynchLevel(&ApcLock);
    KiExitDispatcher(ApcLock.OldIrql);
    return PreviousCount;
}

KeResumeThread 核心做了几件事:

  • 根据 SuspendCount 和 FreezeCount 来判断是否需要恢复线程
  • 通过发送信号量,调用 KiWaitTest 来恢复线程

KiWaitTest 的代码如下:

VOID
FASTCALL
KiWaitTest(IN PVOID ObjectPointer,
           IN KPRIORITY Increment)
{
    PLIST_ENTRY WaitEntry, WaitList;
    PKWAIT_BLOCK WaitBlock;
    PKTHREAD WaitThread;
    PKMUTANT FirstObject = ObjectPointer;
    NTSTATUS WaitStatus;

    /* Loop the Wait Entries */
    WaitList = &FirstObject->Header.WaitListHead;
    WaitEntry = WaitList->Flink;
    while ((FirstObject->Header.SignalState > 0) && (WaitEntry != WaitList))
    {
        /* Get the current wait block */
        WaitBlock = CONTAINING_RECORD(WaitEntry, KWAIT_BLOCK, WaitListEntry);
        WaitThread = WaitBlock->Thread;
        WaitStatus = STATUS_KERNEL_APC;

        /* Check the current Wait Mode */
        if (WaitBlock->WaitType == WaitAny)
        {
            /* Easy case, satisfy only this wait */
            WaitStatus = (NTSTATUS)WaitBlock->WaitKey;
            KiSatisfyObjectWait(FirstObject, WaitThread);
        }

        /* Now do the rest of the unwait */
        KiUnwaitThread(WaitThread, WaitStatus, Increment);
        WaitEntry = WaitList->Flink;
    }
}

KiWaitTest 主要做了以下事情:

  • 通过等待对象找到对象的等待队列
  • 将等待队列中的每一个线程通过 KiUnwaitThread 来进行唤醒
  • 其中 KiSatisfyObjectWait 就是通过设置信号状态,唤醒等待线程、切换上下文来响应等待的线程(这里详述看

KiUnwaitThread 的代码如下:

VOID
FASTCALL
KiUnwaitThread(IN PKTHREAD Thread,
               IN LONG_PTR WaitStatus,
               IN KPRIORITY Increment)
{
    /* Unlink the thread */
    KiUnlinkThread(Thread, WaitStatus);

    /* Tell the scheduler do to the increment when it readies the thread */
    ASSERT(Increment >= 0);
    Thread->AdjustIncrement = (SCHAR)Increment;
    Thread->AdjustReason = AdjustUnwait;

    /* Reschedule the Thread */
    KiReadyThread(Thread);
}

KiUnwaitThread 主要分两块:

  • KiUnlinkThread:主要将线程从它正在等待的内核对象队列中移除,即不等待任何对象
  • KiReadyThread:主要根据线程的状态(优先级、是否被分页了等),将线程置于 ready 状态,待 CPU 调度

总结

所谓的恢复线程,就是将线程的各种状态修改成 Ready 状态,然后告诉系统,我可以被调度了,然后等待 CPU 调度。这里的核心在于线程的调度。

参考链接:

  1. Anatomy of the thread suspension mechanism in Windows (Windows Internals)
  2. https://github.com/reactos/reactos/tree/master
Windows是如何区分互联网下载文件和本地文件的
Windows是如何区分互联网下载文件和本地文件的
如何低成本的获取到应用卡顿情况
如何低成本的获取到应用卡顿情况
【译】ETW 堆跟踪 – 每个分配都被记录
【译】ETW 堆跟踪 – 每个分配都被记录
【译】Wait Analysis – 寻找空闲时间
【译】Wait Analysis – 寻找空闲时间
如何通过 ETW Provider 来记录应用日志
如何通过 ETW Provider 来记录应用日志
如何通过 C++ 实时监听 ETW 事件
如何通过 C++ 实时监听 ETW 事件

0

  1. This post has no comment yet

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注