1. 程式人生 > >C/C++——程式實現過程之編譯、連結和執行

C/C++——程式實現過程之編譯、連結和執行

從寫一個簡單的“hello world!”到完成一個大型程式,當程式從編輯完成到執行成功都會經過5個步驟,分別是預處理(Prepressing)、編譯(Compilation)、彙編(Assembly)、連結(Linking)和執行(Executing)。瞭解這五個過程中所做的工作,對我們理解標頭檔案、庫檔案等在程式中的作用是有幫助的,而且如果能夠清楚的瞭解編譯連結過程,在程式設計時定位錯誤,糾正錯誤,以及程式設計時手動調整編譯器以通過除錯有很大幫助。

1.     預處理

      前處理器,進行預處理。預處理過程主要處理那些原始碼檔案以“#”開始的預編譯指令。比如“#include”、“#define”和條件預編譯指令,如“#if”、“#ifdef”等。預處理時,將所有的“#define”刪除,展開所有的巨集定義,並且替換掉“#include”。

(1)巨集定義指令,如#define a b。對於這種偽指令,預編譯所要做的是將程式中的所有a用b替換,還有#undef,則將取消對某個巨集的定義,使以後該串的出現不再被替換。

(2)條件編譯指令,如#ifdef,#ifndef,#else,#elif,#endif等。這些偽指令的引入使得程式設計師可以通過定義不同的巨集來決定編譯程式對哪些程式碼進行處理。預編譯程式將根據有關的檔案,將那些不必要的程式碼過濾掉。

(3) 標頭檔案包含指令,如#include"FileName"或者#include<FileName>等。兩者的區別是:系統提供的標頭檔案包含用尖括號,系統直接去系統目錄查詢檔案;自己寫的用雙引號,系統從工程目錄中查詢,如果沒有再去系統目錄查詢檔案。在標頭檔案中一般用偽指令#define定義了大量的巨集(最常見的是字元常量),同時包含有各種外部符號的宣告。採用標頭檔案的目的主要是為了使某些定義可以供多個不同的源程式使用。因為在需要用到這些定義的源程式中,只需加上一條#include語句即可,而不必再在此檔案中將這些定義重複一遍。預編譯程式將把標頭檔案中的定義統統都加入到它所產生的輸出檔案中,以供編譯程式對之進行處理。

預編譯程式所完成的基本上是對源程式的“替代”工作。經過此種替代,生成一個沒有巨集定義、沒有條件編譯指令、沒有特殊符號的輸出檔案。這個檔案的含義同沒有經過預處理的原始檔是相同的,但內容有所不同,經過預編譯後產生完整的原始檔,將此檔案作為編譯程式的輸入而被翻譯成為機器指令。

2.     對源程式進行編譯

經過預編譯得到的輸出檔案中,只有常量;如數字、字串、變數的定義,以及C語言的關鍵字等。編譯過程就是把預處理完的檔案進行一系列的詞法分析、語法分析、語義分析以及優化後產生相應的彙編程式碼檔案,這個過程是整個程式構建的核心部分,也是最複雜的部分之一。為了使計算機能執行高階語言源程式,必須先用一種稱為“編譯器(complier)”的軟體(也稱編譯程式或編譯系統)。編譯是以源程式檔案為單位單別編譯的,標頭檔案不參加編譯。

(在VC6.0裡如果編譯標頭檔案則會彈出沒有可以工具函式,在VS2013中,對於標頭檔案,編譯按鈕為灰色,不可用狀態。)

現在編譯器種類很多,不同編譯器區別在於對編譯過程做了優化,添加了一些庫函式或類庫。優化處理是編譯系統中一項比較艱深的技術。它涉及到的問題不僅同編譯技術本身有關,而且同機器的硬體環境也有很大的關係。優化一部分是對中間程式碼的優化。這種優化不依賴於具體的計算機。另一種優化則主要針對目的碼的生成而進行的。對於前一種優化,主要的工作是刪除公共表示式、迴圈優化(程式碼外提、強度削弱、變換迴圈控制條件、已知量的合併等)、複寫傳播,以及無用賦值的刪除,等等。後一種型別的優化同機器的硬體結構密切相關,最主要的是考慮是如何充分利用機器的各個硬體暫存器存放的有關變數的值,以減少對於記憶體的訪問次數。另外,如何根據機器硬體執行指令的特點(如流水線、RISCCISCVLIW)而對指令進行一些調整使目的碼比較短,執行的效率比較高,也是一個重要的研究課題。

