c++從源文件到可執行文件的步驟詳解

編譯與鏈接有四個過程:

1)預處理

2)編譯

3)彙編

4)鏈接

 

 

(1)預處理

源文件和頭文件被預處理成一個.i文件、(-E表示只進行預處理)

g++ -E hello.cpp -o hello.i

-E:意味着只執行到預編譯,直接輸出預編譯結果。

預處理過程主要處理那些源文件中的以“#”開始的預編譯指令。包括#include,#define, #if,等等。

主要的處理規則如下:

(1)將所有的#define刪除,並且展開所有的宏。

如#define a b  就是將所有的a替換成b。但作爲字符串常量a則不替換。

(2)處理所有的條件預編譯指令,,如#if,#ifdef,#else,#endif(以此來決定對哪些代碼進行處理,將那些不必要的

代碼過濾掉)

(3)處理#include預編譯指令,將被包含的文件插入到該預編譯指令的位置。()這個過程是遞歸進行的。其中系統提供的頭文件一般放在/usr/include下面,用<>表示。開發人員自定義的頭文件放在與源程序同一個目錄下,用“”表示。

(4)過濾所有的註釋“//“和”/*  */“之間的內容。

(5)添加行號和文件名標識。比如 #2  "test.c" 2

(6)保留所有的#pragma編譯器指令,因爲編譯器需要使用他們。(下面是pragma的一些參數,詳情看 https://baike.baidu.com/item/%23pragma

 

 

(2) 編譯,彙編

編譯過程就是把預處理的文件進行一系列的詞法分析語法分析語義分析以及優化後產生相應的彙編代

碼文件。相當於:

g++ -S hello.i -o hello.s

-S(大寫):表示只執行到源代碼到彙編代碼的轉換,輸出彙編代碼。

編譯器就是將高級語言翻譯成機器語言的一個工具。

編譯過程分爲6步:

詞法分析(掃描)

語法分析

語義分析

源代碼優化

(其實應該上面四個才叫編譯)

(下面兩個叫彙編了)

代碼生成

目標代碼優化

 

由一個例子來分析:

array[index]=(index+5)*(2+7)

 

1)詞法分析(掃描)

運用類似於有限狀態機的算法將源代碼的字符分割成一系列的記號。如上面的,一共包含28個非空字符

串,產生了16個記號。

詞法分析產生的記號一般分爲幾種:

關鍵字

標識符

字面量(數字,字符串等)

特殊記號(加號,等號等)

另外,掃描器也完成其他一些工作,比如將標識符存放到符號表中,將數字,字符串常量存放到文字

詞法分析工具(lex)

 

2)語法分析

將由掃描器產生的記號進行語法分析,從而產生語法樹。

語法樹:以表達式爲結點的樹。(c語言中,一個語句就是一個表達式)

 

另外,在語法分析時,很多運算符的優先級和含義也被確定下來。

語法分析工具(yacc)

 

3)語義分析

就是看看這個語句是否有意義。比如兩個指針相乘,這是沒有意義的,不過在語法分析的時候是合法的。

編譯器能分析的語義是靜態語義

靜態語義:在編譯期間可以確實能的語義

動態語義:在運行期間才能確定的語義,比如將0作爲除數是一個運行期語義錯誤。

靜態語義通常包括聲明類型的匹配以及類型的轉換

如果有寫了類型轉換需要做隱式轉化,語義分析程序會子啊語法中插入相應的轉換節點。

語義分析也對符號表裏面的符號做了更新

 

4)中間語言的生成

源代碼優化器會在源代碼級別進行優化。上面那個例子中,(2+7)被優化成9。

中間代碼一般跟目標機器和運行時環境是無關的,比如不包含數據的尺寸,變量的地址和寄存器的名字等

等。

中間代碼使得編譯器可以被分爲前端和後端。

前端負責產生機器無關的中間代碼

後端負責將中間代碼轉換爲目標機器代碼

這樣,對於一個跨平臺的編譯器而言,可以針對不同的平臺使用同一個前端和針對不同的機器平臺的後端

個數

 

 

5)目標代碼的生成與優化。(這兩個其實就是彙編)

編譯器後端包含:

