Windows键盘和鼠标输入过程

Windows键盘和鼠标输入过程

通常情况下面,我们使用Windows都是通过鼠标和键盘来和系统进行交互,那么键盘和鼠标是怎么输入和转换到应用程序的各种消息的呢?本文来看一下一个最基本的问题,Windows对于键盘鼠标输入的响应。

1. RawInputThread

鼠标和键盘的输入离不开一个重要的进程就是CSRSS,在这个进程启动的时候就会创建一个输入线程RawInputThread, 这个线程会一直不停的接收从键盘和鼠标过来的消息。

在子系统进程中,这个线程函数的创建过程如下:

06 88252c04 94efd7c7 00000004 00000002 882c7300 win32k!RawInputThread+0x486
07 88252c18 94fb2b5d 00000004 01cafbe8 88252c34 win32k!xxxCreateSystemThreads+0x4a
08 88252c28 83e431ea 00000004 01cafc28 778d70b4 win32k!NtUserCallNoParam+0x1b
09 88252c28 778d70b4 00000004 01cafc28 778d70b4 nt!KiFastCallEntry+0x12a
0a 01cafbd8 75a019ec 75a0288b 00000004 00000000 ntdll!KiFastSystemCallRet
0b 01cafbdc 75a0288b 00000004 00000000 77895e7a winsrv!NtUserCallNoParam+0xc
0c 01cafbe8 77895e7a 00000000 765c35e7 00000000 winsrv!StartCreateSystemThreads+0x12
0d 01cafc28 778f37c8 75a02879 00000000 00000000 ntdll!__RtlUserThreadStart+0x28
0e 01cafc40 00000000 75a02879 00000000 00000000 ntdll!_RtlUserThreadStart+0x1b

在这个函数中会针对鼠标和键盘,投递一个读请求,过程如下:

PDEVICEINFO StartDeviceRead(
    PDEVICEINFO pDeviceInfo)
{

    //...

    pDeviceInfo->ReadStatus = ZwReadFile(
            pDeviceInfo->handle,
            NULL,                // hReadEvent
            InputApc,            // InputApc()
            pDeviceInfo,         // ApcContext
            &pDeviceInfo->iosb,
            (PVOID)((PBYTE)pDeviceInfo + pDevTpl->offData),
            pDevTpl->cbData,
            PZERO(LARGE_INTEGER), NULL);
            
    //...

    return NULL;
}

从这里我们可以发现,这个是一个异步读取操作,如果读取到了鼠标或者键盘信息,那么将会调用APC例程InputApc, 然后再APC例程中继续投递读请求(调用StartDeviceRead).

2. xxxProcessKeyEvent

RawInputThread从键盘读取到击键信息之后,就会调用xxxProcessKeyEvent来处理键盘响应,如下:

3: kd> kb
 # ChildEBP RetAddr  Args to Child              
00 882528d4 95036c0a 00000020 00000039 00030195 win32k!xxxKeyEvent
01 8825290c 94fd7313 88252920 00000000 00000000 win32k!xxxProcessKeyEvent+0x229
02 8825294c 94fd7737 ffa18900 20a18930 00000001 win32k!ProcessKeyboardInputWorker+0x2dc
03 8825296c 94f00f58 ffa18930 a17e8ed0 882529c4 win32k!ProcessKeyboardInput+0x68
04 8825297c 83eb67b4 ffa18930 ffa18958 00000000 win32k!InputApc+0x4e
05 882529c4 83e83685 00000000 00000000 00000000 nt!KiDeliverApc+0x17f
06 88252a08 83e824f7 8d35bb90 8940fc80 89410074 nt!KiSwapThread+0x24e
07 88252a30 83e7e4a4 8940fc80 89410008 00000000 nt!KiCommitThreadWait+0x1df
08 88252ba8 94eed276 00000004 8940c0d0 00000001 nt!KeWaitForMultipleObjects+0x535
09 88252c04 94efd7c7 00000004 00000002 882c7300 win32k!RawInputThread+0x486
0a 88252c18 94fb2b5d 00000004 01cafbe8 88252c34 win32k!xxxCreateSystemThreads+0x4a
0b 88252c28 83e431ea 00000004 01cafc28 778d70b4 win32k!NtUserCallNoParam+0x1b
0c 88252c28 778d70b4 00000004 01cafc28 778d70b4 nt!KiFastCallEntry+0x12a
0d 01cafbd8 75a019ec 75a0288b 00000004 00000000 ntdll!KiFastSystemCallRet
0e 01cafbdc 75a0288b 00000004 00000000 77895e7a winsrv!NtUserCallNoParam+0xc
0f 01cafbe8 77895e7a 00000000 765c35e7 00000000 winsrv!StartCreateSystemThreads+0x12
10 01cafc28 778f37c8 75a02879 00000000 00000000 ntdll!__RtlUserThreadStart+0x28
11 01cafc40 00000000 75a02879 00000000 00000000 ntdll!_RtlUserThreadStart+0x1b

