首頁 > 軟體

C/C++ - 從程式碼到可執行程式的過程詳解

2023-01-16 14:03:04

(1)預編譯

主要處理原始碼檔案中的以“#”開頭的預編譯指令。處理規則見下:

刪除所有的#define,展開所有的宏定義。處理所有的條件預編譯指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。處理“#include”預編譯指令,將檔案內容替換到它的位置,這個過程是遞迴進行的,檔案中包含其
他檔案。刪除所有的註釋,“//”和“/**/”。保留所有的#pragma 編譯器指令,編譯器需要用到他們,如:#pragma once 是為了防止有檔案
被重複參照。新增行號和檔案標識,便於編譯時編譯器產生偵錯用的行號資訊,和編譯時產生編譯錯誤或警告是
能夠顯示行號。

(2)編譯

把預編譯之後生成的xxx.i或xxx.ii檔案,進行一系列詞法分析、語法分析、語意分析及優化後,生成相應
的組合程式碼檔案。

詞法分析:利用類似於“有限狀態機”的演演算法,將原始碼程式輸入到掃描機中,將其中的字元序列分
割成一系列的記號。語法分析:語法分析器對由掃描器產生的記號,進行語法分析,產生語法樹。由語法分析器輸出的
語法樹是一種以表示式為節點的樹。語意分析:語法分析器只是完成了對錶示式語法層面的分析,語意分析器則對錶示式是否有意義進
行判斷,其分析的語意是靜態語意——在編譯期能分期的語意,相對應的動態語意是在執行期才能
確定的語意。優化:原始碼級別的一個優化過程。目的碼生成:由程式碼生成器將中間程式碼轉換成目標機器程式碼,生成一系列的程式碼序列——組合語
言表示。目的碼優化:目的碼優化器對上述的目標機器程式碼進行優化:尋找合適的定址方式、使用位移
來替代乘法運算、刪除多餘的指令等。

(3)組合

將組合程式碼轉變成機器可以執行的指令(機器碼檔案)。 組合器的組合過程相對於編譯器來說更簡單,沒
有複雜的語法,也沒有語意,更不需要做指令優化,只是根據組合指令和機器指令的對照表一一翻譯過
來,組合過程有組合器as完成。經組合之後,產生目標檔案(與可執行檔案格式幾乎一樣)xxx.o(Windows 下)、xxx.obj(Linux下)。

(4)連結

將不同的原始檔產生的目標檔案進行連結,從而形成一個可以執行的程式。連結分為靜態連結和動態鏈
接:

靜態連結

函數和資料被編譯進一個二進位制檔案。在使用靜態庫的情況下,在編譯連結可執行檔案時,連結器從庫
中複製這些函數和資料並把它們和應用程式的其它模組組合起來建立最終的可執行檔案。

以下面這個圖來簡單說明一下從靜態連結到可執行檔案的過程,根據在原始檔中包含的標頭檔案和程式中使用到的庫函數,如stdio.h中定義的printf()函數,在libc.a中找到目標檔案printf.o(這裡暫且不考慮printf()函數的依賴關係),然後將這個目標檔案和我們hello.o這個檔案進行連結形成我們的可執行檔案。

這裡有一個小問題,就是從上面的圖中可以看到靜態執行庫裡面的一個目標檔案只包含一個函數,如libc.a裡面的printf.o只有printf()函數,strlen.o裡面只有strlen()函數。

我們知道,連結器在連結靜態連結庫的時候是以目標檔案為單位的。比如我們參照了靜態庫中的printf()函數,那麼連結器就會把庫中包含printf()函數的那個目標檔案連結進來,如果很多函數都放在一個目標檔案中,很可能很多沒用的函數都被一起連結進了輸出結果中。由於執行庫有成百上千個函數,數量非常龐大,每個函數獨立地放在一個目標檔案中可以儘量減少空間的浪費,那些沒有被用到的目標檔案就不要連結到最終的輸出檔案中。

缺點:
空間浪費:因為每個可執行程式中對所有需要的目標檔案都要有一份副本,所以如果多個程式對同一個
目標檔案都有依賴,會出現同一個目標檔案都在記憶體存在多個副本;
更新困難:每當庫函數的程式碼修改了,這個時候就需要重新進行編譯連結形成可執行程式。
執行速度快:但是靜態連結的優點就是,在可執行程式中已經具備了所有執行程式所需要的任何東西,
在執行的時候執行速度快。

動態連結

動態連結的基本思想是把程式按照模組拆分成各個相對獨立部分,在程式執行時才將它們連結在一起形成一個完整的程式,而不是像靜態連結一樣把所有程式模組都連結成一個單獨的可執行檔案。

假設現在有兩個程式program1.o和program2.o,這兩者共用同一個庫lib.o,假設首先執行程式program1,系統首先載入program1.o,當系統發現program1.o中用到了lib.o,即program1.o依賴於lib.o,那麼系統接著載入lib.o,如果program1.o和lib.o還依賴於其他目標檔案,則依次全部載入到記憶體中。當program2執行時,同樣的載入program2.o,然後發現program2.o依賴於lib.o,但是此時lib.o已經存在於記憶體中,這個時候就不再進行重新載入,而是將記憶體中已經存在的lib.o對映到program2的虛擬地址空間中,從而進行連結(這個連結過程和靜態連結類似)形成可執行程式。

優點:
共用庫:就是即使需要每個程式都依賴同一個庫,但是該庫不會像靜態連結那樣在記憶體中存在多分,副
本,而是這多個程式在執行時共用同一份副本;更新方便:更新時只需要替換原來的目標檔案,而無需將所有的程式再重新連結一遍。當程式下一次執行時,新版本的目標檔案會被自動載入到記憶體並且連結起來,程式就完成了升級的目標。
效能損耗:因為把連結推遲到了程式執行時,所以每次執行程式都需要進行連結,所以效能會有一定損失。

生成可執行檔案

什麼情況會編譯成功但連結失敗

(1)說明並使用了型別、函數、變數,但沒給出相應型別、函數或變數的定義。關於說明和定義的區別見上述教學,說明可以通過#include標頭檔案進行,也可以直接進行說明如int f( )或extern int f( )。若同時給出函數f的函數體則稱為定義。對於變數若有初始值就算定義,例如"extern int x=3; "便是定義,這種語法有其獨特應用背景。

(2)對標準庫的連線失敗,例如呼叫了sin(double x),卻找不到數學運算標準庫進行連線了,可能是安裝後因各種可能原因被刪除了。

(3)全域性變數和靜態變數存放在資料段,而固定大小的資料段不夠用了,例如定義太多的全域性變數和靜態變數,甚至巨型陣列全域性或靜態變數。

(4)資料段被偷用後無法容納較多的全域性變數和靜態變數,這種錯誤最難發現且最難改正。主要是一些常數在偷用資料段,例如字串常數"abc"等等,其它諸多情形參見上述教學。另外虛擬函式入口地址表等也會偷用。

到此這篇關於C/C++ - 從程式碼到可執行程式的過程的文章就介紹到這了,更多相關C++ 從程式碼到可執行程式的過程內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com