代碼生成器(彙編):將中間代碼轉換成目標機器代碼,這個代碼十分依賴於目標機器,因爲不同的機器有着

不同的字長,寄存器,整數數據類型和浮點數數據類型等。

目標代碼優化器:對上面的代碼進行優化,選擇合適的尋址方式,使用位移來代替乘法等。

 

對於index和array,如果他們定義在和源代碼同一個編譯單元裏面,那麼編譯器可以爲他們分配空

間,確定他們的地址。如果他們定義只其他模塊裏面,(定義在其他模塊的全局變量和函數在最終運行時的絕對地址都要在最終鏈接的時候才能確定

 

(3)鏈接

從原理上來說,是把一些指令對其他符號地址的引用加以修正。

 

鏈接過程主要包括:

地址和空間分配

符號決議

重定位

 

庫就是一組目標文件的包,就是一些常用的代碼編譯成目標文件以後打包存放。

 

重定位:就是一開始編譯器在不知道變量的目標地址的情況下,先將目標地址設爲0,鏈接以後再將這個地址修改爲它真正的目標地址。這個地址修正的過程就叫做重定位。

 

每個目標文件除了擁有自己的數據和二進制代碼以外,還提供三個表:

1)未解決符號表:提供了所有在該編譯單元引用但是定義不是在本編譯單元的符號以及其出現

地址。

2)導出符號表:提供了本編譯單元具有定義,並且願意提供給其他單元使用的符號及其地址。

3)地址重定向表:提供了本編譯單元對所有對自身地址的引用的記錄。

編譯器將extern聲明的變量置入未解決符號表,而不置入導出符號表。這屬於外部鏈接。

編譯器將static聲明的全局變量不置入未解決符號表,也不置入導出符號表,因此其他單元無法使

用,這屬於內部鏈接。

普通變量及其函數被置入導出符號表。

 

 

鏈接包含靜態鏈接和動態鏈接。

1)靜態鏈接。

對函數庫的鏈接是放在編譯時期完成的是靜態鏈接。這些函數庫被稱爲靜態庫,通常爲”libXXX.a“形

式。

 

如有5個文件:add.h,add.cpp,sub.h,sub.cpp,main.cpp

先將add.cpp,sub.cpp編譯成.o文件

g++ -c add.cpp

g++ -c sub.cpp

無論是靜態庫文件還是動態庫文件,都是由“.o”文件創建的。

由.o文件創建靜態庫(.a)文件,執行命令:

  ar cr libmymath.a sub.o add.o

這樣就會生成libmymath.a文件。其中lib是庫文件的開頭命名規範,mymyth是庫名字,".a"說明是靜態

庫。

 

在後面指定了 -lmymath。這樣g++會在靜態庫名前加上前綴lib,然後追加擴展名.a得到的靜態庫文件名

來查找靜態庫文件。

 

2)動態鏈接

 

動態鏈接用如下命令:

 

 

 

g++ -o main main.cpp -L. -lmypath   (注意大寫的L後面還有個“.”,表示當前目錄)。

上面的鏈接是正常的,但是執行的時候回出錯。

動態庫搜索路徑爲;

(1)編譯目標代碼時指定的動態庫搜索路徑

(2)環境變量LD_LIBRARY_PATH指定的動態庫搜索路徑

(3)配置文件/etc/ld.so.conf中指定的動態庫搜索路徑;即只需在該文件中追加一行庫所在的完整

路徑如“root/test/conf/lib”即可;然後ldconfig是修改生效

(4)默認的動態庫搜索路徑/lib.

(5)默認的動態庫搜索路徑/usr/lib。

 

   

 

 

3)動態庫與靜態庫重名問題

當靜態庫文件和動態庫文件同名的時候,編譯器會先到path目錄下搜索libXXX.so(動態庫文件),如果沒

有找到,這繼續搜索libXXX.a(靜態庫文件);(先找動態庫文件,若沒找到,再找靜態庫文件

 

 

4)靜態庫鏈接,動態庫鏈接各自的特點

1)動態庫鏈接有利於進程間資源共享。

 

2)將一些程序升級變得簡單。

 

3)甚至可以真正做到鏈接載入完全由程序員在程序代碼中控制

 

4)靜態庫速度更快一些。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章