WaitForSingleObject分析(二)

一、 从 WaitForSingleObject 开始

先看 kernel32!WaitForSingleObject:

c 复制代码
.text:77E2BA90 ; DWORD __stdcall WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds)
.text:77E2BA90                 public _WaitForSingleObject@8
.text:77E2BA90 _WaitForSingleObject@8 proc near        ; CODE XREF: WerpHeapLock(_WER_HEAP_MAIN_HEADER *)+18↑p
.text:77E2BA90                                         ; WerpReportFaultInternal(x,x)+1F5CC↓p ...
.text:77E2BA90
.text:77E2BA90 hHandle         = dword ptr  8
.text:77E2BA90 dwMilliseconds  = dword ptr  0Ch
.text:77E2BA90
.text:77E2BA90                 mov     edi, edi
.text:77E2BA92                 push    ebp
.text:77E2BA93                 mov     ebp, esp
.text:77E2BA95                 push    0               ; 参数3:bAlertable
.text:77E2BA97                 push    [ebp+dwMilliseconds] ; 参数2:dwMilliseconds
.text:77E2BA9A                 push    [ebp+hHandle]   ; 参数1:hHandle
.text:77E2BA9D                 call    _WaitForSingleObjectExImplementation@12 ; WaitForSingleObjectExImplementation(x,x,x)
.text:77E2BAA2                 pop     ebp
.text:77E2BAA3                 retn    8
.text:77E2BAA3 _WaitForSingleObject@8 endp

这个函数本身几乎没有什么实质逻辑,它只是一个很薄的封装层。

从调用过程可以看出:

  • 原始参数有两个:hHandledwMilliseconds
  • 它额外压入了一个 0
  • 这个 0 对应第三个参数 bAlertable

也就是说,WaitForSingleObject 实际上只是固定传入:bAlertable = FALSE;

然后转调:

c 复制代码
WaitForSingleObjectExImplementation(hHandle, dwMilliseconds, FALSE);

所以这里可以先得到一个很重要的结论:

WaitForSingleObject 本质上只是 WaitForSingleObjectEx 的一个简化包装版本,区别在于它把 bAlertable 固定为了 FALSE。

二、WaitForSingleObjectExImplementation 的作用

接着看 kernel32!WaitForSingleObjectExImplementation

c 复制代码
.text:77E2BAB0 ; DWORD __stdcall WaitForSingleObjectExImplementation(HANDLE hHandle, DWORD dwMilliseconds, BOOL bAlertable)
.text:77E2BAB0                 public _WaitForSingleObjectExImplementation@12
.text:77E2BAB0 _WaitForSingleObjectExImplementation@12 proc near
.text:77E2BAB0                                         ; CODE XREF: WaitForSingleObject(x,x)+D↑p
.text:77E2BAB0                                         ; DATA XREF: .text:off_77E94FEC↓o
.text:77E2BAB0
.text:77E2BAB0 hHandle         = dword ptr  8
.text:77E2BAB0 dwMilliseconds  = dword ptr  0Ch
.text:77E2BAB0 bAlertable      = dword ptr  10h
.text:77E2BAB0
.text:77E2BAB0 ; FUNCTION CHUNK AT .text:77E5054D SIZE 00000056 BYTES
.text:77E2BAB0
.text:77E2BAB0                 mov     edi, edi
.text:77E2BAB2                 push    ebp
.text:77E2BAB3                 mov     ebp, esp
.text:77E2BAB5                 mov     eax, [ebp+hHandle]
.text:77E2BAB8                 cmp     eax, 0FFFFFFF4h ; 控制台标准错误
.text:77E2BABB                 jz      loc_77E50575
.text:77E2BAC1                 cmp     eax, 0FFFFFFF5h ; 控制台标准输出
.text:77E2BAC4                 jz      loc_77E50561
.text:77E2BACA                 cmp     eax, 0FFFFFFF6h ; 控制台标准输入
.text:77E2BACD                 jz      loc_77E5054D
.text:77E2BAD3
.text:77E2BAD3 loc_77E2BAD3:                           ; CODE XREF: WaitForSingleObjectExImplementation(x,x,x)+24AAC↓j
.text:77E2BAD3                                         ; WaitForSingleObjectExImplementation(x,x,x)+24AC0↓j ...
.text:77E2BAD3                 mov     ecx, eax
.text:77E2BAD5                 and     ecx, 10000003h  ; 判断是不是特殊句柄
.text:77E2BADB                 push    esi             ; 不是传参,备份寄存器
.text:77E2BADC                 mov     esi, eax
.text:77E2BADE                 cmp     ecx, 3
.text:77E2BAE1                 jz      loc_77E50589    ; 走特殊句柄处理逻辑
.text:77E2BAE7
.text:77E2BAE7 loc_77E2BAE7:                           ; CODE XREF: WaitForSingleObjectExImplementation(x,x,x)+24AE1↓j
.text:77E2BAE7                                         ; WaitForSingleObjectExImplementation(x,x,x)+24AEE↓j
.text:77E2BAE7                 push    [ebp+bAlertable] ; 参数3:bAlertable
.text:77E2BAEA                 push    [ebp+dwMilliseconds] ; 参数2:dwMilliseconds
.text:77E2BAED                 push    esi             ; 参数1:hHandle
.text:77E2BAEE                 call    _WaitForSingleObjectEx@12 ; WaitForSingleObjectEx(x,x,x)
.text:77E2BAF3                 pop     esi
.text:77E2BAF4                 pop     ebp
.text:77E2BAF5                 retn    0Ch
.text:77E2BAF5 _WaitForSingleObjectExImplementation@12 endp

