一、 从 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 等待接口进入内核后的系统服务入口。
它本身并不直接完成等待调度,而是主要负责做三件事:
- 处理来自用户态的参数,尤其是 Timeout
- 把用户传入的
HANDLE转成内核对象 - 调用
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 的核心职责不是"等",而是"把这次等待请求整理成内核可执行的形式"。