天天看點

程式運作流程——連結、裝載及執行

在閱讀完《深入了解計算機系統》第一章(計算機系統漫遊)、第七章(連結)以及第十章(虛拟存儲器)和《程式員的自我修養——連結、裝載與庫》後,曆時悠久的夢想終于要實作了。開篇之初,首先提出一個迷惑了很久的一個問題:什麼是虛拟存儲器?它跟程序的虛拟位址空間有什麼關系?

虛拟存儲器是建立在主存--輔存實體結構基礎上,有附加的硬體裝置及作業系統存儲管理軟體組成的一種存儲體系。

程式運作流程——連結、裝載及執行

顧名思義,虛拟存儲器是虛拟的存儲器,它其實是不存在的,而僅僅是由一些硬體和軟體管理的一種“系統”。他提供了三個重要的能力:1,它将主存看成一個存儲在磁盤上的位址空間的高速緩存,在主存中隻儲存活動區域,并根據需要在磁盤和主存之間來回傳送資料(這裡存在“交換空間”以及“頁面排程”等概念),通過這種方式,高效地利用主存;2,它為每個程序提供了統一的位址空間(以虛拟位址編址),進而簡化了存儲器管理;3,作業系統會為每個程序提供獨立的位址空間,進而保護了每個程序的位址空間不被其他程序破壞。 

虛拟存儲器與虛拟位址空間是兩個不同的概念:虛拟存儲器是假想的存儲器,而虛拟存儲空間是假想的記憶體。它們之間的關系應該與主存儲器與記憶體空間之間的關系類似。

連結部分:

連結就是将不同部分的代碼和資料收叢集組合成一個單一檔案的過程,也就是把不同目标檔案合并成最終可執行檔案的過程。當然,務必知道:這個過程不涉及記憶體。連結可以分為三種情形:1,編譯時連結,也就是我們常說的靜态連結;2,裝載時連結;3,運作時連結。裝載時連結和運作時連結合稱為動态連結。在此,我們的連結部分将主要講述靜态連結,而裝載時連結我們放在裝載部分講,運作時連結忽略。

很多時候,從示例入手比較簡單。我們寫兩個小程式a.c和b.c  

/* a.c */

extern int shared;

int main()

{

int a = 100;

swap(&a,&shared);

}

/* b.c */

int shared = 1;

void swap(int *a,int *b)

{

*a ^= *b ^= *a ^= *b;

}

編譯這兩個檔案得到“a.o”和“b.o”兩個目标檔案

§gcc -c a.c b.c

從代碼中可以看到三個符号:share,swap和main。

靜态連結的整個過程分為兩步:  

第一步:空間和位址配置設定。掃描所有的輸入目标檔案,獲得他們的各個段的長度、屬性和位置,并且将輸入目标檔案中的符号表中所有的符号定義和符号引用收集起來,統一放到一個全局符号表。這樣,連接配接器将能夠獲得所有輸入目标檔案的段長度,并且将它們合并,計算出輸出檔案中各個段合并後的長度與位置,并建立映射關系。

這裡可能會有一個問題:建立了什麼樣的映射關系。看了下面圖,你可能就會有所了解。映射關系就是指可執行檔案與程序虛拟位址空間之間的映射。那麼,這裡程式還沒有執行,更不會出現程序,哪裡來的程序位址空間呢?此時虛拟存儲器便發揮了很大的作用:雖然此時沒有程序,但是每個程序的虛拟位址空間的格式都是一緻的。是以,為可執行檔案的每個段甚至每個符号符号配置設定位址也就不會有什麼錯了。注意:在連結之前,目标檔案中的所有段的虛拟位址都是0,因為虛拟空間還沒有被配置設定,預設都為0.等到連結之後,可執行檔案中的各個段已經都被配置設定到了相應的虛拟位址。仍然看下圖。。。

程式運作流程——連結、裝載及執行