从这里看,我们响应键盘是xxxKeyEvent处理的,这个函数基本流程如下:

  1. if ((pHook = PhkFirstValid(ptiCurrent, WH_KEYBOARD_LL)) != NULL)成立,那么调用xxxCallHook2(pHook, HC_ACTION, (DWORD)msg, (LPARAM)&kbds, &bAnsiHook)先处理WH_KEYBOARD_LL钩子,其中如果按键不是热键(例如CTRL + ALT + DELETE)并且被WH_KEYBOARD_LL过滤掉,那么直接返回。
  2. 调用xxxDoHotKeyStuff处理热键,如果是基本热键,处理完成直接返回。
  3. 如果是普通的需要响应的键盘消息,调用PostInputMessage发送键盘消息,PostInputMessage接着就会调用SetWakeBit来通知相关线程处理消息。
  4. 调用PostInputMessage(gpqForeground,xxxx)给顶层窗口发消息,然后匹配QEVENT_APPCOMMAND是不是windows增强消息,比如键盘按键直接打开浏览器等上网。
  5. PostInputMessage中,先调用StoreQMessage存储在消息队列,然后调用WakeSomeone唤醒目标进程处理.
  6. WakeSomeOne中,匹配消息类型,是wm_keydownwm_char, wm_mousemove等,然后调用 StoreQMessagePti存在目标线程即顶层窗口队列中,当轮到目标线程执行时,处理 wm_keydownwm_char消息。

整体的过程如下:

3: kd> kb
 # ChildEBP RetAddr  Args to Child              
00 88252820 94f71eed ffb54370 00000001 0000011b win32k!SetWakeBit
01 88252840 94f6afad fe8288f0 00000100 fd4056f0 win32k!WakeSomeone+0x189
02 88252860 9503481e fe8288f0 00000000 00000100 win32k!PostInputMessage+0x126
03 882528d4 95036c0a 00000020 00000039 00030195 win32k!xxxKeyEvent+0x8a6
04 8825290c 94fd7313 88252920 00000000 00000000 win32k!xxxProcessKeyEvent+0x229
05 8825294c 94fd7737 ffa18900 20a18930 00000001 win32k!ProcessKeyboardInputWorker+0x2dc
06 8825296c 94f00f58 ffa18930 a17e8ed0 882529c4 win32k!ProcessKeyboardInput+0x68
07 8825297c 83eb67b4 ffa18930 ffa18958 00000000 win32k!InputApc+0x4e
08 882529c4 83e83685 00000000 00000000 00000000 nt!KiDeliverApc+0x17f
09 88252a08 83e824f7 8d35bb90 8940fc80 89410074 nt!KiSwapThread+0x24e
0a 88252a30 83e7e4a4 8940fc80 89410008 00000000 nt!KiCommitThreadWait+0x1df
0b 88252ba8 94eed276 00000004 8940c0d0 00000001 nt!KeWaitForMultipleObjects+0x535
0c 88252c04 94efd7c7 00000004 00000002 882c7300 win32k!RawInputThread+0x486
0d 88252c18 94fb2b5d 00000004 01cafbe8 88252c34 win32k!xxxCreateSystemThreads+0x4a
0e 88252c28 83e431ea 00000004 01cafc28 778d70b4 win32k!NtUserCallNoParam+0x1b
0f 88252c28 778d70b4 00000004 01cafc28 778d70b4 nt!KiFastCallEntry+0x12a
10 01cafbd8 75a019ec 75a0288b 00000004 00000000 ntdll!KiFastSystemCallRet
11 01cafbdc 75a0288b 00000004 00000000 77895e7a winsrv!NtUserCallNoParam+0xc
12 01cafbe8 77895e7a 00000000 765c35e7 00000000 winsrv!StartCreateSystemThreads+0x12
13 01cafc28 778f37c8 75a02879 00000000 00000000 ntdll!__RtlUserThreadStart+0x28
14 01cafc40 00000000 75a02879 00000000 00000000 ntdll!_RtlUserThreadStart+0x1b

