第 7 章 视窗报文 --- 7.3 Win32k 的用户空间回调机制
本节深入剖析 Windows/ReactOS 中 win32k 内核态到 user32 用户态的回调(Callback)机制,这是 WndProc、Hook、IME 等用户态代码被内核调用的核心机制。
概述
Windows 是一个典型的"内核-用户"分层系统。win32k.sys 运行在内核态,但绝大多数 GUI 代码(特别是用户编写的 WndProc、Hook 过程)运行在用户态。这就产生了一个核心问题:内核如何调用用户态代码?
用户空间回调的本质是什么?
用户空间回调(User-Mode Callback)是内核态与用户态之间的"双向通道":内核通过
KeUserModeCallback切换到用户态执行预注册的处理函数,处理完毕后再切回内核态。这种机制既保证了内核态的安全边界,又允许用户态代码参与关键决策。
想象一个"智能客服系统":
- win32k 内核态:核心调度员(只处理系统级决策);
- user32 用户态:前台客服(处理具体业务);
- KeUserModeCallback:内部电话(调度员需要咨询前台时拨打);
- 回调参数:通话内容(订单号、问题描述等);
- 回调返回值:客服的回答(处理结果)。
每次客户问题(消息)需要专业服务(WndProc)时,调度员通过内部电话联系前台,处理完毕后再继续自己的工作。
本节内容概览
- 7.3.0 框架图:内核-用户态回调完整流程;
- 7.3.1 KeUserModeCallback 机制:NT 内核的通用回调机制;
- 7.3.2 回调类型与编号:USER32_CALLBACK_* 枚举;
- 7.3.3 回调内存管理:IntCbAllocateMemory / IntCbFreeMemory;
- 7.3.4 WINDOWPROC_CALLBACK_ARGUMENTS:窗口过程回调参数;
- 7.3.5 co_IntCallWindowProc:调用 WndProc 的核心;
- 7.3.6 消息参数打包 PackParam:跨边界参数序列化;
- 7.3.7 消息参数解包 UnpackParam:跨边界参数反序列化;
- 7.3.8 Ansi/Unicode 转换:lParam 文本转换;
- 7.3.9 线程清理:IntCleanupThreadCallbacks 资源回收;
- 7.3.10 设计哲学问答:5 个关键设计问题解答。
学习目标
读完本节后,读者应当能够:
- 理解 NT 内核的 KeUserModeCallback 通用机制;
- 掌握 win32k 回调内存管理(IntCbAllocate/FreeMemory);
- 分析 WINDOWPROC_CALLBACK_ARGUMENTS 参数结构;
- 解释 PackParam/UnpackParam 的参数序列化与反序列化;
- 理解 Ansi/Unicode 消息转换的实现;
- 掌握回调与线程生命周期的关系。
涉及的内核子系统
| 子系统 | 职责 |
|---|---|
| ntoskrnl/ke | KeUserModeCallback 通用机制 |
| win32k.sys/ntuser | 回调注册、参数打包、内存管理 |
| user32.dll | 回调入口(USER32_CALLBACK_*)实现 |
| ntdll.dll | NtCallbackReturn 系统调用返回结果 |
7.3.0 框架图
┌──────────────────────────────────────────────────────────────────────────────────────┐
│ Win32k 用户空间回调机制完整架构 │
├──────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 内核态 (win32k.sys) │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ ┌───────────────────────────────────────────────────────────────────┐ │ │
│ │ │ 回调发起方(内核) │ │ │
│ │ │ co_IntCallWindowProc │ │ │
│ │ │ co_IntCallHookProc │ │ │
│ │ │ co_IntCallEventProc │ │ │
│ │ │ co_IntCallSentMessageCallback │ │ │
│ │ │ co_IntClientLoadLibrary │ │ │
│ │ └───────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌───────────────────────────────────────────────────────────────────┐ │ │
│ │ │ 参数准备 │ │ │
│ │ │ ├─► IntCbAllocateMemory (回调内存分配 + 链表管理) │ │ │
│ │ │ ├─► PackParam (消息参数打包) │ │ │
│ │ │ └─► IntSetTebWndCallback (TEB 窗口缓存切换) │ │ │
│ │ └───────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌───────────────────────────────────────────────────────────────────┐ │ │
│ │ │ KeUserModeCallback(CallbackId, Arguments, Length, │ │ │
│ │ │ &Result, &ResultLength) │ │ │
│ │ │ 触发:CPU 切换到用户态 │ │ │
│ │ └───────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ │ CPU 切换 │ │
│ │ ▼ │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ 用户态 (user32.dll) │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ ┌───────────────────────────────────────────────────────────────────┐ │ │
│ │ │ KeUserModeCallback 入口 (user32.dll 中) │ │ │
│ │ │ 根据 CallbackId 路由: │ │ │
│ │ │ ├─► USER32_CALLBACK_WINDOWPROC → IntCallWindowProcW │ │ │
│ │ │ ├─► USER32_CALLBACK_SENDASYNCPROC → SendAsyncProc 包装 │ │ │
│ │ │ ├─► USER32_CALLBACK_LOADMENU → LoadSysMenuTemplate │ │ │
│ │ │ ├─► USER32_CALLBACK_CLIENTLOADLIBRARY → ClientLoadLibrary │ │ │
│ │ │ └─► ... │ │ │
│ │ └───────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌───────────────────────────────────────────────────────────────────┐ │ │
│ │ │ 实际用户代码 │ │ │
│ │ │ ├─► WndProc (应用程序定义) │ │ │
│ │ │ ├─► HookProc (WH_CBT/WH_GETMESSAGE/...) │ │ │
│ │ │ └─► WinEventProc (SetWinEventHook 注册) │ │ │
│ │ └───────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ NtCallbackReturn(&Result, ResultLength, Status) │ │
│ │ 触发:CPU 切换回内核态 │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ 返回后,内核态处理 │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ ├─► IntRestoreTebWndCallback (TEB 窗口缓存恢复) │ │
│ │ ├─► RtlMoveMemory (从用户态复制 Result) │ │
│ │ └─► IntCbFreeMemory (释放回调内存) │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────┘
7.3.1 KeUserModeCallback 机制
7.3.1.1 NT 内核的通用回调 API
KeUserModeCallback 是 NT 内核提供的通用机制,允许内核态代码调用用户态预注册的处理函数:
c
NTSTATUS
KeUserModeCallback(
IN ULONG ApiNumber, // 用户态识别码(类似系统调用号)
IN PVOID InputBuffer, // 输入参数缓冲区
IN ULONG InputLength, // 输入参数长度
OUT PVOID *OutputBuffer, // 输出参数缓冲区(用户态填写)
OUT PULONG OutputLength // 输出参数长度
);
7.3.1.2 工作原理
KeUserModeCallback 的执行流程:
- 保存内核态上下文:保存当前内核栈、内核寄存器等;
- 构造用户态栈帧 :在用户态栈上构造
KCALLOUT_FRAME结构,包含 ApiNumber 和 InputBuffer; - 切换到用户态 :通过
KiCallbackReturn跳转到用户态入口; - 用户态分发:user32!_UserModeCallback 入口根据 ApiNumber 路由;
- 执行回调函数:调用 user32 中对应的处理函数;
- 通过 NtCallbackReturn 返回:从用户态切回内核态。
7.3.1.3 关键设计点
- 保护内核安全:CPU 切换时硬件会进行特权级检查(Ring 0 ↔ Ring 3);
- 栈独立:用户态有自己的栈,参数通过预定义缓冲区传递;
- 可重入:用户态回调可以再次发起系统调用,内核能正确处理嵌套;
- 轻量级:相比 IPC/LPC,回调的开销低很多(仅一次模式切换)。
7.3.2 回调类型与编号
7.3.2.1 USER32_CALLBACK_* 枚举
win32k 定义了一组回调类型常量,与 user32 中的分发表对应:
c
// win32ss/user/ntuser/callback.h 或相关头文件
#define USER32_CALLBACK_LOADMENU 1
#define USER32_CALLBACK_CLIENTLOADLIBRARY 2
#define USER32_CALLBACK_WNDPROC 3 // 最重要的回调
#define USER32_CALLBACK_SENDASYNCPROC 4
#define USER32_CALLBACK_LOADDEFAULTCURSORS 5
#define USER32_CALLBACK_LOADSYSTEMCURSORS 6
#define USER32_CALLBACK_SETWNDICONS 7
#define USER32_CALLBACK_IMMTHREADCONNECT 8
#define USER32_CALLBACK_IMMPROCESSKEY 9
#define USER32_CALLBACK_IMMLOADLAYOUT 10
#define USER32_CALLBACK_HOOKPROC 11
#define USER32_CALLBACK_EVENTPROC 12
#define USER32_CALLBACK_GENERICCONSOLE 13
7.3.2.2 关键回调一览
| 编号 | 名称 | 用途 | 调用方 |
|---|---|---|---|
| 3 | USER32_CALLBACK_WNDPROC |
调用 WndProc | co_IntCallWindowProc |
| 4 | USER32_CALLBACK_SENDASYNCPROC |
SendMessage 回调 | co_IntCallSentMessageCallback |
| 11 | USER32_CALLBACK_HOOKPROC |
钩子函数调用 | co_IntCallHookProc |
| 12 | USER32_CALLBACK_EVENTPROC |
事件钩子调用 | co_IntCallEventProc |
| 2 | USER32_CALLBACK_CLIENTLOADLIBRARY |
加载 user32 客户端 DLL | co_IntClientLoadLibrary |
7.3.3 回调内存管理
7.3.3.1 IntCbAllocateMemory
win32ss/user/ntuser/callback.c:26(file:///d:/reactos/win32ss/user/ntuser/callback.c#L26):
c
typedef struct _INT_CALLBACK_HEADER
{
LIST_ENTRY ListEntry; // 链表节点
}
INT_CALLBACK_HEADER, *PINT_CALLBACK_HEADER;
PVOID FASTCALL
IntCbAllocateMemory(ULONG Size)
{
PINT_CALLBACK_HEADER Mem;
PTHREADINFO W32Thread;
if (!(Mem = ExAllocatePoolWithTag(PagedPool, Size + sizeof(INT_CALLBACK_HEADER),
USERTAG_CALLBACK)))
{
return NULL;
}
RtlZeroMemory(Mem, Size + sizeof(INT_CALLBACK_HEADER));
W32Thread = PsGetCurrentThreadWin32Thread();
ASSERT(W32Thread);
/* Insert the callback memory into the thread's callback list */
InsertTailList(&W32Thread->W32CallbackListHead, &Mem->ListEntry);
return (Mem + 1);
}
关键点:
- PagedPool 分配:回调内存不需要驻留(不参与中断处理);
- 链表跟踪 :将分配的内存插入
pti->W32CallbackListHead,便于线程退出时统一释放; - 返回 Mem+1:跳过头部,返回给调用方"纯数据"指针。
7.3.3.2 IntCbFreeMemory
c
VOID FASTCALL
IntCbFreeMemory(PVOID Data)
{
PINT_CALLBACK_HEADER Mem;
PTHREADINFO W32Thread;
W32Thread = PsGetCurrentThreadWin32Thread();
ASSERT(W32Thread);
if (W32Thread->TIF_flags & TIF_INCLEANUP)
{
ERR("CbFM Thread is already in cleanup\n");
return;
}
ASSERT(Data);
Mem = ((PINT_CALLBACK_HEADER)Data - 1);
/* Remove the memory block from the thread's callback list */
RemoveEntryList(&Mem->ListEntry);
/* Free memory */
ExFreePoolWithTag(Mem, USERTAG_CALLBACK);
}
关键点:
- TIF_INCLEANUP 检查:避免在线程清理过程中重复释放;
- 链表移除 :从
W32CallbackListHead中删除节点; - ExFreePoolWithTag:释放回池。
7.3.3.3 IntCleanupThreadCallbacks
win32ss/user/ntuser/callback.c:75(file:///d:/reactos/win32ss/user/ntuser/callback.c#L75):
c
VOID FASTCALL
IntCleanupThreadCallbacks(PTHREADINFO W32Thread)
{
PLIST_ENTRY CurrentEntry;
PINT_CALLBACK_HEADER Mem;
while (!IsListEmpty(&W32Thread->W32CallbackListHead))
{
CurrentEntry = RemoveHeadList(&W32Thread->W32CallbackListHead);
Mem = CONTAINING_RECORD(CurrentEntry, INT_CALLBACK_HEADER, ListEntry);
/* Free memory */
ExFreePoolWithTag(Mem, USERTAG_CALLBACK);
}
}
线程退出时 调用此函数,遍历整个链表并释放所有回调内存。这避免了内存泄漏(即使线程忘记显式调用 IntCbFreeMemory)。
7.3.4 WINDOWPROC_CALLBACK_ARGUMENTS 参数结构
7.3.4.1 结构定义
c
typedef struct _WINDOWPROC_CALLBACK_ARGUMENTS
{
WNDPROC Proc; // 窗口过程函数指针
BOOL IsAnsiProc; // 是否为 ANSI 窗口过程
HWND Wnd; // 目标窗口
UINT Msg; // 消息 ID
WPARAM wParam; // 参数 1
LPARAM lParam; // 参数 2
INT lParamBufferSize; // lParam 指向的缓冲区大小
LRESULT Result; // WndProc 返回值(回调返回时填写)
} WINDOWPROC_CALLBACK_ARGUMENTS, *PWINDOWPROC_CALLBACK_ARGUMENTS;
7.3.4.2 结构用途
WINDOWPROC_CALLBACK_ARGUMENTS 是 win32k 与 user32 之间传递 WndProc 调用信息的"契约结构":
- 输入方向(内核 → user32):Proc、IsAnsiProc、Wnd、Msg、wParam、lParam、lParamBufferSize;
- 输出方向(user32 → 内核):Result。
7.3.4.3 类似的回调参数结构
c
// 钩子回调参数
typedef struct _HOOKPROC_CALLBACK_ARGUMENTS
{
INT HookId; // 钩子类型(WH_CBT 等)
INT Code; // 钩子代码
WPARAM wParam;
LPARAM lParam;
HOOKPROC Proc; // 钩子函数指针
INT Mod; // 模块索引
ULONG_PTR offPfn; // 函数偏移
BOOLEAN Ansi; // 是否为 ANSI 钩子
PUNICODE_STRING ModuleName; // 模块名
LRESULT Result;
} HOOKPROC_CALLBACK_ARGUMENTS;
// 事件钩子回调参数
typedef struct _EVENTPROC_CALLBACK_ARGUMENTS
{
HWINEVENTHOOK hook;
DWORD event;
HWND hwnd;
LONG idObject;
LONG idChild;
DWORD dwEventThread;
DWORD dwmsEventTime;
WINEVENTPROC Proc;
INT Mod;
ULONG_PTR offPfn;
BOOL Ansi;
LRESULT Result;
} EVENTPROC_CALLBACK_ARGUMENTS;
7.3.5 co_IntCallWindowProc 核心
7.3.5.1 函数完整实现
win32ss/user/ntuser/callback.c:281(file:///d:/reactos/win32ss/user/ntuser/callback.c#L281):
c
LRESULT APIENTRY
co_IntCallWindowProc(WNDPROC Proc,
BOOLEAN IsAnsiProc,
HWND Wnd,
UINT Message,
WPARAM wParam,
LPARAM lParam,
INT lParamBufferSize)
{
WINDOWPROC_CALLBACK_ARGUMENTS StackArguments = { 0 };
PWINDOWPROC_CALLBACK_ARGUMENTS Arguments;
NTSTATUS Status;
PVOID ResultPointer, pActCtx;
PWND pWnd;
ULONG ResultLength;
ULONG ArgumentLength;
LRESULT Result;
TRACE("co_IntCallWindowProc(Proc %p, IsAnsiProc: %s, Wnd %p, ...)\n",
Proc, IsAnsiProc ? "TRUE" : "FALSE", Wnd, Message);
/* 不允许桌面线程回调到用户态 */
ASSERT(PsGetCurrentThreadWin32Thread() != gptiDesktopThread);
/* 准备参数缓冲区 */
if (lParamBufferSize != -1)
{
ArgumentLength = sizeof(WINDOWPROC_CALLBACK_ARGUMENTS) + lParamBufferSize;
Arguments = IntCbAllocateMemory(ArgumentLength);
if (NULL == Arguments)
{
ERR("Unable to allocate buffer for window proc callback\n");
return -1;
}
/* 复制 lParam 指向的数据到回调内存 */
RtlMoveMemory((PVOID) ((char *) Arguments + sizeof(WINDOWPROC_CALLBACK_ARGUMENTS)),
(PVOID) lParam, lParamBufferSize);
}
else
{
/* lParam 不含指针,使用栈缓冲区 */
Arguments = &StackArguments;
ArgumentLength = sizeof(WINDOWPROC_CALLBACK_ARGUMENTS);
}
Arguments->Proc = Proc;
Arguments->IsAnsiProc = IsAnsiProc;
Arguments->Wnd = Wnd;
Arguments->Msg = Message;
Arguments->wParam = wParam;
Arguments->lParam = lParam;
Arguments->lParamBufferSize = lParamBufferSize;
ResultPointer = NULL;
ResultLength = ArgumentLength;
/* 切换 TEB 窗口缓存(优化客户端访问) */
IntSetTebWndCallback(&Wnd, &pWnd, &pActCtx);
/* 释放用户态锁(允许其他线程进入 win32k) */
UserLeaveCo();
/* 切换到用户态调用 WndProc */
Status = KeUserModeCallback(USER32_CALLBACK_WINDOWPROC,
Arguments,
ArgumentLength,
&ResultPointer,
&ResultLength);
if (!NT_SUCCESS(Status))
{
ERR("Error Callback to User space Status %lx Message %d\n", Status, Message);
UserEnterCo();
return 0;
}
/* 从用户态复制结果 */
_SEH2_TRY
{
RtlMoveMemory(Arguments, ResultPointer, ArgumentLength);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
ERR("Failed to copy result from user mode, Message %u lParam size %d!\n",
Message, lParamBufferSize);
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
/* 重新获取用户态锁 */
UserEnterCo();
/* 恢复 TEB 窗口缓存 */
IntRestoreTebWndCallback(Wnd, pWnd, pActCtx);
if (!NT_SUCCESS(Status))
{
ERR("Call to user mode failed! 0x%08lx\n", Status);
if (lParamBufferSize != -1)
{
IntCbFreeMemory(Arguments);
}
return -1;
}
Result = Arguments->Result;
if (lParamBufferSize != -1)
{
IntCbFreeMemory(Arguments);
}
return Result;
}
7.3.5.2 关键步骤解析
-
参数缓冲区分配:
lParamBufferSize == -1:lParam 不含指针,使用栈缓冲区(零分配);lParamBufferSize != -1:lParam 含有指针数据,需要分配回调内存并复制。
-
IntSetTebWndCallback:切换 TEB 窗口缓存(PCLIENTINFO->CallbackWnd)以便 user32 快速访问窗口对象。
-
UserLeaveCo:释放 win32k 的用户态锁,允许其他线程进入(重要:避免死锁)。
-
KeUserModeCallback:
- 切换到 Ring 3;
- user32 接收 USER32_CALLBACK_WINDOWPROC 路由;
- 调用 IntCallMessageProc 进一步分发到 WndProc。
-
RtlMoveMemory:从用户态复制结果(含 Result)。
-
UserEnterCo:重新获取用户态锁。
-
IntRestoreTebWndCallback:恢复 TEB 窗口缓存。
-
IntCbFreeMemory:释放回调内存(如果分配过)。
7.3.5.3 lParamBufferSize 语义
| 值 | 含义 | 内存位置 |
|---|---|---|
-1 |
lParam 是标量(如数字、句柄) | 栈 |
0 |
lParam 是 NULL 指针 | 栈 |
>0 |
lParam 指向 lParamBufferSize 字节的数据 | 堆(回调内存) |
当 lParam 包含指针(如 WM_CREATE 的 CREATESTRUCT、WM_COPYDATA 的 COPYDATASTRUCT)时,需要将数据从用户态 A 复制到回调内存(用户态 B),再传递给 WndProc。
7.3.6 消息参数打包 PackParam
7.3.6.1 为什么需要 PackParam?
跨线程 SendMessage 时,lParam 可能包含用户态地址。但目标线程的用户态地址空间与源线程不同(虽然是同一进程但栈、堆独立),直接传递指针是错误的。
更严重的是,接收方 WndProc 在内核态被调用(通过 KeUserModeCallback),无法直接访问用户态内存。
因此需要:
- 发送前:将 lParam 数据复制到内核可访问的内存(PackParam);
- 接收时:将数据复制到目标线程的用户态内存(UnpackParam)。
7.3.6.2 PackParam 函数
win32ss/user/ntuser/message.c:283(file:///d:/reactos/win32ss/user/ntuser/message.c#L283):
c
static NTSTATUS
PackParam(LPARAM *lParamPacked, UINT Msg, WPARAM wParam, LPARAM lParam, BOOL NonPagedPoolNeeded)
{
NCCALCSIZE_PARAMS *UnpackedNcCalcsize;
NCCALCSIZE_PARAMS *PackedNcCalcsize;
CREATESTRUCTW *UnpackedCs;
CREATESTRUCTW *PackedCs;
PLARGE_STRING WindowName;
PUNICODE_STRING ClassName;
POOL_TYPE PoolType;
UINT Size;
PCHAR CsData;
*lParamPacked = lParam;
if (NonPagedPoolNeeded)
PoolType = NonPagedPool;
else
PoolType = PagedPool;
if (WM_NCCALCSIZE == Msg && wParam)
{
UnpackedNcCalcsize = (NCCALCSIZE_PARAMS *) lParam;
PackedNcCalcsize = ExAllocatePoolWithTag(PoolType,
sizeof(NCCALCSIZE_PARAMS) + sizeof(WINDOWPOS), TAG_MSG);
if (NULL == PackedNcCalcsize) return STATUS_NO_MEMORY;
RtlCopyMemory(PackedNcCalcsize, UnpackedNcCalcsize, sizeof(NCCALCSIZE_PARAMS));
PackedNcCalcsize->lppos = (PWINDOWPOS) (PackedNcCalcsize + 1);
RtlCopyMemory(PackedNcCalcsize->lppos, UnpackedNcCalcsize->lppos, sizeof(WINDOWPOS));
*lParamPacked = (LPARAM) PackedNcCalcsize;
}
else if (WM_CREATE == Msg || WM_NCCREATE == Msg)
{
UnpackedCs = (CREATESTRUCTW *) lParam;
WindowName = (PLARGE_STRING) UnpackedCs->lpszName;
ClassName = (PUNICODE_STRING) UnpackedCs->lpszClass;
Size = sizeof(CREATESTRUCTW) + WindowName->Length + sizeof(WCHAR);
if (IS_ATOM(ClassName->Buffer))
{
Size += sizeof(WCHAR) + sizeof(ATOM);
}
else
{
Size += sizeof(WCHAR) + ClassName->Length + sizeof(WCHAR);
}
PackedCs = ExAllocatePoolWithTag(PoolType, Size, TAG_MSG);
if (NULL == PackedCs) return STATUS_NO_MEMORY;
RtlCopyMemory(PackedCs, UnpackedCs, sizeof(CREATESTRUCTW));
CsData = (PCHAR) (PackedCs + 1);
PackedCs->lpszName = (LPCWSTR) (CsData - (PCHAR) PackedCs);
RtlCopyMemory(CsData, WindowName->Buffer, WindowName->Length);
CsData += WindowName->Length;
*((WCHAR *) CsData) = L'\0';
CsData += sizeof(WCHAR);
PackedCs->lpszClass = (LPCWSTR) (CsData - (PCHAR) PackedCs);
// ... 处理 ClassName ...
*lParamPacked = (LPARAM) PackedCs;
}
// ... 其他消息类型 ...
return STATUS_SUCCESS;
}
7.3.6.3 处理的消息类型
PackParam 处理的消息类型有限,因为只有少数消息的 lParam 包含动态大小的数据:
- WM_NCCALCSIZE:lppos 是 WINDOWPOS 数组
- WM_CREATE / WM_NCCREATE:CREATESTRUCTW(包含窗口名、类名)
- WM_COPYDATA:COPYDATASTRUCT(用户数据)
- WM_GETTEXT / WM_SETTEXT:lpszString 是字符串
- WM_WININICHANGE / WM_SETTINGCHANGE / WM_DEVMODECHANGE:lpszSection
- 其他更多(参见 message.c 中的
g_MsgMemory表)
7.3.6.4 消息内存表
win32ss/user/ntuser/message.c:55(file:///d:/reactos/win32ss/user/ntuser/message.c#L55):
c
#define SET(msg) (1 << ((msg) & 31))
static const unsigned int message_pointer_flags[] =
{
/* 0x00 - 0x1f */
SET(WM_CREATE) | SET(WM_SETTEXT) | SET(WM_GETTEXT) |
SET(WM_WININICHANGE) | SET(WM_DEVMODECHANGE),
// ... 大量消息 ...
};
message_pointer_flags 是位图,标识每个消息是否包含指针(lParam 是指针)。
7.3.7 消息参数解包 UnpackParam
7.3.7.1 UnpackParam 函数
win32ss/user/ntuser/message.c:402(file:///d:/reactos/win32ss/user/ntuser/message.c#L402):
UnpackParam 是 PackParam 的反向操作,将内核态的 PackedCs 复制回用户态新的 CREATESTRUCTW。实现方式类似但反向:
c
static NTSTATUS
UnpackParam(LPARAM lParamPacked, UINT Msg, WPARAM wParam, LPARAM lParam, BOOL NonPagedPoolUsed)
{
// ... 与 PackParam 相反的逻辑 ...
// 分配用户态内存
// 复制数据回用户态
// 调整内部指针
// ...
}
7.3.7.2 Pack/Unpack 时机
线程 A: SendMessage(hwndB, WM_CREATE, ...)
│
▼
内核态: PackParam(&packed, WM_CREATE, ...)
│ - 分配内核内存
│ - 复制 CREATESTRUCTW
│ - 调整内部指针(lpstr 改为偏移量)
│
▼
发送消息(UserSendNotifyMessage 流程)
│
▼
线程 B: 接收消息
│
▼
内核态: UnpackParam(packed, WM_CREATE, ...)
│ - 在线程 B 用户态分配新 CREATESTRUCTW
│ - 复制数据
│ - 调整内部指针(恢复为用户态地址)
│
▼
线程 B: WndProc 收到 lParam(用户态地址)
│
▼
WndProc 处理完毕
│
▼
内核态: 释放 PackParam 分配的内存
7.3.7.3 内存池选择
NonPagedPoolNeeded 参数决定使用 PagedPool 还是 NonPagedPool:
- PagedPool:常规情况,可被换出;
- NonPagedPool:在 DISPATCH_LEVEL 或更高 IRQL 时必须使用。
7.3.8 Ansi/Unicode 转换
7.3.8.1 转换需求
Windows 支持两种字符集:
- Ansi(窄字符):单字节编码(如 GBK、UTF-8);
- Unicode(宽字符):UTF-16(Windows 默认)。
应用程序可以注册 WindowProcA(Ansi)或 WindowProcW(Unicode)回调。当 SendMessage 跨字符集时需要转换 lParam 中的字符串。
7.3.8.2 转换实现位置
Ansi/Unicode 转换主要由 user32 端的 MsgiAnsiToUnicodeMessage / MsgiUnicodeToAnsiMessage 实现(见 win32ss/user/user32/windows/message.c),但 win32k 端需要:
- 设置
IsAnsiProc标志; - 提供正确的
CodePage。
7.3.8.3 Win32k 端的角色
win32ss/user/ntuser/message.c:98(file:///d:/reactos/win32ss/user/ntuser/message.c#L98):
c
WCHAR StrUserKernel[3][20] = {{L"intl"}, {L"Environment"}, {L"Policy"}};
static INT PosInArray(_In_ PCWSTR String)
{
INT i;
for (i = 0; i < ARRAYSIZE(StrUserKernel); ++i)
{
if (wcsncmp(String, StrUserKernel[i], _countof(StrUserKernel[0])) == 0)
return i;
}
return -1;
}
此函数判断字符串是否可在用户态/内核态间直接传递(仅限 "intl"/"Environment"/"Policy" 三个固定字符串),避免不必要的复制。
7.3.9 线程清理
7.3.9.1 线程退出时的清理流程
win32ss/user/ntuser/main.c(file:///d:/reactos/win32ss/user/ntuser/main.c) 中的 UserDeleteW32Thread:
c
VOID
UserDeleteW32Thread(PTHREADINFO pti)
{
PPROCESSINFO ppi = pti->ppi;
TRACE_CH(UserThread, "UserDeleteW32Thread pti 0x%p\n", pti);
/* Free the message queue */
if (pti->MessageQueue) MsqDestroyMessageQueue(pti);
MsqCleanupThreadMsgs(pti);
/* 清理回调内存 */
IntCleanupThreadCallbacks(pti);
ObDereferenceObject(pti->pEThread);
ExFreePoolWithTag(pti, USERTAG_THREADINFO);
IntDereferenceProcessInfo(ppi);
// ...
}
7.3.9.2 内存泄漏防护
回调内存管理的关键设计是自动清理:
- 每个
IntCbAllocateMemory的分配都自动加入W32CallbackListHead; - 线程退出时
IntCleanupThreadCallbacks一次性释放所有未释放的内存; - 即使线程忘记调用
IntCbFreeMemory,也不会泄漏。
7.3.9.3 异常处理
co_IntCallWindowProc 中使用 _SEH2_TRY/_SEH2_EXCEPT 捕获用户态异常:
- 用户态 WndProc 抛出访问违规时,内核不会崩溃;
- 返回 -1 表示调用失败;
- 始终释放分配的回调内存。
7.3.10 设计哲学问答
Q1:为什么 win32k 不直接调用用户态函数,而要通过 KeUserModeCallback?
A :保护内核安全。
直接调用用户态函数会面临:
- 栈问题:用户态函数使用用户态栈,内核态栈与之不兼容;
- 异常处理:用户态异常(如访问违规)会传播到内核,导致 BSOD;
- 调度问题:用户态函数可以长时间阻塞(死循环),影响内核响应;
- API 限制:用户态函数可能再次发起系统调用,导致重入问题。
KeUserModeCallback 通过硬件特权级切换和预定义入口解决了这些问题。
Q2:为什么要将回调内存加入链表(Pti->W32CallbackListHead)?
A :自动资源管理。
回调内存的生命周期与"调用"绑定,但用户态代码可能:
- 抛出异常导致 IntCbFreeMemory 永远不会执行;
- 多次调用 IntCbAllocateMemory 但忘记释放;
- 线程意外终止。
通过链表跟踪所有回调分配,线程退出时一次性释放,确保不泄漏。
Q3:为什么 lParam 跨线程时需要 PackParam/UnpackParam?
A :用户态地址空间隔离。
即使两个线程属于同一进程:
- 它们的栈是独立的;
- 它们通过 HeapAlloc 分配的堆地址可能不同;
- 跨线程传递指针到对方进程空间是错误的。
PackParam 将 lParam 数据复制到内核态共享内存 ,UnpackParam 在接收方线程的用户态重新分配并复制数据,保证两边 WndProc 看到的都是正确的本进程地址。
Q4:为什么 IsAnsiProc 标志如此重要?
A :Ansi/Unicode 兼容性。
Windows NT 内核始终是 Unicode 的(NT 内核字符串均为 WCHAR)。但应用程序可能注册 Ansi 窗口过程:
- WndProcA 处理 WM_SETTEXT 时,lParam 是 char*;
- WndProcW 处理 WM_SETTEXT 时,lParam 是 wchar_t*。
IsAnsiProc 标志让 win32k 知道是否需要在分发前进行字符集转换,由 user32 完成具体转换工作。
Q5:为什么需要 IntSetTebWndCallback / IntRestoreTebWndCallback?
A :性能优化。
PCLIENTINFO->CallbackWnd 是 TEB 中的"回调窗口缓存",user32 在 WndProc 期间频繁需要访问窗口对象(PWND)。如果每次都通过 UserGetWindowObject 内核调用,性能会很差。
IntSetTebWndCallback 在回调前缓存当前窗口到 TEB,user32 直接读取 TEB 缓存即可,避免跨边界调用。回调返回后 IntRestoreTebWndCallback 恢复原值(嵌套调用支持)。
总结
Win32k 的用户空间回调机制是内核与用户态协作的核心桥梁。本节介绍了:
- KeUserModeCallback:NT 内核的通用回调机制;
- 回调类型编号:USER32_CALLBACK_* 枚举;
- 回调内存管理:IntCbAllocate/FreeMemory + 自动清理;
- WINDOWPROC_CALLBACK_ARGUMENTS:参数契约结构;
- co_IntCallWindowProc:调用 WndProc 的核心实现;
- PackParam/UnpackParam:跨边界参数序列化与反序列化;
- Ansi/Unicode 转换:字符集兼容性支持;
- 线程清理:IntCleanupThreadCallbacks 自动资源回收。
核心要点回顾:
- KeUserModeCallback 通过硬件特权级切换保证安全;
- 回调内存自动加入 pti 链表,线程退出时统一释放;
- PackParam/UnpackParam 处理 lParam 跨线程传递;
- IsAnsiProc 决定是否进行字符集转换;
- TEB 窗口缓存优化 WndProc 期间的用户态访问。
本章代码索引
| 文件 | 内容 |
|---|---|
| win32ss/user/ntuser/callback.c(file:///d:/reactos/win32ss/user/ntuser/callback.c) | IntCbAllocateMemory、IntCbFreeMemory、IntCleanupThreadCallbacks、co_IntCallWindowProc、co_IntCallHookProc、co_IntCallEventProc |
| win32ss/user/ntuser/callback.h(file:///d:/reactos/win32ss/user/ntuser/callback.h) | 回调函数声明、WINDOWPROC_CALLBACK_ARGUMENTS |
| win32ss/user/ntuser/message.c(file:///d:/reactos/win32ss/user/ntuser/message.c) | PackParam、UnpackParam、lParamMemorySize、FindMsgMemory、message_pointer_flags |
| win32ss/user/ntuser/callproc.c(file:///d:/reactos/win32ss/user/ntuser/callproc.c) | 窗口过程调用辅助 |
| win32ss/user/ntuser/main.c(file:///d:/reactos/win32ss/user/ntuser/main.c) | UserDeleteW32Thread(含 IntCleanupThreadCallbacks 调用) |
| win32ss/user/user32/windows/message.c(file:///d:/reactos/win32ss/user/user32/windows/message.c) | user32 端回调入口、Ansi/Unicode 转换 |
| win32ss/user/ntuser/hook.c(file:///d:/reactos/win32ss/user/ntuser/hook.c) | IntCallHookProc 钩子回调(详见 7.4) |