前言
前一篇,弄清楚了消息队列与线程的关系,简单来说就是,一个GUI线程对应一个消息队列。什么是GUI,什么是消息队列,参考这里,本篇不再赘述。本篇将解决以下3个问题:
- 消息从哪里来?
- 消息到哪里去?
- 谁来做这些事情?
消息从哪里来
进入正题,首先讨论消息从哪里来。
鼠标消息、键盘消息

这张图源于VC++6.0的插件,Spy++,它可以捕捉窗口所接收到的消息。当把鼠标放在窗口上时,移动或者点击,则会接收到鼠标发送的消息,敲击键盘则会接收到键盘发送的消息(图中未显示,测试时消息界面会显示敲击下的键盘符号)。
其它进程消息
另一种情况是其它进程发送的消息。使用CreateWindow创建一个窗口,会获得一个窗口句柄,之前在句柄表中曾提过,此句柄非彼句柄,之前说到的句柄属于内核对象的句柄,而今天提到的句柄则是窗口句柄,相同的是这两类句柄都只是一个编号,用于给3环使用的。窗口句柄的特点,它是全局的,因此只要获取到窗口的句柄,任意进程都可以通过SendMessage或者PostMessage函数给这个窗口发送消息进行交互。
结论
- 消息来源于鼠标,键盘以及其它进程。
消息到哪里去
根据上面的分析,消息会因为鼠标,键盘,以及其它进程与某一个窗口交互时产生,所以表面上来说,消息会到窗口。窗口又是什么呢?
窗口的形成
通常创建窗口使用CreateWindow函数,它有CreateWindowA(ASCII)与CreateWindowW(Unicode)两个宏,会根据当前使用的编码自动调用其中的一种。这两个宏最终都会调用CreateWindowEx,CreateWindowEx的内部调用如下:
调用_VerNtUserCreateWindowEx
调用_NtUserCreateWindow
进入_NtUserCreateWindow内部
这个函数是进入0环的入口,可以得出两点信息:
- 窗口的创建是在0环
- 系统服务号大于0x1000,因此调用的是第二张系统服务表指向的函数地址表中的函数,属于Win32k.sys。
InitInputImpl函数
创建窗口必然要调用win32k.sys中的服务,在初始化Win32k.sys的服务时,会调用一个函数InitInputImpl,其原型如下:
1 | NTSTATUS FASTCALL InitInputImpl(VOID) |
InitInputImpl会在0环创建2个线程,一个监控键盘,另一个监控鼠标,并将消息存储到对应线程的消息队列中。这也能解释为什么有时候程序突然卡死了,但是鼠标还可以动,原因是鼠标有着自己独立的线程。

结论
- 窗口是通过调用win32k.sys系统服务在0环创建的。
- 初始化win32k.sys会调用InitInputImpl函数,InitInputImpl创建的监控线程会将消息存储到对应线程的消息队列中。
窗口找消息队列
现在知道消息从哪来,以及消息会到哪去,接下来就会有一个问题,如何通过窗口找到消息队列。

由图,打开3个窗口,鼠标进行点击与移动,操作系统是怎样准确的将消息发送给不同窗口对应的消息队列的呢?
- 首先,这张图上不止是3个窗口,每个进程窗口内部的按钮,表格,都属于窗口,所以一个进程可以有多个窗口,但是这些窗口只能属于一个进程。
- 前面提到窗口是进入0环后通过win32k.sys提供的服务画出来的,所以,窗口其实是个0环的结构。与进程,线程类似,它们都有对应的内核结构体EPROCESS与ETHREAD,窗口也有自己对应的内核结构体_WINDOW_OBJECT。可惜的是,这个结构体并没有通过符号表导出,我在网上搜也没能搜到,所以只能根据海哥所说的来看。
窗口对象_WINDOW_OBJECT中有一个成员pti,类型是PTHREADINFO,指向THREADINFO结构,就是前一篇讲到的Win32Thread指向的结构。这样就将线程与窗口联系起来了。原本KTHREAD.Win32Thread处指向的值为空,当线程调用win32k.sys中的函数创建一个窗口后,Win32Thread就会指向THREADINFO结构体,该线程也由普通线程变成了GUI线程,此时窗口对应的内核结构WINDOW_OBJECT中的成员pti也会指向这个结构体,而消息队列,正是位于这个结构体中,这样窗口就可以其所在线程的消息队列。
总结
- 窗口是在0环创建的。
- 窗口句柄是全局的。
- 一个线程可以有多个窗口,但每个窗口只能属于一个线程。
参考资料
参考教程:
- 海哥逆向中级预习班
参考链接:
- https://blog.csdn.net/weixin_42052102/article/details/83787929 (My classmates-消息机制学习笔记)