SetWakeBit就是通知消息循环开始消息响应,如下:

VOID SetWakeBit(
    PTHREADINFO pti,
    UINT wWakeBit)
{
    //...
    KeSetEvent(pti->pEventQueueServer, 2, FALSE);
    //...
}

这里有一个比较重要的东西,很多开发经验不足的人员希望通过WH_KEYBOARD_LL来拦截CTRL + ALT + DEL按键,其实这个是无法生效的,因为在xxxKeyEvent的流程如下:

VOID xxxKeyEvent(
    USHORT    usFlaggedVk,
    WORD      wScanCode,
    DWORD     time,
    ULONG_PTR ExtraInfo,
#ifdef GENERIC_INPUT
    HANDLE    hDevice,
    PKEYBOARD_INPUT_DATA pkei,
#endif
    BOOL      bInjected)
{
    //...
    if ((pHook = PhkFirstValid(ptiCurrent, WH_KEYBOARD_LL)) != NULL) {
        //...
        if (xxxCallHook2(pHook, HC_ACTION, (DWORD)msg, (LPARAM)&kbds, &bAnsiHook)) {
            //...
            if (IsSAS(VkHanded, &fsModifiers)) {
                RIPMSG0(RIP_WARNING, "xxxKeyEvent: SAS ignore bad response from low level hook");
            } else {
                return;
            }
        }
        //...
    }
    //...
}

这里SAS是Secure Attention Sequence的缩写,表示安全键,在Windows下面这个键别设置Ctrl + Alt + Del,这也是Ctrl + Alt + Del无法使用消息钩子拦截的原因。

PostInputMessage表示消息投递的函数,这个函数实现如下:

BOOL PostInputMessage(
    PQ    pq,
    PWND  pwnd,
    UINT  message,
    WPARAM wParam,
    LPARAM lParam,
    DWORD time,
    ULONG_PTR dwExtraInfo)
{
    //...
    pqmsgInput = AllocQEntry(&pq->mlInput);
    if (pqmsgInput == NULL) {
        return FALSE;
    }
    StoreQMessage(pqmsgInput, pwnd, message, wParam, lParam, time, 0, dwExtraInfo);
    WakeSomeone(pq, message, pqmsgInput);

    return TRUE;
}

从这里可以得知,我们先从pq->mlInput鼠标和键盘队列中分配一个数据结构,然后使用StoreQMessage填充结构内容,最后使用WakeSomeone通知目标进行处理。

3. gpqForeground

我们知道SendMessage发送给了线程中的消息队列了,那么鼠标键盘的消息放到哪里去了呢?这个结构就是gpqForeground,这个类型如下:

typedef struct tagQ {
    MLIST       mlInput;            // raw mouse and key message list.

    PTHREADINFO ptiSysLock;         // Thread currently allowed to process input
    ULONG_PTR    idSysLock;          // Last message removed
    ULONG_PTR    idSysPeek;          // Last message peeked

    PTHREADINFO ptiMouse;           // Last thread to get mouse msg.
    PTHREADINFO ptiKeyboard;

    PWND        spwndCapture;
    PWND        spwndFocus;
    PWND        spwndActive;
    PWND        spwndActivePrev;

    UINT        codeCapture;
    UINT        msgDblClk;
    WORD        xbtnDblClk;
    DWORD       timeDblClk;
    HWND        hwndDblClk;
    POINT       ptDblClk;

    BYTE        afKeyRecentDown[CBKEYSTATERECENTDOWN];
    BYTE        afKeyState[CBKEYSTATE];

    CARET       caret;

    PCURSOR     spcurCurrent;
    int         iCursorLevel;

    DWORD       QF_flags;            // QF_ flags go here

    USHORT      cThreads;            // Count of threads using this queue
    USHORT      cLockCount;          // Count of threads that don't want this queue freed

    UINT        msgJournal;
    LONG_PTR    ExtraInfo;
} Q;

