網(wǎng)絡安全編程:Windows消息機制實例
SendMessage()將指定的消息發(fā)送給指定的窗口,窗口接收到消息也有相應的行為發(fā)生。那么窗口接收到消息后的一系列行為是如何發(fā)生的?下面通過熟悉Windows的消息機制來理解消息處理背后的秘密。
01 DOS程序與Windows程序執(zhí)行流程對比
Windows下的窗口應用程序都是基于消息機制的,操作系統(tǒng)與應用程序之間、應用程序與應用程序之間,大部分都是通過消息機制進行通信、交互的。要真正掌握Windows應用程序內(nèi)部對消息的處理,必須分析實際的源代碼。在編寫一個基于消息的Windows應用程序前,先來比較DOS程序和Windows程序在執(zhí)行時的流程。
1. DOS程序執(zhí)行流程
在DOS下將編寫完的程序進行執(zhí)行,在執(zhí)行時有較為清晰的流程。比如用C語言編寫程序后,程序執(zhí)行時的大致流程如圖1所示。
圖1 傳統(tǒng)DOS程序執(zhí)行流程
在圖1中可以看出,DOS程序的流程是按照代碼的順序(這里的順序并不是指程序控制結構中的順序、分支和循環(huán)的意思,而是指程序運行的邏輯有明顯的流程)和流程依次執(zhí)行。大致步驟為:DOS程序從main()主函數(shù)開始執(zhí)行(其實程序真正的入口并不是main()函數(shù));執(zhí)行的過程中按照代碼編寫流程依次調(diào)用各個子程序;在執(zhí)行的過程中會等待用戶的輸入等操作;當各個子程序執(zhí)行完成后,最終會返回main()主函數(shù),執(zhí)行main()主函數(shù)的return語句后,程序退出(其實程序真正的出口也并不是main()函數(shù)的return語句)。
2. Windows程序執(zhí)行流程
DOS程序的執(zhí)行流程比較簡單,但是Windows應用程序的執(zhí)行流程就比較復雜了。DOS是單任務的操作系統(tǒng)。在DOS中,通過輸入命令,DOS操作系統(tǒng)會將控制權由Command.com轉交給DOS程序從而執(zhí)行。而Windows是多任務的操作系統(tǒng),在Windows下同時會運行若干個應用程序,那么Windows就無法把控制權完全交給一個應用程序。Windows下的應用程序是如何工作的?首先看一下Windows應用程序內(nèi)部的大致結構圖,如圖2所示。
圖2 Windows應用程序執(zhí)行原理圖
圖2可能看起來比較復雜,其實Windows應用程序的內(nèi)部結構比該示意圖更復雜。在實際開發(fā)Windows應用程序時,需要關注的部分主要是“主程序”和“窗口過程”兩部分。但是從圖2來看,主程序和窗口過程沒有直接的調(diào)用關系,而在主程序和窗口過程之間有一個“系統(tǒng)程序模塊”。“主程序”的功能是用來注冊窗口類、獲取消息和分發(fā)消息。而“窗口過程”中定義了需要處理的消息,“窗口過程”會根據(jù)不同的消息執(zhí)行不同的動作,而不需要程序處理的消息則會交給默認的系統(tǒng)過程進行處理。
在“主程序”中,RegisterClassEx()函數(shù)會注冊一個窗口類,窗口類中的字段中包含了“窗口過程”的地址信息,也就是把“窗口類”的信息(包括“窗口過程的地址信息”)告訴操作系統(tǒng)。然后“主程序”不斷通過調(diào)用GetMessage()函數(shù)獲取消息,再交由DispatchMessge()函數(shù)來分發(fā)消息。消息分發(fā)后并沒有直接調(diào)用“窗口過程”讓其處理消息,而是由系統(tǒng)模塊查找該窗口指定的窗口類,通過窗口類再找到窗口過程的地址,最后將消息送給該窗口過程,由窗口過程處理消息。
02 一個簡單的Windows應用程序
相對一個簡單的DOS程序來說一個簡單的Windows應用程序要很長。下面的例子中只實現(xiàn)了一個特別簡單的Windows程序,這個程序在桌面上顯示一個簡單的窗口,它沒有菜單欄、工具欄、狀態(tài)欄,只是在窗口中輸出一段簡單的字符串。雖然程序如此簡單,但是也要編寫100行左右的代碼。考慮到初學的朋友,這里將一部分一部分地逐步介紹代碼中的細節(jié),以減少代碼的長度,從而方便初學者的學習。
1. Windows窗口應用程序的主函數(shù)——WinMain()
在DOS時代,或編寫Windows下的命令行的程序,要使用C語言編寫代碼的時候都是從main()函數(shù)開始的。而在Windows下編寫有窗口的程序時,要用C語言編寫窗口程序就不再從main()函數(shù)開始了,取而代之的是WinMain()函數(shù)。
既然Windows應用程序的主函數(shù)是WinMain(),那么就從了解WinMain()函數(shù)的定義開始學習Windows應用程序的開發(fā)。WinMain()函數(shù)的定義如下:
- int WINAPI WinMain(
- HINSTANCE hInstance,
- HINSTANCE hPrevInstance,
- LPSTR lpCmdLine,
- int nCmdShow
- );
該函數(shù)的定義取自MSDN中,在看到WinMain()函數(shù)的定義后,很直觀地會發(fā)現(xiàn)WinMain函數(shù)的參數(shù)比main()函數(shù)的參數(shù)變多了。從參數(shù)個數(shù)上來說,WinMain()函數(shù)接收的信息更多了。下面來看每個參數(shù)的含義。
hInstance是應用程序的實例句柄。保存在磁盤上的程序文件是靜態(tài)的,當被加載到內(nèi)存中時,被分配了CPU、內(nèi)存等進程所需的資源后,一個靜態(tài)的程序就被實例化為一個有各種執(zhí)行資源的進程了。句柄的概念隨上下文的不同而不同,句柄是操作某個資源的“把手”。當需要對某個實例化進程操作時,需要借助該實例句柄進行操作。這里的實例句柄是程序裝入內(nèi)存后的起始地址。實例句柄的值也可以通過GetModuleHandle()參數(shù)來獲得(注意系統(tǒng)中沒有GetInstanceHandle()函數(shù),不要誤以為是hInstance就會有GetInstance×××()類的函數(shù))。
句柄這個詞在開發(fā)Windows程序時是非常常見的一個詞。“句柄”一詞的含義隨上下文的不同而所有改變。比如,磁盤上的程序文件被加載到內(nèi)存中后,就創(chuàng)建了一個實例句柄,這個實例句柄是程序裝入內(nèi)存后的“起始地址”,或者說是“模塊的起始地址”。
拿SendMessage()函數(shù)舉例來說,句柄相當于一個操作的面板,對句柄發(fā)送的消息相當于面板上的各個開關按鍵,消息的附加數(shù)據(jù),相當于給開關按鍵送的各種參數(shù),這些參數(shù)根據(jù)按鍵的不同而不同。
hPrevInstance是同一個文件創(chuàng)建的上一個實例的實例句柄。這個參數(shù)是Win16平臺下的遺留物,在Win32下已經(jīng)不再使用了。
lpCmdLine是主函數(shù)的參數(shù),用于在程序啟動時給進程傳遞參數(shù)。比如在“開始”菜單的“運行”中輸入“notepad c:\boot.ini”,這樣就通過記事本打開了C盤下的boot.ini文件。C:\Boot.ini文件是通過WinMain()函數(shù)的lpCmdLine參數(shù)傳遞給notepad.exe程序的。
nCmdShow是進程顯示的方式,可以是最大化顯示、最小化顯示,或者是隱藏等顯示方式(如果是啟動木馬程序的話,啟動方式當然要由自己進行控制)。
主函數(shù)的參數(shù)都介紹完了。編寫Windows的窗口程序,需要主函數(shù)中應該完成哪些操作是下面要討論的內(nèi)容。
2. WinMain()函數(shù)中的流程
編寫Windows下的窗口程序,在WinMain()主函數(shù)中主要完成的任務是注冊一個窗口類,創(chuàng)建一個窗口并顯示創(chuàng)建的窗口,然后不停地獲取屬于自己的消息并分發(fā)給自己的窗口過程,直到收到WM_QUIT消息后退出消息循環(huán)結束進程。這是主函數(shù)中程序的執(zhí)行脈絡,程序中將注冊窗口類、創(chuàng)建窗口的操作封裝為自定義函數(shù)。
代碼如下:
- int WINAPI WinMain(
- HINSTANCE hInstance,
- HINSTANCE hPrevInstance,
- LPSTR lpCmdLine,
- int nCmdShow)
- {
- MSG Msg;
- BOOL bRet;
- // 注冊窗口類
- MyRegisterClass(hInstance);
- // 創(chuàng)建窗口并顯示窗口
- if ( !InitInstance(hInstance, SW_SHOWNORMAL) )
- {
- return FALSE;
- }
- // 消息循環(huán)
- // 獲取屬于自己的消息并進行分發(fā)
- while( (bRet = GetMessage(&Msg, NULL, 0, 0)) != 0 )
- {
- if ( bRet == -1 )
- {
- // handle the error and possibly exit
- break;
- }
- else
- {
- TranslateMessage(&Msg);
- DispatchMessage(&Msg);
- }
- }
- return Msg.wParam;
- }
在代碼中,MyRegisterClass()和InitInstance()是兩個自定義的函數(shù),分別用來注冊窗口類,創(chuàng)建窗口并顯示更新創(chuàng)建的窗口。后面的消息循環(huán)部分用來獲得消息并進行消息分發(fā)。它的流程如圖2所示的“主程序”部分。
代碼中主要是3個函數(shù),分別是GetMessage()、TranslateMessage()和DispatchMessage()。這3個函數(shù)是Windows提供的API函數(shù)。GetMessage()的定義如下:
- BOOL GetMessage(
- LPMSG lpMsg,
- HWND hWnd,
- UINT wMsgFilterMin,
- UINT wMsgFilterMax
- );
該函數(shù)用來獲取屬于自己的消息,并填充MSG結構體。有一個類似于GetMessage()的函數(shù)是PeekMessage(),它可以判斷消息隊列中是否有消息,如果沒有消息,可以主動讓出CPU時間給其他進程。關于PeekMessage()函數(shù)的使用,請參考MSDN:
- BOOL TranslateMessage(CONST MSG *lpMsg);
該函數(shù)是用來處理鍵盤消息的。它將虛擬碼消息轉換為字符消息,也就是將WM_KEYDOWN消息和WM_KEYUP消息轉換為WM_CHAR消息,將WM_SYSKEYDOWN消息和WM_SYSKEYUP消息轉換為WM_SYSCHAR消息:
- LRESULT DispatchMessage(CONST MSG *lpmsg);
該函數(shù)是將消息分發(fā)到窗口過程中。
3. 注冊窗口類的自定義函數(shù)
在WinMain()函數(shù)中,首先調(diào)用了MyRegisterClass()這個自定義函數(shù),需要傳遞進程的實例句柄hInstance作為參數(shù)。該函數(shù)完成窗口類的注冊,分為兩步:第一步是填充WNDCLASSEX結構體,第二步是調(diào)用RegisterClassEx()函數(shù)進行注冊。該函數(shù)相對簡單,但是,該函數(shù)中稍微復雜的是WNDCLASSEX結構體的成員較多。
代碼如下:
- ATOM MyRegisterClass(HINSTANCE hInstance)
- {
- WNDCLASSEX WndCls;
- // 填充結構體為 0
- ZeroMemory(&WndCls, sizeof(WNDCLASSEX));
- // cbSize 是結構體大小
- WndCls.cbSize = sizeof(WNDCLASSEX);
- // lpfnWndProc 是窗口過程地址
- WndCls.lpfnWndProc = WindowProc;
- // hInstance 是實例句柄
- WndCls.hInstance = hInstance;
- // lpszClassName 是窗口類類名
- WndCls.lpszClassName = CLASSNAME;
- // style 是窗口類風格
- WndCls.style = CS_HREDRAW | CS_VREDRAW;
- // hbrBackground 是窗口類背景色
- WndCls.hbrBackground = (HBRUSH)COLOR_WINDOWFRAME + 1;
- // hCursor 是鼠標句柄
- WndCls.hCursor = LoadCursor(NULL, IDC_ARROW);
- // hIcon 是圖標句柄
- WndCls.hIcon = LoadIcon(NULL, IDI_QUESTION);
- // 其他
- WndCls.cbClsExtra = 0;
- WndCls.cbWndExtra = 0;
- return RegisterClassEx(&WndCls);
- }
在代碼中,WNDCLASSEX結構體的成員都介紹了。WNDCLASSEX中最重要的字段是lpfnWndProc,它將保存的是窗口過程的地址。窗口過程是對各種消息進程處理的“匯集地”,也是編寫Windows應用程序的重點部分。代碼中的函數(shù)都比較簡單,主要涉及LoadCursor()、LoadIcon()和RegisterClassEx()這3個函數(shù)。由于這3個函數(shù)使用簡單,通過代碼就可以進行理解,這里不做過多介紹。
注冊窗口類(提到窗口類,你是否想到了FindWindow()函數(shù)的第一個參數(shù)呢?)的重點是在后面的代碼中可以根據(jù)該窗口類創(chuàng)建該種類型的窗口。代碼中,在定義窗口類時指定了背景色、鼠標指針、窗口圖標等,那么使用該窗口類創(chuàng)建的窗口都具有相同的窗口類型。
4. 創(chuàng)建主窗口并顯示更新
注冊窗口類后,根據(jù)該窗口類創(chuàng)建具體的主窗口并顯示和更新窗口。
代碼如下:
- BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
- {
- HWND hWnd = NULL;
- // 創(chuàng)建窗口
- hWnd = CreateWindowEx(WS_EX_CLIENTEDGE,
- CLASSNAME,
- "MyFirstWindow",
- WS_OVERLAPPEDWINDOW,
- CW_USEDEFAULT, CW_USEDEFAULT,
- CW_USEDEFAULT, CW_USEDEFAULT,
- NULL, NULL, hInstance, NULL);
- if ( NULL == hWnd )
- {
- return FALSE;
- }
- // 顯示窗口
- ShowWindow(hWnd, nCmdShow);
- // 更新窗口
- UpdateWindow(hWnd);
- return TRUE;
- }
在調(diào)用該函數(shù)時,需要給該函數(shù)傳遞實例句柄和窗口顯示方式兩個參數(shù)。這兩個參數(shù)的第1個參數(shù)通過WinMain()函數(shù)的參數(shù)hInstance指定,第2個參數(shù)可以通過WinMain()函數(shù)的第3個參數(shù)指定,也可以進行自定義指定。程序中的調(diào)用代碼如下:
- InitInstance(hInstance, SW_SHOWNORMAL);
在創(chuàng)建主窗口時調(diào)用了CreateWindowEx()函數(shù),先來看看它的函數(shù)原型:
- HWND CreateWindowEx(
- DWORD dwExStyle,
- LPCTSTR lpClassName,
- LPCTSTR lpWindowName,
- DWORD dwStyle,
- int x,
- int y,
- int nWidth,
- int nHeight,
- HWND hWndParent,
- HMENU hMenu,
- HINSTANCE hInstance,
- LPVOID lpParam
- );
CreateWindowEx()中的第2個參數(shù)是lpClassName,由注釋可以知道是已經(jīng)注冊的類名。這個已經(jīng)注冊的類名就是WNDCLASSEX結構體的lpszClassName字段。
5. 處理消息的窗口過程
按照如圖2所示的流程,WinMain()主函數(shù)的部分已經(jīng)都實現(xiàn)完成了。接下來看程序中關鍵的部分——窗口過程。從WinMain()主函數(shù)中看出,在WinMain()主函數(shù)中沒有任何地方直接調(diào)用窗口過程,只是在注冊窗口類時指定了窗口過程的地址。那么窗口類是由誰進行調(diào)用的呢?答案是由操作系統(tǒng)進行調(diào)用的。原因有二,首先窗口過程的地址是由系統(tǒng)維護的,注冊窗口類時是將“窗口過程的地址”向操作系統(tǒng)進行注冊。其次是除了應用程序本身會調(diào)用自己的窗口過程外,其他應用程序也會調(diào)用自己的窗口過程,比如前面的例子中調(diào)用SendMessage()函數(shù)發(fā)送消息后,需要系統(tǒng)調(diào)用目標程序的窗口過程來完成相應的動作。如果窗口過程由自己調(diào)用,那么窗口就要自己維護窗口類的信息,進程間消息的通信會非常繁瑣,也會無形中增加系統(tǒng)的開銷。
窗口過程的代碼如下:
- LRESULT CALLBACK WindowProc(
- HWND hwnd,
- UINT uMsg,
- WPARAM wParam,
- LPARAM lParam)
- {
- PAINTSTRUCT ps;
- HDC hDC;
- RECT rt;
- char *pszDrawText = "Hello Windows Program.";
- switch (uMsg)
- {
- case WM_PAINT:
- {
- hDC = BeginPaint(hwnd, &ps);
- GetClientRect(hwnd, &rt);
- DrawTextA(hDC,
- pszDrawText, strlen(pszDrawText),&rt,
- DT_CENTER | DT_VCENTER | DT_SINGLELINE);
- EndPaint(hwnd, &ps);
- break;
- }
- case WM_CLOSE:
- {
- if ( IDYES == MessageBox(hwnd,
- "是否退出程序", "MyFirstWin", MB_YESNO) )
- {
- DestroyWindow(hwnd);
- PostQuitMessage(0);
- }
- break;
- }
- default:
- {
- return DefWindowProc(hwnd, uMsg, wParam, lParam);
- }
- }
- return 0;
- }
在WinMain()函數(shù)中,通過調(diào)用RegisterClassEx()函數(shù)進行了窗口類的注冊,通過調(diào)用CreateWindowEx()函數(shù)創(chuàng)建了窗口,并且GetMessage()函數(shù)不停地獲取消息,但是在主函數(shù)中沒有對被創(chuàng)建的窗口做任何處理。那是因為真正對窗口行為的處理全部放在了窗口過程中。當WinMain()函數(shù)中的消息循環(huán)得到消息以后,通過調(diào)用DispatchMessage()函數(shù)將消息派發(fā)(實際不是由DispatchMessage()函數(shù)直接派發(fā))給了窗口過程,從而由窗口過程對消息進行處理。
窗口過程的定義是按照MSDN上給出的形式進行定義的,MSDN上的定義形式如下:
- LRESULT CALLBACK WindowProc(
- HWND hwnd,
- UINT uMsg,
- WPARAM wParam,
- LPARAM lParam
- );
WindowProc是窗口過程的函數(shù)名,這個函數(shù)名可以隨意改變,但是該窗口過程的函數(shù)名必須與WNDCLASSEX結構體中l(wèi)pfnWndProc的成員變量的值一致。函數(shù)的第1個參數(shù)hwnd是窗口的句柄,第2個參數(shù)uMsg是消息值,第3個和第4個參數(shù)是對于消息值的附加參數(shù)。這4個參數(shù)的類型與SendMessage()函數(shù)的參數(shù)相對應。
上面WindowProc()窗口過程中只對兩個消息進行了處理,分別是WM_PAINT和WM_CLOSE。這里為了演示因此只簡單處理了兩個消息。Windows中有上千種消息,那么多的消息不可能全部都由程序員自己去處理,程序員只處理一些程序中需要的消息,其余的消息就交給了DefWindowProc()函數(shù)進行處理。DefWindowProc()函數(shù)實際上是將消息傳遞給了操作系統(tǒng),由操作系統(tǒng)來處理程序中沒有處理的消息。比如,在調(diào)用CreateWindow()函數(shù)時,系統(tǒng)會發(fā)送消息WM_CREATE給窗口過程,但是這個消息可能對程序的功能并不需要進行特殊的處理,因此直接交由DefWindowProc()函數(shù)讓系統(tǒng)進行處理。
DefWindowProc()函數(shù)的定義如下:
- LRESULT DefWindowProc(
- HWND hWnd,
- UINT Msg,
- WPARAM wParam,
- LPARAM lParam
- );
該函數(shù)的4個參數(shù)跟窗口過程的參數(shù)相同,只要將窗口過程的參數(shù)依次傳遞給DefWindowProc()函數(shù)就可以完成該函數(shù)的調(diào)用。在switch分支結構中的default位置直接調(diào)用DefWindowProc()函數(shù)就可以了。
WM_CLOSE消息是關閉窗口時發(fā)出的消息,在這個消息中需要調(diào)用DestoryWindow()函數(shù)來銷毀窗口,并且調(diào)用PostQuitMessage()來退出消息循環(huán),使程序退出。對于WM_PAINT消息,這里不進行介紹,涉及的幾個API函數(shù)可以參考MSDN進行了解。
有的資料在介紹消息循環(huán)時會給出一個建議,就是把需要經(jīng)常處理的消息放到程序靠上的位置,而將不經(jīng)常處理的消息放到程序靠下的位置,從而提高程序的效率。其實,在窗口過程中往往會使用switch結構對消息進行判斷(如果使用if和else結構進行消息的判斷,那么常用的消息是要放到前面),而switch結構在編譯器進行編譯后會進行優(yōu)化處理,從而大大提高程序的運行效率。




















