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