从名字来看gpqForeground这个是前景窗口的信息了,那么这个东西是什么时候设置的呢?当我们调用SetForegroundWindow设置前景窗口的时候,有如下设置:

BOOL xxxSetForegroundWindow2(
    PWND pwnd,
    PTHREADINFO pti,
    DWORD fFlags)
{
    //...
    if (pwnd != NULL) {
        //...
        gpqForeground = GETPTI(pwnd)->pq;
        //...
    }
    //...
}

我们可以看一下这个结构信息:

3: kd> dd win32k!gpqForeground L1
950ffc00  fe959570
3: kd> dt win32k!tagQ fe959570
   +0x000 mlInput          : tagMLIST
   +0x00c ptiSysLock       : (null) 
   +0x010 idSysLock        : 0
   +0x014 idSysPeek        : 0
   +0x018 ptiMouse         : 0xfd1734f8 tagTHREADINFO
   +0x01c ptiKeyboard      : 0xfd1734f8 tagTHREADINFO
   +0x020 spwndCapture     : (null) 
   +0x024 spwndFocus       : 0xfea1b250 tagWND
   +0x028 spwndActive      : 0xfea1a700 tagWND
   +0x02c spwndActivePrev  : (null) 
   +0x030 codeCapture      : 0
   +0x034 msgDblClk        : 0
   +0x038 xbtnDblClk       : 0
   +0x03c timeDblClk       : 0
   +0x040 hwndDblClk       : (null) 
   +0x044 ptDblClk         : tagPOINT
   +0x04c ptMouseMove      : tagPOINT
   +0x054 afKeyRecentDown  : [32]  ""
   +0x074 afKeyState       : [64]  ""
   +0x0b4 caret            : tagCARET
   +0x0ec spcurCurrent     : 0xffb55c88 tagCURSOR
   +0x0f0 iCursorLevel     : 0n0
   +0x0f4 QF_flags         : 0x40
   +0x0f8 cThreads         : 1
   +0x0fa cLockCount       : 0
   +0x0fc msgJournal       : 0
   +0x100 ExtraInfo        : 0n0
   +0x104 ulEtwReserved1   : 0x2000

所以从PostInputMessage(gpqForeground, NULL, message, wParam, lParam, time, ExtraInfo);我们就可以知道,鼠标和键盘消息都是发给前景窗口线程的。

4. 消息钩子

在Windows中,可以使用SetWindowsHookExW来设置鼠标和键盘钩子

HHOOK SetWindowsHookExW(
  int       idHook,
  HOOKPROC  lpfn,
  HINSTANCE hmod,
  DWORD     dwThreadId
);

对于键盘,有两种消息WH_KEYBOARDWH_KEYBOARD_LL,从上述的分析我们可以知道,对于底层消息钩子,可以捕获到所有的按键信息,然而对于CTRL + ALT + DEL键信息,底层消息钩子并没有进行拦截。而对于普通消息钩子,很明显的是无法捕捉到热键按下的信息,至于热键我们使用WH_KEYBOARD_LL依旧可以进行拦截,关于热键的注册使用如下函数:

BOOL RegisterHotKey(
  HWND hWnd,
  int  id,
  UINT fsModifiers,
  UINT vk
);

关于消息钩子的具体实现原理,我们可以参考(Windows消息钩子实现原理)。

5. 从击键到Win32k