編譯技巧:編譯的作用是對源程式進行詞法檢查、語法檢查和中間程式碼生成。編譯時對檔案中的全部內容進行檢查,如果有語法錯誤,編譯結束後會顯示出所有的編譯出錯資訊,開發人員可以根據錯誤提示修改程式。對於新寫的一個保護多個檔案的工程,一開始採用原始檔分別編譯,這樣容易發現每個原始檔的自身錯誤,限定了錯誤的範圍,如果一開始就採用全部編譯,多個原始檔可能會產生許多錯誤,無形中增加了開發難度。如果每個原始檔都通過了編譯,再將所有檔案進行編譯。對原始檔分別編譯對於除錯,糾錯是一種很好的方法。

3.     彙編

彙編實際上指把組合語言程式碼翻譯成目標機器指令的過程。彙編器的編譯過程相對於編譯器來講比較簡單,它沒有複雜的語法,也沒有語義,也不需要做指令優化,只是根據彙編指令和機器指令的對照表一一翻譯。對於被翻譯系統處理的每一個語言源程式,都將最終經過這一處理而得到相應的目標檔案。目標程式一般以.obj或.o作為字尾,這具體看作業系統,如Windows是下是.obj目標檔案,Linux下是.o目標檔案。目標檔案中所存放的也就是與源程式等效的目標機器語言程式碼。

有時候我們也將預編譯、編譯和彙編統稱為編譯。

4.     將目標檔案連線

前面提到過,編譯是對原始檔分別進行的,每個原始檔都產生一個目標檔案。但由彙編程式生成的目標檔案並不能立即就被執行,因為各個原始檔之間可能是有相互聯絡的,例如,某個原始檔中的函式可能引用了另一個原始檔中定義的某個符號(如變數或者函式呼叫等);在程式中可能呼叫了某個庫檔案中的函式,等等。所有的這些問題都需要經連結解決,即將源程式產生的多個目標檔案連結為一個整體。即通過系統提供的“連線程式(linker)”將一個程式的所有目標程式和系統的庫檔案以及系統提供的其他資訊連線起來,最終形成一個可執行的二進位制檔案,它的字尾是.exe,此時產生了完整的執行檔案。

連結程式的主要工作就是將有關的目標檔案彼此相連線,如原始檔產生的目標檔案和庫檔案等,使得所有的這些目標檔案成為一個能夠被作業系統裝入執行的統一整體。根據指定的庫函式的不同,連結處理可分為兩種:

(1)靜態連結:在這種連結方式下,函式的程式碼將從其所在地靜態連結庫中被拷貝到最終的可執行程式中。這樣該程式在被執行時這些程式碼將被裝入到該程序的虛擬地址空間中。靜態連結庫實際上是一個目標檔案的集合,其中的每個檔案含有庫中的一個或者一組相關函式的程式碼。

(2)動態連結:此種方式下,函式的程式碼被放到稱作是動態連結庫或共享物件的某個目標檔案中。連結程式此時所作的只是在最終的可執行程式中記錄下共享物件的名字以及其它少量的登記資訊。在此可執行檔案被執行時,動態連結庫的全部內容將被對映到執行時相應程序的虛地址空間。動態連結程式將根據可執行程式中記錄的資訊找到相應的函式程式碼。

對於可執行檔案中的函式呼叫,可分別採用動態連結或靜態連結的方法。使用動態連結能夠使最終的可執行檔案比較短小,並且當共享物件被多個程序使用時能節約一些記憶體,因為在記憶體中只需要儲存一份此共享物件的程式碼。但並不是使用動態連結就一定比使用靜態連結要優越。 

連結將相關關聯檔案連結起來,所以這個階段的錯誤不好除錯,發生錯誤可能在我們自己編寫的程式碼中,也有可能是與別的檔案關聯產生的,對於因關聯產生錯誤就比較複雜了,有時需要調整編譯器或連結器。

5.     執行程式

執行階段就比較簡單了,直接執行前面連結過程產生的可執行的二進位制檔案(.exe檔案)即可得到執行結果。通過對執行結果的分析,檢驗設計的程式是否滿足期望和要求。如果執行結果不正確,應檢查程式或演算法,重新編輯程式碼。

        上述全過程可用下圖表示:


參考文獻: