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

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

从调用过程可以看出:

  • 原始参数有两个:hHandle 和 dwMilliseconds
  • 它额外压入了一个 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

这里逻辑很典型:

  • 调用 BaseSetLastNTError 把 NTSTATUS 转换成线程的 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 是否落在用户空间

它拿 Timeout 和 MmUserProbeAddress 比较。

如果地址超过用户空间上界,就把访问地址强行改成 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 的核心职责不是"等",而是"把这次等待请求整理成内核可执行的形式"。

相关推荐
今夕资源网9 分钟前
从夯到拉模拟器RankPix windows本地版 exe
windows·exe·从夯到拉·从夯到拉模拟器·rankpix·本地版
kakakahahahaha43 分钟前
Windows系统时间校准指南:time命令、date命令、w32tm /resync与CMOS排查
windows·笔记本电脑·软件需求
一技安身1 小时前
【mysql】Windows MySQL 8.4.10 绿色免安装喂饭教程
windows·mysql·adb
传奇开心果编程2 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
sukalot3 小时前
Windows 驱动实例分析系列:libwdi 驱动分析 - 应用篇(一)
windows·驱动开发
小道士写程序3 小时前
Windows Rust 环境安装指导
开发语言·windows·rust
传奇开心果编程3 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
Tisfy4 小时前
两台旧电脑修复 —— 绕过WinXP密码登录、备份和恢复Win7数据、安装Win10
windows·数据恢复·系统重装
传奇开心果编程4 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
supabc1234 小时前
Celium:连接 Windows、Mac、Linux 与 Android,让远程访问和设备管理更简单
android·linux·windows·macos·远程访问·网络管理·celium