这个函数开始出现了真正的分发逻辑。

2.1 先检查几个特殊标准句柄

前面连续比较了三个值:

  • 0xFFFFFFF6 → 标准输入 STD_INPUT_HANDLE(-10)
  • 0xFFFFFFF5 → 标准输出 STD_OUTPUT_HANDLE(-11)
  • 0xFFFFFFF4 → 标准错误 STD_ERROR_HANDLE(-12)

这说明 WaitForSingleObjectExImplementation 并不是拿到句柄后就立刻往下传,而是先判断传入的是否为这些 控制台标准句柄常量。

如果命中这些值,就跳到对应分支单独处理,而不是走普通等待路径。

这一步的意义可以理解为:

kernel32 在真正进入更底层等待逻辑之前,会先过滤并处理一部分 Win32 层定义的"特殊句柄值"。

2.2 再检查另一类特殊句柄

c 复制代码
.text:77E2BAD3                 mov     ecx, eax
.text:77E2BAD5                 and     ecx, 10000003h  ; 判断是不是特殊句柄
.text:77E2BADB                 push    esi             ; 不是传参,备份寄存器
.text:77E2BADC                 mov     esi, eax
.text:77E2BADE                 cmp     ecx, 3
.text:77E2BAE1                 jz      loc_77E50589    ; 走特殊句柄处理逻辑

这一段说明它还会根据句柄的某些特征位,判断当前句柄是否属于另一类特殊情况。

如果满足条件,就跳转到专门分支处理;如果不满足,才继续往下调用真正的 KernelBase!WaitForSingleObjectEx

2.3 3. 普通情况下,继续调用 WaitForSingleObjectEx

如果前面的特殊分支都没有命中,那么执行流程就非常直接:

c 复制代码
.text:77E2BAE7                 push    [ebp+bAlertable] ; 参数3:bAlertable
.text:77E2BAEA                 push    [ebp+dwMilliseconds] ; 参数2:dwMilliseconds
.text:77E2BAED                 push    esi             ; 参数1:hHandle
.text:77E2BAEE                 call    _WaitForSingleObjectEx@12 

因此,这一层可以总结为:

  • WaitForSingleObject:只是一个简单包装,固定 bAlertable = FALSE
  • WaitForSingleObjectExImplementation:负责做一层 Win32 风格的句柄分类与分发
  • 普通句柄最终会进入 WaitForSingleObjectEx

到目前为止,用户层调用链可以先写成:

c 复制代码
WaitForSingleObject(hHandle, dwMilliseconds)
    -> WaitForSingleObjectExImplementation(hHandle, dwMilliseconds, FALSE)
        -> [检查标准句柄 / 特殊句柄]
        -> WaitForSingleObjectEx(hHandle, dwMilliseconds, FALSE)

三、KernelBase!WaitForSingleObjectEx