綜上所述。連結後可執行檔案中的各個段的虛拟位址都已經确定了。那麼,各個符号的位址呢?因為各個符号在段中相對位置是固定的,是以這個時候“main”,“share”及“swap”的位址也都确定了。

其中,“main”位于“text”段的最開始處,偏移量為0,是以“main”這個符号在最終的輸出檔案中的位址應該是 0x08048094 + 0 即0x08048094;同理,“swap”的偏移量為0x34, “swap”這個符号在最終的輸出檔案中的位址應該是 0x08048094 + 0x34 即0x080480c8;“shared”相對于“data”的偏移量為0, 在最終的輸出檔案中的位址應該是 0x08049108。這裡可能會有點小小的疑問:shared怎麼在.data段中呢?剛開始不是未初始化嗎?是的,但是我們這裡現在是已經合并好的段,它已經是合并後的檔案格式狀況,shared已經知道它的值為1了,仔細看上圖。 如下表所示

符号 類型 虛拟位址
main 函數 0x08048094
swap 函數 0x080480c8
shared 變量 0x08049108

第二步:符号解析與重定位

首先,符号解析。解析符号就是将每個符号引用與它輸入的可重定位目标檔案中的符号表中的一個确定的符号定義聯系起來。若找不到,則出現編譯時錯誤。       

解釋一下什麼是符号定義和什麼是符号引用吧:須知,這樣的區分是源于某一特定的子產品而言。如上所示,對a.o而言,它裡面的shared即為符号定義而b.o裡面的shared為符号引用;相對地, 對b.o而言,它裡面的shared即為符号定義而a.o裡面的shared為符号引用;

其次,重定位。  

不同的處理器指令對于位址的格式和方式都不一樣。我們這裡采用的是32位的x86處理器,介紹兩種尋址方式。

X86基本重定位類型
宏定義 重定位修正方法
R_386_32 1 絕對尋址修正S + A
R_386_PC32 2 相對尋址修正S + A - P

注:

A:儲存在被修正位置的值,對于32位cpu的話,采用 R_386_PC32尋址的話 它應該為0xFFFFFFFC即-4,它是代表位址的四個位元組;而采用 R_386_32尋址,它應該為0.

P:被修正的位置。考慮以下程式

...

1023: 11 11 11

1026:e8  fc  ff ff ff

102b: 11 11 11

...

上述藍色fc标記處即是被修正的位置,即0x1027.

S:符号的實際位址。也就是第一步中空間和位址配置設定時得到的符号虛拟位址。

舉例來說吧!連結成的可執行檔案中,假設main函數的虛拟位址為0x1000,swap函數的虛拟位址為0x2000;shared變量的虛拟位址為0x3000;

絕對位址修正:對shared變量的位址修正。

l  S:shared的實際位址為0x3000;

l  A:被修正位置的值,即0.

是以最後這個重定位修正位址為:0x3000,不變!

相對尋址修正:對符号“swap”進行修正。

l  S:符号swap的實際位址,即0x2000;

l  A:被修正位置的值,即0xFFFFFFFC(-4);

l  P:被修正位置,及0x1027

最後的重定位修正位址為:S + A -P = 0x2000 +(-4)- 0x1027 = 0xFD5.即修正後的程式為:

...

1023: 11 11 11

1026:e8  d5 0f 00 00

102b: 11 11 11

...

發現熟悉的規則了嗎?下一條指令(PC)的位址為0x102b,加上這個修正值正好等于0x2000,

0x102b + 0xFD5 = 0x2000,剛好是swap函數的位址。

程式運作流程——連結、裝載及執行

以上内容沒有涉及到c标準庫,僅僅是自己實作的兩個c語言程式之間的連結狀況,也就是“程式裡面的printf怎麼處理”沒有說明。這裡,我們就要提及“靜态庫”的概念。其實一個靜态庫可以簡單地看成一組目标檔案的集合,即很多目标檔案經過壓縮打包後形成的一個檔案。與靜态庫連結的過程是這樣的:ld連結器自動查找全局符号表,找到那些為決議的符号,然後查出它們所在的目标檔案,将這些目标檔案從靜态庫中“解壓”出來,最終将它們連結在一起成為一個可執行檔案。也就是說隻有少數幾個庫和目标檔案被連結入了最終的可執行檔案,而非所有的庫一股腦地被連結進了可執行檔案。

