1. 消息机制

窗口可以接收由鼠标、键盘、进程(Windows下的SendMessage、PostMessage等函数)等发出的消息并做出响应,这些消息需要存储在消息队列里。

1.1. Linux的解决方案

消息队列位于三环各个进程内,存在一个专用进程监听各种消息,并发送到对应的进程中。这种解决方案较为简单,但是需要频繁的进程间通信,效率较低。

1.2. Windows的解决方案

Windows将消息队列放入了零环,并设计了很复杂的消息机制。以下讨论内容为Windows方案。

2. GUI线程和消息队列

线程刚创建的时候是普通线程,当线程第一次调用win32k.sys模块的时候(代表使用到了图形界面),会调用函数PsConvertToGuiThread将线程转换为GUI线程。只有GUI线程才有消息队列(只有窗口才能接收消息),一个GUI线程对应一个消息队列,消息队列位于零环,通过KTHREAD.Win32Thread.MessageQueue可以找到。

2.1. 转换成GUI线程:PsConvertToGuiThread

函数功能如下:

  • 拓展内核栈到64KB,普通内核栈大小为12KB
  • 创建一个包含消息队列的结构体,并挂到KTHREAD上
  • 将Thread.ServiceTable从KeServiceDescriptorTable(仅仅包含ntoskrnl.exe模块的函数)重定向至KeServiceDescriptorTableShadow(Shadow表包含有ntoskrnl.exe和win32k.sys两个模块的函数)
  • 将需要的内存数据(win32k.sys中的函数代码)映射至本进程的内存空间

3. 窗口

user32.dll和gdi32.dll里所有的窗口相关函数,最终都会进入零环的win32k.sys模块,由零环进行处理。

3.1. 窗口创建与窗口结构体

窗口在被CreateWindow函数创建后,在零环会有一个对应的结构体_WINDOW_OBJECT,该结构体指示了窗口的属性以及所属线程(PTHREADINFO pti)、窗口回调函数(窗口过程,WNDPROC lpfnWndProc)等信息。

3.2. 窗口表

零环存在一个窗口表,存有所有进程创建的窗口。三环的窗口句柄是该窗口在零环窗口表内的索引。

3.3. 窗口与线程(消息队列)

一个线程可以包含多个窗口,但是一个窗口只能属于一个线程。一个线程的多个窗口共享一个消息队列。

4. 消息接收与存储

  • 鼠标、键盘消息:win32k.sys模块在初始化的时候会启动两个零环线程(MouseThreadMain、KeyboardThreadMain函数),分别负责监听鼠标和键盘发出的消息,并将消息存储到对应线程的消息队列中
  • 进程消息:由SendMessage(同步,函数会阻塞到消息处理完毕结果返回)、PostMessage(异步,函数不会阻塞)等函数完成消息的接收和存储
  • 其它

4.1. 七个消息队列

每个线程的消息队列有七个(KTHREAD.Win32Thread.MessageQueue是一个复杂的结构体,里面有很多成员,其中就包括这七个消息队列),存储不同类型的消息:

  • SentMessagesListHead:接收SendMessage发来的消息
  • PostedMessagesListHead:接收PostMessage发来的消息
  • HardwareMessagesListHead:接收鼠标、键盘等硬件的消息
  • NotifyMessagesListHead
  • DispatchingMessagesHead
  • LocalDispatchingMessagesHead

5. 消息获取:GetMessage函数

1
2
3
4
5
GetMessage(LPMSG lpMsg,            //存储返回的消息
HWND hWnd, //指定窗口,如果为NULL,则接收所有窗口的消息
UINT wMsgFilterMin, //过滤条件一
UINT wMsgFilterMax, //过滤条件二
)

该函数会调用win32k.sys!NtUserGetMessage函数,功能如下:

  • 处理SentMessagesListHead队列中所有的消息
  • 将其它消息队列中的消息存储到参数lpMsg指定的结构,并将消息从消息队列中删除。如果其它消息队列中没有信息,函数阻塞

6. 消息分发:DispatchMessage函数

该函数会调用零环函数NtUserDispatchMessage,将GetMessage函数获取到的消息根据窗口进行分发和处理。

6.1. 消息翻译:TranslateMessage函数

在调用DispatchMessage函数之前,需要调用TranslateMessage函数进行消息翻译。该函数专门针对键盘消息,会将WM_KEYDOWN消息转换为WM_CHAR消息,原本按键的按键码将会被转换成字符串(如按下a键,函数会将按键码65转换成字符a)。

7. 消息处理

7.1. 消息处理流程

通过消息结构体的窗口句柄成员寻找到窗口结构体,然后获取_WINDOW_OBJECT中的回调函数成员,之后调用回调函数进行消息处理,由于回调函数由三环用户提供,消息处理涉及到通过KeUserModeCallback函数返回三环(称为窗口内核回调,是内核回调机制中的一种)。

7.2. 三种消息处理的时机

  • GetMessage函数
  • DispatchMessage函数
  • 有些函数如CreateWindow在零环会直接进行消息处理(不发送消息到消息队列,而是以消息为参数直接调用窗口的回调函数),这种设计是为了在窗口创建或者销毁等时机第一时间调用回调函数处理事件。

7.3. 微软默认回调函数

在用户提供的回调函数中,对于自己不关心的未进行处理的消息,需要在函数返回的时候调用微软提供的默认回调函数对消息进行处理。return DefWindowProc(hwnd, uMsg, wParam, lParam);

8. 内核回调机制

8.1. 零环调用三环函数的三种情况

  • 用户APC
  • 异常
  • 内核回调机制

8.2. KeUserModeCallback函数

内核回调机制中零环通过nt!KeUserModeCallback函数来调用三环函数。该函数有5个参数,第一个参数为句柄,是一个整数值,指示了内核回调类型(宏定义方式,如窗口内核回调)以及三环函数位置(整数值为三环函数地址表的索引),第二个参数指示了三环函数的相关参数(如窗口句柄和窗口回调函数地址)。

8.3. 三环函数位置

  • 用户APC:固定为ntdll!KiUserApcDispatcher
  • 异常:固定为ntdll!KiUserExceptionDispatcher
  • 内核回调机制:PEB + 0x2C指向一个函数地址表,该表存储了所有的内核回调函数地址(位于user32.dll),nt!KeUserModeCallback函数的第一个参数即为三环函数在表内的索引(在窗口内核回调中,窗口回调函数由该三环函数再行调用)。