windows消息機制詳解
前言
windows是一個消息驅動的系統,windows的消息提供了應用程序之間、應用程序與windows 系統之間進行通信的手段。要想深入理解windows,消息機制的知識是必不可少的。
基礎
進程接收來自于鼠標、鍵盤等其他消息都是通過消息隊列進行傳輸的

常規模式下,有一個專用的進程來接收這些消息,然后再插入某個進程的消息隊列,但是這樣的話會涉及到頻繁的進程間的通信,效率很差

windows為了解決這一問題,因為高2G的內核空間每個進程都是共用的,所以微軟想到把消息的接收放到了0環,使用GUI線程
<1> 當線程剛創建的時候,都是普通線程,指向的是SSDT表
Thread.ServiceTable-> KeServiceDescriptorTable
<2> 當線程第一次調用Win32k.sys時,會調用一個函數:PsConvertToGuiThread,我們知道在3環進0環的過程中會取得一個調用號,當調用號在100以下的時候,在ntosknl.exe里面,當調用號大于100則是圖形處理函數,調用Win32k.sys
如果是一個GUI線程,win32Thread指向的就是THREADINFO結構,如果是普通線程,這里就是一個空指針
主要做幾件事:
a. 擴充內核棧,必須換成64KB的大內核棧,因為普通內核棧只有12KB大小。
b.創建一個包含消息隊列的結構體,并掛到KTHREAD上。對應的就是MessageQueue屬性
c.Thread.ServiceTable-> KeServiceDescriptorTableShadow,把Thread.ServiceTable指向SSDTShadow表,這個表既包含了SSDT表里面的函數,又包含了win32k.sys里面的圖形函數
d.把需要的內存數據映射到本進程空間
總結:
<1> 消息隊列存儲在0環,通過KTHREAD.Win32Thread可以找到
<2> 并不是所有線程都要消息隊列,只有GUI線程才有消息隊列
<3> 一個GUI線程對應1個消息隊列
窗口與線程

我們知道創建windows窗口使用的是CreateWindow,而這個函數底層調用的是CreateWindowExA和CreateWindowExW,我們逆向分析一下CreateWindowExW

首先調用CreateWindowEx

然后調用VerNtUserCreateWindowEx

再調用NtUserCreateWindowEx

通過NtUserCreateWindowEx進入0環

windows窗口都在0環有一個結構體,就是WINDOW_OBJECT,pti即窗口對象指向的線程。一個線程可以對應多個窗口,但是在同一個程序里面多個窗口只能對應一個線程
總結
1、窗口是在0環創建的
2、窗口句柄是全局的
3、一個線程可以用多個窗口,但每個窗口只能屬于一個線程
一個GUI線程只有一個消息隊列,一個線程可以有很多個窗口,一個線程中所有的窗口共享同一個消息隊列
消息的接收

首先在3環創建窗口和窗口類的對象,對應0環的_WINDOW_OBJECT結構

消息隊列的結構
<1> SentMessagesListHead //接到SendMessage發來的消息 <2> PostedMessagesListHead //接到PostMessage發來的消息 <3> HardwareMessagesListHead //接到鼠標、鍵盤的消息
如果要取所有隊列的消息,則第二個參數設置為NULL,后兩個參數全部設置為0
GetMessage的主要功能:循環判斷是否有該窗口的消息,如果有,將消息存儲到MSG指定的結構,并將消息從列表中刪除。
GetMessage( LPMSG lpMsg, //返回從隊列中摘下來的消息 HWND hWnd, //過濾條件一:發個這個窗口的消息 UNIT wMsgFilterMin, //過濾條件 UNIT wMsgFilterMax //過濾條件 );
使用GetMessage()獲取信息,另外一個程序利用SendMessage發送給窗口,這里GetMessage會接收到消息并直接處理


NtUserGetMessage
User32!GetMessage 調用 w32k!NtUserGetMessage
do
{
//先判斷SentMessagesListHead是否有消息 如果有處理掉
do
{
....
KeUserModeCallback(USER32_CALLBACK_WINDOWPROC,
Arguments,
ArgumentLength,
&ResultPointer,
&ResultLength);
....
}while(SentMessagesListHead != NULL)
//以此判斷其他的6個隊列,里面如果有消息 返回 沒有繼續
}while(其他隊列!=NULL)
SendMessage/PostMessage
SendMessage為同步,PostMessage為異步,GetMessage只處理第一個鏈表即SentMessagesListHead里面的消息
當一個程序利用SendMessage向另外一個程序發送消息時,另外一個程序會用GetMessage接收,這個過程GetMessage會在0環的SentMessagesListHead鏈表里面搜索是否存在SendMessage,如果存在SendMessage,GetMessage就會在兩個程序的共享內存里面向發送消息的程序發送一個結果,在這個過程中,發送消息的程序是一直處于等待狀態的,只有接收到返回的消息才會結束,這稱為同步


如果利用PostMessage發送消息,處于第二個鏈表里面,GetMessage不會處理,而程序發完消息之后也會立即結束,不會有等待的過程,這成為異步,如果要處理,使用DispatchMessage()處理
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}


