一個microsoft的.exe程式的啟動過程
本文系轉載,原創連結:http://blog.csdn.net/bigwhite20xx/archive/2002/07/19/17515.aspx
學習windows 編程從mfc角度來說可分為兩部分那就是WinMain函數以前的,和WinMain函
數以後的。前者涉及很多windows作業系統內部的知識,後者麼看mfc源碼就可以了。雖然大
多數程式不需要你瞭解太多關於os載入應用程式這方面的知識,但我認為能較深入瞭解wind
ows os的運行情況對程式員是很有協助的。
最近我正在研究一個microsoft的.exe程式的啟動過程,這也是以上所說的關於os如何加
載程式的知識,它包括進程建立,主線程 建立,PE檔案載入,程式c運行時啟動函數以及四種
main函數的調用等許多令我不得不靜下心來好好思索的東東。從mfc編程角度來說,這些都是
不得見的,不過瞭解這些對程式員編製好的windows程式是有好處的。在平時的學習中中我有
很多疑點,到底在案頭雙擊一個exe程式,os調用的第一個函數是什嗎?甚至到現在我研究很
長時間後,還是得不到令我滿意的答案。不過在學習的過程中我還是又說收穫的,下面和大
家一起分享一下,我也把這段時間所學作一下總結。
要瞭解一個.exe程式的啟動過程就不得不瞭解一下有關作業系統方面的知識,such as“
進程,線程,虛擬記憶體"的基本的知識。當然這裡我就不詳細介紹了,有興趣的同學可以自己
去查一查這方面的資料。在未真正開始之前,先統一一下本文出現的一些名詞的含義:
App.exe----------假定為應用mfc的AppWizard做出的一個SDI程式,App是它的名字。你
可以把它看為一個標準的"hello mfc!"程式。
PE------------不要以為它是“體育課”的縮寫呦。它可是微軟的標準win32可執行檔
.exe和動態連結程式庫.dll的檔案格式,它的english name是Portable Executable File Forma
t。
下面可要正式開始了。
一個microsoft的.exe程式的啟動方法有很多,這裡我們以雙擊App.exe表徵圖啟動為例(
其他方法,我想也是一樣的)。在補充一下,我所用的os是Windows2000Server,所以這裡也
主要討論win2000下的應用程式,過要涉及較多關於NT核心,畢竟微軟主推win2000/winxp和
Unicode麼。
一個microsoft的.exe程式的啟動過程如下:
(1)當我們雙擊App.exe表徵圖啟動程式時,系統首先做什麼呢,讓我們先聽一聽侯捷是如何
說的吧“執行起來的App進程其實是shell調用CreateProcess啟用的”----"深入淺出MFC se
cond edition" page39載。很多書上都是如是說的,shell又名“命令直譯器”,是win32操
作系統基於瀏覽器的一個32位使用者介面,它是一個多線程的好例子,螢幕上每一個檔案夾瀏
覽視窗都是它的一個線程。它是作業系統引導時載入的系統進程,它具體表現為windows ex
plorer.exe。explorer.exe是所有使用者應用程式的創造者。你完全可以將shell看成是所有應
用程式進程的父進程,就像案頭(desktop)可看成所有視窗的父視窗一樣。shell的用途很多
,如啟動應用程式,管理檔案系統,將應用程式與相應檔案相關聯等等。我們常見的案頭上
的帶有小箭頭的捷徑(shortcut)就是一個shell連結,shell負責管理一個叫"名字空間"
的類似檔案系統似的“超檔案系統”,它允許應用程式在任何地方在不知訪問對象名字和位置
的前提下訪問到這個對象,此類對象有:檔案,目錄,磁碟機,印表機以及網路資源。而名
字空間就是shell把這些對象有層次組織起來的一個結構。名字空間為使用者和應用程式提供了
一種可靠和高效的方法來訪問和管理對象。好了不論它是什麼,凡正它調用了CreateProcess,一切就從這裡開始了。
(2)CreateProcess這個函數可作了不少工作。App進程由此誕生。當CreateProcess這個函
數被調用,系統就會建立一個“進程核心對象”。進程核心對象可以看作一個作業系統用來
管理進程的核心對象,它也是系統用來存放關於進程統計資訊的地方(一個小的資料結構)
,其實它的真正建立者是一個叫NtCreateProcess的windows2000系統服務函數(也叫執行體服
務函數),他建立了進程核心對象供使用者擴充。進程核心對象的初始使用計數為1。然後系統
為該進程建立4GB(=2^32)的虛擬位址空間(所謂虛擬就不是真的建立4GB的實體記憶體空間,這
些空間不是真在實體記憶體上).用於載入App.exe可執行檔和任何必要的dll檔案的資料和代
碼。
(3)下面概述一下系統的載入器(可稱為loader)是如何載入這些東東的。首先瞭解一下系
統為該進程建立4GB的虛擬位址空間是如何分配的,對於win2000/winxp來說,預設情況下每
個使用者進程可以佔有2GB的私人地址空間;作業系統佔有剩餘的2GB空間。
在32位x86系統上,
從0x00000000到0x7fffffff的空間中存放著 應用程式代碼,全域變數,每個線程堆棧,dll
代碼。
從0x80000000到0xc0000000的空間中存放著 核心和執行體,HAL(硬體抽象層),引導驅動程式
。
從0xc0000000到0xc0800000的空間中存放著 進程頁表和超空間。
從0xc0800000到0xffffffff的空間中存放著 系統快取,分頁集區,非分頁集區。
首先,CreateProcess開啟應用程式檔案(.exe),它先掃描該檔案的檔案頭,該檔案頭裡含有文
件能運行在那個環境之下,如果是win32環境,系統就直接負載檔案的代碼和資料並輸入(im
port)該檔案執行所需的dll函數。如果不是win32環境比如時os/2的.exe則先載入相應的環境
子系統,載由該環境載入該檔案的代碼和資料以及該檔案執行所需的dll函數。至於系統是如
何知道檔案的代碼和資料以及該檔案執行所需的dll函數所在的位置就需要你瞭解一下PE檔案
格式了,其實也很簡單,PE檔案擁有很多sections,資料和代碼都放在不同的section裡面,
檔案執行所需的dll也放在單獨的section(.idata)裡,這裡就不詳述了。而且在載入過程中涉
及到有關虛擬記憶體,記憶體對應檔等很多較深的知識,我會在以後的系列文章中詳細專題論
述的。
(4)進程載入代碼和資料完畢後,就開始建立線程來執行進程空間內的代碼。進程是靜態
,它只是線程的容器。一個進程至少因該有一個線程(main thread),其它線程都是主線程通
過調用CreateThread函數建立的。線程也是核心對象,他的實際建立者是一個叫NtCreateTh
read的windows2000系統服務函數。一個線程其實只是一個線程核心對象和兩個堆棧(一個核
心堆棧,用於線程運行在核心態;一個使用者堆棧,用於線程運行在使用者態),線程與進程類似
,也擁有線程核心對象計數和線程控制代碼,這裡不詳述。線程用於描述進程中的運行路徑。每
當進程被初始化時,系統就要建立一個主線程。該線程與c/c++執行階段程式庫的啟動代碼一道開始
運行,啟動代碼則調用進入點函數(就是我們的main函數,它也是主線程的進入點函數),並
且繼續運行直到進入點函數返回並且c/c++執行階段程式庫的啟動代碼調用ExitProcess為止。每個
線程都有自己的進入點函數,主線程進入點函數名字必須是main,wmain,WinMain或wWinMain
.而其他的線程進入點函數名字可使用任何名字。每個線程函數必須有一個傳回值,它將作為
線程的結束代碼。對於主線程來說,這個傳回值將傳給c/c++執行階段程式庫的啟動函數。
(5)c/c++執行階段程式庫的啟動函數它其實是一個程式的真正調用的第一個函數,它是在程式鏈
接時由連結程式選擇相應的啟動函數並加到程式的開始處。c/c++執行階段程式庫有四個版本的啟動
函數,他們分別對應不同類型的應用程式。比如,需要ANSI字元和字串的GUI應用程式的啟
動函數是WinMainCRTStartup,其對應的進入點函數是
WinMain,需要Unicode字元和字串的GUI應用程式的啟動函數是wWinMainCRTStartup,其對應
的進入點函數是
wWinMain,而需要ANSI字元和字串的CUI應用程式(如控制台console程式)的應用程式的啟動
函數是mainCRTStartup,對應的進入點函數為main;需要Unicode字元和字串的CUI應用程式
(如控制台console程式)的應用程式的啟動函數為wmainCRTStartup,對應的進入點函數為wma
in;c/c++執行階段程式庫的啟動函數的功能如下:
以wWinMainCRTStartup(大多數運行在windows2000下的應用程式的啟動函數都是它)為例。它
負責:
*檢索指向新進程的完整命令列指標;
*檢索指向新進程的環境變數的指標;
*對c/c++運行時的全域變數進行初始化;
*對c運行期的記憶體單元分配函數(比如malloc,calloc)和其他低層I/O常式使用的記憶體棧進
行初始化。
*為C++的全域和靜態類調用建構函式。
當這些初始化工作完成後,該啟動函數就調用wWinMain函數進入應用程式的執行。
當wWinMain函數執行完畢返回時,wWinMainCRTStartup啟動函數就調用c運行期的exit()函
數,將傳回值
(nMainRetVal)傳遞給它。之後exit()便開始收尾工作:
*調用由_onexit()函數調用和註冊的任何函數。
*為C++的全域和靜態類調用解構函式;
*叫用作業系統的ExitProcess函數,將nMainRetVal傳遞給它,這使得作業系統能夠撤銷進
程並設定它的exit 代碼。
(6)至此啟動函數的任務完成,至於中間wWinMain函數的運行過程看看mfc源碼即可。不過我還要提一下,wWinMain函數其實只是調用了mfc的AfxWinMain()函數,而一切的真正代碼的運
行也是從AfxWinMain()開始的。
以上只是粗略將一下一個microsoft的.exe程式的啟動過程,其中有很多深奧的知識我只是提了一下,有些知識在以後的文章中還會陸續提到的。
bigwhite
2002.5.18