第 7 章 视窗报文 --- 7.1 视窗线程与 Win32k 扩充系统调用
本节深入剖析 Windows/ReactOS 中"视窗线程"(Win32 Thread)的内核态实现,以及 Win32k 子系统提供给用户态的"扩充系统调用"(NtUserXXX)分发机制。
概述
视窗线程是 Windows 子系统的核心抽象之一------它是将"普通内核线程"与"Win32 用户态线程"绑定的桥梁。每个拥有消息循环的 GUI 线程(创建了窗口的线程)都被关联到一个 THREADINFO(也称 PTHREADINFO 或 pti)结构,win32k 通过这个结构管理该线程的窗口、消息队列、钩子、IME、键盘布局等所有"视窗"状态。
视窗线程的本质是什么?
视窗线程是 win32k 在 NT 内核线程模型之上叠加的一层"线程对象",它把分散在用户态(USER32)和内核态(win32k.sys)中的状态合并为一个逻辑整体。任何与消息、窗口、输入相关的系统调用,都必须先定位到"当前线程的 pti"才能正确执行。
想象一个办公大楼场景:
- NT 内核线程:大楼里的每个员工(最底层身份);
- 视窗线程:员工在大楼里挂靠的"部门身份",决定了能进哪些办公室(窗口)、能接哪些电话(消息)、能使用哪些公共设施(钩子/IME);
- THREADINFO:员工的部门档案,包含所有部门信息;
- Win32k 扩充系统调用:部门之间的内部电话总机,所有跨部门请求都必须经过它登记和路由。
本节内容概览
- 7.1.0 框架图:视窗线程与系统调用整体架构;
- 7.1.1 视窗线程的概念:THREADINFO 在 Windows 子系统中的角色;
- 7.1.2 THREADINFO 数据结构:核心字段详解;
- 7.1.3 进程与线程的回调初始化:InitProcessCallback / InitThreadCallback;
- 7.1.4 AllocW32Thread 实现:线程上下文分配;
- 7.1.5 消息队列的创建与挂接:MsqCreateMessageQueue;
- 7.1.6 NtUserMessageCall 大分发函数:扩充系统调用的统一入口;
- 7.1.7 NtUserProcessConnect:用户态连接 Win32k 的协议;
- 7.1.8 NtUserInitializeClientPfnArrays:用户态回调函数注册;
- 7.1.9 设计哲学问答:5 个关键设计问题的深入解答。
学习目标
读完本节后,读者应当能够:
- 理解视窗线程与普通 NT 线程的区别和联系;
- 掌握 THREADINFO 数据结构的核心字段含义;
- 分析 ReactOS 中 W32Thread 回调的注册与初始化流程;
- 解释 NtUserMessageCall 的统一分发机制;
- 掌握用户态 Win32 子系统(user32)连接内核态 win32k 的协议。
涉及的内核子系统
| 子系统 | 职责 |
|---|---|
| ntoskrnl/ps | 进程/线程对象管理(PsCreateThread、PsSetThreadWin32Thread) |
| ntoskrnl/ke | 线程调度与线程上下文切换 |
| win32k.sys | 视窗线程的回调初始化与维护(InitThreadCallback) |
| win32k.sys/ntuser | THREADINFO 分配、消息队列挂接、扩充系统调用 |
| user32.dll | 用户态 Win32 API 的封装(与 win32k 对接) |
| csrss.exe | Win32 子系统进程(持有特殊 TIF_CSRSSTHREAD 标志) |
7.1.0 框架图
┌──────────────────────────────────────────────────────────────────────────────────────┐
│ 视窗线程与 Win32k 扩充系统调用整体架构 │
├──────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 用户态层 │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ ntdll.dll │ │
│ │ ├─► NtCreateThread / RtlCreateUserThread (创建 NT 线程) │ │
│ │ ├─► NtUserProcessConnect (与 win32k 建立连接) │ │
│ │ └─► NtUserMessageCall (大分发系统调用入口) │ │
│ │ │ │
│ │ user32.dll │ │
│ │ ├─► InitializeClientPfnArrays (注册 client 端回调函数) │ │
│ │ ├─► GetMessage / PeekMessage / SendMessage (高层 API) │ │
│ │ └─► CreateWindowEx / DispatchMessage (使用 NtUserMessageCall) │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ (syscall: int 2Eh / syscall 指令) │
│ 内核态层 │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ ntoskrnl │ │
│ │ ├─► PsCreateSystemThread / PspCreateThread │ │
│ │ ├─► PsSetThreadWin32Thread (注册 W32Thread = pti) │ │
│ │ ├─► PspExitThread / PspExitProcess │ │
│ │ └─► W32kProcessCallback / W32kThreadCallback (win32k 的回调注册) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ win32k.sys │ │
│ │ ├─► main.c: │ │
│ │ │ ├─► InitProcessCallback (AllocW32Process + UserProcessCreate) │ │
│ │ │ └─► InitThreadCallback (AllocW32Thread + UserThreadCreate) │ │
│ │ ├─► object.c: THREADINFO / PROCESSINFO 对象管理 │ │
│ │ ├─► msgqueue.c: MsqCreateMessageQueue (消息队列挂接) │ │
│ │ ├─► ntstubs.c: │ │
│ │ │ ├─► NtUserProcessConnect (用户态连接协议) │ │
│ │ │ └─► NtUserInitializeClientPfnArrays (回调函数表) │ │
│ │ └─► message.c: │ │
│ │ └─► NtUserMessageCall (扩充系统调用统一分发) │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ 核心数据结构关系 │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ ETHREAD (内核线程) │ │
│ │ └─► W32Thread ──► THREADINFO (pti) │ │
│ │ ├─► ppi (PPROCESSINFO) │ │
│ │ ├─► MessageQueue (PUSER_MESSAGE_QUEUE) │ │
│ │ ├─► WindowListHead (该线程拥有的窗口链表) │ │
│ │ ├─► W32CallbackListHead (回调内存链表) │ │
│ │ ├─► PostedMessagesListHead (投递消息链表) │ │
│ │ ├─► SentMessagesListHead (发送消息链表) │ │
│ │ │ │ │
│ │ └─► PTHREADINFO.fsHooks ──► aphkStart[NB_HOOKS] (钩子链表) │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────┘
7.1.1 视窗线程的概念
7.1.1.1 为什么需要"视窗线程"?
NT 内核本身只有"进程"(EPROCESS)和"线程"(ETHREAD)两层抽象,它们不包含任何 GUI 相关的信息。但 Win32 子系统需要每个 GUI 线程维护以下状态:
- 消息队列:投递消息、发送消息、硬件消息三类队列;
- 窗口列表:该线程创建的所有窗口;
- 钩子链:安装在该线程上的各种 WH_* 钩子;
- IME / 键盘布局:每线程可能使用不同的输入法;
- 线程局部状态:消息过滤器、退出代码、焦点窗口、捕获窗口等;
- 回调内存:用于用户态/内核态共享消息数据。
这些状态的总和就是"视窗线程"(Win32 Thread / W32 Thread)。在 ReactOS 与 Windows NT 中,它通过 THREADINFO 结构(位于 win32k 内核态)配合 CLIENTINFO 结构(位于用户态 TEB 中)共同实现。
7.1.1.2 三层抽象的层次关系
┌─────────────────────────────────────────────────────────────────┐
│ 层次 1:NT 进程 ─► EPROCESS (内核对象) │
│ ─► PEB (用户态) │
│ ─► PPROCESSINFO (win32k 进程信息) │
├─────────────────────────────────────────────────────────────────┤
│ 层次 2:NT 线程 ─► ETHREAD (内核对象) │
│ ─► TEB (用户态) │
│ ─► PTHREADINFO (win32k 线程信息) ◄─ 视窗线程 │
├─────────────────────────────────────────────────────────────────┤
│ 层次 3:视窗线程 = PTHREADINFO │
│ 包含:消息队列、窗口列表、钩子、IME 等 GUI 状态 │
└─────────────────────────────────────────────────────────────────┘
7.1.1.3 视窗线程的"窗口"(PTHREADINFO 抽象)
PTHREADINFO 在很多代码中又被称为"窗口"(Window),但这里的"窗口"不是"可视化窗口",而是"线程的执行上下文"。win32k 把"消息循环所在的线程"视为一个虚拟的"窗口"或"会话"。这种命名方式源自 Microsoft 早期设计,已被代码广泛使用。
7.1.2 THREADINFO 数据结构
7.1.2.1 THREADINFO 核心字段
THREADINFO 结构定义在 win32ss/user/ntuser/ntuser.h 中,是 win32k 最重要的数据结构之一。其核心字段包括:
c
typedef struct _THREADINFO
{
/* 链表节点 */
LIST_ENTRY PtiLink; // 同一进程内所有 pti 的链表
PTHREADINFO ptiSibling; // 同进程中的兄弟线程
/* 进程和线程对象 */
PEPROCESS peProcess; // 关联的 EPROCESS
PETHREAD pEThread; // 关联的 ETHREAD
struct _PROCESSINFO *ppi; // 所属进程的 PROCESSINFO
/* 消息队列 */
PUSER_MESSAGE_QUEUE MessageQueue; // 线程消息队列
LIST_ENTRY PostedMessagesListHead; // 投递消息链表
LIST_ENTRY SentMessagesListHead; // 发送消息链表
/* 窗口列表 */
LIST_ENTRY WindowListHead; // 该线程创建的窗口列表
PWND spwndDefault; // 默认窗口
/* 钩子 */
DWORD fsHooks; // 已安装的钩子位图
PHOOK aphkStart[NB_HOOKS]; // 各类型钩子链头
/* 回调内存管理 */
LIST_ENTRY W32CallbackListHead; // 回调分配内存链表
/* 客户端信息 */
PCLIENTINFO pClientInfo; // 指向 TEB 中的 CLIENTINFO
/* 键盘/输入法 */
PKL KeyboardLayout; // 当前键盘布局
PTHREADINFO ptiKeyboard; // 拥有键盘焦点的线程
PTHREADINFO ptiMouse; // 拥有鼠标捕获的线程
/* 状态标志 */
DWORD TIF_flags; // 线程信息标志
// 0x00000001 TIF_INCLEANUP 正在清理
// 0x00000002 TIF_CSRSSTHREAD CSRSS 线程
// 0x00000004 TIF_DONTATTACHQUEUE 不挂接消息队列
// 0x00000008 TIF_INACTIVATE 失活中
// ... 等等
/* 等待状态 */
PTHREADINFO ptiSysLock; // 系统锁拥有者
ULONG_PTR idSysLock; // 系统锁标识
ULONG_PTR idSysPeek; // PeekMessage 锁
/* 同步事件 */
HANDLE hEventQueueClient; // 客户端事件句柄
PKEVENT pEventQueueServer; // 服务器端事件对象
/* 杂项 */
INT iCursorLevel; // 光标显示层级
LPARAM ExtraInfo; // 消息附加信息
DWORD dwExpWinVer; // 期望 Windows 版本
} THREADINFO, *PTHREADINFO;
7.1.2.2 TIF_flags 标志详解
c
#define TIF_INCLEANUP 0x00000001
#define TIF_CSRSSTHREAD 0x00000002
#define TIF_DONTATTACHQUEUE 0x00000004
#define TIF_INACTIVATE 0x00000008
#define TIF_MOVESIZE 0x00000010
#define TIF_IGNOREPLAYBACKDELAY 0x00000020
#define TIF_GHOSTTHREAD 0x00000040
#define TIF_FOCUSRINGING 0x00000080
#define TIF_ALLOWFOREGROUND 0x00000100
#define TIF_DIALOGWINDOW 0x00000200
#define TIF_SHAREDWOW 0x00000400
#define TIF_VDMAPP 0x00000800
#define TIF_16BIT 0x00001000
#define TIF_GLOBALHOOKER 0x00002000
#define TIF_INGRABTHREAD 0x00004000
最关键的标志是 TIF_CSRSSTHREAD 和 TIF_DONTATTACHQUEUE:CSRSS 线程拥有特殊权限,可直接与 win32k 通信而无需经过标准的 client/server 协议;DONTATTACHQUEUE 则用于 CSRSS 内部线程,它们不需要消息队列。
7.1.2.3 CLIENTINFO 结构(用户态对应物)
CLIENTINFO 位于 TEB 的 Win32ClientInfo 字段中,是 THREADINFO 在用户态的"投影":
c
typedef struct _CLIENTINFO
{
DWORD cbSize;
DWORD fsHooks; // 镜像 THREADINFO.fsHooks
DWORD dwTIFlags; // 镜像 TIF_flags
PVOID pti; // 指向内核 pti(仅供 kernel 引用)
struct _PROCESSINFO *ppi; // 进程信息
HKL hKL; // 键盘布局
DWORD CodePage; // 代码页
DWORD dwExpWinVer; // 期望 Windows 版本
PVOID pDeskInfo; // 桌面信息(共享内存)
ULONG_PTR ulSharedDelta; // 共享内存偏移
// ... 等等
} CLIENTINFO, *PCLIENTINFO;
CLIENTINFO 允许用户态代码(如 user32)快速访问线程状态而无需每次都进入内核。这种"用户态镜像"是 Win32 性能优化的关键。
7.1.3 进程与线程的回调初始化
7.1.3.1 win32k 在 NT 内核中的注册
ReactOS 在启动阶段会向 ntoskrnl 注册 win32k 的进程/线程回调函数:
c
// win32k 入口
NTSTATUS
NTAPI
DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath)
{
// ... 初始化 GDI、USER 子系统 ...
// 注册进程/线程回调
Status = PsSetCreateProcessNotifyRoutine(Win32kProcessCallback, FALSE);
Status = PsSetCreateThreadNotifyRoutine(Win32kThreadCallback);
// ...
}
这些回调在内核态被调用:
- Win32kProcessCallback:每个进程创建/销毁时被调用;
- Win32kThreadCallback:每个线程创建/销毁时被调用。
7.1.3.2 InitProcessCallback 实现
InitProcessCallback 位于 win32ss/user/ntuser/main.c:230(file:///d:/reactos/win32ss/user/ntuser/main.c#L230):
c
NTSTATUS NTAPI
InitProcessCallback(PEPROCESS Process)
{
NTSTATUS Status;
PPROCESSINFO ppiCurrent;
PVOID KernelMapping = NULL, UserMapping = NULL;
/* We might be called with an already allocated win32 process */
ppiCurrent = PsGetProcessWin32Process(Process);
if (ppiCurrent != NULL)
{
/* There is no more to do for us (this is a success code!) */
return STATUS_ALREADY_WIN32;
}
/* Allocate a new Win32 process info */
Status = AllocW32Process(Process, &ppiCurrent);
if (!NT_SUCCESS(Status))
{
ERR_CH(UserProcess, "Failed to allocate ppi for PID:0x%lx\n",
HandleToUlong(Process->UniqueProcessId));
return Status;
}
/* Map the global user heap into the process */
Status = MapGlobalUserHeap(Process, &KernelMapping, &UserMapping);
if (!NT_SUCCESS(Status))
{
TRACE_CH(UserProcess, "Failed to map the global heap! 0x%x\n", Status);
goto error;
}
/* Initialize USER process info */
Status = UserProcessCreate(Process);
if (!NT_SUCCESS(Status))
{
ERR_CH(UserProcess, "UserProcessCreate failed, Status 0x%08lx\n", Status);
goto error;
}
/* Initialize GDI process info */
Status = GdiProcessCreate(Process);
if (!NT_SUCCESS(Status))
{
ERR_CH(UserProcess, "GdiProcessCreate failed, Status 0x%08lx\n", Status);
goto error;
}
/* Add the process to the global list */
ppiCurrent->ppiNext = gppiList;
gppiList = ppiCurrent;
return STATUS_SUCCESS;
error:
ERR_CH(UserProcess, "InitProcessCallback failed! Freeing ppi 0x%p for PID:0x%lx\n",
ppiCurrent, HandleToUlong(Process->UniqueProcessId));
ExitProcessCallback(Process);
return Status;
}
关键步骤:
- 检查是否已经初始化(避免重复);
- 分配 PPROCESSINFO(AllocW32Process);
- 映射全局用户堆(MapGlobalUserHeap);
- 初始化 USER 进程信息(UserProcessCreate);
- 初始化 GDI 进程信息(GdiProcessCreate);
- 将进程添加到全局列表
gppiList。
7.1.3.3 InitThreadCallback 实现
InitThreadCallback 位于 win32ss/user/ntuser/main.c:455(file:///d:/reactos/win32ss/user/ntuser/main.c#L455):
c
NTSTATUS NTAPI
InitThreadCallback(PETHREAD Thread)
{
PEPROCESS Process;
PCLIENTINFO pci;
PTHREADINFO ptiCurrent;
int i;
NTSTATUS Status = STATUS_SUCCESS;
PTEB pTeb;
PRTL_USER_PROCESS_PARAMETERS ProcessParams;
PKL pDefKL;
BOOLEAN bFirstThread;
Process = Thread->ThreadsProcess;
pTeb = NtCurrentTeb();
ASSERT(pTeb);
ProcessParams = pTeb->ProcessEnvironmentBlock->ProcessParameters;
/* Allocate a new Win32 thread info */
Status = AllocW32Thread(Thread, &ptiCurrent);
if (!NT_SUCCESS(Status))
{
ERR_CH(UserThread, "Failed to allocate pti for TID:0x%lx\n",
HandleToUlong(Thread->Cid.UniqueThread));
return Status;
}
/* Initialize the THREADINFO */
ptiCurrent->pEThread = Thread;
ptiCurrent->ppi = PsGetProcessWin32Process(Process);
IntReferenceProcessInfo(ptiCurrent->ppi);
pTeb->Win32ThreadInfo = ptiCurrent;
ptiCurrent->pClientInfo = (PCLIENTINFO)pTeb->Win32ClientInfo;
ptiCurrent->pcti = &ptiCurrent->cti;
bFirstThread = !(ptiCurrent->ppi->W32PF_flags & W32PF_THREADCONNECTED);
/* Mark the process as having threads */
ptiCurrent->ppi->W32PF_flags |= W32PF_THREADCONNECTED;
InitializeListHead(&ptiCurrent->WindowListHead);
InitializeListHead(&ptiCurrent->W32CallbackListHead);
InitializeListHead(&ptiCurrent->PostedMessagesListHead);
InitializeListHead(&ptiCurrent->SentMessagesListHead);
InitializeListHead(&ptiCurrent->PtiLink);
for (i = 0; i < NB_HOOKS; i++)
{
InitializeListHead(&ptiCurrent->aphkStart[i]);
}
ptiCurrent->ptiSibling = ptiCurrent->ppi->ptiList;
ptiCurrent->ppi->ptiList = ptiCurrent;
ptiCurrent->ppi->cThreads++;
ptiCurrent->hEventQueueClient = NULL;
Status = ZwCreateEvent(&ptiCurrent->hEventQueueClient, EVENT_ALL_ACCESS,
NULL, SynchronizationEvent, FALSE);
if (!NT_SUCCESS(Status))
{
ERR_CH(UserThread, "Event creation failed, Status 0x%08x.\n", Status);
goto error;
}
Status = ObReferenceObjectByHandle(ptiCurrent->hEventQueueClient, 0,
*ExEventObjectType, UserMode,
(PVOID*)&ptiCurrent->pEventQueueServer, NULL);
if (!NT_SUCCESS(Status))
{
ERR_CH(UserThread, "Failed referencing the event object, Status 0x%08x.\n", Status);
ObCloseHandle(ptiCurrent->hEventQueueClient, UserMode);
ptiCurrent->hEventQueueClient = NULL;
goto error;
}
ptiCurrent->pcti->timeLastRead = EngGetTickCount32();
ptiCurrent->MessageQueue = MsqCreateMessageQueue(ptiCurrent);
if (ptiCurrent->MessageQueue == NULL)
{
ERR_CH(UserThread, "Failed to allocate message loop\n");
Status = STATUS_NO_MEMORY;
goto error;
}
pDefKL = W32kGetDefaultKeyLayout();
UserAssignmentLock((PVOID*)&(ptiCurrent->KeyboardLayout), pDefKL);
ptiCurrent->TIF_flags &= ~TIF_INCLEANUP;
/* CSRSS threads have some special features */
if (Process == gpepCSRSS || !gpepCSRSS)
ptiCurrent->TIF_flags = TIF_CSRSSTHREAD | TIF_DONTATTACHQUEUE;
/* Initialize the CLIENTINFO */
pci = (PCLIENTINFO)pTeb->Win32ClientInfo;
RtlZeroMemory(pci, sizeof(*pci));
pci->ppi = ptiCurrent->ppi;
pci->fsHooks = ptiCurrent->fsHooks;
pci->dwTIFlags = ptiCurrent->TIF_flags;
if (pDefKL)
{
pci->hKL = pDefKL->hkl;
pci->CodePage = pDefKL->CodePage;
}
/* Populate dwExpWinVer */
if (Process->Peb)
ptiCurrent->dwExpWinVer = RtlGetExpWinVer(Process->SectionBaseAddress);
else
ptiCurrent->dwExpWinVer = WINVER_WINNT4;
pci->dwExpWinVer = ptiCurrent->dwExpWinVer;
// ... 启动信息处理 ...
return Status;
error:
// 错误处理
return Status;
}
关键步骤:
- 分配 PTHREADINFO(AllocW32Thread);
- 初始化 THREADINFO 核心字段(EThread、Ppi、链表头等);
- 创建消息队列同步事件(hEventQueueClient / pEventQueueServer);
- 创建消息队列(MsqCreateMessageQueue);
- 分配默认键盘布局(KeyboardLayout);
- 初始化 TEB 中的 CLIENTINFO(用户态镜像);
- 设置期望 Windows 版本(dwExpWinVer)。
7.1.3.4 UserThreadDestroy / 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);
ObDereferenceObject(pti->pEThread);
ExFreePoolWithTag(pti, USERTAG_THREADINFO);
IntDereferenceProcessInfo(ppi);
// 重新分派鼠标消息给其他队列
{
MSG msg;
msg.message = WM_MOUSEMOVE;
msg.wParam = UserGetMouseButtonsState();
msg.lParam = MAKELPARAM(gpsi->ptCursor.x, gpsi->ptCursor.y);
msg.pt = gpsi->ptCursor;
co_MsqInsertMouseMessage(&msg, 0, 0, TRUE);
}
}
清理流程:
- 销毁消息队列;
- 清理线程消息;
- 释放 EThread 引用;
- 释放 pti 内存;
- 减少进程线程计数;
- 重新分派鼠标光标消息。
7.1.4 AllocW32Thread 实现
7.1.4.1 AllocW32Thread 源代码
AllocW32Thread 位于 win32ss/user/ntuser/main.c(file:///d:/reactos/win32ss/user/ntuser/main.c),其简化实现:
c
NTSTATUS
AllocW32Thread(PETHREAD Thread, PTHREADINFO *W32Thread)
{
PTHREADINFO ptiCurrent;
/* Allocate THREADINFO from non-paged pool */
ptiCurrent = ExAllocatePoolWithTag(NonPagedPool,
sizeof(THREADINFO),
USERTAG_THREADINFO);
if (!ptiCurrent)
{
ERR_CH(UserThread, "Failed to allocate pti\n");
return STATUS_NO_MEMORY;
}
/* Zero out the structure */
RtlZeroMemory(ptiCurrent, sizeof(THREADINFO));
/* Associate the THREADINFO with the ETHREAD */
PsSetThreadWin32Thread(Thread, ptiCurrent, NULL);
ObReferenceObject(Thread);
IntReferenceThreadInfo(ptiCurrent);
*W32Thread = ptiCurrent;
return STATUS_SUCCESS;
}
7.1.4.2 关键操作
- ExAllocatePoolWithTag:从非分页池分配 THREADINFO;
- RtlZeroMemory:清零结构(重要:所有链表头、标志位必须为零);
- PsSetThreadWin32Thread:将 pti 注册到 ETHREAD.Win32Thread 字段(这是 NT 内核为支持 Win32 线程预留的字段);
- ObReferenceObject(Thread):增加 EThread 引用计数(pti 引用了 EThread);
- IntReferenceThreadInfo:增加 pti 自身引用计数。
7.1.4.3 PsSetThreadWin32Thread 的作用
PsSetThreadWin32Thread 是 NT 内核为支持 Win32 线程提供的官方接口:
c
// ntoskrnl 内部实现
VOID
PsSetThreadWin32Thread(
IN PETHREAD Thread,
IN PVOID Win32Thread,
IN PVOID OldWin32Thread OPTIONAL)
{
// 把 Win32Thread 保存到 ETHREAD 的 Win32Thread 字段
Thread->Tcb.Win32Thread = Win32Thread;
// 调用者可以获取旧值
if (OldWin32Thread) {
*(PVOID*)OldWin32Thread = OldValue;
}
}
这相当于 NT 内核和 win32k 之间的"挂钩点"------NT 不知道 Win32Thread 是什么,但提供了存储位置;win32k 把 pti 存在这里,并通过 PsGetThreadWin32Thread 读取。
7.1.5 消息队列的创建与挂接
7.1.5.1 MsqCreateMessageQueue
MsqCreateMessageQueue 位于 win32ss/user/ntuser/msgqueue.c:2395(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L2395):
c
PUSER_MESSAGE_QUEUE FASTCALL
MsqCreateMessageQueue(PTHREADINFO pti)
{
PUSER_MESSAGE_QUEUE MessageQueue;
PCLIENTINFO pci = pti->pClientInfo;
MessageQueue = ExAllocatePoolWithTag(NonPagedPool,
sizeof(USER_MESSAGE_QUEUE),
USERTAG_Q);
if (!MessageQueue)
{
ERR("MsqCreateMessageQueue(): Unable to allocate message queue\n");
return NULL;
}
RtlZeroMemory(MessageQueue, sizeof(USER_MESSAGE_QUEUE));
MessageQueue->Desktop = pti->rpdesk;
IntReferenceObject(pti->rpdesk);
MessageQueue->ptiMouse = pti;
MessageQueue->ptiKeyboard = pti;
MessageQueue->References = 1;
/* Initialize list heads */
InitializeListHead(&MessageQueue->HardwareMessagesListHead);
InitializeListHead(&MessageQueue->PostedMessagesListHead);
/* Initialize key state from system state */
RtlCopyMemory(MessageQueue->afKeyState, gafAsyncKeyState, sizeof(gafAsyncKeyState));
RtlCopyMemory(MessageQueue->afKeyRecentDown, gafAsyncKeyStateRecentDown,
sizeof(gafAsyncKeyStateRecentDown));
/* Initialize cursor state */
MessageQueue->iCursorLevel = 0;
MessageQueue->CursorObject = pci->Cursor;
/* Update CLIENTINFO */
pci->MessageQueue = MessageQueue;
pti->MessageQueue = MessageQueue;
return MessageQueue;
}
关键点:
- 分配 USER_MESSAGE_QUEUE 结构(位于非分页池);
- 初始化桌面引用、鼠标/键盘 pti;
- 从全局
gafAsyncKeyState复制当前键盘状态; - 设置用户态镜像 pci->MessageQueue。
7.1.5.2 消息队列的核心字段
win32ss/user/ntuser/msgqueue.h:44(file:///d:/reactos/win32ss/user/ntuser/msgqueue.h#L44):
c
typedef struct _USER_MESSAGE_QUEUE
{
LONG References; // 引用计数
struct _DESKTOP *Desktop; // 关联桌面
PTHREADINFO ptiSysLock; // 系统锁拥有者
ULONG_PTR idSysLock; // 系统锁 ID
ULONG_PTR idSysPeek; // PeekMessage 锁 ID
PTHREADINFO ptiMouse; // 拥有鼠标光标的 pti
PTHREADINFO ptiKeyboard; // 拥有键盘焦点的 pti
LIST_ENTRY HardwareMessagesListHead; // 硬件消息链表
MSG msgDblClk; // 用于双击判定的上次消息
PWND spwndCapture; // 鼠标捕获窗口
PWND spwndFocus; // 焦点窗口
PWND spwndActive; // 活动窗口
PWND spwndActivePrev; // 前一个活动窗口
HWND MoveSize; // 当前 Move/Size 窗口
HWND MenuOwner; // 当前菜单所有者
BYTE MenuState; // 菜单状态
DWORD QF_flags; // 队列标志
DWORD cThreads; // 共享队列计数
LPARAM ExtraInfo; // 消息附加信息
BYTE afKeyRecentDown[256 / 8]; // 最近按下的键(1 bit/键)
BYTE afKeyState[256 * 2 / 8]; // 键状态(2 bit/键)
INT iCursorLevel; // 光标显示层级
PCURICON_OBJECT CursorObject; // 光标对象
THRDCARETINFO CaretInfo; // 插入符信息
} USER_MESSAGE_QUEUE, *PUSER_MESSAGE_QUEUE;
7.1.5.3 QF_flags 队列标志
c
#define QF_UPDATEKEYSTATE 0x00000001
#define QF_FMENUSTATUSBREAK 0x00000004
#define QF_FMENUSTATUS 0x00000008
#define QF_FF10STATUS 0x00000010
#define QF_MOUSEMOVED 0x00000020
#define QF_ACTIVATIONCHANGE 0x00000040
#define QF_TABSWITCHING 0x00000080
#define QF_KEYSTATERESET 0x00000100
#define QF_INDESTROY 0x00000200
#define QF_LOCKNOREMOVE 0x00000400
#define QF_FOCUSNULLSINCEACTIVE 0x00000800
#define QF_DIALOGACTIVE 0x00004000
#define QF_EVENTDEACTIVATEREMOVED 0x00008000
#define QF_TRACKMOUSELEAVE 0x00020000
#define QF_TRACKMOUSEHOVER 0x00040000
#define QF_TRACKMOUSEFIRING 0x00080000
#define QF_CAPTURELOCKED 0x00100000
#define QF_ACTIVEWNDTRACKING 0x00200000
这些标志影响消息分派、窗口激活、键盘状态更新等行为。
7.1.6 NtUserMessageCall 大分发函数
7.1.6.1 函数签名
NtUserMessageCall 是 win32k 中最重要的"大分发"系统调用 。它将原本零散的若干 NtUserXXX 函数合并为单个入口,通过 dwType 参数路由:
c
BOOL APIENTRY
NtUserMessageCall(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam,
ULONG_PTR ResultInfo,
DWORD dwType, // fnID: 决定具体分发的函数
BOOL Ansi)
{
LRESULT lResult = 0;
BOOL Ret = FALSE;
PWND Window = NULL;
USER_REFERENCE_ENTRY Ref;
UserEnterExclusive();
switch (dwType)
{
case FNID_SCROLLBAR:
lResult = ScrollBarWndProc(hWnd, Msg, wParam, lParam);
break;
case FNID_DESKTOP:
// ...
case FNID_MENU:
// ...
case FNID_MESSAGEWND:
// ...
case FNID_DEFWINDOWPROC:
// ...
case FNID_SENDNOTIFYMESSAGE:
// ...
// ... 几十个 FNID_* ...
}
UserLeave();
return Ret;
}
7.1.6.2 FNID 分类
FNID(Function ID)是 Windows 内部的函数分类标识符,决定了 NtUserMessageCall 的具体分发目标。常见 FNID 包括:
| FNID | 含义 | 对应函数 |
|---|---|---|
FNID_SCROLLBAR |
滚动条控件 | ScrollBarWndProc |
FNID_DESKTOP |
桌面窗口 | DesktopWindowProc |
FNID_MENU |
弹出菜单 | PopupMenuWndProc |
FNID_MESSAGEWND |
Message-Only 窗口 | UserMessageWindowProc |
FNID_DEFWINDOWPROC |
默认窗口过程 | IntDefWindowProc |
FNID_SENDNOTIFYMESSAGE |
通知消息 | UserSendNotifyMessage |
FNID_BROADCASTSYSTEMMESSAGE |
系统广播 | (系统消息广播) |
FNID_DESTROYWINDOW |
销毁窗口 | (窗口销毁处理) |
FNID_FINALIZEFOREGROUND |
前台终结 | (前台切换收尾) |
7.1.6.3 设计目的
将数十个 NtUserXXX 函数合并为单一 NtUserMessageCall 的原因:
- 减少系统调用表项:NT 内核的系统调用表项是稀缺资源,合并后只需一项;
- 统一安全检查:所有变体共享同一套 UserEnterExclusive/UserLeave 包装;
- 简化 client 端 stub:user32 只需一个 stub 函数即可调用所有变体;
- 集中日志/审计:所有消息类操作都经过同一个入口,便于追踪。
7.1.6.4 重要:FNID_DEFWINDOWPROC 详解
FNID_DEFWINDOWPROC 是最常用的一个分支:
c
case FNID_DEFWINDOWPROC:
/* Validate input */
if (hWnd)
{
Window = UserGetWindowObject(hWnd);
if (!Window)
{
UserLeave();
return FALSE;
}
UserRefObjectCo(Window, &Ref);
}
lResult = IntDefWindowProc(Window, Msg, wParam, lParam, Ansi);
Ret = TRUE;
if (hWnd)
UserDerefObjectCo(Window);
break;
它处理 DefWindowProc 调用:当应用程序调用 DefWindowProc 时,user32 会通过 NtUserMessageCall 路由到这里,调用内核态的 IntDefWindowProc,避免用户态/内核态来回切换。
7.1.7 NtUserProcessConnect
7.1.7.1 连接协议概述
NtUserProcessConnect 是 user32 与 win32k 之间建立"连接"的协议。user32 在初始化时调用它,从 win32k 接收必要的内核信息(如共享堆地址、客户端句柄表指针等):
c
NTSTATUS
APIENTRY
NtUserProcessConnect(
IN HANDLE ProcessHandle,
OUT PUSERCONNECT pUserConnect,
IN ULONG Size)
{
NTSTATUS Status;
PEPROCESS Process = NULL;
PPROCESSINFO W32Process;
TRACE("NtUserProcessConnect\n");
if (pUserConnect == NULL || Size != sizeof(*pUserConnect))
{
return STATUS_UNSUCCESSFUL;
}
/* Get the process object the user handle was referencing */
Status = ObReferenceObjectByHandle(ProcessHandle,
PROCESS_VM_OPERATION,
*PsProcessType,
UserMode,
(PVOID*)&Process,
NULL);
if (!NT_SUCCESS(Status)) return Status;
UserEnterShared();
/* Get Win32 process information */
W32Process = PsGetProcessWin32Process(Process);
_SEH2_TRY
{
ProbeForWrite(pUserConnect, sizeof(*pUserConnect), sizeof(PVOID));
// 关键:返回共享堆的地址偏移
pUserConnect->siClient.ulSharedDelta =
(ULONG_PTR)W32Process->HeapMappings.KernelMapping -
(ULONG_PTR)W32Process->HeapMappings.UserMapping;
#define SERVER_TO_CLIENT(ptr) \
((PVOID)((ULONG_PTR)ptr - pUserConnect->siClient.ulSharedDelta))
// 返回 SERVER 端 gpsi / gHandleTable 的 CLIENT 端地址
pUserConnect->siClient.psi = SERVER_TO_CLIENT(gpsi);
pUserConnect->siClient.aheList = SERVER_TO_CLIENT(gHandleTable);
pUserConnect->siClient.pDispInfo = NULL;
// ...
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
UserLeave();
ObDereferenceObject(Process);
return Status;
}
7.1.7.2 USERCONNECT 结构
USERCONNECT 定义于 win32ss/user/ntuser/include/(file:///d:/reactos/win32ss/user/ntuser/include):
c
typedef struct _USERCONNECT
{
// 共享堆信息
SHAREDINFO siClient;
// 其他 client 端需要的内核信息
} USERCONNECT, *PUSERCONNECT;
7.1.7.3 共享堆机制
NtUserProcessConnect 返回的关键信息是 ulSharedDelta:
内核地址 ─────────┐
│ KERNEL_MAPPING
│ ┌──────────────┐
▼ ▼ │
┌─────────┐ │
│ 共 享 堆 │ ◄───────── KernelMapping
│ (内存) │
└─────────┘
│ USER_MAPPING (映射到用户态)
▼
用户地址 ─────┐
▼ ┌──────────────┐
│ 共 享 堆(视角)│
└──────────────┘
ulSharedDelta = KernelMapping - UserMapping 是一个常量偏移,用于在 server/client 端地址之间转换。
7.1.7.4 共享堆的意义
- 性能:用户态可以直接读取共享堆数据(gpsi、句柄表等),避免每次都进入内核;
- 限制:用户态只能读取,不能写入(受页面保护);
- 一致性:所有进程共享同一份数据(gpsi、句柄表等全局信息)。
7.1.8 NtUserInitializeClientPfnArrays
7.1.8.1 用户态回调函数注册
user32 在启动时将自己实现的回调函数数组地址传递给 win32k:
c
NTSTATUS
APIENTRY
NtUserInitializeClientPfnArrays(
PPFNCLIENT pfnClientA, // ANSI 回调函数表
PPFNCLIENT pfnClientW, // Unicode 回调函数表
PPFNCLIENTWORKER pfnClientWorker, // Worker 函数表
HINSTANCE hmodUser)
{
NTSTATUS Status = STATUS_SUCCESS;
TRACE("Enter NtUserInitializeClientPfnArrays User32 0x%p\n", hmodUser);
if (ClientPfnInit) return Status;
UserEnterExclusive();
_SEH2_TRY
{
ProbeForRead(pfnClientA, sizeof(PFNCLIENT), 1);
ProbeForRead(pfnClientW, sizeof(PFNCLIENT), 1);
ProbeForRead(pfnClientWorker, sizeof(PFNCLIENTWORKER), 1);
// 保存到全局 gpsi
RtlCopyMemory(&gpsi->apfnClientA, pfnClientA, sizeof(PFNCLIENT));
RtlCopyMemory(&gpsi->apfnClientW, pfnClientW, sizeof(PFNCLIENT));
RtlCopyMemory(&gpsi->apfnClientWorker, pfnClientWorker, sizeof(PFNCLIENTWORKER));
// FIXME: HAX! 临时复制
RtlCopyMemory(&gpsi->aStoCidPfn, pfnClientW, sizeof(gpsi->aStoCidPfn));
hModClient = hmodUser;
ClientPfnInit = TRUE;
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
if (!NT_SUCCESS(Status))
{
ERR("Failed reading Client Pfns from user space.\n");
SetLastNtError(Status);
}
UserLeave();
return Status;
}
7.1.8.2 PFNCLIENT 包含哪些函数?
PFNCLIENT 是一个函数指针数组,包含 user32 提供的关键回调函数:
c
typedef struct _PFNCLIENT
{
// ANSI/Unicode 窗口过程
PROC pfnScrollBarWndProc; // 滚动条
PROC pfnDefWindowProcA; // 默认窗口过程 (ANSI)
PROC pfnDefWindowProcW; // 默认窗口过程 (Unicode)
PROC pfnCallWindowProcA; // 调用窗口过程 (ANSI)
PROC pfnCallWindowProcW; // 调用窗口过程 (Unicode)
// 菜单
PROC pfnPopupMenuWndProc; // 弹出菜单
PROC pfnDesktopWndProc; // 桌面窗口
PROC pfnMessageWndProc; // Message-Only 窗口
// 其他
PROC pfnSwitchWindowProc; // Alt+Tab 切换
PROC pfnControlPanel; // 控制面板入口
// ... 等等
} PFNCLIENT, *PPFNCLIENT;
7.1.8.3 调用流程
当 win32k 需要调用一个原本在 user32 实现的函数时(如 DefWindowProc),它通过保存的函数指针跳转:
win32k 内核态
│
├─► 需要调用 DefWindowProcW
│ │
│ └─► 读取 gpsi->apfnClientW.pfnDefWindowProcW
│ │
│ └─► KeUserModeCallback(...) 切换到用户态
│ │
│ └─► user32!DefWindowProcW 函数
│ │
│ └─► 又通过 NtUserMessageCall(..., FNID_DEFWINDOWPROC, ...)
│ │
│ └─► 回到 win32k 的 IntDefWindowProc
这是一个"内核 → 用户 → 内核"的循环调用,是 Win32 消息处理的核心机制。
7.1.9 设计哲学问答
Q1:为什么 NT 内核不直接提供 GUI 线程概念?
A:NT 内核的设计哲学是"微内核式"分工:
- NT 内核只负责调度、内存、对象、I/O;
- **GUI 子系统(win32k)**负责所有 GUI 相关功能。
这种分层的好处是:
- 内核代码简洁、可移植;
- GUI 子系统可以独立升级;
- 其他子系统(POSIX、OS/2)也可以使用 NT 内核。
代价是需要在两者之间维护"挂钩点"(PsSetThreadWin32Thread)以及"扩充系统调用"机制。
Q2:为什么需要 THREADINFO 与 CLIENTINFO 双份状态?
A:性能与一致性的权衡:
- THREADINFO(内核态):是真正的"权威"状态,所有修改都通过它;
- CLIENTINFO(用户态):是"投影/缓存",允许用户态代码快速读取而无需进入内核。
当 user32 启动时,从内核读取 THREADINFO 填充 CLIENTINFO;后续修改需同步两者。pci->fsHooks、pci->dwTIFlags 等字段都通过这种镜像机制保持同步。
Q3:NtUserMessageCall 为什么要用 dwType 区分?
A:将数十个 NtUserXXX 合并为单一入口有三大优势:
- 系统调用表项节省:NT 内核的 syscall 表项是稀缺资源;
- 统一安全检查:所有变体共享 UserEnterExclusive/UserLeave;
- 简化 stub:user32 只需一个 stub 调用。
dwType 实际是 FNID(Function ID),每个 FNID 对应一个具体功能。Windows 在内部广泛使用这种"大分发"模式(类似的还有 NtSetInformationFile、NtQueryInformationFile)。
Q4:为什么 CSRSS 线程需要 TIF_CSRSSTHREAD 特殊标志?
A:CSRSS(Client/Server Runtime Subsystem)是 Win32 子系统的主进程,它承担多项特殊职责:
- 接收所有 Win32 API 调用的 LPC 请求;
- 管理控制台窗口;
- 处理硬错误(Hard Error)对话框。
由于 CSRSS 早于任何 GUI 线程存在,它的消息队列和钩子机制与普通 GUI 线程不同:
- TIF_CSRSSTHREAD:标识这是一个 CSRSS 线程;
- TIF_DONTATTACHQUEUE:不挂接消息队列(CSRSS 不需要消息循环)。
这些特殊标志让 win32k 在处理 CSRSS 线程时跳过标准的消息队列初始化。
Q5:为什么 THREADINFO 引用计数使用 InterlockedIncrement?
A:多线程并发访问。THREADINFO 可能在以下场景被并发访问:
- 线程 A 在 GetMessage 中读取自己的 pti;
- 线程 B 在 SendMessage 中向 A 发送消息;
- 线程 C 在 hook 处理中检查 A 的 pti;
- 线程 A 退出时销毁 pti。
IntReferenceThreadInfo 使用 InterlockedIncrement 保证引用计数的原子性,避免出现"已销毁但仍在使用"的问题。这是所有内核态对象共有的模式。
总结
视窗线程(Win32 Thread)是 Win32 子系统在内核线程之上叠加的核心抽象。本节介绍了:
- 概念:PTHREADINFO 结构是"视窗线程"的具体实现;
- 数据结构:THREADINFO、CLIENTINFO、USER_MESSAGE_QUEUE 三层结构;
- 回调初始化:InitProcessCallback / InitThreadCallback 流程;
- 消息队列挂接:MsqCreateMessageQueue 创建并关联到 pti;
- 扩充系统调用:NtUserMessageCall 大分发函数;
- 连接协议:NtUserProcessConnect / NtUserInitializeClientPfnArrays。
核心要点回顾:
- 视窗线程 = NT 线程 + THREADINFO(win32k 维护);
- THREADINFO 与 CLIENTINFO 是"内核态-用户态"镜像;
- InitThreadCallback 是线程进入 Win32 世界的入口;
- NtUserMessageCall 将数十个 NtUserXXX 合并为单入口;
- CSRSS 线程拥有 TIF_CSRSSTHREAD 特殊标志。
本章代码索引
| 文件 | 内容 |
|---|---|
| win32ss/user/ntuser/main.c(file:///d:/reactos/win32ss/user/ntuser/main.c) | InitProcessCallback、InitThreadCallback、AllocW32Thread、UserDeleteW32Thread |
| win32ss/user/ntuser/msgqueue.c(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c) | MsqCreateMessageQueue、消息队列管理 |
| win32ss/user/ntuser/msgqueue.h(file:///d:/reactos/win32ss/user/ntuser/msgqueue.h) | USER_MESSAGE_QUEUE、QF_* 标志 |
| win32ss/user/ntuser/ntstubs.c(file:///d:/reactos/win32ss/user/ntuser/ntstubs.c) | NtUserMessageCall、NtUserProcessConnect、NtUserInitializeClientPfnArrays |
| win32ss/user/ntuser/ntuser.h(file:///d:/reactos/win32ss/user/ntuser/ntuser.h) | THREADINFO、TIF_* 标志 |
| win32ss/user/ntuser/message.c(file:///d:/reactos/win32ss/user/ntuser/message.c) | NtUserMessageCall 实现(行 2527) |
| win32ss/user/ntuser/callback.c(file:///d:/reactos/win32ss/user/ntuser/callback.c) | 回调内存管理(详见 7.3) |
| win32ss/user/ntuser/object.c(file:///d:/reactos/win32ss/user/ntuser/object.c) | THREADINFO/PROCESSINFO 对象管理 |