程序運行流程——鏈接、裝載及執行

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

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

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

 

虛擬存儲器與虛擬地址空間是兩個不同的概念:虛擬存儲器是假想的存儲器,而虛擬存儲空間是假想的內存。它們之間的關係應該與主存儲器與內存空間之間的關係類似。

鏈接部分:

鏈接就是將不同部分的代碼和數據收集和組合成一個單一文件的過程,也就是把不同目標文件合併成最終可執行文件的過程。當然,務必知道:這個過程不涉及內存。鏈接可以分爲三種情形:1,編譯時鏈接,也就是我們常說的靜態鏈接;2,裝載時鏈接;3,運行時鏈接。裝載時鏈接和運行時鏈接合稱爲動態鏈接。在此,我們的鏈接部分將主要講述靜態鏈接,而裝載時鏈接我們放在裝載部分講,運行時鏈接忽略。

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


編譯這兩個文件得到“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變量的地址修正。

S:shared的實際地址爲0x3000;

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

所以最後這個重定位修正地址爲:0x3000,不變!

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

S:符號swap的實際地址,即0x2000;

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

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地址處的啓動代碼如下

 

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

一個典型的程序的運行步驟大致如下:

1,操作系統創建進程(裝載了),將控制權交給程序的入口,即運行庫的入口函數。

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

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

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

Hello程序的執行

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

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

 當我們在鍵盤上敲回車鍵時,外殼程序就知道我們已經結束了命令的輸入。然後外殼執行一系列指令來加載可執行的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 代碼。

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

 

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

發佈了259 篇原創文章 · 獲贊 7 · 訪問量 51萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章