Reactos 第 7 章 视窗报文 — 7.6 键盘输入线程

第 7 章 视窗报文 --- 7.6 键盘输入线程

本节深入剖析 Windows/ReactOS 中"键盘输入"(Keyboard Input)的内核态处理,包括 RIT(Raw Input Thread)架构、原始 KEYBOARD_INPUT_DATA 转换、VK 码处理、Numpad 翻译、LED 指示灯同步、字符消息生成、IME 与 LL 钩子等。

概述

键盘输入是 Windows GUI 系统最核心的 I/O 来源之一。Windows 的键盘处理需要解决以下问题:

  • 原始数据 → 消息转换 :将键盘类驱动(KBDCLASS)产生的 KEYBOARD_INPUT_DATA 转换为 WM_KEYDOWN/WM_KEYUP/WM_CHAR/WM_DEADCHAR 等消息;
  • VK 码处理:区分左右 Shift/Ctrl/Alt、处理扩展键标志;
  • 键盘布局(Layout):根据当前语言(美式/法语/日语)将 VK 码翻译为字符;
  • 状态维护:NumLock/CapsLock/ScrollLock 状态、按键 down/up 状态;
  • 特殊输入:Alt+Numpad 字符输入(Unicode 直接输入)、VK_PACKET(来自 SendInput 的 Unicode);
  • IME(输入法):将按键路由到 IME 进行中文/日文/韩文输入;
  • 热键(HotKey):RegisterHotKey 注册的全局热键;
  • LL 钩子:在原始数据级别拦截(WH_KEYBOARD_LL)。

键盘输入的本质是什么?

键盘输入是一种"原始数据 → 消息流"的转换过程。键盘类驱动(KBDCLASS)从硬件读取扫描码,包装为 KEYBOARD_INPUT_DATA 投递到 RIT(Raw Input Thread,原始输入线程);RIT 在 win32k 内核态将原始数据转换为标准的窗口消息,并维护全局按键状态供 GetAsyncKeyState/GetKeyState 查询。

想象一个"国际邮件分拣中心"场景:

  • 键盘硬件:各国的寄件人(说不同语言的);
  • 扫描码(ScanCode):邮件上的"国家代码"(如 0x1C 是键盘 A 键的物理位置);
  • VK 码:经过初步分拣后的"语言代码"(如 VK_A);
  • KBDTABLES(键盘布局表):翻译字典,将 VK 码翻译为特定语言的字符;
  • WM_KEYDOWN/WM_KEYUP:邮件本身的"按下/释放"状态;
  • WM_CHAR:翻译后的"文字内容"(如 'a' 或 'A');
  • IME:分拣中心的"特殊处理柜台"(把邮件路由到日语/中文处理);
  • LL 钩子:分拣中心的"安检 X 光机"(可拦截并改派邮件);
  • gafAsyncKeyState:分拣中心的"实时状态板"(记录哪个键当前是 down)。

本节内容概览

  1. 7.6.0 框架图:键盘输入子系统的整体架构;
  2. 7.6.1 键盘输入子系统架构(KBDCLASS → win32k → 窗口消息);
  3. 7.6.2 键盘初始化(InitKeyboardImpl、UserInitKeyboard);
  4. 7.6.3 VK 码转换:IntSimplifyVk / IntFixVk;
  5. 7.6.4 Numpad 翻译:IntTranslateNumpadKey;
  6. 7.6.5 LED 指示灯与锁定键
  7. 7.6.6 gafAsyncKeyState 异步按键状态数组
  8. 7.6.7 Alt+Numpad 字符输入
  9. 7.6.8 co_IntProcessKeyboardMessage 核心处理
  10. 7.6.9 字符消息生成(WM_CHAR / WM_DEADCHAR / ToUnicodeEx);
  11. 7.6.10 IME 处理与全局热键
  12. 7.6.11 键盘 LL 钩子
  13. 7.6.12 设计哲学问答:5 个关键设计问题解答。

学习目标

读完本节后,读者应当能够:

  • 理解 RIT 原始输入线程的角色与数据流;
  • 掌握 VK 码的左右手区分、扩展标志处理;
  • 解释键盘布局(KBDTABLES)与字符翻译机制;
  • 理解 NumLock/CapsLock/ScrollLock 状态同步与 LED 控制;
  • 掌握 gafAsyncKeyState 的两位状态语义(down / locked);
  • 分析 Alt+Numpad Unicode 字符输入状态机;
  • 理解 IME 在键盘事件流中的角色;
  • 掌握 WH_KEYBOARD_LL 钩子的实现。

涉及的内核子系统

子系统 职责
ntoskrnl / kbdclass 键盘类驱动:硬件扫描码 → KEYBOARD_INPUT_DATA
win32k.sys/ntuser/keyboard.c 键盘消息处理、VK 转换、字符翻译、LL 钩子
win32k.sys/ntuser/msgqueue.c co_IntProcessKeyboardMessage:消息分派
win32k.sys/ntuser/kbdlayout.c KBDTABLES 加载与维护
win32k.sys/ntuser/ime.c IntImmProcessKey:IME 处理
win32k.sys/ntuser/input.c UserProcessKeyboardInput:原始输入入口
user32.dll ToUnicodeEx、GetKeyState、RegisterHotKey 用户态 API