c 复制代码
.text:0DCE1730 ; DWORD __stdcall WaitForSingleObjectEx(HANDLE hHandle, DWORD dwMilliseconds, BOOL bAlertable)
.text:0DCE1730                 public _WaitForSingleObjectEx@12
.text:0DCE1730 _WaitForSingleObjectEx@12 proc near     ; CODE XREF: WaitForSingleObject(x,x)+D↓p
.text:0DCE1730                                         ; DATA XREF: .text:off_DCE18FC↓o
.text:0DCE1730
.text:0DCE1730 Frame           = _RTL_CALLER_ALLOCATED_ACTIVATION_CONTEXT_STACK_FRAME_EXTENDED ptr -48h
.text:0DCE1730 var_24          = qword ptr -24h
.text:0DCE1730 status          = dword ptr -1Ch
.text:0DCE1730 ms_exc          = CPPEH_RECORD ptr -18h
.text:0DCE1730 hHandle         = dword ptr  8
.text:0DCE1730 dwMilliseconds  = dword ptr  0Ch
.text:0DCE1730 bAlertable      = dword ptr  10h
.text:0DCE1730
.text:0DCE1730 ; FUNCTION CHUNK AT .text:0DCE1690 SIZE 0000000A BYTES
.text:0DCE1730 ; FUNCTION CHUNK AT .text:0DCE75C4 SIZE 0000002E BYTES
.text:0DCE1730 ; FUNCTION CHUNK AT .text:0DD04853 SIZE 0000000F BYTES
.text:0DCE1730 ; FUNCTION CHUNK AT .text:0DD0ACDC SIZE 00000043 BYTES
.text:0DCE1730
.text:0DCE1730 ; __unwind { // __SEH_prolog4
.text:0DCE1730                 push    38h
.text:0DCE1732                 push    offset stru_DCE17C8
.text:0DCE1737                 call    __SEH_prolog4
.text:0DCE173C                 mov     [ebp+Frame.Size], 24h ; '$'
.text:0DCE1743                 mov     [ebp+Frame.Format], 1
.text:0DCE174A                 push    7
.text:0DCE174C                 pop     ecx
.text:0DCE174D                 xor     eax, eax
.text:0DCE174F                 lea     edi, [ebp+Frame.Frame]
.text:0DCE1752                 rep stosd               ; 创建一个结构
.text:0DCE1754                 xor     ebx, ebx
.text:0DCE1756                 cmp     [ebp+bAlertable], ebx ; 判断bAlertable是否为0
.text:0DCE1759                 jnz     loc_DCE75E2     ; TRUE跳转
.text:0DCE175F
.text:0DCE175F loc_DCE175F:                            ; CODE XREF: WaitForSingleObjectEx(x,x,x)+5EBD↓j
.text:0DCE175F                 mov     [ebp+status], ebx
.text:0DCE1762                 mov     [ebp+ms_exc.registration.TryLevel], ebx
.text:0DCE1765                 mov     esi, [ebp+hHandle]
.text:0DCE1768                 cmp     esi, 0FFFFFFF4h
.text:0DCE176B                 jz      loc_DD0AD04
.text:0DCE1771                 cmp     esi, 0FFFFFFF5h
.text:0DCE1774                 jz      loc_DD0ACF0
.text:0DCE177A                 cmp     esi, 0FFFFFFF6h
.text:0DCE177D                 jz      loc_DD0ACDC
.text:0DCE1783
.text:0DCE1783 loc_DCE1783:                            ; CODE XREF: WaitForSingleObjectEx(x,x,x)+295BB↓j
.text:0DCE1783                                         ; WaitForSingleObjectEx(x,x,x)+295CF↓j ...
.text:0DCE1783                 push    [ebp+dwMilliseconds]
.text:0DCE1786                 lea     eax, [ebp+var_24]
.text:0DCE1789                 push    eax
.text:0DCE178A                 call    _BaseFormatTimeOut@8 ; 把传入的毫秒超时值,转换成 Nt 层等待函数使用的 LARGE_INTEGER 超时格式。
.text:0DCE178F                 mov     edi, eax
.text:0DCE1791
.text:0DCE1791 loc_DCE1791:                            ; CODE XREF: WaitForSingleObjectEx(x,x,x)+5E9F↓j
.text:0DCE1791                 push    edi             ; Timeout
.text:0DCE1792                 push    [ebp+bAlertable] ; Alertable
.text:0DCE1795                 push    esi             ; Handle
.text:0DCE1796                 call    ds:__imp__NtWaitForSingleObject@12 ; NtWaitForSingleObject(x,x,x)
.text:0DCE179C                 mov     [ebp+status], eax
.text:0DCE179F                 cmp     eax, ebx        ; 判断返回值
.text:0DCE17A1                 jl      loc_DD04853     ; 如果小于0跳转,设置错误码返回
.text:0DCE17A7                 cmp     [ebp+bAlertable], ebx ; 判断bAlertable是否为0
.text:0DCE17AA                 jnz     loc_DCE75C4     ; 为1跳转,继续下一步判断是否继续循环
.text:0DCE17B0
.text:0DCE17B0 loc_DCE17B0:                            ; CODE XREF: WaitForSingleObjectEx(x,x,x)+5E99↓j
.text:0DCE17B0                                         ; WaitForSingleObjectEx(x,x,x)+2312D↓j
.text:0DCE17B0                 mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:0DCE17B7                 call    loc_DCE1690
.text:0DCE17BC ; ---------------------------------------------------------------------------
.text:0DCE17BC
.text:0DCE17BC loc_DCE17BC:                            ; CODE XREF: WaitForSingleObjectEx(x,x,x):loc_DCE1699↑j
.text:0DCE17BC                 mov     eax, [ebp+status]
.text:0DCE17BF                 call    __SEH_epilog4
.text:0DCE17C4                 retn    0Ch
.text:0DCE17C4 ; } // starts at DCE1730
.text:0DCE17C4 _WaitForSingleObjectEx@12 endp



.text:0DCE75E2 loc_DCE75E2:                            ; CODE XREF: WaitForSingleObjectEx(x,x,x)+29↑j
.text:0DCE75E2                 xor     edx, edx        ; Context
.text:0DCE75E4                 lea     ecx, [ebp+Frame] ; Frame
.text:0DCE75E7                 call    ds:__imp_@RtlActivateActivationContextUnsafeFast@8 ; RtlActivateActivationContextUnsafeFast(x,x)
.text:0DCE75ED                 jmp     loc_DCE175F