裝載部分:

首先,小議一下動态連結。動态連結其實有分為裝載時連結和運作時連結,在這裡,我們隻考慮裝載時連結而不考慮運作時連結。

為什麼要動态連結呢?

主要原因有兩個:第一,考慮記憶體和磁盤空間。靜态連結極大地浪費記憶體空間。因為在靜态連結的情況下,假設有兩個程式共享一個子產品,那麼在靜态連結後輸出的兩個可執行檔案中各有一個共享子產品的副本。如果同時運作這兩個可執行檔案,那麼這個共享子產品将在磁盤和記憶體中都有兩個副本,對磁盤和記憶體造成極大地浪費;第二,程式的更新。一旦程式中的一個子產品被修改,那麼整個程式都要重新連結、釋出給使用者。如果這個程式相當的大,那麼後果就會更加嚴重!

動态連結做了什麼?

務必知道,動态連結是相對于共享對象而言的。動态連結器将程式所需要的所有共享庫裝載到程序的位址空間,并且将程式彙總所有為決議的符号綁定到相應的動态連結庫(共享庫)中,并進行重定位工作。

下面開始說說裝載。裝載的方式主要有兩種:覆寫裝入和頁映射。因為虛拟存儲器的出現,覆寫裝入已經被淘汰了。而頁映射是虛拟存儲機制的一部分,伴随着虛拟存儲器的發明而誕生。具體的頁映射可以參考《深入了解計算機系統》的第十章“虛拟存儲器”。

以Linux核心裝載ELF為例簡述一下裝載過程。當我們在Linux系統的bash下輸入一個指令執行某個ELF程式時,在使用者層面,bash程序會調用fork()系統調用建立一個新的程序,然後新的程序調用execve()來執行指定的ELF檔案,原先的bash程序繼續傳回等待剛才啟動時新程序結束,然後繼續等待使用者輸入指令。這裡需注意,随着一個新程序的出現,作業系統會為它建立一個獨立的虛拟位址空間。

【建立虛拟位址空間】我們知道一個虛拟空間由一組映射函數将虛拟空間的各個頁映射到相應的實體空間,那麼建立一個虛拟空間實際上并不是建立空間而是建立映射函數所需要的資料結構。舉例來說,在x86的Linux下建立虛拟位址空間實際上隻是配置設定一個頁目錄(頁表)就可以了,甚至不設定頁映射關系,這些映射關系等到後面程式發生“缺頁”時在進行設定。

在進入execve()系統調用之後,Linux核心就開始進行真正的裝載工作。在核心中,execve()系統調用相應的入口是sys_execve(),作用:參數的檢查複制;調用do_execve(),流程:查找被執行的檔案,讀取檔案的前128個位元組以判斷檔案的格式是elf還是其它;調用search_binary_handle(),流程:通過判斷檔案頭部的魔數确定檔案的格式,并且調用相應的裝載處理程式。ELF可執行檔案的裝載處理過程叫load_elf_binary(),它的主要步驟如下:

1,檢查ELF可執行檔案格式的有效性,比如魔數、程式頭表中段的數量。

2,尋找動态連結的“.interp”段,找到動态連結器的路徑,以便于後面動态連結時會用上。

3,讀取可執行檔案的程式頭,并且建立虛拟空間與可執行檔案的映射關系。