7.6.0 框架图

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────┐
│                    键盘输入子系统完整架构                                                │
├──────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                      │
│   硬件层                                                                              │
│   ┌────────────────────────────────────────────────────────────────────────────┐     │
│   │  PS/2 / USB 键盘硬件                                                         │     │
│   │  产生中断(IRQ1 / USB HID)                                                  │     │
│   │  键盘端口驱动(i8042prt / kbdhid)读取扫描码                                 │     │
│   └────────────────────────────────────────────────────────────────────────────┘     │
│                              │                                                       │
│                              ▼                                                       │
│   内核类驱动层                                                                          │
│   ┌────────────────────────────────────────────────────────────────────────────┐     │
│   │  kbdclass.sys  (键盘类驱动)                                                   │     │
│   │  ├─► 维护 IRP_MJ_READ 队列                                                  │     │
│   │  └─► 将扫描码包装为 KEYBOARD_INPUT_DATA 投递到 RIT                          │     │
│   └────────────────────────────────────────────────────────────────────────────┘     │
│                              │                                                       │
│                              ▼  (RIT: \Device\KeyboardClass0 的读 IRP 完成)            │
│   win32k 用户子系统(RIT 在 Win32k 内核态)                                              │
│   ┌────────────────────────────────────────────────────────────────────────────┐     │
│   │  input.c  UserProcessKeyboardInput(KEYBOARD_INPUT_DATA)                    │     │
│   │      │                                                                     │     │
│   │      ▼                                                                     │     │
│   │  keyboard.c                                                                 │     │
│   │  ├─► 扫描码 → VK 码 (IntVkFromSc / pKbdTbl->pusVSCtoVK)                   │     │
│   │  ├─► 修正 VK (IntFixVk: 左右 Shift/Ctrl/Alt)                              │     │
│   │  ├─► 简化 VK (IntSimplifyVk)                                               │     │
│   │  ├─► 更新 gafAsyncKeyState (UpdateAsyncKeyState)                           │     │
│   │  ├─► WH_KEYBOARD_LL 钩子 (co_CallLowLevelKeyboardHook)                     │     │
│   │  ├─► 更新 LED 灯 (IntKeyboardUpdateLeds)                                   │     │
│   │  ├─► Alt+Numpad 状态机 (gAltNumPadState)                                    │     │
│   │  └─► 投递硬件消息 (MsqPostHardwareMessage)                                 │     │
│   │      │                                                                     │     │
│   │      ▼                                                                     │     │
│   │  msgqueue.c  HardwareMessagesListHead ─► co_MsqPeekHardwareMessage        │     │
│   │      │                                                                     │     │
│   │      ▼                                                                     │     │
│   │  co_IntProcessKeyboardMessage                                              │     │
│   │  ├─► 修正 VK 区分左右 (IntSimplifyVk)                                      │     │
│   │  ├─► WH_JOURNALRECORD 钩子                                                 │     │
│   │  ├─► F1 键特殊处理 (WM_HELP)                                               │     │
│   │  ├─► Alt-Tab/ESC 系统键处理 (SC_NEXTWINDOW/SC_PREVWINDOW)                  │     │
│   │  ├─► WH_KEYBOARD 钩子                                                      │     │
│   │  └─► IME 处理 (IntImmProcessKey)                                          │     │
│   │      │                                                                     │     │
│   │      ▼                                                                     │     │
│   │  user32.dll  TranslateMessage / DispatchMessage                            │     │
│   │  ├─► ToUnicodeEx 字符翻译                                                  │     │
│   │  └─► WndProc 收到 WM_KEYDOWN / WM_CHAR / WM_DEADCHAR                      │     │
│   └────────────────────────────────────────────────────────────────────────────┘     │
│                                                                                      │
│   键盘状态与翻译表                                                                       │
│   ┌────────────────────────────────────────────────────────────────────────────┐     │
│   │  gafAsyncKeyState[64]        : 256 个 VK 码 × 2 bit (down / locked)        │     │
│   │  gafAsyncKeyStateRecentDown  : 256 个 VK 码 × 1 bit (最近 down)            │     │
│   │  gAltNumPadState             : Alt+Numpad 状态机                            │     │
│   │  gKeyboardInfo               : 键盘类型/子类型/功能键数量                   │     │
│   │  gIndicators                 : LED 状态 (Caps/Num/Scroll Lock)              │     │
│   │  gpKeyboardIndicatorTrans    : 指示灯翻译表                                 │     │
│   │  PKBDTABLES pKbdTbl          : 当前线程的键盘布局表                          │     │
│   │      ├─► pVkToWcharTable      : VK → WCHAR 翻译                             │     │
│   │      ├─► pusVSCtoVK           : ScanCode → VK                               │     │
│   │      └─► pDeadKey             : Dead Key 组合规则                           │     │
│   └────────────────────────────────────────────────────────────────────────────┘     │
│                                                                                      │
└──────────────────────────────────────────────────────────────────────────────────────┘

7.6.1 键盘输入子系统架构

7.6.1.1 RIT(Raw Input Thread)原始输入线程

RIT 是 Windows 子系统启动时创建的特殊线程,专门负责处理来自设备的原始输入数据(键盘、鼠标、其他 HID 设备)。它的工作流程:

复制代码
键盘硬件
  │  中断(IRQ1)
  ▼
i8042prt / kbdhid 端口驱动
  │  读取扫描码
  ▼
kbdclass 键盘类驱动
  │  包装为 KEYBOARD_INPUT_DATA
  │  完成 IRP_MJ_READ
  ▼
RIT 线程(等待 IRP 完成)
  │  UserProcessKeyboardInput(data)
  ▼
win32k 键盘处理

为什么需要 RIT?

  • 统一输入源:所有键盘设备(PS/2、USB、蓝牙)的输入都汇总到 RIT,避免每个设备驱动各自处理;
  • 单线程输入上下文 :避免多线程并发修改 gafAsyncKeyState 等全局状态;
  • 优先级控制:RIT 在高优先级运行,确保输入响应实时性。

7.6.1.2 UserProcessKeyboardInput

win32ss/user/ntuser/input.c(file:///d:/reactos/win32ss/user/ntuser/input.c) 中定义 UserProcessKeyboardInput

c 复制代码
VOID NTAPI
UserProcessKeyboardInput(KEYBOARD_INPUT_DATA *KeyData)
{
    WORD wVk;
    PKBDTABLES pKbdTbl;
    DWORD dwFlags = 0;
    BOOL bIsDown;

    // 1. 获取当前线程的键盘布局
    pKbdTbl = IntGetCurrentKbdTables();

    // 2. 扫描码 → VK 码
    wVk = IntVkFromSc(pKbdTbl, KeyData->MakeCode, KeyData->Flags & KEY_E0 ? 1 : 0);

    // 3. 区分左右 Shift/Ctrl/Alt
    if (KeyData->Flags & KEY_E0)
        wVk = IntFixVk(wVk, TRUE);
    else
        wVk = IntFixVk(wVk, FALSE);

    // 4. 设置 KeyEventF 标志
    if (!(KeyData->Flags & KEY_E0)) dwFlags |= 0;
    if (KeyData->Flags & KEY_E1)   dwFlags |= KEYEVENTF_EXTENDEDKEY;
    bIsDown = !(KeyData->Flags & KEY_BREAK);

    // 5. 更新全局按键状态
    UpdateAsyncKeyState(wVk, bIsDown);

    // 6. 更新 LED 灯
    if (wVk == VK_CAPITAL || wVk == VK_NUMLOCK || wVk == VK_SCROLL)
        IntKeyboardUpdateLeds(...);

    // 7. 处理 Alt+Numpad 字符输入
    if (IntHandleAltNumpad(...))
        return;

    // 8. WH_KEYBOARD_LL 钩子
    if (co_CallLowLevelKeyboardHook(wVk, ...))
        return;

    // 9. 投递硬件消息
    MsqPostHardwareMessage(WM_KEYDOWN/WM_KEYUP, ...);
}

7.6.1.3 消息队列中的硬件消息

键盘消息进入硬件消息队列HardwareMessagesListHead),与普通投递消息(PostedMessagesListHead)分离:

c 复制代码
// win32ss/user/ntuser/msgqueue.c
typedef struct _USER_MESSAGE_QUEUE
{
    LIST_ENTRY HardwareMessagesListHead;   // 硬件消息(键盘/鼠标)
    LIST_ENTRY PostedMessagesListHead;     // 普通投递消息
    // ...
}

为何分离

  • 优先级:硬件消息优先级高于投递消息(避免输入延迟);
  • 过滤机制GetMessage 可以选择只读取硬件消息(wMsgFilterMin/Max 设置为 WM_KEYFIRST...WM_KEYLAST);
  • 抢占 :通过 co_MsqPeekHardwareMessage 中的 ptiSysLock 机制,确保同一时间只有一个线程处理硬件消息。

7.6.2 键盘初始化

7.6.2.1 InitKeyboardImpl

win32ss/user/ntuser/keyboard.c:45-57(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L45-L57):

c 复制代码
CODE_SEG("INIT")
NTSTATUS NTAPI
InitKeyboardImpl(VOID)
{
    RtlZeroMemory(&gafAsyncKeyState, sizeof(gafAsyncKeyState));
    RtlZeroMemory(&gafAsyncKeyStateRecentDown, sizeof(gafAsyncKeyStateRecentDown));

    // 初始化默认键盘信息
    RtlZeroMemory(&gKeyboardInfo, sizeof(gKeyboardInfo));
    gKeyboardInfo.KeyboardIdentifier.Type = 4;        // AT-101
    gKeyboardInfo.NumberOfFunctionKeys = 12;          // 12 个 F 键

    return STATUS_SUCCESS;
}