消息的分發
這里如果只有GetMessage的話,關閉窗口是關閉不了的

DispatchMessage
User32!DispatchMessage 調用 w32k!NtUserDispatchMessage
<1> 根據窗口句柄找到窗口對象
<2> 根據窗口對象得到窗口過程函數,由0環發起調用
如果使用DispatchMessage分發消息,根據窗口句柄調用相關的窗口過程,即可關閉
因為很多個消息共用一個消息隊列,所以通過GetMessage取出消息之后,需要用DispatchMessage進行消息的分發
DispatchMessage通過GetMessage取出的句柄,進入0環找到Window_Object對象,再找到對應的窗口過程調用

TranslateMessage是用來處理鍵盤輸出的函數,定義一個函數
case WM_CHAR:
{
sprintf(szBuffer, "Down : %c", wParam);
MessageBox(hwnd, szBuffer, "", 0);
return 0;
}

這里如果不使用TranslateMessage,則沒有WM_CHAR這個消息,需要自己定義WM_KEYDOWN
case WM_KEYDOWN:
{
sprintf(szBuffer, "Down : %d", wParam);
MessageBox(hwnd, szBuffer, "", 0);
return 0;
}


消息有很多,但是不是每個消息都需要我們自己去處理,所以與我們無關的消息就使用windows提供的DefWindowProc讓微軟替我們處理即可

內核回調機制
窗口過程函數除了GetMessage和DispatchMessage 能夠調用,一些在0環的函數也能夠直接進行調用。例如CreateWindow不向消息隊列里面發送消息,而是直接調用3環提供的函數
這些消息類型可以被直接調用

這里對WM_CREATE進行修改,當創建成功的時候彈窗

這里并沒有執行到GetMessage和TranslateMessage就彈窗,說明被CreateWindow調用0環函數,0環函數通過回調機制(KeUserModeCallBack),再調用窗口過程函數

所以調用窗口過程只能是以下三種情況
<1> GetMessage()在處理SentMessagesListHead中消息時 <2> DispatchMessage()在處理其他隊列中的消息時 <3> 內核代碼
1、從0環調用3環函數的幾種方式:
APC、異常、內核回調
2、凡是有窗口的程序就有可能0環直接調用3環的程序。回調機制中0環調用3環的的代碼是函數:KeUserModeCallback
3、回到3環的落腳點:
APC:ntdll!KiUserApcDispatcher
異常:ntdll!KiUserExceptionDispatcher
KeUserModeCallback
KeUserModeCallback在0環對應NtUserDispatchMessage,調用IntDispatchMessage。通過UserGetWindowObject獲得一個Window_Object類型,通過對象得到當前窗口的對應的窗口函數,然后調用co_IntCallWindowProc。
調用KeUserModeCallback,第一個值為索引,第二個值為窗口回調過程中所有有用的信息。第一個索引值, KeUserModeCallback函數的第一個參數就是索引,其實它是一個宏,有很多個對應的值
內核回調在3環的落腳點,有很多個地方,我們拿著索引去3環里面找回調函數地址表,如果索引為0,則取表里面的第一個函數,如果索引為1,則取表里面的第二個函數
PEB+0x2C 回調函數地址表,由user32.dll提供這里打開一個exe,通過fs:[0]找到TEB

TEB的0x30偏移為PEB

PEB的0x2C偏移即為回調地址函數表

這里通過KeUserModeCallback的第一個值,即索引找到函數之后,這個函數再去調用窗口過程函數,窗口過程函數已經通過Arguments放在了堆棧里面