【讀取可執行檔案的程式頭,并且建立虛拟空間與可執行檔案的映射關系】建立虛拟空間時的頁映射關系函數是虛拟空間到實體記憶體的映射關系,而這一步所做的事虛拟空間與可執行檔案的映射關系。我們知道,當程式發生缺頁是,作業系統會為實體記憶體配置設定一個實體頁,然後将該缺頁從磁盤中讀取到記憶體,在設定缺頁的虛拟頁與實體頁之間的映射關系,這樣程式才可以得以正常運作。但是明顯的一點是,當作業系統捕獲到缺頁錯誤時,他應當知道程式目前需要的頁在可執行檔案中的哪一個位置。而這就是虛拟存儲與可執行檔案之間的映射關系。實際上,這種映射關系僅僅是儲存在作業系統内部的一個資料結構。當發生缺頁錯誤是,CPU将控制權交給作業系統,作業系統利用專門的缺頁處理例程來查詢這個資料結構(映射關系),然後找到所需頁所在的虛拟記憶體區域,以及在可執行檔案的偏移,然後把該頁加載進實體記憶體,同時将該虛拟頁與實體頁之間建立映射關系,最後把控制權還給程序,程序從剛才缺頁位置重新開始執行。

4,初始化ELF程序環境。

5,将系統調用的傳回位址修改成ELF可執行檔案的入口點,這個入口點取決于程式的連結方式,對于靜态連結的ELF可執行檔案,它就是ELF檔案的檔案頭中e_entry所指的位址;對于動态連結的ELF可執行檔案,程式入口點就是動态連結器。

【将CPU指令寄存器設定成可執行檔案的入口,啟動運作】對動态連結來講,此時就啟動了動态連結器。

當load_elf_binary()執行完畢,傳回至do_execve()在傳回至sys_execve()時,系統調用的傳回位址已經被改寫成了被裝載的ELF程式的入口位址了。是以,當sys_execve()系統調用從核心态傳回到使用者态時,EIP寄存器直接跳轉到ELF程式的入口位址。此時,ELF可執行檔案裝載完成。接下來就是動态連結器對程式進行動态連結了。

動态連結基本分為三步:先是啟動動态連結器本身,然後裝載所有需要的共享對象,最後重定位和初始化。

1,動态連結器自舉

就我們所知道的,對普通的共享對象檔案來說,它的重定位工作是由動态連結器來完成;它也可以依賴于其他共享對象,其中被依賴的共享對象由動态連結器負責連結和裝載。那麼,對于動态連結器本身呢,它也是一個共享對象,它的重定位工作由誰完成?它是否可以依賴于其他的共享對象檔案?

動态連結器有其自身的特殊性:首先,動态連結器本身不可以依賴其他任何共享對象(人為控制);其次動态連結器本身所需要的全局和靜态變量的重定位工作由它自身完成(自舉代碼)。

我們知道,在Linux下,動态連結器ld.so實際上也是一個共享對象,作業系統同樣通過映射的方式将它加載到程序的位址空間中。作業系統在加載完動态連結器之後,就将控制權交給動态連結器。動态連結器入口位址即是自舉代碼的入口。動态連結器啟動後,它的自舉代碼即開始執行。自舉代碼首先會找到它自己的GOT(全局偏移表,記錄每個段的偏移位置)。而GOT的第一個入口儲存的就是“.dynamic”段的偏移位址,由此找到動态連結器本身的“.dynamic”段。通過“.dynamic”段中的資訊,自舉代碼便可以獲得動态連結器本身的重定位表和符号表等,進而得到動态連結器本身的重定位入口,然後将它們重定位。完成自舉後,就可以自由地調用各種函數和全局變量。

2,裝載共享對象

完成自舉後,動态連結器将可執行檔案和連結器本身的符号表都合并到一個符号表當中,稱之為“全局符号表”。然後連結器開始尋找可執行檔案所依賴的共享對象:從“.dynamic”段中找到DT_NEEDED類型,它所指出的就是可執行檔案所依賴的共享對象。由此,動态連結器可以列出可執行檔案所依賴的所有共享對象,并将這些共享對象的名字放入到一個裝載集合中。然後連結器開始從集合中取出一個所需要的共享對象的名字,找到相應的檔案後打開該檔案,讀取相應的ELF檔案頭和“.dynamic”,然後将它相應的代碼段和資料段映射到程序空間中。如果這個ELF共享對象還依賴于其他共享對象,那麼将依賴的共享對象的名字放到裝載集合中。如此循環,直到所有依賴的共享對象都被裝載完成為止。