InitKeyboardImpl 在 win32k 启动时(DriverEntry)调用,清零全局状态并设置默认键盘类型。

7.6.2.2 UserInitKeyboard

win32ss/user/ntuser/keyboard.c:177-227(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L177-L227):

c 复制代码
VOID NTAPI
UserInitKeyboard(HANDLE hKeyboardDevice)
{
    NTSTATUS Status;
    IO_STATUS_BLOCK Block;

    // 1. 获取指示灯翻译表(哪个 ScanCode 控制哪个 LED)
    IntKeyboardGetIndicatorTrans(hKeyboardDevice, &gpKeyboardIndicatorTrans);

    // 2. 查询当前 LED 状态
    Status = ZwDeviceIoControlFile(hKeyboardDevice, NULL, NULL, NULL, &Block,
                                   IOCTL_KEYBOARD_QUERY_INDICATORS,
                                   NULL, 0, &gIndicators, sizeof(gIndicators));

    if (!NT_SUCCESS(Status))
    {
        WARN("NtDeviceIoControlFile() failed, ignored\n");
        gIndicators.LedFlags = 0;
        gIndicators.UnitId = 0;
    }

    // 3. 同步 gafAsyncKeyState 的 LED 锁定状态
    SET_KEY_LOCKED(gafAsyncKeyState, VK_CAPITAL,
                   gIndicators.LedFlags & KEYBOARD_CAPS_LOCK_ON);
    SET_KEY_LOCKED(gafAsyncKeyState, VK_NUMLOCK,
                   gIndicators.LedFlags & KEYBOARD_NUM_LOCK_ON);
    SET_KEY_LOCKED(gafAsyncKeyState, VK_SCROLL,
                   gIndicators.LedFlags & KEYBOARD_SCROLL_LOCK_ON);

    // 4. 查询键盘属性
    Status = ZwDeviceIoControlFile(hKeyboardDevice, NULL, NULL, NULL, &Block,
                                   IOCTL_KEYBOARD_QUERY_ATTRIBUTES,
                                   NULL, 0, &gKeyboardInfo, sizeof(gKeyboardInfo));
    // ...
}

UserInitKeyboard 在每个键盘设备打开时调用,与硬件同步初始状态。

7.6.2.3 IntKeyboardGetIndicatorTrans

win32ss/user/ntuser/keyboard.c:66-115(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L66-L115):

通过 IOCTL_KEYBOARD_QUERY_INDICATOR_TRANSLATION 查询"哪个 ScanCode 控制哪个 LED"。这是因为不同键盘的 LED 控制方式不同:

c 复制代码
typedef struct _KEYBOARD_INDICATOR_TRANSLATION {
    USHORT NumberOfIndicatorKeys;
    struct {
        USHORT MakeCode;        // ScanCode
        USHORT IndicatorFlags;  // 对应的 LED 标志
    } IndicatorList[];
} KEYBOARD_INDICATOR_TRANSLATION;

7.6.3 VK 码转换:IntSimplifyVk / IntFixVk

7.6.3.1 左右手键区分

Windows VK 码中,部分键区分左右手:

简化 VK 左手 右手 备注
VK_SHIFT (0x10) VK_LSHIFT (0xA0) VK_RSHIFT (0xA1) 物理上是两个键
VK_CONTROL (0x11) VK_LCONTROL (0xA2) VK_RCONTROL (0xA3)
VK_MENU (0x12) VK_LMENU (0xA4) VK_RMENU (0xA5) 右 Alt = AltGr

两种语义

  • 简化 VKVK_SHIFT):用于 GetAsyncKeyStatelParam 中------只关心"是否按下了 Shift";
  • 完整 VKVK_LSHIFT / VK_RSHIFT):用于 WM_KEYDOWNwParam------需要区分左右手(如游戏中 WASD 走位用左 Shift 蹲下、右 Shift 喷气)。

7.6.3.2 IntSimplifyVk

win32ss/user/ntuser/keyboard.c:234-255(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L234-L255):

c 复制代码
static WORD IntSimplifyVk(WORD wVk)
{
    switch (wVk)
    {
        case VK_LSHIFT:
        case VK_RSHIFT:
            return VK_SHIFT;
        case VK_LCONTROL:
        case VK_RCONTROL:
            return VK_CONTROL;
        case VK_LMENU:
        case VK_RMENU:
            return VK_MENU;
        default:
            return wVk;
    }
}

将左右手 VK 合并为简化 VK。

7.6.3.3 IntFixVk

win32ss/user/ntuser/keyboard.c:262-280(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L262-L280):

c 复制代码
static WORD IntFixVk(WORD wVk, BOOL bExt)
{
    switch (wVk)
    {
        case VK_SHIFT:
            return bExt ? VK_RSHIFT : VK_LSHIFT;
        case VK_CONTROL:
            return bExt ? VK_RCONTROL : VK_LCONTROL;
        case VK_MENU:
            return bExt ? VK_RMENU : VK_LMENU;
        default:
            return wVk;
    }
}

bExt 表示扩展键标志 (来自扫描码的 E0/E1)。当用户按下右 Shift 时,硬件会发送 ScanCode=0x36 + Flags=KEY_E0,win32k 据此区分左右。

7.6.3.4 简化 vs 修正的语义差异

场景 :用户在键盘输入线程中接收到 WM_KEYDOWNwParam 是哪个 VK?

  • wParam :经过 IntFixVk 修正,区分左右 (如 VK_LSHIFT);
  • lParam :包含 KF_ALTDOWNKF_EXTENDED 等位标志;
  • GetAsyncKeyState(VK_SHIFT) :用 IntSimplifyVk 简化,不区分左右;
  • GetKeyboardState():返回 256 字节的全局状态数组,每键 1 bit down + 1 bit locked。

7.6.4 Numpad 翻译:IntTranslateNumpadKey

7.6.4.1 NumLock 状态差异

数字小键盘(Numpad)在 NumLock 开启时是数字键,关闭时是方向键/翻页键:

ScanCode NumLock 开 NumLock 关
0x52 (Insert) VK_NUMPAD0 ('0') VK_INSERT
0x4F (End) VK_NUMPAD1 ('1') VK_END
0x50 (Down) VK_NUMPAD2 ('2') VK_DOWN
... ... ...

7.6.4.2 IntTranslateNumpadKey

win32ss/user/ntuser/keyboard.c:287-306(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L287-L306):

c 复制代码
static WORD IntTranslateNumpadKey(WORD wVk)
{
    switch (wVk)
    {
        case VK_INSERT: return VK_NUMPAD0;
        case VK_END:    return VK_NUMPAD1;
        case VK_DOWN:   return VK_NUMPAD2;
        case VK_NEXT:   return VK_NUMPAD3;
        case VK_LEFT:   return VK_NUMPAD4;
        case VK_CLEAR:  return VK_NUMPAD5;
        case VK_RIGHT:  return VK_NUMPAD6;
        case VK_HOME:   return VK_NUMPAD7;
        case VK_UP:     return VK_NUMPAD8;
        case VK_PRIOR:  return VK_NUMPAD9;
        case VK_DELETE: return VK_DECIMAL;
        default:        return wVk;
    }
}