.text:0DD04853 loc_DD04853:                            ; CODE XREF: WaitForSingleObjectEx(x,x,x)+71↑j
.text:0DD04853 ; __unwind { // __SEH_prolog4           ; Status
.text:0DD04853                 push    eax
.text:0DD04854                 call    _BaseSetLastNTError@4 ; BaseSetLastNTError(x)
.text:0DD04859                 or      [ebp+status], 0FFFFFFFFh
.text:0DD0485D                 jmp     loc_DCE17B0

.text:0DCE75C4 loc_DCE75C4:                            ; CODE XREF: WaitForSingleObjectEx(x,x,x)+7A↑j
.text:0DCE75C4 ; __unwind { // __SEH_prolog4
.text:0DCE75C4                 cmp     eax, 101h
.text:0DCE75C9                 jnz     loc_DCE17B0     ;  判断返回值是否为STATUS_ALERTED
.text:0DCE75C9                                         ;  不是就跳转,返回
.text:0DCE75CF                 jmp     loc_DCE1791     ; 是,跳回去继续循环
.text:0DCE75D4 ; ----------------------------------------------------------




.text:0DCE1690 loc_DCE1690:                            ; CODE XREF: WaitForSingleObjectEx(x,x,x)+87↓j
.text:0DCE1690                                         ; WaitForSingleObjectEx(x,x,x)+295EA↓j
.text:0DCE1690 ; __unwind { // __SEH_prolog4
.text:0DCE1690                 cmp     [ebp+bAlertable], ebx
.text:0DCE1693                 jnz     loc_DCE75D4
.text:0DCE1699
.text:0DCE1699 loc_DCE1699:                            ; CODE XREF: WaitForSingleObjectEx(x,x,x)+5EAD↓j
.text:0DCE1699                 retn



.text:0DCE75D4 loc_DCE75D4:                            ; CODE XREF: WaitForSingleObjectEx(x,x,x)-9D↑j
.text:0DCE75D4                 lea     ecx, [ebp+Frame] ; Frame
.text:0DCE75D7                 call    ds:__imp_@RtlDeactivateActivationContextUnsafeFast@4 ; RtlDeactivateActivationContextUnsafeFast(x)
.text:0DCE75DD                 jmp     loc_DCE1699


.text:0DCE1699 loc_DCE1699:                            ; CODE XREF: WaitForSingleObjectEx(x,x,x)+5EAD↓j
.text:0DCE1699                 retn
.text:0DCE1699 ; } // starts at DCE1690

先看函数原型:

c 复制代码
DWORD WINAPI WaitForSingleObjectEx(
    HANDLE hHandle,
    DWORD  dwMilliseconds,
    BOOL   bAlertable
);

从反汇编可以看出,这个函数虽然有一些异常处理和 Activation Context 相关代码,但它的主线逻辑并不复杂,核心流程大致如下:

c 复制代码
WaitForSingleObjectEx(...)
{
    // 1. 如果是 alertable wait,先处理 Activation Context
    // 2. 再次检查特殊句柄
    // 3. 调用 BaseFormatTimeOut 转换超时参数
    // 4. 调用 NtWaitForSingleObject
    // 5. 如果 Nt 调用失败,则设置 LastError 并返回 WAIT_FAILED
    // 6. 如果是 alertable wait 且返回 STATUS_ALERTED,则继续重试
    // 7. 返回等待结果
}

3.1 函数一开始先建立 SEH 环境

函数开头这部分:

c 复制代码
.text:0DCE1730                 push    38h
.text:0DCE1732                 push    offset stru_DCE17C8
.text:0DCE1737                 call    __SEH_prolog4

这是典型的 MSVC 编译器生成代码,用来建立 SEH 异常处理框架。

这一部分不是等待逻辑的核心,可以在文章里简单带过,不需要展开分析。

后面还有一段:

c 复制代码
.text:0DCE173C                 mov     [ebp+Frame.Size], 24h ; '$'
.text:0DCE1743                 mov     [ebp+Frame.Format], 1
.text:0DCE174A                 push    7
.text:0DCE174C                 pop     ecx
.text:0DCE174D                 xor     eax, eax
.text:0DCE174F                 lea     edi, [ebp+Frame.Frame]
.text:0DCE1752                 rep stosd               ; 创建一个结构

它是在初始化一个 _RTL_CALLER_ALLOCATED_ACTIVATION_CONTEXT_STACK_FRAME_EXTENDED 结构,供后面的 Activation Context 相关调用使用。

这部分也不用展开太深,只需要知道:

WaitForSingleObjectEx 在某些情况下会临时激活当前线程的 Activation Context,因此函数开头先准备了对应的栈上结构。

3.2 当 bAlertable != FALSE 时,会先激活 Activation Context

这一段:

c 复制代码
.text:0DCE1754                 xor     ebx, ebx
.text:0DCE1756                 cmp     [ebp+bAlertable], ebx ; 判断bAlertable是否为0
.text:0DCE1759                 jnz     loc_DCE75E2     ; TRUE跳转

如果 bAlertable 不为 0,就跳到:

c 复制代码
.text:0DCE75E2 loc_DCE75E2:                            ; CODE XREF: WaitForSingleObjectEx(x,x,x)+29↑j
.text:0DCE75E2                 xor     edx, edx        ; Context
.text:0DCE75E4                 lea     ecx, [ebp+Frame] ; Frame
.text:0DCE75E7                 call    ds:__imp_@RtlActivateActivationContextUnsafeFast@8 ; RtlActivateActivationContextUnsafeFast(x,x)
.text:0DCE75ED                 jmp     loc_DCE175F
  • bAlertable == 0:直接继续往下执行
  • bAlertable != 0:先调用 RtlActivateActivationContextUnsafeFast,然后再继续主流程

当调用者请求 alertable wait 时,KernelBase 会先做一层 Activation Context 相关处理,然后再进入真正的等待逻辑。

3.3 KernelBase 中再次检查特殊句柄

接下来主流程从这里开始:

c 复制代码
.text:0DCE1765                 mov     esi, [ebp+hHandle]
.text:0DCE1768                 cmp     esi, 0FFFFFFF4h
.text:0DCE176B                 jz      loc_DD0AD04
.text:0DCE1771                 cmp     esi, 0FFFFFFF5h
.text:0DCE1774                 jz      loc_DD0ACF0
.text:0DCE177A                 cmp     esi, 0FFFFFFF6h
.text:0DCE177D                 jz      loc_DD0ACDC

这三个值仍然是:

  • 0xFFFFFFF6 → STD_INPUT_HANDLE(-10)
  • 0xFFFFFFF5 → STD_OUTPUT_HANDLE(-11)
  • 0xFFFFFFF4 → STD_ERROR_HANDLE(-12)

3.4 BaseFormatTimeOut:把毫秒超时值转换成 Nt 层可用格式

如果句柄不是前面的特殊值,就会走到这里:

c 复制代码
.text:0DCE1783                 push    [ebp+dwMilliseconds]
.text:0DCE1786                 lea     eax, [ebp+var_24]
.text:0DCE1789                 push    eax
.text:0DCE178A                 call    _BaseFormatTimeOut@8 ; 把传入的毫秒超时值,转换成 Nt 层等待函数使用的 LARGE_INTEGER 超时格式。
.text:0DCE178F                 mov     edi, eax

把 Win32 层的 dwMilliseconds 转换成 NtWaitForSingleObject 所需要的超时参数格式。

3.5 真正调用 NtWaitForSingleObject

接下来就是核心调用:

c 复制代码
.text:0DCE1791                 push    edi             ; Timeout
.text:0DCE1792                 push    [ebp+bAlertable] ; Alertable
.text:0DCE1795                 push    esi             ; Handle
.text:0DCE1796                 call    ds:__imp__NtWaitForSingleObject@12 ; NtWaitForSingleObject(x,x,x)
.text:0DCE179C                 mov     [ebp+status], eax

也就是说,用户层这条调用链最终在这里进入 Native API:

c 复制代码
status = NtWaitForSingleObject(
    hHandle,
    bAlertable,
    Timeout
);

3.6 如果 NtWaitForSingleObject 返回失败,转换成 Win32 错误风格

调用返回后先判断:

c 复制代码
.text:0DCE179F                 cmp     eax, ebx        ; 判断返回值
.text:0DCE17A1                 jl      loc_DD04853     ; 如果小于0跳转,设置错误码返回

因为 ebx 前面被清零了,所以这里等价于:

c 复制代码
if ((LONG)status < 0)

也就是判断 NTSTATUS 是否为失败状态。

失败时走到:

c 复制代码
.text:0DD04853                 push    eax
.text:0DD04854                 call    _BaseSetLastNTError@4 ; BaseSetLastNTError(x)
.text:0DD04859                 or      [ebp+status], 0FFFFFFFFh
.text:0DD0485D                 jmp     loc_DCE17B0

这里逻辑很典型:

  • 调用 BaseSetLastNTErrorNTSTATUS 转换成线程的 LastError
  • 然后把返回值改成 0xFFFFFFFF

3.7 bAlertable == TRUE 时,对 STATUS_ALERTED 做了重试处理

c 复制代码
.text:0DCE17A7                 cmp     [ebp+bAlertable], ebx ; 判断bAlertable是否为0
.text:0DCE17AA                 jnz     loc_DCE75C4     ; 为1跳转,继续下一步判断是否继续循环

如果是 alertable wait,就跳到:

c 复制代码
.text:0DCE75C4                 cmp     eax, 101h
.text:0DCE75C9                 jnz     loc_DCE17B0     ;  判断返回值是否为STATUS_ALERTED
.text:0DCE75C9                                         ;  不是就跳转,返回
.text:0DCE75CF                 jmp     loc_DCE1791     ; 是,跳回去继续循环

这里的 0x101 就是:STATUS_ALERTED

意思是:

  • 如果当前是 alertable wait
  • NtWaitForSingleObject 返回 STATUS_ALERTED
  • 那么 KernelBase 并不立刻把这个值返回给上层
  • 而是跳回去,再次调用 NtWaitForSingleObject

也就是形成一个循环重试逻辑。

所以它的真实语义是:

  • bAlertable == 1
  • 且底层 NtWaitForSingleObject 返回 STATUS_ALERTED
  • 那么 KernelBase!WaitForSingleObjectEx 认为"这次等待只是被 alert 打断了,还不算最终等待结果"
  • 因此再次调用 NtWaitForSingleObject 继续等。

3.8 可以写成伪代码

c 复制代码
DWORD WaitForSingleObjectEx(HANDLE hHandle, DWORD dwMilliseconds, BOOL bAlertable)
{
    if (bAlertable)
        RtlActivateActivationContextUnsafeFast(...);

    // 特殊句柄分流
    if (hHandle is special std handle)
        ...;

    PLARGE_INTEGER Timeout = BaseFormatTimeOut(...);

Retry:
    status = NtWaitForSingleObject(hHandle, bAlertable, Timeout);

    if (status < 0)
    {
        BaseSetLastNTError(status);
        status = WAIT_FAILED;
        goto Exit;
    }

    if (bAlertable && status == STATUS_ALERTED)
        goto Retry;

Exit:
    if (bAlertable)
        RtlDeactivateActivationContextUnsafeFast(...);

    return status;
}

目前已经走到:

c 复制代码
WaitForSingleObject
    -> WaitForSingleObjectExImplementation        // kernel32
        -> WaitForSingleObjectEx                  // KernelBase
            -> BaseFormatTimeOut
            -> NtWaitForSingleObject

四、NtWaitForSingleObject 的整体作用

NtWaitForSingleObject 是 Win32 等待接口进入内核后的系统服务入口。

它本身并不直接完成等待调度,而是主要负责做三件事:

  1. 处理来自用户态的参数,尤其是 Timeout
  2. 把用户传入的 HANDLE 转成内核对象
  3. 调用 KeWaitForSingleObject,把等待真正交给内核等待机制

所以可以先把它理解成:

c 复制代码
NtWaitForSingleObject(...)
{
    // 1. 处理用户参数
    // 2. 句柄 -> 内核对象
    // 3. 调用 KeWaitForSingleObject
    // 4. 清理并返回
}

4.1 先取当前线程的 PreviousMode

函数一开始:

c 复制代码
PAGE:00629AF3                 mov     eax, large fs:_KPCR.PrcbData.CurrentThread
PAGE:00629AF9                 mov     al, [eax+_KTHREAD.PreviousMode]
PAGE:00629AFF                 mov     [ebp+AccessMode], al ; 保存先前模式

这里取的是当前线程的 PreviousMode,也就是这次系统调用进入内核前,调用者原本处于什么模式:

  • UserMode
  • KernelMode

这个值后面有两个用途:

  • 决定是否要探测用户传进来的 Timeout
  • 作为 WaitMode 继续传给 KeWaitForSingleObject

也就是说,内核首先要分清楚:

这次调用到底是用户态发起的等待,还是内核态自己调用这个系统服务。

4.2 如果 Timeout != NULL 且来自用户态,就先探测并复制到内核栈

c 复制代码
PAGE:00629B02                 mov     ebx, [ebp+Timeout]
PAGE:00629B05                 xor     esi, esi
PAGE:00629B07                 cmp     ebx, esi        ; 判断Timeout是否为0
PAGE:00629B09                 jz      short loc_629B37 ; 为0跳转
PAGE:00629B0B                 test    al, al          ; 判断先前模式
PAGE:00629B0D                 jz      short loc_629B37 ; 先前模式是r0跳转

含义就是:

  • 如果 Timeout == NULL,跳过
  • 如果 PreviousMode == KernelMode,也跳过
  • 只有当 Timeout != NULL 且调用来自用户态时,才需要探测这个指针

接着就是探测和复制:

c 复制代码
PAGE:00629B14                 mov     eax, ds:_MmUserProbeAddress
PAGE:00629B19                 cmp     ebx, eax        ; 校验地址是否是3环地址
PAGE:00629B1B                 jb      short loc_629B1F ; 如果addr<MmUserProbeAddress
PAGE:00629B1B                                         ; 就跳转
PAGE:00629B1D                 mov     ecx, eax        ; 如果Timeout不是3环合法地址,
PAGE:00629B1D                                         ; 就让Timeout=MmUserProbeAddress
PAGE:00629B1F
PAGE:00629B1F loc_629B1F:                             ; CODE XREF: NtWaitForSingleObject(x,x,x)+34↑j
PAGE:00629B1F                 mov     eax, [ecx+LARGE_INTEGER.u.LowPart]
PAGE:00629B21                 mov     [ebp+time], eax ; 把取出来的Timeout低32位值,保存到0环的局部变量中
PAGE:00629B24                 mov     eax, [ecx+LARGE_INTEGER.u.HighPart]
PAGE:00629B27                 mov     [ebp+var_38], eax ; 把取出来的Timeout高32位值,保存到0环的局部变量中
PAGE:00629B2A                 lea     ebx, [ebp+time]
PAGE:00629B2D                 mov     [ebp+Timeout], ebx

这里做了两件事:

第一,检查 Timeout 是否落在用户空间

它拿 TimeoutMmUserProbeAddress 比较。

如果地址超过用户空间上界,就把访问地址强行改成 MmUserProbeAddress,这样后续读内存时就会触发异常,由 SEH 来接住。

也就是说,这里不是"信任用户传进来的指针",而是:

先验证它是不是一个合法的用户态地址,再决定是否读取

第二,把用户态的 LARGE_INTEGER 复制到内核栈上的局部变量

它把:

  • 低 32 位读到 time
  • 高 32 位读到 var_38

然后把 Timeout 参数改成指向栈上这份副本

所以这一段的本质是:

如果 Timeout 来自 3 环,就先把它安全地拷贝到 0 环栈上,后面只使用这份内核副本。

这样做的目的很明显:

  • 避免后续等待过程中再次访问用户地址
  • 避免用户在检查之后偷偷修改这块内存
  • 保证内核后续拿到的是一份受控的参数副本

4.3 ObReferenceObjectByHandle:把句柄转成内核对象

接下来这段就是第二块重点:

c 复制代码
PAGE:00629B37                 push    esi             ; 参数5:HandleInformation(可选)
PAGE:00629B38                 lea     eax, [ebp+object]
PAGE:00629B3B                 push    eax             ; 参数4:Object(out)
PAGE:00629B3C                 push    dword ptr [ebp+AccessMode] ; 参数4:AccessMode
PAGE:00629B3F                 push    esi             ; 参数3:ObjectType
PAGE:00629B40                 push    100000h         ; 参数2:DesiredAccess
PAGE:00629B45                 push    [ebp+Object]    ; 参数1:Handle
PAGE:00629B48                 call    _ObReferenceObjectByHandle@24 ; 把句柄转成内核对象

它就是把用户传入的 HANDLE 转成内核对象指针。

这里的 DesiredAccess = 0x00100000,这个值就是标准访问权限里的 SYNCHRONIZE,也就是说,句柄至少要有"可等待/可同步"的权限,才能拿来做等待。

所以这一步的意义可以概括成:

NtWaitForSingleObject 不会直接相信一个句柄能不能等,它会先通过对象管理器检查这个句柄,并确认调用者对该对象至少拥有 SYNCHRONIZE 权限。

如果这里失败,比如:

  • 句柄无效
  • 类型不对
  • 权限不足

那么函数就直接返回错误,不会继续往下走。

4.4 并不是所有对象都能直接拿对象体去等,还要先算出真正的"等待对象"

这个地方很关键:

c 复制代码
PAGE:00629B57                 mov     esi, [ebp+object]
PAGE:00629B5A                 movzx   eax, byte ptr [esi-0Ch] ; 内核对象的上面是 对象头(_OBJECT_HEADER)
PAGE:00629B5A                                         ; 这里是取到对象的对象头_OBJECT_HEADER.TypeIndex
PAGE:00629B5E                 mov     eax, _ObTypeIndexTable[eax*4] ; 通过类型索引,在对象类型表中,拿到对象类型
PAGE:00629B5E                                         ; _object_type
PAGE:00629B65                 mov     eax, [eax+_OBJECT_TYPE.DefaultObject] ; 拿到偏移

这里先通过对象头里的 TypeIndex 找到对象类型,然后取出该类型的 DefaultObject 字段。

后面这段逻辑:

c 复制代码
PAGE:00629B6B                 test    cl, al          ; 并不是所有对象都能直接拿对象体去等,还要先算出真正的"等待对象"
PAGE:00629B6B                                         ; _OBJECT_TYPE.DefaultObject的bit0决定了怎么取真正的等待对象
PAGE:00629B6B                                         ;
PAGE:00629B6B                                         ; 判断偏移的bit0位是否有值
PAGE:00629B6D                 jz      short loc_629B95 ; 没有值跳走
PAGE:00629B6F                 and     eax, 0FFFFFFFEh ; 下面是第一种情况bi0==1:
PAGE:00629B6F                                         ;
PAGE:00629B6F                                         ; 先去掉最低位标志
PAGE:00629B6F                                         ; 把剩余值当成偏移加到对象体上
PAGE:00629B6F                                         ; 再取出那个位置上的指针
PAGE:00629B6F                                         ;
PAGE:00629B6F                                         ; 也就是:
PAGE:00629B6F                                         ; 对象体里某个字段本身是一个指针,真正要等待的是这个字段指向的对象
PAGE:00629B72                 add     eax, esi
PAGE:00629B74                 mov     eax, [eax]
PAGE:00629B76                 jmp     short loc_629B9B



PAGE:00629B95 loc_629B95:                             ; CODE XREF: NtWaitForSingleObject(x,x,x)+86↑j
PAGE:00629B95                 test    eax, eax        ; 情况2:最低位为 0,且值非负
PAGE:00629B95                                         ;
PAGE:00629B95                                         ; 表示:把它当成对象体内的偏移,
PAGE:00629B95                                         ; 真正等待的是对象内部嵌入的某个子对象
PAGE:00629B97                 jl      short loc_629B9B ; 情况3:
PAGE:00629B97                                         ; 最低位为 0,但值是负数
PAGE:00629B97                                         ; 这种情况下代码不再加 esi,直接把 eax 作为最终对象传下去。
PAGE:00629B99                 add     eax, esi

说明 DefaultObject 不是简单的"固定偏移",它实际上是一个编码后的描述值,用来告诉内核:

这个对象类型真正应该拿什么地址去等待

从这段代码看,大致有三种情况:

情况 1:最低位为 1

c 复制代码
test cl, al
jnz ...

表示这不是直接的对象地址,而是:

  • 先去掉最低位标志
  • 把剩余值当成偏移加到对象体上
  • 再取出那个位置上的指针

也就是:

对象体里某个字段本身是一个指针,真正要等待的是这个字段指向的对象

情况 2:最低位为 0,且值非负

c 复制代码
test eax, eax
jl  short loc_629B9B
add eax, esi

表示:

把它当成对象体内的偏移,真正等待的是对象内部嵌入的某个子对象

情况 3:最低位为 0,但值是负数

这种情况下代码不再加 esi,直接把 eax 作为最终对象传下去。

从代码形态看,这意味着:

这个值本身就已经是一个有效的内核地址,不需要再相对当前对象体做偏移换算

这一段非常重要,因为它说明:

NtWaitForSingleObject 不是简单把对象体地址直接丢给 KeWaitForSingleObject,而是会根据对象类型,先解析出"真正可等待的对象地址"。

这也是对象管理器层和调度器层之间的一个衔接点。

4.5.真正进入等待:KeWaitForSingleObject

算出最终等待对象以后,才调用:

c 复制代码
PAGE:00629B9E                 push    ebx             ; Timeout
PAGE:00629B9F                 push    dword ptr [ebp+Alertable] ; Alertable
PAGE:00629BA2                 push    dword ptr [ebp+AccessMode] ; WaitMode
PAGE:00629BA5                 push    6               ; WaitReason
PAGE:00629BA7                 push    eax             ; Object
PAGE:00629BA8                 call    _KeWaitForSingleObject@20 ; KeWaitForSingleObject(x,x,x,x,x)

这里的第 2 个参数是 KWAIT_REASON。根据 WRK 中 KWAIT_REASON 的定义,枚举值 6 对应的正是 UserRequest。也就是说,NtWaitForSingleObject 在把等待请求交给调度器时,会把这次等待标记成"由用户请求触发的等待"。

因此,这里 5 个参数可以这样理解:

  • Object:前面解析出来的真正可等待对象
  • WaitReason = UserRequest:表示这次等待属于用户请求导致的等待
  • WaitMode = AccessMode:沿用当前线程进入系统调用前的模式
  • Alertable:是否允许可警醒等待
  • Timeout:前面已经准备好的超时参数

4.6 等待结束后,释放对象引用

无论等待成功还是失败,只要前面 ObReferenceObjectByHandle 成功拿到了对象引用,后面都会执行:

c 复制代码
loc_629BE5:               
PAGE:00629BE5                 mov     ecx, esi        ; Object
PAGE:00629BE7                 call    @ObfDereferenceObject@4 ; ObfDereferenceObject(x)

也就是对对象做一次解引用。

4.7 可以把整个函数概括成下面这个伪代码

c 复制代码
NTSTATUS NtWaitForSingleObject(
    HANDLE Handle,
    BOOLEAN Alertable,
    PLARGE_INTEGER Timeout
)
{
    KPROCESSOR_MODE AccessMode = KeGetPreviousMode();

    // 如果 Timeout 来自用户态,先探测并复制到内核栈
    if (Timeout != NULL && AccessMode != KernelMode)
    {
        ProbeAndCaptureTimeout(&KernelTimeout, Timeout);
        Timeout = &KernelTimeout;
    }

    // 句柄 -> 内核对象
    Status = ObReferenceObjectByHandle(
        Handle,
        SYNCHRONIZE,
        NULL,
        AccessMode,
        &Object,
        NULL
    );
    if (!NT_SUCCESS(Status))
        return Status;

    // 根据对象类型,找到真正应该等待的对象
    WaitObject = ResolveWaitObjectFromType(Object);

    // 真正进入等待
    Status = KeWaitForSingleObject(
        WaitObject,
        6,
        AccessMode,
        Alertable,
        Timeout
    );

    ObfDereferenceObject(Object);
    return Status;
}

NtWaitForSingleObject 并不直接实现"等待算法",它更像是 Win32/Native 参数和内核调度器之间的一层桥梁。

它先根据 PreviousMode 安全处理用户参数,再通过 ObReferenceObjectByHandle 把句柄解析为内核对象,然后根据对象类型计算出真正可等待的对象,最后把等待请求交给 KeWaitForSingleObject。

因此,NtWaitForSingleObject 的核心职责不是"等",而是"把这次等待请求整理成内核可执行的形式"。

相关推荐
handsomestWei1 小时前
claude-code在win环境安装使用
windows·ai编程·claude·安装配置·cc-switch
秦时明月之君临天下2 小时前
Windows如何删除任务管理器中的某个服务?
windows
麦田里的守望者_zhg2 小时前
Windows 执行 wsl --update 报错 1603:注册表权限导致 WSL 安装损坏问题排查与修复
windows
❆VE❆4 小时前
Claude Code 安装与配置完整指南(Windows)
windows·claude code
航Hang*4 小时前
VMware vSphere 云平台运维与管理基础——第5章:VMware vSphere 5.5 高级特性
运维·服务器·开发语言·windows·学习·虚拟化
Mapleay4 小时前
Ubuntu 源的重要性!之 libgmp-dev 无法安装
linux·服务器·windows
humors2215 小时前
微软工具包下载网址
windows·microsoft·微软·office·工具包·sysintervals
寺中人5 小时前
硬盘提示初始化的损坏,手动恢复MBR及EBR分区教程
windows·工具·硬盘修复
冷色系里的一抹暖调5 小时前
OpenClaw Docker 部署避坑指南:服务启动成功但网页打不开?
人工智能·windows·docker·ai·容器·opencode
开开心心就好6 小时前
能把网页藏在Word里的实用摸鱼工具
linux·运维·服务器·windows·随机森林·逻辑回归·excel