windows核心情景分析--視窗訊息
訊息與鉤子
眾所周知,Windows系統是訊息驅動的,現在我們就來看Windows的訊息機制.
早期的Windows的視窗圖形機制是在使用者空間實現的,後來為了提高圖形處理效率,將這部分移入核心空間,在Win32k.sys模組中實現。這個模組作為一個擴充套件的核心模組,提高了一個擴充套件額系統服務表,專用於視窗圖形操作,相應的,這個模組中添加了一個擴充套件系統呼叫服務表Shadow SSDT,以及一個擴充套件的系統呼叫服務表描述符表:KeServiceDescriptorTableShadow.(系統中 不僅有兩張SSDT,還有兩張系統服務表描述符表)。當一個執行緒首次呼叫這個模組中的系統服務函式時,這個執行緒就自然變成了GUI執行緒。GUI執行緒結構的ServiceTable指向的就是這個shadow描述符表。
指向這個表的系統服務號的bit12位(也即第13位)為1,如0x1XXX表示使用的是shadow服務表。
每個執行緒建立時都是普通執行緒,但是隻要那個執行緒在執行的過程中發起了一次對win32k.sys模組中的系統呼叫,就會轉變成GUI執行緒,下面的函式就是這個用途。
NTSTATUS PsConvertToGuiThread(VOID)
{
ULONG_PTR NewStack;
PVOID OldStack;
PETHREAD Thread = PsGetCurrentThread();
PEPROCESS Process = PsGetCurrentProcess();
NTSTATUS Status;
if (KeGetPreviousMode() == KernelMode) return STATUS_INVALID_PARAMETER;
ASSERT(PspW32ProcessCallout != NULL);//確保win32k.sys模組已載入到記憶體
if (Thread->Tcb.ServiceTable != KeServiceDescriptorTable)
return STATUS_ALREADY_WIN32;//表示先前已經轉換為GUI執行緒了
if (!Thread->Tcb.LargeStack)//if 尚未換成大核心棧
{
NewStack = (ULONG_PTR)MmCreateKernelStack(TRUE, 0);//分配一個64KB的大核心棧
//更為大核心棧
OldStack = KeSwitchKernelStack(NewStack, (NewStack - KERNEL_STACK_SIZE));
MmDeleteKernelStack(OldStack, FALSE);//銷燬原來的普通核心棧
}
if (!Process->Win32Process)//if 尚未分配W32PROCESS結構(也即if是該程序中的第一個GUI執行緒)
Status = PspW32ProcessCallout(Process, TRUE);//分配Win32Process結構(表示GUI程序)
Thread->Tcb.ServiceTable = KeServiceDescriptorTableShadow;//關鍵。更改描述符表
//為當前執行緒分配一個W32THREAD結構
Status = PspW32ThreadCallout(Thread, PsW32ThreadCalloutInitialize);
if (!NT_SUCCESS(Status)) Thread->Tcb.ServiceTable = KeServiceDescriptorTable;//改為原來的
return Status;
}
如上,每個執行緒在轉換為GUI執行緒時,必須換用64KB的大核心棧,因為普通的核心棧只有12KB大小,不能支援開銷大的圖形任務。然後分配一個W32PROCESS結構,將程序轉換為GUI程序,然後分配W32THREAD結構,更改系統服務表描述符表。上面的PspW32ProcessCallout和PspW32ThreadCallout函式都是回撥函式,分別指向win32k.sys模組中的Win32kProcessCallback、Win32kThreadCallback函式。
NTSTATUS
Win32kProcessCallback(struct _EPROCESS *Process,
BOOLEAN Create)//指是要建立還是要銷燬
{
PPROCESSINFO Win32Process;
Win32Process = PsGetProcessWin32Process(Process);//獲得當前程序的W32PROCESS結構指標
if (!Win32Process)//if 尚未分配該結構,就分配一個
{
Win32Process = ExAllocatePoolWithTag(NonPagedPool,sizeof(PROCESSINFO),'p23W');
RtlZeroMemory(Win32Process, sizeof(PROCESSINFO));
PsSetProcessWin32Process(Process, Win32Process);
}
if (Create)
{
SIZE_T ViewSize = 0;
LARGE_INTEGER Offset;
PVOID UserBase = NULL;
NTSTATUS Status;
extern PSECTION_OBJECT GlobalUserHeapSection;
Offset.QuadPart = 0;
//將全域性使用者堆對映到本程序的地址空間
Status = MmMapViewOfSection(GlobalUserHeapSection,PsGetCurrentProcess(),&UserBase,
0,0,&Offset,&ViewSize,ViewUnmap,SEC_NO_CHANGE,
PAGE_EXECUTE_READ);
Win32Process->HeapMappings.Next = NULL;
Win32Process->HeapMappings.KernelMapping = (PVOID)GlobalUserHeap;
Win32Process->HeapMappings.UserMapping = UserBase;
Win32Process->HeapMappings.Count = 1;
InitializeListHead(&Win32Process->ClassList);
InitializeListHead(&Win32Process->MenuListHead);
InitializeListHead(&Win32Process->GDIBrushAttrFreeList);
InitializeListHead(&Win32Process->GDIDcAttrFreeList);
InitializeListHead(&Win32Process->PrivateFontListHead);
ExInitializeFastMutex(&Win32Process->PrivateFontListLock);
InitializeListHead(&Win32Process->DriverObjListHead);
ExInitializeFastMutex(&Win32Process->DriverObjListLock);
Win32Process->KeyboardLayout = W32kGetDefaultKeyLayout();
if(Process->Peb != NULL)
{
//對映全域性的GDI物件控制代碼表到本程序的地址空間中
Process->Peb->GdiSharedHandleTable = GDI_MapHandleTable(GdiTableSection, Process);
Process->Peb->GdiDCAttributeList = GDI_BATCH_LIMIT;
}
Win32Process->peProcess = Process;
Win32Process->W32PF_flags = 0;
}
else
{
IntCleanupMenus(Process, Win32Process);
IntCleanupCurIcons(Process, Win32Process);
CleanupMonitorImpl();
DestroyProcessClasses(Win32Process);
GDI_CleanupForProcess(Process);
co_IntGraphicsCheck(FALSE);
if(LogonProcess == Win32Process)
LogonProcess = NULL;
}
return STATUS_SUCCESS;
}
每個含有GUI執行緒的程序都是GUI程序,每個GUI程序的EPROCESS結構含有W32PROCESS結構
typedef struct _W32PROCESS
{
PEPROCESS peProcess;
DWORD RefCount;
ULONG W32PF_flags;
PKEVENT InputIdleEvent;
DWORD StartCursorHideTime;
struct _W32PROCESS * NextStart;
PVOID pDCAttrList;
PVOID pBrushAttrList;
DWORD W32Pid;
LONG GDIHandleCount;
LONG UserHandleCount;
PEX_PUSH_LOCK GDIPushLock; /* Locking Process during access to structure. */
RTL_AVL_TABLE GDIEngUserMemAllocTable; /* Process AVL Table. */
LIST_ENTRY GDIDcAttrFreeList;
LIST_ENTRY GDIBrushAttrFreeList;
} W32PROCESS, *PW32PROCESS;
NTSTATUS
Win32kThreadCallback(struct _ETHREAD *Thread,
PSW32THREADCALLOUTTYPE Type) //指是初始化還是清理
{
struct _EPROCESS *Process;
PTHREADINFO Win32Thread;
DECLARE_RETURN(NTSTATUS);
Process = Thread->ThreadsProcess;
Win32Thread = PsGetThreadWin32Thread(Thread);
if (!Win32Thread)//if 尚未分配Win32Thread結構,就分配
{
Win32Thread = ExAllocatePoolWithTag(NonPagedPool,sizeof(THREADINFO),'t23W');
RtlZeroMemory(Win32Thread, sizeof(THREADINFO));
PsSetThreadWin32Thread(Thread, Win32Thread);
}
if (Type == PsW32ThreadCalloutInitialize)//if 初始化
{
HWINSTA hWinSta = NULL;
PTEB pTeb;
HDESK hDesk = NULL;
NTSTATUS Status;
PUNICODE_STRING DesktopPath;
PRTL_USER_PROCESS_PARAMETERS ProcessParams = (Process->Peb ? Process->Peb->ProcessParameters : NULL);
InitializeListHead(&Win32Thread->WindowListHead);
InitializeListHead(&Win32Thread->W32CallbackListHead);
InitializeListHead(&Win32Thread->PtiLink);
DesktopPath = (ProcessParams ? ((ProcessParams->DesktopInfo.Length > 0) ? &ProcessParams->DesktopInfo : NULL) : NULL);
Status = IntParseDesktopPath(Process,DesktopPath,&hWinSta,&hDesk);
if(NT_SUCCESS(Status))
{
…
Win32Thread->MessageQueue = MsqCreateMessageQueue(Thread);//關鍵。建立訊息佇列
Win32Thread->KeyboardLayout = W32kGetDefaultKeyLayout();
Win32Thread->pEThread = Thread;
}
}
Else …
Return STATUS_SUCCESS;
}
typedef struct _W32THREAD
{
PETHREAD pEThread;
ULONG RefCount;
PTL ptlW32;
PVOID pgdiDcattr;
PVOID pgdiBrushAttr;
PVOID pUMPDObjs;
PVOID pUMPDHeap;
DWORD dwEngAcquireCount;
PVOID pSemTable;
PVOID pUMPDObj;
} W32THREAD, *PW32THREAD;
typedef struct _THREADINFO
{
W32THREAD; //開頭是一個W32THREAD結構
PTL ptl;
PPROCESSINFO ppi;
struct _USER_MESSAGE_QUEUE* MessageQueue;
struct _KBL* KeyboardLayout;
PCLIENTTHREADINFO pcti;
struct _DESKTOP* rpdesk;
PDESKTOPINFO pDeskInfo;
PCLIENTINFO pClientInfo;
FLONG TIF_flags;
PUNICODE_STRING pstrAppName;
LONG timeLast;
ULONG_PTR idLast;
INT exitCode;
HDESK hdesk;
UINT cPaintsReady; /* Count of paints pending. */
UINT cTimersReady; /* Count of timers pending. */
DWORD dwExpWinVer;
DWORD dwCompatFlags;
DWORD dwCompatFlags2;
struct _USER_MESSAGE_QUEUE* pqAttach;
PTHREADINFO ptiSibling;
ULONG fsHooks;
PHOOK sphkCurrent;
LPARAM lParamHkCurrent;
WPARAM wParamHkCurrent;
struct tagSBTRACK* pSBTrack;
HANDLE hEventQueueClient;
PKEVENT pEventQueueServer;
LIST_ENTRY PtiLink;
CLIENTTHREADINFO cti; // Used only when no Desktop or pcti NULL.
/* ReactOS */
LIST_ENTRY WindowListHead;
LIST_ENTRY W32CallbackListHead;
SINGLE_LIST_ENTRY ReferencesList;
} THREADINFO;
typedef struct _WINDOW_OBJECT //每個視窗物件的內部結構
{
THRDESKHEAD head;
PWND Wnd;//內部結構
PTHREADINFO pti; //所屬執行緒
HMENU SystemMenu;//左上角的系統選單
HWND hSelf;//視窗控制代碼是核心全域性的
ULONG state;
HANDLE hrgnUpdate;//當前無效區域(指更新區域)的控制代碼
HANDLE hrgnClip;//剪裁區域的控制代碼
struct _WINDOW_OBJECT* spwndChild;//第一個子視窗
struct _WINDOW_OBJECT* spwndNext;//下一個兄弟視窗
struct _WINDOW_OBJECT* spwndPrev;//上一個兄弟視窗
struct _WINDOW_OBJECT* spwndParent;//父視窗
struct _WINDOW_OBJECT* spwndOwner;//擁有者視窗與父視窗是兩碼事
PSBINFOEX pSBInfo;//滾動條資訊
LIST_ENTRY ThreadListEntry;//用來掛入執行緒的視窗連結串列
} WINDOW_OBJECT;
typedef struct _WND
{
THRDESKHEAD head;
DWORD state;
DWORD state2;
DWORD ExStyle;//擴充套件樣式
DWORD style;//標準樣式
HINSTANCE hModule;//建立本視窗的模組
DWORD fnid;
struct _WND *spwndNext;
struct _WND *spwndPrev;
struct _WND *spwndParent;
struct _WND *spwndChild;
struct _WND *spwndOwner;
RECT rcWindow;//整個區域
RECT rcClient;//客戶區域
WNDPROC lpfnWndProc;//關鍵。視窗過程
PCLS pcls;//視窗類
HRGN hrgnUpdate;
LIST_ENTRY PropListHead;//屬性連結串列
ULONG PropListItems;
PSBINFO pSBInfo;//滾動條資訊
HMENU SystemMenu;
UINT IDMenu;
HRGN hrgnClip;
HRGN hrgnNewFrame;
LARGE_UNICODE_STRING strName;//視窗標題
ULONG cbwndExtra;//附加資料區的大小
HWND hWndLastActive;
struct _WND *spwndLastActive;
LONG dwUserData;//使用者自定義資料
struct _WND *spwndClipboardListener;
DWORD ExStyle2;
struct
{
RECT NormalRect;
POINT IconPos;
POINT MaxPos;
} InternalPos;
UINT Unicode : 1; // !(WNDS_ANSICREATOR|WNDS_ANSIWINDOWPROC) ?
UINT InternalPosInitialized : 1;
UINT HideFocus : 1; // WS_EX_UISTATEFOCUSRECTHIDDEN ?
UINT HideAccel : 1; // WS_EX_UISTATEKBACCELHIDDEN ?
} WND, *PWND;
下面重點講述訊息迴圈機制中的幾個重點函式:
GetMessage、DispatchMessage 、SendMessage、PostMessage、PeekMessage
下面的函式從本執行緒的訊息佇列中取出一條符合指定過濾條件的訊息
BOOL
GetMessageW(LPMSG lpMsg,//返回從佇列中摘下來的訊息
HWND hWnd,//過濾條件一:發給這個視窗的訊息
UINT wMsgFilterMin, //過濾條件二:最小值
UINT wMsgFilterMax) //過濾條件三:最大值
{
BOOL Res;
MSGCONVERSION Conversion;//用於核心訊息、使用者模式訊息互轉
NTUSERGETMESSAGEINFO Info;//一個專用於NtUserGetMessage函式的中間結構
PUSER32_THREAD_DATA ThreadData = User32GetThreadData();//每個執行緒的User32 tls資料
MsgConversionCleanup(lpMsg, FALSE, FALSE, NULL);
//實質函式。從本執行緒的訊息佇列中尋找符合對應過濾條件的一條訊息,摘下來。
Res = NtUserGetMessage(&Info, hWnd, wMsgFilterMin, wMsgFilterMax);
if (-1 == (int) Res) return Res;
Conversion.LParamSize = Info.LParamSize;
Conversion.KMMsg = Info.Msg;
//將核心模式的訊息格式轉換為使用者模式的訊息格式
if (! MsgiKMToUMMessage(&Conversion.KMMsg, &Conversion.UnicodeMsg))
return (BOOL) -1;
*lpMsg = Conversion.UnicodeMsg;//返回取得的轉換後的訊息
Conversion.Ansi = FALSE;
Conversion.FinalMsg = lpMsg;
MsgConversionAdd(&Conversion);//加入全域性的轉換陣列
if (Res && lpMsg->message != WM_PAINT && lpMsg->message != WM_QUIT)
ThreadData->LastMessage = Info.Msg;//記錄本執行緒上次取下來的訊息
return Res;
}
typedef struct tagNTUSERGETMESSAGEINFO //中間結構
{
MSG Msg;
ULONG LParamSize;//L引數附件包的長度(注意有的訊息型別是帶有L附件包的)
} NTUSERGETMESSAGEINFO, *PNTUSERGETMESSAGEINFO;
typedef struct tagMSG
{
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;//lparam有可能是個指標
DWORD time;//訊息進隊時間
POINT pt;//訊息進隊時,桌面游標的位置
} MSG,*LPMSG,*PMSG;
我們看實質的核心函式:NtUserGetMessage
BOOL
NtUserGetMessage( PNTUSERGETMESSAGEINFO UnsafeInfo,//使用者空間中的不安全地址
HWND hWnd,
UINT MsgFilterMin,
UINT MsgFilterMax )
{
BOOL GotMessage;
NTUSERGETMESSAGEINFO Info;//核心中的安全緩衝
NTSTATUS Status;
PWINDOW_OBJECT Window = NULL;
PMSGMEMORY MsgMemoryEntry;
PVOID UserMem;
UINT Size;
USER_MESSAGE Msg;//訊息佇列中的原生訊息結構
DECLARE_RETURN(BOOL);
UserEnterExclusive();
//檢測視窗控制代碼是否無效
if (hWnd && !(Window = UserGetWindowObject(hWnd)))
RETURN(-1);
//過濾條件自相矛盾,就不過濾
if (MsgFilterMax < MsgFilterMin)
{
MsgFilterMin = 0;
MsgFilterMax = 0;
}
//迴圈等待,直到取出一條符合指定條件的訊息出來
do
{
GotMessage = co_IntPeekMessage(&Msg, Window, MsgFilterMin, MsgFilterMax, PM_REMOVE);
if (GotMessage)//如果取出了
{
Info.Msg = Msg.Msg;
//檢測這種訊息是否帶有L引數附件包,若有,返回它的L附件包大小計演算法
MsgMemoryEntry = FindMsgMemory(Info.Msg.message);
if (NULL == MsgMemoryEntry)//若不帶L附件包
Info.LParamSize = 0;
else
{
//根據這種訊息的L附件包長度計演算法 計算出該訊息的L附件包大小
Size = MsgMemorySize(MsgMemoryEntry, Info.Msg.wParam,Info.Msg.lParam);
Info.LParamSize = Size;
UserMem = NULL;//使用者空間的L附件包地址
//在當前程序的使用者空間分配一塊L附件包大小的記憶體
Status = ZwAllocateVirtualMemory(NtCurrentProcess(), &UserMem, 0,
&Info.LParamSize, MEM_COMMIT, PAGE_READWRITE);
//關鍵。將核心中的L附件包轉移到使用者空間
Status = MmCopyToCaller(UserMem, (PVOID) Info.Msg.lParam, Size);
Info.Msg.lParam = (LPARAM) UserMem;//重定向指向使用者空間中的地址
}
if (Msg.FreeLParam && NULL != Msg.Msg.lParam)
ExFreePool((void *) Msg.Msg.lParam);//釋放核心中的L附件包
//將取下來的訊息上傳給使用者
Status = MmCopyToCaller(UnsafeInfo, &Info, sizeof(NTUSERGETMESSAGEINFO));
if (! NT_SUCCESS(Status))
{
SetLastNtError(Status);
RETURN( (BOOL) -1);
}
}
//一直等待訊息佇列中出現對應過濾條件的訊息
else if (! co_IntWaitMessage(Window, MsgFilterMin, MsgFilterMax))
{
RETURN( (BOOL) -1);
}
}
while (! GotMessage);
RETURN( WM_QUIT != Info.Msg.message);
CLEANUP:
UserLeave();
END_CLEANUP;
}
上面這個函式首先會判斷視窗區域性是否有效。它根據視窗控制代碼從核心中的全域性使用者物件控制代碼表中檢索出對應的使用者物件(所謂使用者物件指視窗、選單、快捷鍵、游標、鉤子等相對於核心物件的GUI物件),由於該控制代碼表是全域性的,所有使用者物件的控制代碼因此也是全域性的,各個程序通用,不像核心物件的控制代碼是侷限在各個程序的控制代碼表中。
下面的函式根據視窗控制代碼找到對應的視窗物件
WINDOW_OBJECT* FASTCALL UserGetWindowObject(HWND hWnd)
{
THREADINFO* ti;
WINDOW_OBJECT* Window;
if (PsGetCurrentProcess() != PsInitialSystemProcess)//若不是’system’程序
{
ti = GetW32ThreadInfo();
if (ti == NULL)
{
SetLastWin32Error(ERROR_ACCESS_DENIED);
return NULL;
}
}
//實質函式,gHandleTable指向全域性的GUI物件控制代碼表
Window = (WINDOW_OBJECT*)UserGetObject(gHandleTable, hWnd, otWindow);
if (!Window || 0 != (Window->state & WINDOWSTATUS_DESTROYED))
{
SetLastWin32Error(ERROR_INVALID_WINDOW_HANDLE);
return NULL;
}
return Window;
}
PVOID UserGetObject(PUSER_HANDLE_TABLE ht,//控制代碼表
HANDLE handle,//使用者物件的控制代碼
USER_OBJECT_TYPE type )//使用者物件型別
{
PUSER_HANDLE_ENTRY entry = handle_to_entry(ht, handle);//根據控制代碼值找到對應的控制代碼表項
if (entry == NULL || entry->type != type)//若找不到或者型別不匹配
{
SetLastWin32Error(ERROR_INVALID_HANDLE);
return NULL;
}
return entry->ptr;//返回對應的物件
}
PUSER_HANDLE_ENTRY handle_to_entry(PUSER_HANDLE_TABLE ht, HANDLE handle )
{
unsigned short generation;
int index = (((unsigned int)handle & 0xffff) - FIRST_USER_HANDLE) >> 1;
if (index < 0 || index >= ht->nb_handles)
return NULL;
if (!ht->handles[index].type)
return NULL;
generation = (unsigned int)handle >> 16;
if (generation == ht->handles[index].generation || !generation || generation == 0xffff)
return &ht->handles[index];
return NULL;
}
如上,根據handle找到對應的控制代碼表項,有點類似核心物件的控制代碼,可以把GUI物件的控制代碼也看作是一個簡單的陣列索引值。下面是具體的GUI物件控制代碼表項的結構
typedef struct _USER_HANDLE_ENTRY
{
void *ptr;//指向對應的GUI物件
union
{
PVOID pi;
PTHREADINFO pti; // 指向所屬執行緒
PPROCESSINFO ppi; // 指向所屬程序
};
unsigned char type;
unsigned char flags;
unsigned short generation;//不明
} USER_HANDLE_ENTRY, * PUSER_HANDLE_ENTRY;
下面是GUI物件的控制代碼表
typedef struct _USER_HANDLE_TABLE //控制代碼表描述符
{
PUSER_HANDLE_ENTRY handles;//句表的地址
PUSER_HANDLE_ENTRY freelist;//空閒表項鍊表
int nb_handles;//表的容量
int allocated_handles;//表中實際已分配的表項個數
} USER_HANDLE_TABLE, * PUSER_HANDLE_TABLE;
前面的函式會將訊息從佇列取下來後,將L附件包轉移到使用者空間。絕大多數訊息都不帶L附件包,有的訊息則帶有L附加包,核心中有一個全域性表給出了所有含有L附件包的訊息型別以及他們的L附件包大小的計算方法。如下:
static MSGMEMORY MsgMemory[] = //全域性表
{
{ WM_CREATE, MMS_SIZE_SPECIAL, MMS_FLAG_READWRITE },
{ WM_DDE_ACK, sizeof(KMDDELPARAM), MMS_FLAG_READ },
{ WM_DDE_EXECUTE, MMS_SIZE_WPARAM, MMS_FLAG_READ },
{ WM_GETMINMAXINFO, sizeof(MINMAXINFO), MMS_FLAG_READWRITE },
{ WM_GETTEXT, MMS_SIZE_WPARAMWCHAR, MMS_FLAG_WRITE },
{ WM_NCCALCSIZE, MMS_SIZE_SPECIAL, MMS_FLAG_READWRITE },
{ WM_NCCREATE, MMS_SIZE_SPECIAL, MMS_FLAG_READWRITE },
{ WM_SETTEXT, MMS_SIZE_LPARAMSZ, MMS_FLAG_READ },
{ WM_STYLECHANGED, sizeof(STYLESTRUCT), MMS_FLAG_READ },
{ WM_STYLECHANGING, sizeof(STYLESTRUCT), MMS_FLAG_READWRITE },
{ WM_COPYDATA, MMS_SIZE_SPECIAL, MMS_FLAG_READ },
{ WM_WINDOWPOSCHANGED, sizeof(WINDOWPOS), MMS_FLAG_READ },
{ WM_WINDOWPOSCHANGING, sizeof(WINDOWPOS), MMS_FLAG_READWRITE },
};
typedef struct tagMSGMEMORY
{
UINT Message;//訊息ID,即訊息型別
UINT Size;//該類訊息的L附件包大小或計算方法
INT Flags;//資料流動方向
}
MSGMEMORY, *PMSGMEMORY;
如上面:WM_GETTEXT這種訊息的L附件包大小計算方法是:蘊含在它的wparam引數中,wparam引數給定了L附件包的寬字元個數,MMS_FLAG_WRITE標誌則表示需要寫使用者空間,也即將L附件包回寫複製到使用者空間。
WM_SETTEXT這種訊息的L附件包大小計算方法是:蘊含在它的lparam引數,lparam引數是一個以0結尾的字串,MMS_FLAG_READ標誌則表示需要讀使用者空間,也即將使用者空間中的文字複製到核心空間中。
MMS_SIZE_SPECIAL則表示L附件包的大小特定於具體的訊息。
除開上面表中的那些訊息,其他訊息都不帶L附件包。
下面的函式用於查詢指定型別訊息的L附件包計演算法。
PMSGMEMORY FASTCALL
FindMsgMemory(UINT Msg)
{
PMSGMEMORY MsgMemoryEntry;
for (MsgMemoryEntry = MsgMemory;
MsgMemoryEntry < MsgMemory + sizeof(MsgMemory) / sizeof(MSGMEMORY);
MsgMemoryEntry++)
{
if (Msg == MsgMemoryEntry->Message)
return MsgMemoryEntry;
}
return NULL;
}
下面的函式根據計算的計演算法計算對應訊息的L附件包大小
UINT FASTCALL
MsgMemorySize(PMSGMEMORY MsgMemoryEntry, WPARAM wParam, LPARAM lParam)
{
CREATESTRUCTW *Cs;
PUNICODE_STRING WindowName;
PUNICODE_STRING ClassName;
UINT Size = 0;
_SEH2_TRY
{
if (MMS_SIZE_WPARAM == MsgMemoryEntry->Size)
Size = (UINT)wParam;
else if (MMS_SIZE_WPARAMWCHAR == MsgMemoryEntry->Size)
Size = (UINT) (wParam * sizeof(WCHAR));
else if (MMS_SIZE_LPARAMSZ == MsgMemoryEntry->Size)
Size = (UINT) ((wcslen((PWSTR) lParam) + 1) * sizeof(WCHAR));
else if (MMS_SIZE_SPECIAL == MsgMemoryEntry->Size)//具體訊息具體計算
{
switch(MsgMemoryEntry->Message)
{
…
case WM_COPYDATA:
Size = sizeof(COPYDATASTRUCT) + ((PCOPYDATASTRUCT)lParam)->cbData;
break;
…
}
}
else
Size = MsgMemoryEntry->Size;
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
Size = 0;
}
_SEH2_END;
return Size;
}
前面的NtUserGetNessage函式實際上呼叫co_IntPeekMessage函式從訊息佇列中取下訊息。
訊息佇列的結構如下:
typedef struct _USER_MESSAGE_QUEUE //執行緒的訊息佇列描述符
{
LONG References;//本佇列的引用計數
struct _ETHREAD *Thread;//本佇列的所屬執行緒
LIST_ENTRY SentMessagesListHead;//通過SendMessage方式發來的訊息進入這個佇列(send佇列)
LIST_ENTRY PostedMessagesListHead; //通過PostMessage方式發來的訊息進入這個佇列(post佇列)
LIST_ENTRY NotifyMessagesListHead;//專用於存放SendMessage回撥函式呼叫通知的訊息佇列
LIST_ENTRY HardwareMessagesListHead;//來自硬體裝置的訊息(指滑鼠訊息)佇列
KMUTEX HardwareLock;
PUSER_MESSAGE MouseMoveMsg;//當前的MouseMove訊息(所有MouseMove訊息合併成一個訊息)
BOOLEAN QuitPosted;//指佇列中是否收到了一個WM_QUIT訊息正處於Pengding中
ULONG QuitExitCode;//收到的WM_QUIT訊息的退出碼(wparam)
PKEVENT NewMessages;//一個標記佇列中是否含有WakeMask掩碼訊息的事件
HANDLE NewMessagesHandle;//上面事件物件的控制代碼
ULONG LastMsgRead;//上次發出PeekMessage請求的時間
HWND FocusWindow;//當前鍵盤焦點視窗
ULONG PaintCount;//阻塞中的WM_PAINT訊息計數
HWND ActiveWindow; //當前活動視窗
HWND CaptureWindow;//當前滑鼠焦點視窗(一般是沒有的,滑鼠訊息一般發給游標處所在視窗)
HWND MoveSize; /* Current move/size window for this queue */
HWND MenuOwner; /* Current menu owner window for this queue */
BYTE MenuState; /* Identifes the menu state */
PTHRDCARETINFO CaretInfo; /* Caret information for this queue */
PHOOKTABLE Hooks;//每個執行緒區域性的鉤子表(還有一個全域性的鉤子表)
WORD WakeMask;//喚醒訊息掩碼
WORD QueueBits;
WORD ChangedBits;
LPARAM ExtraInfo;
LIST_ENTRY DispatchingMessagesHead;//本執行緒已傳送出去的,但尚未被目標執行緒處理的訊息佇列
LIST_ENTRY LocalDispatchingMessagesHead;//本執行緒正在進行Dispatching處理的訊息佇列
struct _DESKTOP *Desktop;//本佇列所屬的Desktop
} USER_MESSAGE_QUEUE, *PUSER_MESSAGE_QUEUE;
訊息佇列按用途分類實際微觀上包含好幾種,只不過從巨集觀上我們理解每個執行緒一個訊息佇列。
微觀上:分為4個接收佇列,一個處理中佇列和一個已傳送等待處理完畢的佇列。
當執行緒收到訊息時,會先從接收佇列取下來,轉入處理中佇列,當處理完畢後,再退出處理中佇列。
當一個執行緒通過SendMesage傳送訊息時,發出的訊息會被放到DispatchingMessagesHead佇列,當被接收方處理後,從中移出。
訊息佇列中的每個訊息的結構定義如下:
typedef struct _USER_MESSAGE
{
LIST_ENTRY ListEntry;//用來掛入訊息佇列
BOOLEAN FreeLParam;//表示本訊息被取出後是否需要是否核心lparam
MSG Msg;//訊息主體
} USER_MESSAGE, *PUSER_MESSAGE;
PostMessage與SendMessage的區別。前者是非同步的,發生方直接把訊息傳送到接收方的訊息佇列中就返回,不管訊息有沒有被處理完成。而後者是同步的,要一直等到訊息被接收方處理後才會返回。
回到NtUserGetMessage,我們看到實際呼叫的正題函式是下面的這個函式
BOOL FASTCALL
co_IntPeekMessage( PUSER_MESSAGE Msg,//返回取下來的原生訊息
PWINDOW_OBJECT Window,//過濾條件
UINT MsgFilterMin,//過濾條件
UINT MsgFilterMax,//過濾條件
UINT RemoveMsg )//表示是否需要從佇列中移除
{
PTHREADINFO pti;
LARGE_INTEGER LargeTickCount;
PUSER_MESSAGE_QUEUE ThreadQueue;
PUSER_MESSAGE Message;
BOOL Present, RemoveMessages;
USER_REFERENCE_ENTRY Ref;
USHORT HitTest;
pti = PsGetCurrentThreadWin32Thread();
ThreadQueue = pti->MessageQueue;
RemoveMessages = RemoveMsg & PM_REMOVE;
CheckMessages:
HitTest = HTNOWHERE;
Present = FALSE;//表示是否找到了對應的訊息
KeQueryTickCount(&LargeTickCount);
ThreadQueue->LastMsgRead = LargeTickCount.u.LowPart;//記錄上次取訊息請求的時間
//在取訊息前,先處理完所有其他執行緒send過來的訊息(指SendMessage方式)
while (co_MsqDispatchOneSentMessage(ThreadQueue));
//檢測是否收到了WM_QUIT訊息,特殊處理
if (ThreadQueue->QuitPosted)
{
Msg->Msg.hwnd = NULL;
Msg->Msg.message = WM_QUIT;
Msg->Msg.wParam = ThreadQueue->QuitExitCode;
Msg->Msg.lParam = 0;
Msg->FreeLParam = FALSE;
if (RemoveMessages)
ThreadQueue->QuitPosted = FALSE;
goto MsgExit;
}
//先查詢post佇列中是否有對應的訊息(注意鍵盤訊息也是在post佇列中)
Present = co_MsqFindMessage( ThreadQueue,FALSE,RemoveMessages,
Window,MsgFilterMin,MsgFilterMax,&Message );
if (Present)
{
RtlCopyMemory(Msg, Message, sizeof(USER_MESSAGE));//取下來,返回給使用者
if (RemoveMessages)
MsqDestroyMessage(Message);
goto MessageFound;
}
//再檢查硬體訊息佇列中是否有滑鼠訊息
Present = co_MsqFindMessage( ThreadQueue,TRUE,RemoveMessages,
Window,MsgFilterMin,MsgFilterMax,&Message );
if (Present)
{
RtlCopyMemory(Msg, Message, sizeof(USER_MESSAGE)); //取下來,返回給使用者
if (RemoveMessages)
MsqDestroyMessage(Message);
goto MessageFound;
}
//上面的查詢過程會耗時一段時間,因此可能會又積累了send訊息,處理掉
while (co_MsqDispatchOneSentMessage(ThreadQueue));
//檢測是否有WM_PAINT訊息
if ( IntGetPaintMessage( Window,MsgFilterMin,MsgFilterMax,pti,&Msg->Msg,RemoveMessages))
{
Msg->FreeLParam = FALSE;
goto MsgExit;
}
if (PostTimerMessages(Window))//檢測是否有WM_TIMER訊息
goto CheckMessages;
if(Present)//如果在佇列中找到了訊息
{
MessageFound:
if(RemoveMessages)
{
PWINDOW_OBJECT MsgWindow = NULL;
if( Msg->Msg.hwnd && ( MsgWindow = UserGetWindowObject(Msg->Msg.hwnd) ) &&
Msg->Msg.message >= WM_MOUSEFIRST && Msg->Msg.message <= WM_MOUSELAST )
{
USHORT HitTest;
UserRefObjectCo(MsgWindow, &Ref);
if ( co_IntTranslateMouseMessage( ThreadQueue,&Msg->Msg,&HitTest,TRUE))
{
UserDerefObjectCo(MsgWindow);
goto CheckMessages;//丟棄該條訊息,重新查詢
}
if(ThreadQueue->CaptureWindow == NULL)
{
co_IntSendHitTestMessages(ThreadQueue, &Msg->Msg);
if ( ( Msg->Msg.message != WM_MOUSEMOVE && Msg->Msg.message != WM_NCMOUSEMOVE ) && IS_BTN_MESSAGE(Msg->Msg.message, DOWN) &&
co_IntActivateWindowMouse(ThreadQueue, &Msg->Msg, MsgWindow, &HitTest) )
{
UserDerefObjectCo(MsgWindow);
goto CheckMessages; //丟棄該條訊息,重新查詢
}
}
UserDerefObjectCo(MsgWindow);
}
else
co_IntSendHitTestMessages(ThreadQueue, &Msg->Msg);
goto MsgExit;
}
if ( ( Msg->Msg.hwnd && Msg->Msg.message >= WM_MOUSEFIRST &&
Msg->Msg.message <= WM_MOUSELAST ) &&
co_IntTranslateMouseMessage( ThreadQueue,&Msg->Msg,&HitTest,FALSE) )
{
goto CheckMessages; //丟棄該條訊息,重新查詢
}
MsgExit:
if ( ISITHOOKED(WH_MOUSE) && IS_MOUSE_MESSAGE(Msg->Msg.message))
{
if(!ProcessMouseMessage(&Msg->Msg, HitTest, RemoveMsg))
return FALSE;
}
if ( ISITHOOKED(WH_KEYBOARD) && IS_KBD_MESSAGE(Msg->Msg.message))
{
if(!ProcessKeyboardMessage(&Msg->Msg, RemoveMsg))
return FALSE;
}
//每當取出一個訊息後,都會呼叫WH_GETMESSAGE鉤子
if (ISITHOOKED(WH_GETMESSAGE))
{
co_HOOK_CallHooks( WH_GETMESSAGE, HC_ACTION, RemoveMsg & PM_REMOVE, (LPARAM)&Msg->Msg);
}
return TRUE;
}
return Present;
}
如上,這個函式會從post訊息佇列和硬體訊息佇列中查詢,取出一個符合指定條件的訊息出來。不過,在進行查詢前,會優先處理掉send佇列中其他執行緒發來的訊息。
查詢訊息的順序是:send佇列、post佇列、鍵盤滑鼠訊息、重繪訊息、定時器訊息【send、post、鍵鼠、重、定時】
注意send佇列中的訊息結構與post佇列、硬體佇列中的訊息結構不同,它的結構定義如下:
typedef struct _USER_SENT_MESSAGE
{
LIST_ENTRY ListEntry;//用來掛入接收方的Dispatching處理佇列
MSG Msg;//主體
PKEVENT CompletionEvent;//完成處理後,用於通知傳送方的事件
LRESULT* Result;//處理結果
struct _USER_MESSAGE_QUEUE* SenderQueue;//傳送方的訊息佇列(方便用)
SENDASYNCPROC CompletionCallback;//傳送方提供的訊息處理完成例程
ULONG_PTR CompletionCallbackContext;
LIST_ENTRY DispatchingListEntry;//用來掛入傳送方的Dispatching未完成佇列
INT HookMessage;//鉤子相關
BOOL HasPackedLParam;//表示L附件包是否打包到使用者空間了
} USER_SENT_MESSAGE, *PUSER_SENT_MESSAGE;
下面的函式用於處理send佇列中的一條訊息
BOOLEAN FASTCALL
co_MsqDispatchOneSentMessage(PUSER_MESSAGE_QUEUE MessageQueue)
{
PUSER_SENT_MESSAGE Message;
PLIST_ENTRY Entry;
LRESULT Result;
if (IsListEmpty(&MessageQueue->SentMessagesListHead))
return(FALSE);
Entry = RemoveHeadList(&MessageQueue->SentMessagesListHead);
Message = CONTAINING_RECORD(Entry, USER_SENT_MESSAGE, ListEntry);
//轉入接收方執行緒的正在Dispatching處理佇列
InsertTailList(&MessageQueue->LocalDispatchingMessagesHead,&Message->ListEntry);
//if 這個訊息本身就是‘請求執行鉤子過程’,那麼message表示HookId,hwnd表示code
//其他執行緒在呼叫底層鍵盤滑鼠鉤子時,會給鉤子的原建立者執行緒發一個訊息,請求執行鉤子過程
if (Message->HookMessage == MSQ_ISHOOK)
{
//檢查、呼叫指定型別的鉤子
Result = co_HOOK_CallHooks(Message->Msg.message,Message->Msg.hwnd,
Message->Msg.wParam,Message->Msg.lParam);
}
else if (Message->HookMessage == MSQ_ISEVENT)
{
Result = co_EVENT_CallEvents( Message->Msg.message,Message->Msg.hwnd,
Message->Msg.wParam,Message->Msg.lParam);
}
Else //最典型
{
//關鍵。將訊息派遣到目標視窗的視窗過程,處理訊息,返回處理結果
Result = co_IntSendMessage(Message->Msg.hwnd,Message->Msg.message,
Message->Msg.wParam,Message->Msg.lParam);
}
RemoveEntryList(&Message->ListEntry);//處理完後從接收方的Dispatching佇列移除
if (!(Message->HookMessage & MSQ_SENTNOWAIT))
{
if (Message->DispatchingListEntry.Flink != NULL)
RemoveEntryList(&Message->DispatchingListEntry);//從傳送方的Dispatching佇列移除
}
if (Message->Result != NULL)
*Message->Result = Result;//返回處理結果
if (Message->HasPackedLParam == TRUE && Message->Msg.lParam)
ExFreePool((PVOID)Message->Msg.lParam);
if (Message->CompletionEvent != NULL)//喚醒傳送方
KeSetEvent(Message->CompletionEvent, IO_NO_INCREMENT, FALSE);
//傳送方可以呼叫SendMessageCallback這個API設定一個完成回撥函式
if (Message->CompletionCallback != NULL)
{
//給傳送方傳送一個通知訊息,通知它呼叫這個回撥函式
//為什麼不直接執行回撥函式?因為回撥函式是傳送方提供的,是在傳送方程序地址空間中
co_IntCallSentMessageCallback(Message->CompletionCallback,Message->Msg.hwnd,
Message->Msg.message,Message->CompletionCallbackContext,
Result);
}
if (!(Message->HookMessage & MSQ_SENTNOWAIT))
{
IntDereferenceMessageQueue(Message->SenderQueue);
IntDereferenceMessageQueue(MessageQueue);
}
ExFreePoolWithTag(Message, TAG_USRMSG);
return(TRUE);
}
看看訊息是怎麼派遣到目標視窗的視窗過程的
LRESULT FASTCALL
co_IntSendMessage( HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam )
{
ULONG_PTR Result = 0;
if(co_IntSendMessageTimeout(hWnd, Msg, wParam, lParam, SMTO_NORMAL, 0, &Result))//不超時
return (LRESULT)Result;
return 0;
}
LRESULT FASTCALL
co_IntSendMessageTimeout( HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam,UINT uFlags,
UINT uTimeout, //傳送方可以呼叫SendMessageTimeout規定一個超時值
ULONG_PTR *uResult )
{
PWINDOW_OBJECT DesktopWindow;
HWND *Children;
HWND *Child;
if (hWnd!=HWND_BROADCAST)//if不是廣播訊息
{
return co_IntSendMessageTimeoutSingle(hWnd, Msg, wParam, lParam, uFlags, uTimeout,uResult);
}
//若是廣播訊息,則廣播發送到桌面所有頂層視窗
DesktopWindow = UserGetWindowObject(IntGetDesktopWindow());
Children = IntWinListChildren(DesktopWindow);
for (Child = Children; NULL != *Child; Child++)//遍歷桌面視窗的所有子視窗
co_IntSendMessageTimeoutSingle(*Child, Msg, wParam, lParam, uFlags, uTimeout, uResult);
ExFreePool(Children);
return (LRESULT) TRUE;
}
最終的實質函式如下:
LRESULT FASTCALL
co_IntSendMessageTimeoutSingle( HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam,
UINT uFlags,UINT uTimeout,ULONG_PTR *uResult )
{
PWINDOW_OBJECT Window = NULL;
if (!(Window = UserGetWindowObject(hWnd)))//判斷控制代碼是否有效
RETURN( FALSE);
UserRefObjectCo(Window, &Ref);
Win32Thread = PsGetCurrentThreadWin32Thread();
IntCallWndProc( Window, hWnd, Msg, wParam, lParam);//呼叫前,先呼叫WH_CALLWNDPROC鉤子
//if 目標視窗就是當前執行緒中的視窗(一般都這樣)
if ( NULL != Win32Thread && Window->pti->MessageQueue == Win32Thread->MessageQueue)
{
if (Win32Thread->TIF_flags & TIF_INCLEANUP)//執行緒正在終止
RETURN( FALSE);
MsgMemoryEntry = FindMsgMemory(Msg);//查詢這種訊息的L附件包大小計演算法
if (NULL == MsgMemoryEntry)
lParamBufferSize = -1;
else
lParamBufferSize = MsgMemorySize(MsgMemoryEntry, wParam, lParam);//計算L附件包大小
PackParam(&lParamPacked, Msg, wParam, lParam, FALSE);//L附件包打包到使用者空間
Result = (ULONG_PTR)co_IntCallWindowProc( Window->Wnd->lpfnWndProc,
!Window->Wnd->Unicode,hWnd,Msg,wParam,
lParamPacked,lParamBufferSize );
if(uResult)
*uResult = Result;//返回處理結果
//呼叫完後,呼叫WH_CALLWNDPROCRET鉤子
IntCallWndProcRet( Window, hWnd, Msg, wParam, lParam, (LRESULT *)uResult);
UnpackParam(lParamPacked, Msg, wParam, lParam, FALSE);
RETURN( TRUE);
}
//不是當前執行緒中的視窗這種情況很少見,略
CLEANUP:
if (Window) UserDerefObjectCo(Window);
END_CLEANUP;
}
下面的函式用於在硬體訊息佇列或post訊息佇列中查詢訊息
BOOLEAN APIENTRY
co_MsqFindMessage(IN PUSER_MESSAGE_QUEUE MessageQueue,
IN BOOLEAN Hardware,//指是在硬體訊息佇列還是post訊息佇列中查詢
IN BOOLEAN Remove,//是否移除佇列
IN PWINDOW_OBJECT Window,//過濾條件
IN UINT MsgFilterLow,//過濾條件
IN UINT MsgFilterHigh,//過濾條件
OUT PUSER_MESSAGE* Message)//返回找到的訊息
{
PLIST_ENTRY CurrentEntry;
PUSER_MESSAGE CurrentMessage;
PLIST_ENTRY ListHead;
if (Hardware)//硬體訊息(滑鼠訊息)需要特殊處理
{
return(co_MsqPeekHardwareMessage( MessageQueue,Window,MsgFilterLow,MsgFilterHigh,
Remove,Message));
}
CurrentEntry = MessageQueue->PostedMessagesListHead.Flink;
ListHead = &MessageQueue->PostedMessagesListHead;
while (CurrentEntry != ListHead)
{
CurrentMessage = CONTAINING_RECORD(CurrentEntry, USER_MESSAGE,ListEntry);
if ( ( !Window ||PtrToInt(Window) == 1 || Window->hSelf == CurrentMessage->Msg.hwnd ) &&
( (MsgFilterLow == 0 && MsgFilterHigh == 0) ||
( MsgFilterLow <= CurrentMessage->Msg.message &&
MsgFilterHigh >= CurrentMessage->Msg.message ) ) )
{
if (Remove)
RemoveEntryList(&CurrentMessage->ListEntry);
*Message = CurrentMessage;
return(TRUE);
}
CurrentEntry = CurrentEntry->Flink;
}
return(FALSE);
}
上面的程式碼我想不用解釋了。
由此可以看到:在GetMessage、PeekMessage函式內部,會一直等待到訊息佇列中出現符合指定條件的訊息,這就是為什麼GUI執行緒絕大多數時刻都處於睡眠狀態的原因,因為它在等待訊息。
另外,在GetMessage、PeekMessage內部,即使因為佇列中一直沒有符合指定條件的訊息而等待,也為
在內部不斷的處理別的執行緒通過SendMessage方式發來的訊息,以儘快完成這種訊息的處理。
Windows訊息的產生來源(也即傳送方的型別)
1、 一個執行緒