Reactos 第 7 章 视窗报文 — 7.1 视窗线程与 Win32k 扩充系统调用

第 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 扩充系统调用:部门之间的内部电话总机,所有跨部门请求都必须经过它登记和路由。

本节内容概览

  1. 7.1.0 框架图:视窗线程与系统调用整体架构;
  2. 7.1.1 视窗线程的概念:THREADINFO 在 Windows 子系统中的角色;
  3. 7.1.2 THREADINFO 数据结构:核心字段详解;
  4. 7.1.3 进程与线程的回调初始化:InitProcessCallback / InitThreadCallback;
  5. 7.1.4 AllocW32Thread 实现:线程上下文分配;
  6. 7.1.5 消息队列的创建与挂接:MsqCreateMessageQueue;
  7. 7.1.6 NtUserMessageCall 大分发函数:扩充系统调用的统一入口;
  8. 7.1.7 NtUserProcessConnect:用户态连接 Win32k 的协议;
  9. 7.1.8 NtUserInitializeClientPfnArrays:用户态回调函数注册;
  10. 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_CSRSSTHREADTIF_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;
}

关键步骤

  1. 检查是否已经初始化(避免重复);
  2. 分配 PPROCESSINFO(AllocW32Process);
  3. 映射全局用户堆(MapGlobalUserHeap);
  4. 初始化 USER 进程信息(UserProcessCreate);
  5. 初始化 GDI 进程信息(GdiProcessCreate);
  6. 将进程添加到全局列表 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;
}

关键步骤

  1. 分配 PTHREADINFO(AllocW32Thread);
  2. 初始化 THREADINFO 核心字段(EThread、Ppi、链表头等);
  3. 创建消息队列同步事件(hEventQueueClient / pEventQueueServer);
  4. 创建消息队列(MsqCreateMessageQueue);
  5. 分配默认键盘布局(KeyboardLayout);
  6. 初始化 TEB 中的 CLIENTINFO(用户态镜像);
  7. 设置期望 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);
    }
}

清理流程:

  1. 销毁消息队列;
  2. 清理线程消息;
  3. 释放 EThread 引用;
  4. 释放 pti 内存;
  5. 减少进程线程计数;
  6. 重新分派鼠标光标消息。

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;
}

关键点

  1. 分配 USER_MESSAGE_QUEUE 结构(位于非分页池);
  2. 初始化桌面引用、鼠标/键盘 pti;
  3. 从全局 gafAsyncKeyState 复制当前键盘状态;
  4. 设置用户态镜像 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 的原因:

  1. 减少系统调用表项:NT 内核的系统调用表项是稀缺资源,合并后只需一项;
  2. 统一安全检查:所有变体共享同一套 UserEnterExclusive/UserLeave 包装;
  3. 简化 client 端 stub:user32 只需一个 stub 函数即可调用所有变体;
  4. 集中日志/审计:所有消息类操作都经过同一个入口,便于追踪。

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->fsHookspci->dwTIFlags 等字段都通过这种镜像机制保持同步。

Q3:NtUserMessageCall 为什么要用 dwType 区分?

A:将数十个 NtUserXXX 合并为单一入口有三大优势:

  1. 系统调用表项节省:NT 内核的 syscall 表项是稀缺资源;
  2. 统一安全检查:所有变体共享 UserEnterExclusive/UserLeave;
  3. 简化 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 子系统在内核线程之上叠加的核心抽象。本节介绍了:

  1. 概念:PTHREADINFO 结构是"视窗线程"的具体实现;
  2. 数据结构:THREADINFO、CLIENTINFO、USER_MESSAGE_QUEUE 三层结构;
  3. 回调初始化:InitProcessCallback / InitThreadCallback 流程;
  4. 消息队列挂接:MsqCreateMessageQueue 创建并关联到 pti;
  5. 扩充系统调用:NtUserMessageCall 大分发函数;
  6. 连接协议:NtUserProcessConnect / NtUserInitializeClientPfnArrays。

核心要点回顾

  1. 视窗线程 = NT 线程 + THREADINFO(win32k 维护);
  2. THREADINFO 与 CLIENTINFO 是"内核态-用户态"镜像;
  3. InitThreadCallback 是线程进入 Win32 世界的入口;
  4. NtUserMessageCall 将数十个 NtUserXXX 合并为单入口;
  5. 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 对象管理
相关推荐
caimouse2 小时前
Reactos 第 9 章 设备驱动 — 9.7 一个过滤设备驱动模块的示例
windows
caimouse2 小时前
Reactos 第 7 章 视窗报文 — 7.7 鼠标器输入线程
windows
caimouse3 小时前
Reactos 第 7 章 视窗报文 — 7.2 视窗报文的接收
windows
caimouse3 小时前
Reactos 第 8 章 结构化异常处理 — 8.3 用户空间的结构化异常处理
windows
caimouse3 小时前
Reactos 第 9 章 设备驱动 — 9.6 中断处理
网络·windows
caimouse3 小时前
Reactos 第 7 章 视窗报文 — 7.6 键盘输入线程
windows
yinhunzw4 小时前
Claude code windows 安装
windows
七仔啊4 小时前
windows server 2022 部署前后端项目
windows
caimouse5 小时前
Reactos 第 7 章 视窗报文 — 7.4 用户空间的外挂函数
windows