调用时机TranslateMessage 函数中,当 VK 在 Numpad 范围时调用,决定产生 WM_KEYDOWN 时是 VK_NUMPAD0 还是 VK_INSERT


7.6.5 LED 指示灯与锁定键

7.6.5.1 IntKeyboardUpdateLeds

win32ss/user/ntuser/keyboard.c:122-170(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L122-L170):

c 复制代码
static NTSTATUS APIENTRY
IntKeyboardUpdateLeds(HANDLE hKeyboardDevice, WORD wVk, WORD wScanCode)
{
    NTSTATUS Status;
    UINT i;
    USHORT LedFlag = 0;
    IO_STATUS_BLOCK Block;

    if (!gpKeyboardIndicatorTrans)
        return STATUS_NOT_SUPPORTED;

    // 1. 根据 VK 码或 ScanCode 确定 LED 标志
    switch (wVk)
    {
        case VK_CAPITAL: LedFlag = KEYBOARD_CAPS_LOCK_ON; break;
        case VK_NUMLOCK: LedFlag = KEYBOARD_NUM_LOCK_ON;  break;
        case VK_SCROLL:  LedFlag = KEYBOARD_SCROLL_LOCK_ON; break;
        default:
            for (i = 0; i < gpKeyboardIndicatorTrans->NumberOfIndicatorKeys; i++)
            {
                if (gpKeyboardIndicatorTrans->IndicatorList[i].MakeCode == wScanCode)
                {
                    LedFlag = gpKeyboardIndicatorTrans->IndicatorList[i].IndicatorFlags;
                    break;
                }
            }
    }

    if (LedFlag)
    {
        // 2. 切换 LED 状态
        gIndicators.LedFlags ^= LedFlag;

        // 3. 发送 IOCTL 给键盘驱动
        Status = ZwDeviceIoControlFile(hKeyboardDevice, NULL, NULL, NULL, &Block,
                                       IOCTL_KEYBOARD_SET_INDICATORS,
                                       &gIndicators, sizeof(gIndicators),
                                       NULL, 0);
        return Status;
    }
    return STATUS_SUCCESS;
}

7.6.5.2 LED 同步

LED 状态需要内核态gIndicators)与硬件 (键盘上的物理灯)以及用户态gafAsyncKeyState 的 locked 位)三方同步:

复制代码
[用户按下 CapsLock 键]
        │
        ▼
[键盘中断] ──► 扫描码 = 0x3A
        │
        ▼
[kbdclass] ──► KEYBOARD_INPUT_DATA { MakeCode=0x3A, Flags=KEY_BREAK (这里假设是 release) }
        │
        ▼
[RIT: UserProcessKeyboardInput]
        │
        ├─► wVk = VK_CAPITAL
        ├─► UpdateAsyncKeyState(VK_CAPITAL, TRUE) ──► gafAsyncKeyState 翻转 locked 位
        ├─► IntKeyboardUpdateLeds(VK_CAPITAL)
        │   ├─► gIndicators.LedFlags ^= KEYBOARD_CAPS_LOCK_ON
        │   └─► IOCTL_KEYBOARD_SET_INDICATORS ──► 键盘驱动
        │                                              └─► 硬件 LED 灯亮/灭
        ▼
[MsqPostHardwareMessage(WM_KEYDOWN, VK_CAPITAL, ...)]
        │
        ▼
[WndProc 收到 WM_KEYDOWN VK_CAPITAL]

关键设计 :三个状态(gafAsyncKeyState / gIndicators / 硬件 LED)始终保持一致。任何修改都必须三处同时更新。


7.6.6 gafAsyncKeyState 异步按键状态数组

7.6.6.1 数据结构

win32ss/user/ntuser/keyboard.c:13-14(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L13-L14):

c 复制代码
BYTE gafAsyncKeyState[256 * 2 / 8];           // 2 bits per key × 256 keys = 64 bytes
static BYTE gafAsyncKeyStateRecentDown[256 / 8]; // 1 bit per key × 256 keys = 32 bytes

gafAsyncKeyState:每键 2 bit

  • bit 0 (0x01):key down state(当前是否按下)
  • bit 1 (0x02):key locked state(是否处于 locked 状态,如 Caps Lock)

gafAsyncKeyStateRecentDown:每键 1 bit

  • bit 0 (0x01):key was down since last call(自上次查询以来是否被按下过)

7.6.6.2 操作宏

c 复制代码
#define IS_KEY_DOWN(kst, vk)    ((kst)[(vk) * 2 / 8] & (1 << ((vk) & 7) * 2))
#define SET_KEY_DOWN(kst, vk, f) ...
#define IS_KEY_LOCKED(kst, vk)  ((kst)[(vk) * 2 / 8] & (1 << (((vk) & 7) * 2 + 1)))
#define SET_KEY_LOCKED(kst, vk, f) ...

7.6.6.3 UpdateAsyncKeyState

win32ss/user/ntuser/keyboard.c:681-696(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L681-L696):

c 复制代码
static VOID NTAPI
UpdateAsyncKeyState(WORD wVk, BOOL bIsDown)
{
    if (bIsDown)
    {
        /* If it's first key down event, xor lock bit */
        if (!IS_KEY_DOWN(gafAsyncKeyState, wVk))
            SET_KEY_LOCKED(gafAsyncKeyState, wVk, !IS_KEY_LOCKED(gafAsyncKeyState, wVk));

        SET_KEY_DOWN(gafAsyncKeyState, wVk, TRUE);
        gafAsyncKeyStateRecentDown[wVk / 8] |= (1 << (wVk % 8));
    }
    else
        SET_KEY_DOWN(gafAsyncKeyState, wVk, FALSE);
}

关键逻辑 :从"未按下"转为"按下"时(首次 down 事件),翻转 locked 位。这就是 CapsLock/NumLock/ScrollLock 的工作原理:

  • 第一次按 CapsLock:down=TRUE,prev down=FALSE ──► 翻转 locked ──► locked=TRUE;
  • 第二次按 CapsLock:down=TRUE,prev down=FALSE ──► 翻转 locked ──► locked=FALSE。

7.6.6.4 NtUserGetAsyncKeyState

win32ss/user/ntuser/keyboard.c:647-674(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L647-L674):

c 复制代码
SHORT APIENTRY
NtUserGetAsyncKeyState(INT Key)
{
    WORD wRet = 0;

    if (Key >= 0x100 || Key < 0)
    {
        EngSetLastError(ERROR_INVALID_PARAMETER);
        return 0;
    }

    UserEnterExclusive();

    if (IS_KEY_DOWN(gafAsyncKeyState, Key))
        wRet |= 0x8000;   // 高位 = 0x8000 表示当前 down

    if (gafAsyncKeyStateRecentDown[Key / 8] & (1 << (Key % 8)))
        wRet |= 0x1;      // 低位 = 0x1 表示自上次查询以来曾 down

    gafAsyncKeyStateRecentDown[Key / 8] &= ~(1 << (Key % 8));  // 清除 recent down

    UserLeave();

    return wRet;
}

返回值语义

  • 0x8000 位:当前键是否 down(实时状态);
  • 0x0001 位:自上次调用 GetAsyncKeyState 以来该键是否被按下过(边沿触发,调用后清除);
  • 完整 SHORT 状态:如 0x8001 表示"当前 down 且自上次查询以来曾 down"。

