Reactos 第 7 章 视窗报文 — 7.3 Win32k 的用户空间回调机制

第 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)时,调度员通过内部电话联系前台,处理完毕后再继续自己的工作。

本节内容概览

  1. 7.3.0 框架图:内核-用户态回调完整流程;
  2. 7.3.1 KeUserModeCallback 机制:NT 内核的通用回调机制;
  3. 7.3.2 回调类型与编号:USER32_CALLBACK_* 枚举;
  4. 7.3.3 回调内存管理:IntCbAllocateMemory / IntCbFreeMemory;
  5. 7.3.4 WINDOWPROC_CALLBACK_ARGUMENTS:窗口过程回调参数;
  6. 7.3.5 co_IntCallWindowProc:调用 WndProc 的核心;
  7. 7.3.6 消息参数打包 PackParam:跨边界参数序列化;
  8. 7.3.7 消息参数解包 UnpackParam:跨边界参数反序列化;
  9. 7.3.8 Ansi/Unicode 转换:lParam 文本转换;
  10. 7.3.9 线程清理:IntCleanupThreadCallbacks 资源回收;
  11. 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 的执行流程:

  1. 保存内核态上下文:保存当前内核栈、内核寄存器等;
  2. 构造用户态栈帧 :在用户态栈上构造 KCALLOUT_FRAME 结构,包含 ApiNumber 和 InputBuffer;
  3. 切换到用户态 :通过 KiCallbackReturn 跳转到用户态入口;
  4. 用户态分发:user32!_UserModeCallback 入口根据 ApiNumber 路由;
  5. 执行回调函数:调用 user32 中对应的处理函数;
  6. 通过 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 关键步骤解析

  1. 参数缓冲区分配

    • lParamBufferSize == -1:lParam 不含指针,使用栈缓冲区(零分配);
    • lParamBufferSize != -1:lParam 含有指针数据,需要分配回调内存并复制。
  2. IntSetTebWndCallback:切换 TEB 窗口缓存(PCLIENTINFO->CallbackWnd)以便 user32 快速访问窗口对象。

  3. UserLeaveCo:释放 win32k 的用户态锁,允许其他线程进入(重要:避免死锁)。

  4. KeUserModeCallback

    • 切换到 Ring 3;
    • user32 接收 USER32_CALLBACK_WINDOWPROC 路由;
    • 调用 IntCallMessageProc 进一步分发到 WndProc。
  5. RtlMoveMemory:从用户态复制结果(含 Result)。

  6. UserEnterCo:重新获取用户态锁。

  7. IntRestoreTebWndCallback:恢复 TEB 窗口缓存。

  8. 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),无法直接访问用户态内存。

因此需要:

  1. 发送前:将 lParam 数据复制到内核可访问的内存(PackParam);
  2. 接收时:将数据复制到目标线程的用户态内存(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 标志如此重要?

AAnsi/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 的用户空间回调机制是内核与用户态协作的核心桥梁。本节介绍了:

  1. KeUserModeCallback:NT 内核的通用回调机制;
  2. 回调类型编号:USER32_CALLBACK_* 枚举;
  3. 回调内存管理:IntCbAllocate/FreeMemory + 自动清理;
  4. WINDOWPROC_CALLBACK_ARGUMENTS:参数契约结构;
  5. co_IntCallWindowProc:调用 WndProc 的核心实现;
  6. PackParam/UnpackParam:跨边界参数序列化与反序列化;
  7. Ansi/Unicode 转换:字符集兼容性支持;
  8. 线程清理:IntCleanupThreadCallbacks 自动资源回收。

核心要点回顾

  1. KeUserModeCallback 通过硬件特权级切换保证安全;
  2. 回调内存自动加入 pti 链表,线程退出时统一释放;
  3. PackParam/UnpackParam 处理 lParam 跨线程传递;
  4. IsAnsiProc 决定是否进行字符集转换;
  5. 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)
相关推荐
caimouse1 小时前
Reactos 第 9 章 设备驱动 — 9.5 一组PnP设备驱动模块的实例
网络·windows
神成12 小时前
vmware 上 win7 系统按照 vmware tool
windows
虾壳云官方2 小时前
OpenClaw 2.7.9 Windows 一键部署教程:零基础也能搭建 AI 自动化助手
运维·人工智能·windows·自动化·openclaw·openclaw一键部署
xcLeigh4 小时前
鸿蒙平台 KeePass 密码管理器适配实战:从 Windows 到 鸿蒙PC 的 Electron 迁移指南
windows·electron·web·harmonyos·加密算法·keepass
caimouse7 小时前
Reactos 第 9 章 设备驱动 — 9.1 Windows的设备驱动框架
windows
宸丶一7 小时前
Day 10:LangGraph - Agent 的图执行引擎
java·windows·python
ylscode7 小时前
GreatXML BitLocker绕过漏洞深度解析:Windows Defender离线扫描如何被改造成本地提权后门
windows·安全
caimouse9 小时前
Reactos 第 8 章 结构化异常处理 — 8.1 结构化异常处理的程序框架
windows
caimouse9 小时前
Reactos 第 7 章 视窗报文 — 7.1 视窗线程与 Win32k 扩充系统调用
windows