当我们按下键盘的时候,大致过程如下:

  1. 键盘产生硬件中断信号发到总线,总线将信号发送到ioapic寄存器接收,ioapic寄存器存储了键盘中断的中断idt的索引号,和发给哪个cpu来处理(针对多核),然后发给该cpu的local apic 寄存器。
  2. local apic接收到信号,里面存储了中断idt 索引号到idt键盘中断处理地址的映射。然后发给键盘中断处理例程,该例程再依次传给键盘端口驱动,端口类驱动,将按键的break code放到端口类驱动的设备扩展中。自下而上的过程结束。

针对i8042,这个中断向量为i8042prt!I8042KeyboardInterruptService,此时我们可以看到整个调用关系如下:

0: kd> kb
 # ChildEBP RetAddr  Args to Child              
00 83f2d940 83e3f7ad 8d4a3a00 8d3edb10 83f2d96c i8042prt!I8042KeyboardInterruptService
01 83f2d940 84219ca6 8d4a3a00 8d3edb10 83f2d96c nt!KiInterruptDispatch+0x6d
02 83f2d9dc 8421ca4d 8d4a3780 8d400b90 83f2dabc hal!HalpGenerateInterrupt+0x1d2
03 83f2d9fc 8421cc7f 83f2da14 83e3f808 5eb8ae02 hal!HalpLowerIrqlHardwareInterrupts+0xf5
04 83f2da04 83e3f808 5eb8ae02 00000061 83f2dabc hal!HalEndSystemInterrupt+0x23
05 83f2da04 90d6c46a 5eb8ae02 00000061 83f2dabc nt!KiInterruptDispatch+0xc8
06 83f2dabc 90d6c74a 634b0160 83f2daec 83f2dafb E1G60I32!RxPacketAssemble+0x126
07 83f2dafc 90d6b77e 00000001 8d50a460 00000000 E1G60I32!RxProcessReceiveInterrupts+0x5e
08 83f2db14 86ce389a 014b0160 00000000 83f2db40 E1G60I32!E1000HandleInterrupt+0x80
09 83f2db50 86c8ea0f 8d50a474 0050a460 00000000 ndis!ndisMiniportDpc+0xe2
0a 83f2db78 83e7d1b5 8d50a474 8d50a460 00000000 ndis!ndisInterruptDpc+0xaf
0b 83f2dbd4 83e7d018 83f30d20 83f3a380 00000000 nt!KiExecuteAllDpcs+0xf9
0c 83f2dc20 83e7ce38 00000000 0000000e 00000000 nt!KiRetireDpcList+0xd5
0d 83f2dc24 00000000 0000000e 00000000 00000000 nt!KiIdleLoop+0x38

我们知道,中断响应例程一般都是不做事情,交给DPC去完成,我们找到这个DPC为I8042KeyboardIsrDpc,调用如下:

0: kd> kb
 # ChildEBP RetAddr  Args to Child              
00 83f2db78 83e7d1b5 8d3edd74 8d3edb10 00000000 i8042prt!I8042KeyboardIsrDpc
01 83f2dbd4 83e7d018 83f30d20 83f3a380 00000000 nt!KiExecuteAllDpcs+0xf9
02 83f2dc20 83e7ce38 00000000 0000000e 00000000 nt!KiRetireDpcList+0xd5
03 83f2dc24 00000000 0000000e 00000000 00000000 nt!KiIdleLoop+0x38

然后读取端口信息,将信息传递给Win32k。i8042键盘和鼠标的驱动在DDK中有例子,如果感兴趣可以看一下DDK的例子更加明确。

6. 总结

从上面分析我们可以知道针对鼠标和键盘有两种处理方案。

首先使用SetWindowsHookExW来设置鼠标键盘钩子(或者低级钩子),这样就可以在鼠标和键盘响应之前捕获消息,但是如果需要针对CTRL + ALT + DELETE键进行处理,那这两个操作就达不到要求了。

鉴于SetWindowsHookExW的局限,网上有人实现了一种比较偏门的键盘热键拦截的方案,就是HOOK函数xxxKeyEvent,从上面的分析我们可以发现,这个函数在所有键盘事件处理之前,因此肯定可以拦截所有消息了。但是对于这种方案的稳定性和兼容性本人没法做相关保证,因此如果正式项目中还是谨慎使用。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值