寫在前面: 最近在研究一個(gè)VC界面庫(kù)DuiLib,在細(xì)讀它的源碼時(shí)遇到些問題,比如它的界面是如何繪制上去的,底層操作是如何實(shí)現(xiàn)的,就是CreateWindow和 ShowWindow又是如何實(shí)現(xiàn)的, 也不得而知, 因此我想有必要重新認(rèn)識(shí)下Win32 應(yīng)用程序的啟動(dòng)/運(yùn)行原理才好。 如題所述, 本文講的的Windows環(huán)境下exe可執(zhí)行文件的運(yùn)行原理, 這里 面涉及的知識(shí)很多,需要讀者對(duì)Windows操作系統(tǒng)(如注冊(cè)表、進(jìn)程、線程、 內(nèi)存管理、PE文件格式等) 、Windows編程(本文使用c++ 語(yǔ)言)等有所了解。 本文試圖以通俗易懂的語(yǔ)言描述, 讓更多的人看的懂, 從運(yùn)行原理上對(duì)程序的運(yùn) 行有個(gè)好的了解。 文章安排方面, 我這里是以大家都懂的main/WinMain函數(shù)執(zhí)行前, 執(zhí)行時(shí), 執(zhí)行后分為三個(gè)部分:exe程序的初始化;主函數(shù)的運(yùn)行過程;程序收尾工作 PS:本人的技術(shù)也是有限的,文章中難免會(huì)有錯(cuò)誤疏漏之處,還請(qǐng)各位高手批 評(píng)指正。轉(zhuǎn)載請(qǐng)注明出處,謝謝 本文使用的例子444.exe程序下載地址: 下載地址1: http://183.60.157.31/file/D4/53/tzydH05zcHTmkwTlAE44iue6 a61fc1a1fb190d2b250dc7cd lrmhi8bwxl49201cxedj&dir=/&name=xunlei.zip 下載地址2: http:///file/clg0o6il# 一、exe程序的初始化 打開一個(gè)軟件是如此簡(jiǎn)單, 雙擊軟件的圖標(biāo)就是了! 但你是否想過, 當(dāng)你雙 擊那個(gè)圖標(biāo)時(shí),系統(tǒng)都做了哪些工作?為什么雙擊圖標(biāo)軟件就運(yùn)行起來了? 沒錯(cuò), 就是Shell (以Explorer.exe進(jìn)程實(shí)現(xiàn)) 。 當(dāng)你啟動(dòng)電腦進(jìn)入桌面時(shí), 系統(tǒng)創(chuàng)建Explorer.exe進(jìn)程, 而其它的進(jìn)程, 可以說都是Explorer.exe的子進(jìn) 程, 因?yàn)樗鼈兌际怯蒃xplorer.exe進(jìn)程創(chuàng)建的。 也就是說, 當(dāng)你雙擊圖標(biāo)時(shí)Shell 會(huì)偵測(cè)到這個(gè)動(dòng)作, 注冊(cè)表中有相關(guān)的項(xiàng)保存著雙擊操作的信息, 如exe文件關(guān) 聯(lián)、啟動(dòng)exe的Shell是哪個(gè)。 注冊(cè)表中保存的exe文件關(guān)聯(lián)信息 打開一個(gè)exe時(shí)指定的參數(shù)信息 指定啟動(dòng)exe程序的Shell 我們看到, 啟動(dòng)exe文件指定的Shell就是Explorer.exe啦。 因此, 我們應(yīng)該 知道了,雙擊exe文件圖標(biāo)時(shí)Explorer.exe進(jìn)程的一個(gè)線程會(huì)偵測(cè)到這個(gè)操作, 它根據(jù)注冊(cè)表中的信息取得文件名(根據(jù)%1這個(gè)參數(shù)) ,然后Explorer.exe以這 個(gè)文件名調(diào)用CreateProcess函數(shù),這個(gè)函數(shù)做了很多工作。 CreateProcess函數(shù)的定義是這樣的: BOOLWINAPICreateProcess( __in_optLPCTSTRlpApplicationN __inout_optLPTSTRlpCommandLine, __in_optLPSECURITY_ATTRIBUTESlpProcessAttri __in_optLPSECURITY_ATTRIBUTESlpThreadAttrib __inBOOLbInheritHandles, __inDWORDdwCreationFlags, __in_optLPVOIDlpEnvironment, __in_optLPCTSTRlpCurrentDirec __inLPSTARTUPINFOlpStartup __outLPPROCESS_INFORMATIONlpProcessInfo ); 此函數(shù)的具體信息和用法在MSDN上已有詳細(xì)的描述,在此就不作介紹了 http://msdn.microsoft.com/en-us/library/ms682425(v=VS.85).aspx 那么CreateProcess函數(shù)內(nèi)部都做了哪些工作呢?別急,馬上為你一一道來! 創(chuàng)建進(jìn)程內(nèi)核對(duì)象 CreateProcess實(shí)際上是通過NtCreateProcess函數(shù)實(shí)現(xiàn)的, 此時(shí), 系統(tǒng)會(huì)創(chuàng)建 一個(gè)被稱為內(nèi)核對(duì)象的對(duì)象, 這里是進(jìn)程內(nèi)核對(duì)象。 進(jìn)程內(nèi)核對(duì)象可以看作一個(gè) 操作系統(tǒng)用來管理進(jìn)程的內(nèi)核對(duì)象, 它也是系統(tǒng)用來存放關(guān)于進(jìn)程統(tǒng)計(jì)信息的地 方(一個(gè)小的數(shù)據(jù)結(jié)構(gòu)) ,進(jìn)程內(nèi)核對(duì)象維護(hù)了一個(gè)句柄表的結(jié)構(gòu)。 當(dāng)進(jìn)程被初始化之后, 其句柄表是空的。 當(dāng)進(jìn)程內(nèi)的一線程通過指定的函數(shù) 創(chuàng)建了一個(gè)內(nèi)核對(duì)象時(shí),內(nèi)核會(huì)為對(duì)象分配一塊內(nèi)存區(qū)域并初始化這塊區(qū)域。 然 后內(nèi)核會(huì)在進(jìn)程的句柄表中查找一個(gè)空的入口, 找到之后會(huì)初始化句柄表的以索 引定位的區(qū)域。 初始化的主要過程就是填充句柄表的一個(gè)單元, 包括指定內(nèi)核對(duì) 象地址,指定訪問碼,指定標(biāo)記等。 關(guān)于句柄表的描述,可以看看<< Windows進(jìn)程內(nèi)核對(duì)象句柄表>> 這篇文章 創(chuàng)建進(jìn)程的虛擬地址空間 進(jìn)程內(nèi)核對(duì)象創(chuàng)建后, 它的引用計(jì)數(shù)被置為1。 然后系統(tǒng)為剛剛創(chuàng)建的進(jìn)程 分配4GB的進(jìn)程虛擬地址空間。 之所以是4GB, 是因?yàn)樵?2位的操作系統(tǒng)中, 一 個(gè) 指 針 長(zhǎng) 度 是4字 節(jié) , 而4字 節(jié) 指 針 的 尋 址 能 力 是 從 0x00000000~0xFFFFFFFF最大值0xFFFFFFFF表示的即為4GB大小的容量。 與虛擬地址空間相對(duì)的, 還有一個(gè)物理地址空間, 這個(gè)地址空間對(duì)應(yīng)的是真 實(shí)的物理內(nèi)存。如果你的計(jì)算機(jī)上安裝了512M大小的內(nèi)存,那么這個(gè)物理地 址空間表示的范圍是0x00000000~0x1FFFFFFF。 當(dāng)操作系統(tǒng)做虛擬地址到物理地址映射時(shí), 只能映射到這一范圍, 操作系統(tǒng) 也只會(huì)映射到這一范圍。 當(dāng)進(jìn)程創(chuàng)建時(shí), 每個(gè)進(jìn)程都會(huì)有一個(gè)自己的4GB虛擬 地址空間。 要注意的是這個(gè)4GB的地址空間是 “虛擬” 的, 并不是真實(shí)存在的, 而且每個(gè)進(jìn)程只能訪問自己虛擬地址空間中的數(shù)據(jù),無(wú)法訪問別的進(jìn)程中的數(shù) 據(jù), 通過這種方法實(shí)現(xiàn)了進(jìn)程間的地址隔離。 那是不是這4GB的虛擬地址空間 應(yīng)用程序可以隨意使用呢?很遺憾, 在Windows系統(tǒng)下, 這個(gè)虛擬地址空間被 分成了4部分:NULL指針區(qū)、用戶區(qū)、64KB禁入?yún)^(qū)、內(nèi)核區(qū)。應(yīng)用程序 能使用的只是用戶區(qū)而已,大約2GB左右(最大可以調(diào)整到3GB)。內(nèi)核區(qū) 為2GB, 內(nèi)核區(qū)保存的是系統(tǒng)線程調(diào)度、 內(nèi)存管理、 設(shè)備驅(qū)動(dòng)等數(shù)據(jù), 這部分 數(shù)據(jù)供所有的進(jìn)程共享,但應(yīng)用程序是不能直接訪問的。 虛擬地址空間如何劃分 每個(gè)進(jìn)程的虛擬地址空間都要?jiǎng)澐殖筛鱾€(gè)分區(qū)。地址空間的分區(qū)是根據(jù)操 作系統(tǒng)的基本實(shí)現(xiàn)方法來進(jìn)行的。不同的Windows內(nèi)核,其分區(qū)也略有不同。 下 圖顯 示 了 每 種 平 臺(tái) 是 如 何 對(duì) 進(jìn) 程 的 地 址 空 間 進(jìn) 行 分 區(qū) 的 。 32位Windows2000的內(nèi)核與64位Windows2000的內(nèi)核擁有大體相同的分 區(qū),差別在于分區(qū)的大小和位置有所不同。另一方面,可以看到Windows98下 的分區(qū)有著很大的不同。 初始化進(jìn)程的虛擬地址空間 進(jìn)程地址空間創(chuàng)建后, Windows的裝載器 (loader, 也稱為PE裝載器) 開始 工作。Loader會(huì)讀取exe文件的信息(PE文件) ,這里又涉及到PE文件格式的 知識(shí),如不了解,可能會(huì)對(duì)下面的理解有些難度。趕快去百度充下電啦! ! 此時(shí)loader會(huì)檢查PE文件的有效性,如果PE文件有錯(cuò)誤,可能會(huì)顯示出 Thisprogramcannotberunin PE文件的內(nèi)容(二進(jìn)制代碼)映射到進(jìn)程的地址空間中,原則是低地址的映射 到低地址的虛擬地址空間, 高地址的映射到高地址的虛擬地址空間。 實(shí)際上映射 時(shí)是增高的地址空間,因?yàn)镻E文件中和地址空間的對(duì)齊方式大小不一樣,你可 以通過/align開關(guān)調(diào)整這個(gè)數(shù)值。 然后是讀取PE文件的導(dǎo)入地址表(ImportTable) ,這里存放有exe文件需 要導(dǎo)入的模塊文件 (DLL) , 系統(tǒng)會(huì)一一加載這些dll到進(jìn)程的地址空間中, 具體 做法是調(diào)用LoadLibrary函數(shù)加載程序代碼到某個(gè)地址,然后系統(tǒng)會(huì)映射這些代 碼到進(jìn)程的地址空間中,要知道dll只需加載一次就可映射到所有進(jìn)程的地址空 間中, 并為每個(gè)dll維護(hù)一個(gè)引用計(jì)數(shù), 當(dāng)引用計(jì)數(shù)為0時(shí), dll就從內(nèi)存中卸載 掉,釋放占用的內(nèi)存。Dll里面可能又引用了其它的dll,因此加載dll時(shí)是遞歸 形式的,直到加載完ImportTable里描述的所有dll模塊,此時(shí)進(jìn)程初始化部分 完成。 創(chuàng)建進(jìn)程的主線程 當(dāng)進(jìn)程的初始化完成后, 開始創(chuàng)建進(jìn)程的主線程, 一個(gè)進(jìn)程至少要有一個(gè)主 線程才能運(yùn)行, 可以說進(jìn)程只是充當(dāng)一個(gè)容器的作用, 而線程才是執(zhí)行用戶代碼 的載體。 線程是用CreateThread這個(gè)函數(shù)創(chuàng)建的,它的定義如下: HANDLEWINAPICreateThread __in_optLPSECURITY_ATTRIBUTESlpThreadAttrib __inSIZE_TdwStackSize, __inLPTHREAD_START_ROUTINElpStartAddress, __in_optLPVOIDlpParameter, __inDWORDdwCreationFlags, __out_optLPDWORDlpThreadId ); 該函數(shù)的具體信息請(qǐng)看MSDN的說明: http://msdn.microsoft.com/en-us/library/ms682453(v=VS.85).aspx 創(chuàng)建線程時(shí),也和進(jìn)程相似,系統(tǒng)會(huì)創(chuàng)建線程內(nèi)核對(duì)象,初始化線程堆棧。 線程堆棧有兩個(gè), 一個(gè)是核心堆棧, 由核心態(tài)維護(hù); 另一個(gè)是用戶堆棧, 運(yùn)行在 用戶態(tài)下。同樣的,線程的引用計(jì)數(shù)也置為1。 當(dāng)進(jìn)程、線程創(chuàng)建完成后(進(jìn)程地址空間也有了,需要的dll也都加載完畢 了) ,CreateProcess函數(shù)返回,相關(guān)的信息會(huì)保存在PROCESS_INFORMATION 結(jié)構(gòu)中,它的定義如下: typedefstruct_PROCESS_INFORMATION{ HANDLEhProcess; HANDLEhThread; DWORDdwProcessId; DWORDdwThreadId; }PROCESS_INFORMATION,*LPPROCESS_INFORMATION; 可以看到是進(jìn)程(線程)的句柄和進(jìn)程(線程)的ID,有了句柄那樣調(diào)用 CloseHandle函數(shù)并把句柄傳入就使用引用計(jì)數(shù)減1,若引用計(jì)數(shù)為0了,就從 內(nèi)存中卸載釋放內(nèi)存。還有一個(gè)是進(jìn)程(線程)ID,這個(gè)參數(shù)可作為 OpenProcess(OpenThread)的傳入?yún)?shù)打開一個(gè)已存在的進(jìn)程(線程) 。 而其它的信息,如進(jìn)程使用的環(huán)境變量(lpEnvironment指定) ,啟動(dòng)信息則 是保存在STARTUPINFO這個(gè)結(jié)構(gòu)中,它被定義為: typedefstruct_STARTUPINFO{ DWORDcb; LPTSTRlpReserved; LPTSTRlpDesktop; LPTSTRlpTitle; DWORDdwX; DWORDdwY; DWORDdwXSize; DWORDdwYSize; DWORDdwXCountChars; DWORDdwYCountChars; DWORDdwFillAttribute; DWORDdwFlags; WORDwShowWindow; WORDcbReserved2; LPBYTElpReserved2; HANDLEhStdInput; HANDLEhStdOutput; HANDLEhStdError; }STARTUPINFO,*LPSTARTUPINFO; 由于此結(jié)構(gòu)的成員較多,就不詳細(xì)介紹了,看MSDN上的解釋吧 http://msdn.microsoft.com/en-us/library/ms686331(v=VS.85).aspx 如果你想取得進(jìn)程的啟動(dòng)信息, 可以調(diào)用GetStartupInfo這個(gè)函數(shù)。 事實(shí)上, 在下面要講到的運(yùn)行期庫(kù)代碼中,就調(diào)用了此函數(shù)以取得啟動(dòng)相關(guān)的信息。 C/C++ 運(yùn)行期庫(kù)的初始化 當(dāng)進(jìn)程的主線程初始化完成后, 并且線程得到了CPU時(shí)間片, CPU把CS:IP 指向程序入口點(diǎn)(OEP) ,這里以444.exe程序(一個(gè)普通的Windows程序,加 入了一些跟蹤消息的輸出)為例。 首先使用Stud_PE工具查看444.exe的PE文件信息,如下圖所示: 可以看到444.exe的程序入口點(diǎn)是000111B3(下面那個(gè)000005B3的地址是 PE文件中的相對(duì)虛擬地址,而我們要是OEP是指映射到地址空間中的地址) 。 注意這個(gè)地址是相對(duì)虛擬地址,還要加上基址(這里是00400000)才是OEP的 地址, 所以O(shè)EP=00400000+000111B3=004111B3,這個(gè)地址相當(dāng)重要, 因?yàn)?/span> 這是程序運(yùn)行時(shí)CS:IP指向的地址,即:程序運(yùn)行的第一條指令就在這個(gè)地址 處! ! 下面我們來使用PEBrowse工具反匯編下.text節(jié)的內(nèi)容,.text節(jié)就是存放代 碼(指令)的節(jié),關(guān)于節(jié)(section)的概念請(qǐng)查閱PE文件的相關(guān)資料。 從上面的圖中可以看到0x4111B3地址處是一條JMP指令,它跳轉(zhuǎn)到了 WinMainCRTStartup (地址是0x412220)這個(gè)函數(shù)的地址處執(zhí)行, 好了, 終于引 出了這個(gè)神秘的“真正的入口函數(shù)” 。 事實(shí)上,入口函數(shù)有以下4種形式: 1、mainCRTStartup(用于ANSI版本的控制臺(tái)應(yīng)用程序) 2、wmainCRTStartup(用于Unicode版本的控制臺(tái)應(yīng)用程序) 3、WinMainCRTStartup(用于ANSI版本的窗口應(yīng)用程序) 4、wWinMainCRTStartup(用于Unicode版本的窗口應(yīng)用程序) 很顯然, 我這個(gè)例子中使用的是第3種, ANSI版本的窗口應(yīng)用程序, 值得 一提的是, 我這里是使用反匯編工具查看這些信息的, 目的是讓程序運(yùn)行過程一 目了然。 但你也可以看看C/C++ 運(yùn)行期庫(kù)的源代碼文件, 相信你看了后會(huì)對(duì)程序 的運(yùn)行原理更有體會(huì)。這些源碼隨你安裝VC++6.0或VisualStudio時(shí)已經(jīng)附 帶有了,我電腦上裝的是VisualStudio2010,由于我安裝在H盤,所以在我電 腦上CRT庫(kù)源代碼文件的路徑是: H:\ProgramFiles\MicrosoftVisualStudio10.0\VC\crt\src 而WinMainCRTStartup這個(gè)函數(shù)的定義是在crtexe.c這個(gè)文件中,你可以打 開看看是不是和我這反匯編出來的代碼是一樣的呢。 再接著看WinMainCRTStartup源碼,為了方便用XXX表示各種版本的調(diào)用 intXXXCRTStartup( void ) { __security_init_cookie(); return__tmainCRTStartup(); } 可以看到,這個(gè)“真正的入口函數(shù)”首先調(diào)用__security_init_cookie()這個(gè)函 數(shù)完成一些安全方面的初始化, 然后是返回對(duì)__tmainCRTStartup()函數(shù)的調(diào)用, 這樣一來,全部的操作都是在這個(gè)函數(shù)中一一去完成了! ! 在__tmainCRTStartup中首先調(diào)用了GetStartupInfoW函數(shù)取得父進(jìn)程創(chuàng)建 本進(jìn)程時(shí)的啟動(dòng)信息, 然后又是一系列的初始化, 其中包括C++ 構(gòu)造函數(shù)的調(diào)用, 還有靜態(tài)變量,全局變量的初始化,這些操作是在_initterm這個(gè)函數(shù)中完成的。 然后往下看,你就會(huì)發(fā)現(xiàn)(w)WinMain/(w)main函數(shù)被調(diào)用了! ! ! 我們對(duì)這個(gè)函數(shù)再熟悉不過了! !我們寫程序時(shí)要寫的第一個(gè)函數(shù)就是這個(gè) 所謂的主函數(shù), 但你都看到了, 經(jīng)過這么多復(fù)雜的一系列的初始化之后, 這個(gè)函 數(shù)才最終 “被” 調(diào)用, 其中的mainret就是主函數(shù)的返回值了, 習(xí)慣于寫voidmain() 的朋友可看清楚了,從來就沒有voidmain()形式的調(diào)用,不要被潭浩強(qiáng)的書給 “誤導(dǎo)”了,呵呵。 #ifdefWPRFLAG mainret=wWinMain( #else mainret=WinMain( #endif (HINSTANCE)&__ImageBase, NULL, lpszCommandLine, StartupInfo.dwFlags&STARTF_USESHOWWINDOW ?StartupInfo.wShowWindow :SW_SHOWDEFAULT); #else #ifdefWPRFLAG __winitenv=envp; mainret=wmain(argc,argv,envp); #else __initenv=envp; mainret=main(argc,argv,envp); #endif 本文使用的例子444.exe是一個(gè)ANSI版本的窗口程序,因此調(diào)用的是 WinMain函數(shù)了, 當(dāng)然, 既然WinMain函數(shù)的調(diào)用已經(jīng)出現(xiàn)了, 第一部分的 講解也就完畢了,下面請(qǐng)看第二部分的內(nèi)容。 二、主函數(shù)的運(yùn)行過程 終于要講主函數(shù)的運(yùn)行原理了,是否很期待呢,是否覺得第一部分的東西 太復(fù)雜了呢,其實(shí)第一部分exe程序的初始化過程是非常非常復(fù)雜和繁瑣的, 我 這里只是蜻蜓點(diǎn)水一樣大概講了下, 具體的許多細(xì)節(jié)不是查閱相關(guān)資料就能明白 的, 因?yàn)檫@是微軟的東西, 不可能讓你了解內(nèi)部的核心的東西的, 不然他們就沒 飯吃咯,呵呵。你要想了解的話也是可以去研究下的。 其實(shí)本文的重點(diǎn)也是放在這部分的, 就是我們熟悉的窗口程序的運(yùn)行原理, 知道了這個(gè)原理, 對(duì)Windows程序設(shè)計(jì)是很有幫助的, 好了, 廢話就不多說了, 請(qǐng)繼續(xù)往下看。 Windows Windows窗口程序運(yùn)行原理 不知有多少朋友一上來就是MFC,創(chuàng)建工程后,運(yùn)行向?qū)?,下一步,下?/span> 步, 一個(gè)窗口程序就出來了, 但是不知道為什么要這樣做, 你可能連個(gè)WinMain 都找不到,也不知程序是怎么運(yùn)行起來的,本文不是講MFC的原理,而是講原 生的Windows窗口程序的原理,說到MFC,本質(zhì)上是C++ 對(duì)API函數(shù)的包裝, 它的內(nèi)部實(shí)現(xiàn)離不了API函數(shù)的調(diào)用, 只要你懂了這部分講的原理, 那MFC的 原理也就好理解了。 在講原理之前,我想有必要先講下幾個(gè)概念: ?窗口:是一個(gè)可視化的對(duì)象, 窗口上一般有標(biāo)題欄,菜單欄, 最小化,最大 化,關(guān)閉按鈕,這個(gè)大家都懂的 ?句柄:是一個(gè)DWORD(32位)值,它用來標(biāo)識(shí)各種不同的對(duì)象,如窗口, 圖 標(biāo), 菜單, 文件, 字體等等, 相應(yīng)的就有窗口句柄 ( HWND) , 圖標(biāo)句柄 (HICON ) 菜單句柄(HMENU) ,文件句柄(HANDLE)等等 ?消息:表示用戶與程序交互時(shí)產(chǎn)生的各種操作的標(biāo)識(shí),也是一個(gè)DWORD 值,有窗口消息(以WM_XXX為標(biāo)識(shí)) ,通知消息(WM_NOFITY) ,命 名消息(WM_COMMAND) ,各種控件消息等等 ?窗口函數(shù):各種消息的處理程序 (Handler) , 有消息到達(dá)時(shí), 在窗口函數(shù)中 處理你想要的消息 還有些概念大家可以查找相關(guān)的資料,這里就不多說了。有了這些概念, 下 面就好辦了。 首先要知道, Windows窗口程序是基于消息的, 消息的產(chǎn)生是用戶與程序的 交互產(chǎn)生的,也可以是各種系統(tǒng)消息。消息是不斷產(chǎn)生的,許多消息連在一直, 構(gòu)成消息隊(duì)列,系統(tǒng)會(huì)維護(hù)這些消息隊(duì)列,主要有兩種,一種是系統(tǒng)消息隊(duì)列, 一種是用戶消息隊(duì)列。 由于消息不斷的產(chǎn)生, 因此產(chǎn)生一個(gè)消息循環(huán), 通常窗口 創(chuàng)建完成后會(huì)有這一句while(GetMessage(&msg,NULL,0,0,0))來獲取消息。 這 樣消息就源源不斷的產(chǎn)生, 再把消息發(fā)送到窗口, 在窗口函數(shù)中處理, 程序就能 一直運(yùn)行下去直到關(guān)閉退出。 消息是一個(gè)結(jié)構(gòu),它的定義如下: typedefstructtagMSG{ HWNDhwnd; UINTmessage; WPARAMwParam; LPARAMlParam; DWORDtime; POINTpt; }MSG,*PMSG,*LPMSG; hwnd是和消息相關(guān)的窗口, message就是消息的標(biāo)識(shí), wParam,lParam是附 加的兩個(gè)參數(shù), 在使用SendMessage或PostMessage時(shí)指定的, time表示消息被 發(fā)送的時(shí)間,pt表示消息發(fā)送時(shí)鼠標(biāo)的位置(相對(duì)于屏幕坐標(biāo)) 。 那么WinMain的執(zhí)行流程是怎樣的呢?別急,且往下看。 。 。 首先要把窗口創(chuàng)建出來, 但在創(chuàng)建窗口之前,還需要注冊(cè)窗口類, 這里的類 并不是面向?qū)ο笾械念悾?而是指定一個(gè)窗口的風(fēng)格, 樣式等等, 如下面把創(chuàng)建窗 口類的操作寫在一個(gè)函數(shù)中 ATOMMyRegisterClass(HINSTANCEhInstance) { WNDCLASSEXwcex; wcex.cbSize=sizeof (WNDCLASSEX); wcex.style=CS_HREDRAW|CS_VREDRAW; wcex.lpfnWndProc=WndProc; wcex.cbClsExtra=0; wcex.cbWndExtra=0; wcex.hInstance=hInstance; wcex.hIcon=LoadIcon(hInstance,MAKEINTRESOURCE(IDI_MY444)); wcex.hCursor=LoadCursor(NULL,IDC_ARROW); wcex.hbrBackground=(HBRUSH)(COLOR_WINDOW+1); wcex.lpszMenuName=MAKEINTRESOURCE(IDC_MY444); wcex.lpszClassName=szWindowClass; wcex.hIconSm=LoadIcon(wcex.hInstance,MAKEINTRESOURCE(IDI_SALL)); returnRegisterClassEx(&wcex); } hInstace是指應(yīng)用程序載入的模塊, win32系統(tǒng)下通常是被載入模塊的線性地 址, 還記得第一部分講的嗎, WinMain的第一個(gè)參數(shù)就是它, 它是這樣被傳入的 mainret=WinMain((HINSTANCE)&__ImageBase,。 。 。 )那這個(gè)__ImageBase又是 什么東西,看下面的圖你就明白了 這個(gè)圖是我用VS2010調(diào)試時(shí)查看內(nèi)存時(shí)截下來的,從圖中可以看出 __ImageBase應(yīng)該就是值0x00905A4D,而這4個(gè)字節(jié)剛才是PE文件的頭4個(gè) 字節(jié)(PE文件頭兩個(gè)字節(jié)是MZ標(biāo)記)它是映射到地址空間中的,& 取地址后 便是hInstance , 很顯然本例中hInstance應(yīng)該是0x008B0000, 要注意的是每次運(yùn) 行時(shí)hInstance的值都是不同的,這是被映射到地址空間中的地址。它和 HMODULE其實(shí)是一樣的, 再看WNDCLASSEX這個(gè)結(jié)構(gòu),看到有個(gè)成員lpfnWndProc,這個(gè)是指定 窗口函數(shù)的地址, 這里千萬(wàn)不能寫錯(cuò)了, 不然程序運(yùn)行不起來, 而且沒有任何錯(cuò) 誤提示。lpszClassName這個(gè)成員指定了窗口的類名,在使用spy++工具查看窗 口時(shí), 可以看到這個(gè)類名。 之后就是返回RegisterClassEx函數(shù)的調(diào)用, 這樣窗口 類就注冊(cè)好了。 接下來的工作是創(chuàng)建窗口了,創(chuàng)建窗口使用函數(shù)CreateWindow(Ex),這里也 把創(chuàng)建窗口的操作寫函數(shù)中: BOOLInitInstance(HINSTANCEhInstance,intnCmdShow) { HWNDhWnd; hInst=hInstance; hWnd=CreateWindow(szWindowClass,szTitle,WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,0,CW_USEDEFAULT,0,NULL,NULL,hInstance,NULL); if(!hWnd) { returnFALSE; } ShowWindow(hWnd,nCmdShow); UpdateWindow(hWnd); returnTRUE; } 創(chuàng)建窗口成功后會(huì)返回一個(gè)窗口句柄HWND, 這個(gè)創(chuàng)建窗口的過程其實(shí)也是 相當(dāng)復(fù)雜的,創(chuàng)建時(shí)會(huì)發(fā)送幾個(gè)消息到窗口函數(shù)中,比如WM_NCCREATE, WM_CREATE,具體的細(xì)節(jié)下一節(jié)跟蹤消息的執(zhí)行時(shí)會(huì)有介紹。有了這個(gè)窗口 句柄后就可以做很多事了,因?yàn)樵S多函數(shù)的調(diào)用都需要傳入一個(gè)窗口句柄值, 接 下來是顯示窗口ShowWindow,這個(gè)函數(shù)也會(huì)發(fā)送幾個(gè)消息出去,而后是更新 窗口UpdateWindow,主要是發(fā)送了WM_PAINT消息,讓窗口重繪。 到了最后,就是一個(gè)消息循環(huán)了,消息循環(huán)典型的寫法如下: MSGmsg; while(GetMessage(&msg,NULL,0,0)) { TranslateMessage(&msg); DispatchMessage(&msg); } 只要程序在運(yùn)行, 這個(gè)循環(huán)就不會(huì)退出,程序也不會(huì)終止, 當(dāng)用戶關(guān)閉窗口 時(shí),會(huì)產(chǎn)生WM_CLOSE消息,默認(rèn)的WM_CLOSE實(shí)現(xiàn)(在DefWindowProc 中)是發(fā)送了WM_DESTROY消息,我們需要在WM_DESTROY消息中調(diào)用 PostQuitMessage函數(shù), 以便讓它發(fā)送WM_QUIT消息, 當(dāng)GetMessage函數(shù)取到 的消息是WM_QUIT消息時(shí)它會(huì)返回FALSE,這樣循環(huán)就退出了,程序也會(huì)終 于。 還有一個(gè)重要的函數(shù)是窗口函數(shù), 窗口函數(shù)是一個(gè)回調(diào)函數(shù), 就是系統(tǒng)會(huì)自 己去調(diào)用這個(gè)函數(shù), 你只要按要求寫好, 當(dāng)消息到來時(shí), 系統(tǒng)會(huì)去處理, 不用你 去處理,就是Dot'tCallMe,IWillCallYou。這個(gè)函數(shù)是在注冊(cè)窗口類時(shí)由 lpfnWndProc指定的,窗口函數(shù)的定義如下: LRESULTCALLBACKWindowPro __inHWNDhwnd, __inUINTuMsg, __inWPARAMwParam, __inLPARAMlParam ); hwnd是和消息的發(fā)出相關(guān)的窗口句柄, uMsg就是發(fā)送的消息, 后兩個(gè)是附 加參數(shù)。 窗口函數(shù)的處理比如可以這樣寫: LRESULTCALLBACKWndProc(HWNDhWnd,UINTmessage,WPARAMwParam,LPARAMlParam) { PAINTSTRUCTps; HDChdc; switch(message) { caseWM_PAINT: hdc=BeginPaint(hWnd,&ps); ::Rectangle(hdc,50,100,200,400); EndPaint(hWnd,&ps); break ; caseWM_DESTROY: PostQuitMessage(0); break ; default : returnDefWindowProc(hWnd,message,wParam,lParam); } return0; } 這里只處理了兩條消息, 而往往我們要做的就是處理各種你想處理的消息, 因?yàn)閃indows是基于消息的嗎,所以如果要處理的消息很多,那么就會(huì)有很多 的caseWM_XXX的處理,如果用MFC的話,它把這些消息分流出去了,變成 了一些ON_XXX的處理函數(shù),MFC里面會(huì)維護(hù)一個(gè)消息路由表。 總結(jié)下它的運(yùn)行原理,是這樣的一個(gè)過程: 注冊(cè)窗口類--> 創(chuàng)建窗口--> 顯示窗口--> 更新窗口--> 消息循環(huán)(取得消息, 分 發(fā)消息) ,窗口函數(shù)對(duì)消息的處理 跟蹤運(yùn)行過程 本節(jié)用一個(gè)實(shí)例來跟蹤Windows程序的運(yùn)行過程, 也就是探討下程序啟動(dòng)時(shí) 產(chǎn)生了哪些消息,又是怎樣處理的,使用的例子還是444.exe程序,此程序也沒 有什么特別之處, 只是在調(diào)用一些函數(shù)前后輸出一些信息, 還有在窗口函數(shù)的實(shí) 現(xiàn)代碼中, 寫上幾句輸出該消息是什么消息而已, 這樣就可以對(duì)程序運(yùn)行時(shí)產(chǎn)生 消息的順序有一個(gè)認(rèn)識(shí)。 從調(diào)試的信息可以看出,0x00F00000是hInstance (其實(shí)是一個(gè)地址)的值, 0x00905A4D是該地址處的值,就是PE文件的頭幾個(gè)字節(jié)。 往下看輸出了EntryMainMethod表示主方法開始了,然后是調(diào)用 RegisterClassEx注冊(cè)窗口類,寫上EndXXX是為了看看函數(shù)被調(diào)用時(shí)會(huì)不會(huì)有 消息發(fā)出,這樣就可以在窗口函數(shù)中捕獲該消息。 注冊(cè)完窗口類,開始調(diào)用CreateWindow函數(shù)創(chuàng)建窗口,前面說過,此函數(shù) 的內(nèi)部操作是很復(fù)雜的,從圖中也可以看出來,CreateWindow函數(shù)在返回前會(huì) 發(fā)出幾個(gè)消息,首先是WM_GETMINMAXINFO (0x0024)消息,這樣看來這 個(gè)消息是窗口創(chuàng)建后的第一條消息,而WM_QUIT則是最后一條消息 (GetMessage取得此消息后返回FALSE) 。 這里要注意的是以上這些消息的發(fā)出是在消息循環(huán)之前產(chǎn)生的,因?yàn)檫@是 CreateWindow函數(shù)內(nèi)部發(fā)出的消息,而消息循環(huán)還沒開始。然后是第二條消息 WM_NCCREATE(0x0081) ,消息產(chǎn)生在WM_CREATE之前,NC是notclient 非客戶區(qū)的意思,是指標(biāo)題欄,邊框等幾個(gè)區(qū)域。 后面有三條NULL的消息, 表示什么呢, 這個(gè)我本人沒研究, 而且是僅有數(shù) 值并沒有WM_XXX的定義,所以這里用NULL代替。CreateWindow在返回前 發(fā)出的最后一條消息是WM_CREATE消息,這個(gè)消息中的lParam參數(shù)就是 CreateWindow函數(shù)中的最后一個(gè)參數(shù)傳入的,往往我們會(huì)在這里完成一些初始 化的設(shè)置,不建議攔截WM_NCCREATE消息,這個(gè)默認(rèn)DefWindowProc中會(huì) 有處理。 接著是顯示窗口和更新窗口了, ShowWindow的內(nèi)部也是做了許多工作,請(qǐng)看 在ShowWindow函數(shù)返回前會(huì)發(fā)送許多消息出去,我們看到第一條消息是 WM_SHOWWINDOW,最后一條是WM_MOVE,然后函數(shù)返回,而在 UpdateWindow中只發(fā)送了一條WM_PAINT消息,讓窗口重繪。 這些都完成之后,就進(jìn)入消息循環(huán) 這些都是消息循環(huán)和窗口函數(shù)中的處理(通過DispatchMessage將消息送入 窗口函數(shù))這樣程序就一直在運(yùn)行著,消息循環(huán)中通過GetMessage函數(shù)取得消 息以便交給窗口函數(shù)處理。 你也可以使用PeekMessage, 關(guān)于它們的區(qū)別你可以 百度下相關(guān)的資料。 當(dāng)用戶關(guān)閉窗口時(shí),我們來看看又是怎樣的一個(gè)過程呢 這 就 是 關(guān) 閉 窗 口 時(shí) 的 過 程 。 可 以 看 到 首 先 是 產(chǎn) 生了 WM_NCLBUTTONDOWN消息, 因?yàn)殛P(guān)閉按鈕是在非客戶區(qū), 然后下面有一個(gè) WM_SYSCOMMAND消息, 這個(gè)消息是系統(tǒng)菜單的消息, 你在標(biāo)題欄上右鍵時(shí) 可以看到系統(tǒng)菜單,然后是WM_CLOSE消息,默認(rèn)的實(shí)現(xiàn)在DefWindowProc 中,我想是調(diào)用了DestroyWindow這個(gè)函數(shù),它是送出了一個(gè)WM_DESTROY 和WM_NCDESTROY消息,而我們的程序中在WM_DESTROY處理了,調(diào)用 了PostQuitMessage函數(shù),發(fā)出了WM_QUIT消息,由于GetMessage函數(shù)取到 該消息時(shí)返回FALSE了,因此循環(huán)體內(nèi)的語(yǔ)句沒有執(zhí)行,WM_QUIT消息也就 沒有被打印出來, 這樣WM_NCDESTROY就是最后一條消息 (不知還有沒有其 它消息) ,while循環(huán)結(jié)束后,最后是主函數(shù)返回return(int)msg.wParam,至此, 主函數(shù)執(zhí)行完畢。 總結(jié)下這部分的內(nèi)容,消息產(chǎn)生的順序應(yīng)該是這樣一個(gè)過程: WM_GETMINMAXINFO(0x0024) WM_NCCREATE(0x0081) WM_NCCALCSIZE(0x0083) NULL(0x0093) NULL(0x0094) NULL(0x0094) WM_CREATE(0x0001) WM_SHOWWINDOW(0x0018) WM_WINDOWPOSCHANGING(0x0046) WM_WINDOWPOSCHANGING(0x0046) WM_ACTIVATEAPP(0x001C) WM_NCACTIVATE(0x0086) NULL(0x0093) WM_GETICON(0x007F) WM_GETICON(0x007F) WM_GETICON(0x007F) NULL(0x0093) NULL(0x0091) NULL(0x0092) NULL(0x0092) WM_ACTIVATE(0x0006) WM_IME_SETCONTENT(0x0281) WM_IME_NOTIFY(0x0282) WM_SETFOCUS(0x0007) WM_NCPAINT(0x0085) NULL(0x0093) NULL(0x0093) NULL(0x0091) NULL(0x0092) NULL(0x0092) WM_ERASEBKGND(0x0014) WM_WINDOWPOSCHANGED(0x0047) NULL(0x0093) WM_SIZE(0x0005) WM_MOVE(0x0003) WM_PAINT(0x000F) BeginMessageLoop... WM_GETICON(0x007F) WM_GETICON(0x007F) WM_GETICON(0x007F) WM_SYNCPAINT(0x0088) ############################### #####消息循環(huán)中的其它消息###### ############################### #####下面是窗口關(guān)閉后消息###### ############################### WM_NCLBUTTONDOWN(0x00A1) WM_CAPTURECHANGED(0x0215) WM_SYSCOMMAND(0x0112) WM_CLOSE(0x0010) NULL(0x0090) WM_WINDOWPOSCHANGING(0x0046) WM_WINDOWPOSCHANGED(0x0047) WM_NCACTIVATE(0x0086) NULL(0x0093) NULL(0x0093) NULL(0x0091) NULL(0x0092) NULL(0x0092) WM_ACTIVATE(0x0006) WM_ACTIVATEAPP(0x001C) WM_KILLFOCUS(0x0008) WM_IME_SETCONTENT(0x0281) WM_IME_NOTIFY(0x0282) WM_DESTROY(0x0002) WM_NCDESTROY(0x0082) EndMessageLoop... 相信這一部分的內(nèi)容會(huì)讓你受益很多, 呵呵。這部分的內(nèi)部也結(jié)束了, 請(qǐng)接 著看第三部分的內(nèi)容。 三、程序收尾工作 WinMain函數(shù)結(jié)束之后,又發(fā)生了什么事呢。我們繼續(xù)來分析。 經(jīng)調(diào)試發(fā)現(xiàn)WinMain函數(shù)返回后回到了C/C++ 運(yùn)行期庫(kù)函數(shù), 返回值賦給了 mainret。然后是一些內(nèi)存回收清理工作,請(qǐng)看圖 這里的工作主要是調(diào)用exit函數(shù)完成的,exit函數(shù)的實(shí)現(xiàn)其實(shí)是調(diào)用doexit 函數(shù), 然后執(zhí)行一些清理工作, 包括釋放不用的指針, 被占用的內(nèi)存等等。 而后 執(zhí)行到了這里: 到這里就是退出進(jìn)程了,如果__crtCorExitProcess函數(shù)沒有成功,則后面的 ExitProcess保證成功調(diào)用。 在我調(diào)試時(shí), 調(diào)試到ExitProcess這個(gè)函數(shù)就結(jié)束了, 調(diào)試程序也退出了,可見這樣一個(gè)exe程序的執(zhí)行的生命周期也就完成了。 總結(jié) 從這三部分講的內(nèi)容來看,總結(jié)出一個(gè)exe程序的執(zhí)行過程是這樣: 1、Shell(Explorer.exe )調(diào)用CreateProcess函數(shù)激活exe程序 2、系統(tǒng)創(chuàng)建一個(gè)進(jìn)程內(nèi)核對(duì)象,引用計(jì)數(shù)置為1 3、系統(tǒng)為進(jìn)程創(chuàng)建一個(gè)4GB的進(jìn)程虛擬地址空間 4、PE裝載器把exe的代碼映射到地址空間,并查找ImportTable引入相關(guān) 的動(dòng)態(tài)鏈接庫(kù)(DLLs ) 5、系統(tǒng)為進(jìn)程創(chuàng)建一個(gè)主線程,線程得到CPU后,把CS:IP指向.text節(jié)中 的程序進(jìn)入點(diǎn)(OEP) ,此處是一條JMP指令,它跳到XXXCRTStartup 函數(shù)處執(zhí)行 6、這里完成c/c++運(yùn)行期庫(kù)的一些初始化設(shè)置,包括c++ 構(gòu)造函數(shù)的調(diào)用 全局變量,靜態(tài)變量的初始化 7、調(diào)用WinMain/main函數(shù),進(jìn)入主函數(shù) 8、注冊(cè)窗口類,創(chuàng)建窗口,顯示窗口,更新窗口,進(jìn)入消息循環(huán) 9、窗口關(guān)閉,循環(huán)退出,返回到C/C++ 運(yùn)行期庫(kù) 10、完成一些清理工作 11 、最后是ExitProcess退出進(jìn)程 后話 本文的內(nèi)容至此就講完了, 希望這篇文章能給迷茫, 不知怎么學(xué)習(xí)Windows 程序設(shè)計(jì)的人一個(gè)指導(dǎo),同時(shí)也對(duì)學(xué)習(xí)MFC有一個(gè)好的認(rèn)識(shí)。 本人也不是什么高手, 只是肯去研究下這方面的技術(shù), 如果你也喜歡去研究, 喜歡技術(shù), 相信你也可以做, 也可能做的更好, 因此本文的一些錯(cuò)誤之處還請(qǐng)多 多指教。完。 。 。 PS: 本人是一個(gè)喜歡編程, 喜歡技術(shù)的人, 就把技術(shù)當(dāng)做吃飯一樣, 那么你 也是嗎?我的微博^_^http://weibo.com/zhuhuigong 作者:秋傷~~! 創(chuàng)建時(shí)間:2011/09/1400:01 完成時(shí)間:2011/09/1623:40 參考文章: 1、windows啟動(dòng)以及exe文件的加載簡(jiǎn)介 2、一個(gè)microsoft的.exe程序的啟動(dòng)過程 |
|