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

两种语义:

  • 简化 VK (VK_SHIFT):用于 GetAsyncKeyState、lParam 中------只关心"是否按下了 Shift";
  • 完整 VK (VK_LSHIFT / VK_RSHIFT):用于 WM_KEYDOWN 的 wParam------需要区分左右手(如游戏中 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_KEYDOWN,wParam 是哪个 VK?

  • wParam :经过 IntFixVk 修正,区分左右 (如 VK_LSHIFT);
  • lParam :包含 KF_ALTDOWN、KF_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 消息,跳过 TranslateMessage 和 ToUnicodeEx。

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. 键盘初始化 :InitKeyboardImpl、UserInitKeyboard、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 队列)
相关推荐
小羊没烦恼!3 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
rockmelodies3 天前
# Windows Server2008 R2 Standard(SP1镜像U盘)重置本地管理员密码【完整详细步骤】
windows·密码破解
kakakahahahaha3 天前
Windows C盘空间不足的安全清理与迁移流程
windows·电脑·笔记本电脑·软件需求·c盘清理
做咩啊~3 天前
远程连接提示:身份验证错误,要求的函数不受支持
windows
百事牛科技3 天前
PPT只读怎么取消?四种情况分别处理
windows·powerpoint
熊明才4 天前
WSL2 网络突然瘫痪(Network is unreachable / 没有 eth0)最短修复指南(2026年9月20日亲测可用)
linux·windows·wsl2
xbzb4 天前
Windows 启动故障修复知识整理:蓝屏 / 无限重启 / 盘符 X / 无 PE 盘
windows
我命由我123454 天前
Mac 操作系统 - 一些使用记录
运维·windows·学习·系统安全·运维开发·mac·学习方法
kakakahahahaha5 天前
Windows Steam 游戏存档在哪里?定位和恢复排查流程
windows·其他·电脑·笔记本电脑·内容运营·软件需求
Xy-unu5 天前
Windows 开启外接显示器 HDR 后外接显示器无信号、无法扩展模式故障记录
windows