當一個新的共享對象被裝載進來的時候,它的符号表會被合并到全局符号表中。是以當所有的共享對象都被裝載進來的時候,全局符号表裡面将包含動态連結器所需要的所有符号。

3,重定位和初始化

當上述兩步完成以後,動态連結器開始重新周遊可執行檔案和每個共享對象的重定位表,将表中每個需要重定位的位置進行修正,原理同前。

重定位完成以後,如果某個共享對象有“.init”段,那麼動态連結器會執行“.init”段中的代碼,用以實作共享對象特有的初始化過程。

此時,所有的共享對象都已經裝載并連結完成了,動态連結器的任務也到此結束。同時裝載連結部分也将告一段落!接下來便是程式的執行了。。。

執行部分

對于寫過c程式的人來說,一個公認的事實是:程式是從main函數開始的。然而,真的是 這樣嗎?其實不然,在程式執行到main函數之前,很多事情已經由入口函數(入口點)完成了。接下來将通過一個Linux下的可執行檔案p來說明它的執行過程。

Unix> ./p

因為p不是一個内置的shell指令,是以shell會認為p是一個可執行檔案,通過調用某個駐留在存儲器中稱為“加載器”的作業系統代碼來為我們運作之。裝載部分已經在上述部分較長的描述了。裝載完成後,控制權跳轉到程式的入口點,也就是符号_start的位址,在_start位址處的啟動代碼如下

0x080480c0 < _start>

Call _libc_init_first

Call _init

Call main

Call atexit

Call _exit

/* 一下不會執行 */  

首先從.init和.text節中調用初始化例程後,啟動代碼調用應用程式main程式,執行我們的c程式主體。在應用程式傳回後,啟動代碼調用atexit注冊的函數,然後調用_exit結束程序,将控制傳回給作業系統。

一個典型的程式的運作步驟大緻如下:

1,作業系統建立程序(裝載了),将控制權交給程式的入口,即運作庫的入口函數。

2,入口函數對運作庫和程式運作環境進行初始化,包括堆、I/O、線程、全局變量構造等等。

3,入口函數在完成這些初始化之後,調用main函數,正式開始執行程式的主體部分。

4,main函數執行完成後,傳回到入口函數,入口函數進行清理工作,包括全局變量析構、堆銷毀、關閉I/O等,然後進行系統調用來結束程序。

Hello程式的執行

    前面簡單描述了系統的硬體組成和操作,現在開始介紹當我們運作示例程式時到底發生了些什麼。在這裡我們必須省略很多細節稍後再做補充,但是從現在起我們将很滿意這種整體上的描述。  

    初始時,外殼程式執行它的指令,等待我們輸入一個指令。當我們在鍵盤上輸入字元串“./hello”後,外殼程式将字元逐一讀入寄存器,再把它存放到存儲器中,如圖1-5所示。

程式運作流程——連結、裝載及執行

 當我們在鍵盤上敲Enter鍵時,外殼程式就知道我們已經結束了指令的輸入。然後外殼執行一系列指令來加載可執行的hello檔案,将hello目标檔案中的代碼和資料從磁盤複制到主存。資料包括最終會被輸出的字元串“hello, world/n”。

一旦目标檔案hello中的代碼和資料被加載到主存,處理器就開始執行hello程式的main程式中的機器語言指令。這些指令将“hello, world/n”字元串中的位元組從主存複制到寄存器檔案,再從寄存器檔案中複制到顯示裝置,最終顯示在螢幕上。這個步驟如圖1-7所示。

程式運作流程——連結、裝載及執行