注意 :调用后 RecentDown 自动清除------这是"边沿触发"语义,用于检测"是否发生了按键"而非"键是否持续按下"。


7.6.7 Alt+Numpad 字符输入

7.6.7.1 功能描述

按住 Alt 键,在小键盘上输入数字,可以输入任意 Unicode 字符(包括 ASCII 控制字符)。例如:

  • Alt + 65(小键盘)→ 'A'
  • Alt + 233(小键盘)→ 'é'
  • Alt + 0x1F600(需要 HexNumpad,ReactOS 默认禁用)→ '😀'

7.6.7.2 状态机

win32ss/user/ntuser/keyboard.c:26-36(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L26-L36):

c 复制代码
static enum _ALTNUM_STATE
{
    ALTNUM_INACTIVE,      // 非活动
    ALTNUM_OEM,           // Alt xxx    (十进制 OEM)
    ALTNUM_ACP,           // Alt 0xxx   (十进制 ANSI 代码页)
    ALTNUM_HEX_ACP,       // Alt .xxx   (十六进制 ANSI 代码页)
    ALTNUM_HEX_UTF        // Alt +xxx   (十六进制 UTF-16)
} gAltNumPadState = ALTNUM_INACTIVE;

static ULONG gAltNumPadValue = 0;
BOOL gbEnableHexNumpad = FALSE;

状态转换

复制代码
ALTNUM_INACTIVE
   │ (Alt down)
   ▼
ALTNUM_OEM
   │ (0xxx 在 Numpad)
   ▼
ALTNUM_ACP
   │ (xxx 在 Numpad)
   ▼
ALTNUM_OEM (累积更多数字)
   │ (Alt up)
   ▼  产生 WM_CHAR
ALTNUM_INACTIVE

HexNumpad 扩展gbEnableHexNumpad = TRUE):

  • Alt + 进入 ALTNUM_HEX_UTF
  • Alt . 进入 ALTNUM_HEX_ACP
  • 后续 Numpad 数字视为十六进制。

7.6.7.3 处理位置

IntHandleAltNumpad 函数在 UserProcessKeyboardInput 中调用:

c 复制代码
if (IntHandleAltNumpad(KeyData))
    return;  // AltNumpad 字符已处理,不投递硬件消息

核心逻辑

  1. 检查是否处于 Alt 按下状态;
  2. 检查 VK 是否在 Numpad 范围(VK_NUMPAD0-VK_NUMPAD9, VK_DECIMAL, VK_ADD, VK_SUBTRACT);
  3. 累积 gAltNumPadValue 数值;
  4. 当 Alt 释放时,调用 UserPostMessage(WM_CHAR, gAltNumPadValue, 0)

7.6.7.4 设计意图

Alt+Numpad 是 Windows 提供的直接字符输入机制,绕过键盘布局:

  • 不需要切换输入法;
  • 输入生僻字符(如希腊字母、特殊符号);
  • 兼容 DOS 时代的 Alt+数字输入方式。

7.6.8 co_IntProcessKeyboardMessage 核心处理

7.6.8.1 函数签名

win32ss/user/ntuser/msgqueue.c:1772-1895(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L1772-L1895):

c 复制代码
BOOL co_IntProcessKeyboardMessage(MSG* Msg, BOOL* RemoveMessages);

调用时机co_MsqPeekHardwareMessage 检测到键盘消息时调用。

7.6.8.2 完整处理流程

c 复制代码
BOOL co_IntProcessKeyboardMessage(MSG* Msg, BOOL* RemoveMessages)
{
    EVENTMSG Event;
    USER_REFERENCE_ENTRY Ref;
    PWND pWnd;
    UINT ImmRet;
    BOOL Ret = TRUE, bKeyUpDown = FALSE;
    PTHREADINFO pti = PsGetCurrentThreadWin32Thread();
    const UINT uMsg = Msg->message;

    // 1. VK_PACKET 是 SendInput 注入的 Unicode 字符
    if (uMsg == VK_PACKET)
        pti->wchInjected = HIWORD(Msg->wParam);

    // 2. 简化 VK 区分(左右 Shift/Ctrl/Alt → VK_SHIFT/CONTROL/MENU)
    if (uMsg == WM_KEYDOWN || uMsg == WM_SYSKEYDOWN ||
        uMsg == WM_KEYUP   || uMsg == WM_SYSKEYUP)
    {
        bKeyUpDown = TRUE;
        switch (Msg->wParam)
        {
            case VK_LSHIFT: case VK_RSHIFT:     Msg->wParam = VK_SHIFT;   break;
            case VK_LCONTROL: case VK_RCONTROL: Msg->wParam = VK_CONTROL; break;
            case VK_LMENU: case VK_RMENU:       Msg->wParam = VK_MENU;    break;
        }
    }

    // 3. 引用窗口对象
    pWnd = ValidateHwndNoErr(Msg->hwnd);
    if (pWnd) UserRefObjectCo(pWnd, &Ref);

    // 4. WH_JOURNALRECORD 钩子(记录输入事件)
    Event.message = uMsg;
    Event.hwnd    = Msg->hwnd;
    Event.time    = Msg->time;
    Event.paramL  = (Msg->wParam & 0xFF) | (HIWORD(Msg->lParam) << 8);
    Event.paramH  = Msg->lParam & 0x7FFF;
    if (HIWORD(Msg->lParam) & 0x0100) Event.paramH |= 0x8000;
    co_HOOK_CallHooks(WH_JOURNALRECORD, HC_ACTION, 0, (LPARAM)&Event);

    // 5. 特殊键处理(仅 RemoveMessages=TRUE)
    if (*RemoveMessages)
    {
        if (uMsg == WM_KEYDOWN)
        {
            // 5.1 F1 键 ──► WM_HELP
            if (Msg->wParam == VK_F1)
                UserPostMessage(Msg->hwnd, WM_KEYF1, 0, 0);
            // 5.2 浏览器/启动键 ──► WM_APPCOMMAND
            else if (Msg->wParam >= VK_BROWSER_BACK && Msg->wParam <= VK_LAUNCH_APP2)
                co_IntSendMessage(Msg->hwnd, WM_APPCOMMAND, ...);
        }
        else if (uMsg == WM_KEYUP && Msg->wParam == VK_APPS)
        {
            // 5.3 Apps 键(菜单键)──► WM_CONTEXTMENU
            UserPostMessage(Msg->hwnd, WM_CONTEXTMENU, (WPARAM)Msg->hwnd, -1);
        }
    }

    // 6. 系统键处理(Alt+Tab/Alt+Esc)
    if (*RemoveMessages && uMsg == WM_SYSKEYDOWN)
    {
        if (HIWORD(Msg->lParam) & KF_ALTDOWN)
        {
            if (Msg->wParam == VK_ESCAPE || Msg->wParam == VK_TAB)
            {
                WPARAM wParamTmp = UserGetKeyState(VK_SHIFT) & 0x8000
                                   ? SC_PREVWINDOW : SC_NEXTWINDOW;
                co_IntSendMessage(Msg->hwnd, WM_SYSCOMMAND, wParamTmp, Msg->wParam);
                Ret = FALSE;
                goto Exit;
            }
        }
    }

    // 7. WH_KEYBOARD 钩子
    if (co_HOOK_CallHooks(WH_KEYBOARD,
                          *RemoveMessages ? HC_ACTION : HC_NOREMOVE,
                          LOWORD(Msg->wParam), Msg->lParam))
    {
        // 钩子消费了消息
        co_HOOK_CallHooks(WH_CBT, HCBT_KEYSKIPPED, LOWORD(Msg->wParam), Msg->lParam);
        *RemoveMessages = TRUE;
        Ret = FALSE;
    }

    // 8. IME 处理
    if (pWnd && Ret && *RemoveMessages && bKeyUpDown && !(pti->TIF_flags & TIF_DISABLEIME))
    {
        ImmRet = IntImmProcessKey(pti->MessageQueue, pWnd, uMsg, Msg->wParam, Msg->lParam);
        if (ImmRet)
        {
            if (ImmRet & (IPHK_HOTKEY|IPHK_SKIPTHISKEY))
                ImmRet = 0;
            if (ImmRet & IPHK_PROCESSBYIME)
                Msg->wParam = VK_PROCESSKEY;  // IME 处理中
        }
    }

Exit:
    if (pWnd) UserDerefObjectCo(pWnd);
    return Ret;
}

