編譯與鏈接有四個過程:
(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)靜態庫速度更快一些。
————————————————
版權聲明:本文為CSDN博主「scut_yp」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/ypshowm/article/details/89374706