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