7.6.8.3 关键钩子与处理

步骤 处理对象 备注
1 VK_PACKET 注入字符 来自 SendInput
2 简化 VK 区分 便于 GetKeyState 查询
4 WH_JOURNALRECORD 钩子 记录所有输入事件
5.1 F1 键 → WM_HELP 帮助键
5.2 浏览器/启动键 → WM_APPCOMMAND 多媒体键
5.3 Apps 键 → WM_CONTEXTMENU 右键菜单
6 Alt+Tab/Alt+Esc 窗口切换
7 WH_KEYBOARD 钩子 可消费消息
8 IME 输入法处理

7.6.9 字符消息生成

7.6.9.1 user32 TranslateMessage 流程

win32ss/user/user32/windows/message.c(file:///d:/reactos/win32ss/user/user32/windows/message.c) 中定义 TranslateMessage

c 复制代码
BOOL TranslateMessage(CONST MSG *lpMsg)
{
    // 仅处理 WM_KEYDOWN
    if (lpMsg->message != WM_KEYDOWN && lpMsg->message != WM_SYSKEYDOWN)
        return FALSE;

    // 1. 简化 VK
    wVk = IntSimplifyVk(LOWORD(lpMsg->wParam));

    // 2. Numpad 翻译
    if (wVk >= VK_NUMPAD0 && wVk <= VK_DECIMAL)
        wVk = IntTranslateNumpadKey(wVk);

    // 3. 调用 ToUnicodeEx 翻译为字符
    if (ToUnicodeEx(wVk, sc, afKeyState, wsz, 1, 0, pKbdTbl) > 0)
    {
        // 4. 投递 WM_CHAR
        PostMessage(lpMsg->hwnd, WM_CHAR, wsz[0], lpMsg->lParam);
    }
    return TRUE;
}

7.6.9.2 ToUnicodeEx 字符翻译

win32ss/user/ntuser/keyboard.c:447-498(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L447-L498):

c 复制代码
int APIENTRY
IntToUnicodeEx(UINT wVirtKey, UINT wScanCode, PBYTE pKeyState,
               LPWSTR pwszBuff, int cchBuff, UINT wFlags, PKBDTABLES pKbdTbl)
{
    WCHAR wchTranslatedChar;
    BOOL bDead, bLigature;
    static WCHAR wchDead = 0;
    int iRet = 0;

    // 1. 调用 IntTranslateChar 翻译当前 VK
    if (!IntTranslateChar(wVirtKey, pKeyState, &bDead, &bLigature, &wchTranslatedChar, pKbdTbl))
        return 0;

    if (bLigature) return 0;  // 连字暂不支持

    // 2. Dead Key 组合
    if (wchDead)
    {
        // 尝试将当前字符与之前的 dead char 组合
        for (i = 0; pKbdTbl->pDeadKey[i].dwBoth; i++)
        {
            if (pKbdTbl->pDeadKey[i].dwBoth >> 16 == wchDead &&
                pKbdTbl->pDeadKey[i].dwBoth & 0xFFFF == wchTranslatedChar)
            {
                wchTranslatedChar = pKbdTbl->pDeadKey[i].wchComposed;
                wchDead = 0;
                bDead = FALSE;
                break;
            }
        }
        // 组合失败:先投递之前 dead char 的字符,再保留当前字符
        if (wchDead)
        {
            pwszBuff[iRet++] = wchDead;
            wchDead = 0;
        }
    }

    if (bDead)
    {
        // 当前字符是 dead key,记录等下次组合
        wchDead = wchTranslatedChar;
    }
    else
    {
        pwszBuff[iRet++] = wchTranslatedChar;
    }
    return iRet;
}

7.6.9.3 IntTranslateChar

win32ss/user/ntuser/keyboard.c:335-440(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L335-L440):

复杂的字符翻译函数,根据 VK 码和修饰键状态查找 pKbdTbl->pVkToWcharTable 表:

c 复制代码
static BOOL
IntTranslateChar(WORD wVirtKey, PBYTE pKeyState, PBOOL pbDead, PBOOL pbLigature,
                 PWCHAR pwcTranslatedChar, PKBDTABLES pKbdTbl)
{
    DWORD i, dwModBits, dwVkModBits, dwModNumber = 0;
    WCHAR wch;

    // 1. 获取当前修饰键位(KBDCTRL/KBDALT/KBDSHIFT/KBDKANA)
    dwModBits = pKeyState ? IntGetModBits(pKbdTbl, pKeyState) : 0;

    // 2. 遍历 pVkToWcharTable 表
    for (i = 0; pKbdTbl->pVkToWcharTable[i].pVkToWchars; i++)
    {
        PVK_TO_WCHARS10 pVkToVch = (PVK_TO_WCHARS10)(pKbdTbl->pVkToWcharTable[i].pVkToWchars);
        while (pVkToVch->VirtualKey)
        {
            if (wVirtKey == (pVkToVch->VirtualKey & 0xFF))
            {
                // 3. 应用 CapsLock 翻转(仅对字母键)
                if ((pVkToVch->Attributes & wCaplokAttr) && IS_KEY_LOCKED(pKeyState, VK_CAPITAL))
                    dwVkModBits ^= KBDSHIFT;

                // 4. 查找当前修饰键对应的字符
                dwModNumber = pKbdTbl->pCharModifiers->ModNumber[dwVkModBits];
                wch = pVkToVch->wch[dwModNumber];

                *pwcTranslatedChar = wch;
                *pbDead = (wch == WCH_DEAD);
                *pbLigature = (wch == WCH_LGTR);
                return TRUE;
            }
            pVkToVch = (PVK_TO_WCHARS10)(((BYTE *)pVkToVch) + pVkToVchTbl->cbSize);
        }
    }

    // 5. Ctrl+A-Z 翻译为 ASCII 控制字符
    if (wVirtKey >= 'A' && wVirtKey <= 'Z' && IS_KEY_DOWN(pKeyState, VK_CONTROL))
    {
        *pwcTranslatedChar = (wVirtKey - 'A') + 1;
        return TRUE;
    }

    return FALSE;
}

7.6.9.4 字符消息分类

消息 触发条件 wParam 用途
WM_KEYDOWN 任何键按下 VK 码 通知应用"键被按下"
WM_KEYUP 任何键释放 VK 码 通知应用"键被释放"
WM_CHAR 可打印字符 Unicode 字符 通知应用"产生了字符"
WM_DEADCHAR 死键(如 ´、`) 死键字符 通知应用"死键已输入,等组合"
WM_SYSKEYDOWN Alt + 键 VK 码 系统键(菜单激活等)
WM_SYSCHAR Alt + 字符 Unicode 字符 系统字符
WM_SYSDEADCHAR Alt + 死键 死键字符 系统死键

为什么需要 WM_KEYDOWN + WM_CHAR 两套消息?

  • WM_KEYDOWN:低层、按键为粒度(按下哪个 VK 键、Shift/Ctrl 是否按下);
  • WM_CHAR:高层、字符为粒度('a' 还是 'A'、是否死键组合)。

应用可以根据需求选择处理:游戏通常只用 WM_KEYDOWN(需要 VK 码),文本编辑器只用 WM_CHAR(需要字符)。


7.6.10 IME 处理与全局热键

7.6.10.1 IME 在键盘处理中的位置

IME(输入法编辑器)将按键事件拦截并转换为"组合中"的字符:

复制代码
用户按键 ──► co_IntProcessKeyboardMessage
                       │
                       ▼
                  IntImmProcessKey (IME 处理)
                       │
                       ├─► IPHK_HOTKEY       (热键:Shift+Space 等)
                       ├─► IPHK_SKIPTHISKEY   (IME 消费此键)
                       └─► IPHK_PROCESSBYIME  (VK_PROCESSKEY,WndProc 知道是 IME 处理中)

7.6.10.2 IntImmProcessKey

win32ss/user/ntuser/ime.c(file:///d:/reactos/win32ss/user/ntuser/ime.c) 中定义:

c 复制代码
UINT IntImmProcessKey(PUSER_MESSAGE_QUEUE MessageQueue, PWND pWnd,
                      UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    // 1. 检查 IME 是否启用
    if (!pWnd || !MessageQueue->spwndFocus)
        return 0;

    // 2. 检查热键
    if (wParam == VK_SPACE && (GetAsyncKeyState(VK_SHIFT) & 0x8000))
    {
        // Shift+Space 切换中英文
        return IPHK_HOTKEY;
    }

    // 3. 调用 IME 钩子
    // 4. 转换结果
    if (IME 想要处理此键)
        return IPHK_PROCESSBYIME;  // 替换为 VK_PROCESSKEY

    return 0;
}

返回值

  • IPHK_HOTKEY:识别为 IME 热键(如 Shift+Space 切换中英文),不发字符消息;
  • IPHK_SKIPTHISKEY:IME 消费此键(不投递 WM_CHAR);
  • IPHK_PROCESSBYIME:产生 WM_KEYDOWN VK_PROCESSKEY(WndProc 应跳过,等待 IME 后续产生 WM_IME_COMPOSITION)。

7.6.10.3 全局热键 RegisterHotKey

c 复制代码
BOOL RegisterHotKey(HWND hWnd, int id, UINT fsModifiers, UINT vk)
{
    // 1. 创建 HOTKEY_ITEM
    // 2. 加入线程/桌面的热键链表
    // 3. 任何按键事件都会检查热键链表
}

实现位置win32ss/user/ntuser/hotkey.c(file:///d:/reactos/win32ss/user/ntuser/hotkey.c)(ReactOS 中存在)。

热键检查时机:UserProcessKeyboardInput 投递硬件消息之前,先扫描热键链表。


7.6.11 键盘 LL 钩子

7.6.11.1 co_CallLowLevelKeyboardHook

win32ss/user/ntuser/keyboard.c:703-732(file:///d:/reactos/win32ss/user/ntuser/keyboard.c#L703-L732):

c 复制代码
static LRESULT
co_CallLowLevelKeyboardHook(WORD wVk, WORD wScanCode, DWORD dwFlags,
                            BOOL bInjected, DWORD dwTime, DWORD dwExtraInfo)
{
    KBDLLHOOKSTRUCT KbdHookData;
    UINT uMsg;

    // 1. 填充 KBDLLHOOKSTRUCT
    KbdHookData.vkCode = wVk;
    KbdHookData.scanCode = wScanCode;
    KbdHookData.flags = 0;
    if (dwFlags & KEYEVENTF_EXTENDEDKEY) KbdHookData.flags |= LLKHF_EXTENDED;
    if (IS_KEY_DOWN(gafAsyncKeyState, VK_MENU)) KbdHookData.flags |= LLKHF_ALTDOWN;
    if (dwFlags & KEYEVENTF_KEYUP)            KbdHookData.flags |= LLKHF_UP;
    if (bInjected)                             KbdHookData.flags |= LLKHF_INJECTED;
    KbdHookData.time = dwTime;
    KbdHookData.dwExtraInfo = dwExtraInfo;

    // 2. 决定 LL 钩子的 uMsg
    if (dwFlags & KEYEVENTF_KEYUP)
        uMsg = WM_KEYUP;
    else if (IS_KEY_DOWN(gafAsyncKeyState, VK_MENU) && !IS_KEY_DOWN(gafAsyncKeyState, VK_CONTROL))
        uMsg = WM_SYSKEYDOWN;
    else
        uMsg = WM_KEYDOWN;

    // 3. 调用 WH_KEYBOARD_LL 钩子
    return co_HOOK_CallHooks(WH_KEYBOARD_LL, HC_ACTION, uMsg, (LPARAM)&KbdHookData);
}

7.6.11.2 LL 钩子的特殊性

维度 普通 WH_KEYBOARD WH_KEYBOARD_LL
触发位置 WndProc 之前 原始输入(RIT 阶段)
DLL 注入 全局钩子需要 不需要
lParam MSG 结构 KBDLLHOOKSTRUCT
可修改数据 不能 可以(修改 flags、vkCode)
用途 应用程序消息过滤 全局热键、键位记录器

LL 钩子的关键优势不需要 DLL 注入 ,因为它在内核态实现并通过 KeUserModeCallback 切换到用户态调用钩子函数(参见 7.3 节)。

7.6.11.3 钩子返回值

co_HOOK_CallHooks 返回非 0 表示钩子消费了事件

c 复制代码
if (co_CallLowLevelKeyboardHook(...))
    return;  // 钩子消费,停止后续处理

这允许钩子函数阻止特定按键被处理(用于实现"屏蔽某些键"的应用)。


7.6.12 设计哲学问答

Q1:为什么 RIT 必须在专门的线程中处理输入?

A统一输入源 + 避免多线程竞争

  • 多设备统一:所有键盘设备(PS/2、USB、蓝牙)的 IRP_MJ_READ 完成都汇入 RIT,避免每个设备驱动重复实现输入处理逻辑;
  • 单线程状态修改gafAsyncKeyState 是全局状态,如果多线程并发修改需要锁,影响性能;
  • 优先级隔离:RIT 在较高优先级运行,确保输入响应实时(游戏、交互场景必需);
  • 避免 UI 线程死锁:键盘处理可能涉及跨线程消息(同步发送),如果由 UI 线程处理可能导致嵌套死锁。

Q2:为什么需要区分 gafAsyncKeyState 的 down 位和 locked 位?

A两种状态语义不同

  • down 位 :瞬时状态(按下时为 1,释放时为 0)------ GetAsyncKeyState(VK_LSHIFT) & 0x8000 检测;
  • locked 位 :持续状态(CapsLock 按下后保持为 1,再次按下恢复为 0)------ GetKeyState(VK_CAPITAL) & 0x01 检测。

典型应用

  • 游戏中检测"Shift 是否按住"------用 down 位;
  • 文本编辑器中检测"CapsLock 是否开启"------用 locked 位。

通过两个独立位表示两个独立状态,简洁高效。

Q3:为什么 Alt+Numpad 输入绕过键盘布局(KBDTABLES)?

A支持任意 Unicode 字符,不受语言限制

普通字符输入需要 ToUnicodeEx 查表,受当前键盘布局限制(美式布局只能输入 ASCII 字符)。Alt+Numpad 直接输入字符码点

  • 不需要安装/切换输入法;
  • 输入生僻字符(如希腊字母、数学符号);
  • 兼容 DOS 时代的 Alt+数字输入。

实现方式:在 RIT 阶段识别"Alt + Numpad 数字",累积为数值,Alt 释放时直接产生 WM_CHAR 消息,跳过 TranslateMessageToUnicodeEx

Q4:为什么 WH_KEYBOARD_LL 不需要 DLL 注入,而普通全局 WH_KEYBOARD 需要?

A实现位置不同

  • 普通全局 WH_KEYBOARD :钩子函数在用户态。SetWindowsHookEx 时 win32k 需要将含钩子函数的 DLL 注入到所有 GUI 进程,使每个进程的线程都能调用钩子。
  • WH_KEYBOARD_LL :钩子函数在用户态但调用点在内核态 。RIT 在内核态通过 co_HOOK_CallHooks(WH_KEYBOARD_LL, ...) 触发钩子,co_HOOK_CallHooks 内部使用 KeUserModeCallback 切换到当前进程的用户态调用钩子。

LL 钩子的实现避免了 DLL 注入:

  • 钩子函数指针存储在内核的 gpsi->aphkStart[WH_KEYBOARD_LL],所有进程共享;
  • RIT 触发时,KeUserModeCallback当前 RIT 线程的用户态调用钩子;
  • 不需要把 DLL 映射到其他进程。

Q5:为什么键盘消息要进入"硬件消息队列"(HardwareMessagesListHead)而不是普通"投递队列"(PostedMessagesListHead)?

A优先级 + 抢占机制

  • 优先级:硬件消息优先于投递消息,避免输入延迟(用户按键必须及时响应,不能被排在 WM_USER 之类的消息后);
  • 抢占机制 :硬件消息的检查 (co_MsqPeekHardwareMessage) 在 co_IntPeekMessage先于普通消息;
  • GetMessage 过滤 :应用程序可以通过 wMsgFilterMin=WM_KEYFIRST, wMsgFilterMax=WM_KEYLAST 选择读取硬件消息;
  • ptiSysLock:硬件消息的读取是"线程独占"的(同一时间只有一个线程读取硬件消息),避免鼠标移动消息在两个线程间分配(参见 7.7)。

总结

键盘输入子系统是 Windows GUI 最复杂的 I/O 路径之一。本节介绍了:

  1. RIT 架构:原始输入线程统一处理所有键盘设备;
  2. 键盘初始化InitKeyboardImplUserInitKeyboard、IOCTL 查询指示灯与属性;
  3. VK 码处理IntSimplifyVk(简化左右手)、IntFixVk(修正扩展标志);
  4. Numpad 翻译IntTranslateNumpadKey(NumLock 切换数字/方向键);
  5. LED 同步 :内核态 gIndicators、硬件 LED、gafAsyncKeyState 三方同步;
  6. 异步按键状态gafAsyncKeyState[64](每键 2 bit:down/locked)、gafAsyncKeyStateRecentDown(边沿触发);
  7. Alt+Numpad 字符输入:绕过键盘布局的 Unicode 直接输入;
  8. co_IntProcessKeyboardMessage:处理 8 类特殊事件(F1、Apps 键、Alt+Tab、WH_KEYBOARD、IME 等);
  9. 字符消息生成TranslateMessage + ToUnicodeEx + KBDTABLES 翻译表;
  10. IME 与热键:IME 在按键流中的拦截和转换;
  11. LL 钩子:内核态实现的全局键盘钩子,无需 DLL 注入。

核心要点回顾

  1. RIT 是所有键盘设备的统一处理点;
  2. gafAsyncKeyState 维护全局按键状态(down/locked/recent);
  3. VK 码通过 IntSimplifyVk/IntFixVk 在"区分左右"和"不区分左右"之间转换;
  4. 字符消息通过 TranslateMessage + KBDTABLES 翻译表生成;
  5. WH_KEYBOARD_LL 在内核态实现,避免 DLL 注入。

本章代码索引

文件 内容
win32ss/user/ntuser/keyboard.c(file:///d:/reactos/win32ss/user/ntuser/keyboard.c) InitKeyboardImpl、UserInitKeyboard、IntKeyboardUpdateLeds、IntSimplifyVk、IntFixVk、IntTranslateNumpadKey、IntTranslateChar、IntToUnicodeEx、IntVkFromSc、UpdateAsyncKeyState、co_CallLowLevelKeyboardHook、IntHandleAltNumpad、NtUserGetAsyncKeyState
win32ss/user/ntuser/msgqueue.c(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c) co_IntProcessKeyboardMessage、co_MsqPeekHardwareMessage、co_IntProcessHardwareMessage
win32ss/user/ntuser/input.c(file:///d:/reactos/win32ss/user/ntuser/input.c) UserProcessKeyboardInput(RIT 原始输入入口)
win32ss/user/ntuser/kbdlayout.c(file:///d:/reactos/win32ss/user/ntuser/kbdlayout.c) KBDTABLES 加载与维护、IntGetCurrentKbdTables
win32ss/user/ntuser/ime.c(file:///d:/reactos/win32ss/user/ntuser/ime.c) IntImmProcessKey(IME 处理)
win32ss/user/ntuser/hotkey.c(file:///d:/reactos/win32ss/user/ntuser/hotkey.c) 全局热键(RegisterHotKey)
win32ss/user/user32/windows/message.c(file:///d:/reactos/win32ss/user/user32/windows/message.c) TranslateMessage、ToUnicode、ToAscii 用户态 API
ntoskrnl/kbdclass/kbdclass.c(file:///d:/reactos/ntoskrnl/kbdclass/kbdclass.c) kbdclass 键盘类驱动(IRP_MJ_READ 队列)
相关推荐
yinhunzw2 小时前
Claude code windows 安装
windows
七仔啊2 小时前
windows server 2022 部署前后端项目
windows
caimouse3 小时前
Reactos 第 7 章 视窗报文 — 7.4 用户空间的外挂函数
windows
辣香牛肉面3 小时前
Windows发票工具大全
windows·发票助手
caimouse3 小时前
Reactos 第 9 章 设备驱动 — 9.3 DPC函数及其执行
windows
caimouse4 小时前
Reactos 第 9 章 设备驱动 — 9.8 设备驱动模块的装载
windows
caimouse4 小时前
Reactos 第 9 章 设备驱动 — 9.2 一个“老式“驱动模块的实例
windows
caimouse5 小时前
Reactos 第 9 章 设备驱动 — 9.4 内核劳务线程
开发语言·windows
星栈独行5 小时前
Rust + Makepad 应用怎么打包发布:Windows、macOS、Linux 全平台交付
windows·程序人生·macos·ui·rust