附錄 Windows下面可執行檔案a.exe的運作過程

一個microsoft的.exe程式的啟動過程如下:

     (1)當我們輕按兩下a.exe圖示啟動程式時,系統首先做什麼呢,讓我們先聽一聽侯捷是如何說的吧“執行起來的App程序其實是shell調用CreateProcess激活的”。很多書上都是如是說的,shell又名“指令解釋器”,是win32作業系統基于浏覽器的一個32位使用者接口,它是一個多線程的好例子,螢幕上每一個檔案夾浏覽視窗都是它的一個線程。它是作業系統引導時加載的系統程序,它具體表現為windows explorer.exe。explorer.exe是所有使用者應用程式的創造者。你完全可以将shell看成是所有應用程式程序的父程序,就像桌面(desktop)可看成所有視窗的父視窗一樣。shell的用途很多,如啟動應用程式,管理檔案系統,将應用程式與相應檔案相關聯等等。我們常見的桌面上的帶有小箭頭的快捷方式(shortcut)就是一個shell連結,shell負責管理一個叫"名字空間"的類似檔案系統似的“超檔案系統”,它允許應用程式在任何地方在不知通路對象名字和位置的前提下通路到這個對象,此類對象有:檔案,目錄,驅動器,列印機以及網絡資源。而名字空間就是shell把這些對象有層次組織起來的一個結構。名字空間為使用者和應用程式提供了一種可靠和高效的方法來通路和管理對象。好了不論它是什麼,凡正它調用了CreateProcess,一切就從這裡開始了。

     (2)CreateProcess這個函數可做了不少工作。a程序由此誕生。當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環境,系統就直接加載檔案的代碼和資料并輸入(import)該檔案執行所需的dll函數。如果不是win32環境比如時os/2的.exe則先加載相應的環境子系統,由該環境加載該檔案的代碼和資料以及該檔案執行所需的dll函數。至于系統是如何知道檔案的代碼和資料以及該檔案執行所需的dll函數所在的位置就需要你了解一下PE檔案格式了,其實也很簡單,PE檔案擁有很多sections,資料和代碼都放在不同的section裡面,檔案執行所需的dll也放在單獨的section(.idata)裡,這裡就不詳述了。

     (4)程序加載代碼和資料完畢後,就開始建立線程來執行程序空間内的代碼。程序是靜态的,它隻是線程的容器。一個程序至少因該有一個線程(main thread),其它線程都是主線程通過調用CreateThread函數建立的。線程也是核心對象,他的實際建立者是一個叫NtCreateThread的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,對應的入口點函數為wmain;c/c++運作時庫的啟動函數的功能如下:

以wWinMainCRTStartup(大多數運作在windows2000下的應用程式的啟動函數都是它)為例。它負責:

  *檢索指向新程序的完整指令行指針;

  *檢索指向新程序的環境變量的指針;

  *對c/c++運作時的全局變量進行初始化;

  *對c運作期的記憶體單元配置設定函數(比如malloc,calloc)和其他低層I/O例程使用的記憶體棧進行初始化。

  *為C++的全局和靜态類調用構造函數。

當這些初始化工作完成後,該啟動函數就調用wWinMain函數(相對于main函數)進入應用程式的執行。當wWinMain函數執行完畢傳回時,wWinMainCRTStartup啟動函數就調用c運作期的exit()函數,将傳回值(nMainRetVal)傳遞給它。之後exit()便開始收尾工作:

  *調用由_onexit()函數調用和注冊的任何函數。

  *為C++的全局和靜态類調用析構函數;

  *調用作業系統的ExitProcess函數,将nMainRetVal傳遞給它,這使得作業系統能夠撤銷進 程并設定它的exit 代碼。

  至此啟動函數的任務完成!

整整兩天,才把這小小一篇文章搞定,但是寫的過程中感覺非常的好,不斷地思考怎麼寫,怎麼組織,不斷地鍛煉自己吧!

繼續閱讀