【Linux】gcc--Linux上最標準的編譯程式
什麼是編譯程式(編譯器)?
編譯器就是將“一種語言(通常為高階語言)”翻譯為“另一種語言(通常為低階語言)”的程式。
因為高階計算機語言便於人編寫和閱讀交流以及維護,而機器語言是計算機能直接解讀和執行的。所以編譯器將彙編或高階計算機語言源程式作為輸入,翻譯成目標語言機器程式碼的等價程式。這個翻譯的過程稱為編譯。
一個編譯器工作的主要流程包括以下三步:預處理、編譯、彙編和連結。先總體來看:
上面的每一步都必須經過gcc編譯器如下處理:
gcc [選項] 要編譯的檔案 [選項] 將生成的檔案
預處理(進行巨集替換)
預處理功能主要包括:巨集替換、檔案包含、條件編譯和去註釋等。
預處理階段:gcc -E .c檔案 -o .i檔案
選項“-E”的作用:使gcc在預處理結束後停止編譯過程。
選型“-o”是指目標檔案(即該階段將會生成的檔案),“.-i”檔案為已經過預處理的C原始程式。
編譯(生成彙編程式碼)
編譯:gcc主要檢查是否有語法錯誤和程式碼的規範性,以確定程式碼實際要做的工作,在檢查無誤後,gcc把程式碼翻譯為組合語言。
編譯階段:gcc -S .i檔案 -o .s檔案
選項“-S”的作用:使gcc在編譯階段結束,生成彙編程式碼後停止編譯過程,不進行彙編。
彙編(生成機器可識別的程式碼)
彙編:把編譯階段生成的“.s”檔案轉成目標檔案。
彙編階段:gcc -c .s檔案 -o .o檔案
選項“-c”的作用:檢視已生成的二進位制目的碼。
連結(生成可執行檔案或庫檔案)
連結階段:gcc .o檔案 -o [可執行檔名]
在生成目標檔案後,有的時候,還需要在程式當中引用和呼叫其他的外部子程式,或是利用其他軟體提供的“函式功能”,此時就必須早編譯的過程中將該函式庫加入進去。這樣,編譯程式就可以將所有的程式程式碼與函式庫做一個連結
什麼是函式庫?
函式庫:類似子程式的角色,可以被呼叫來執行的一段功能函式。
舉個例子:在C程式中,並未定義“printf”函式的實現,且在預編譯中也只包含該函式的宣告,那麼“printf”函式是在哪實現的呢?原來:系統把這些函式實現都放置在了名為libc.so.6的庫檔案中,在沒有特別指定時,gcc會到系統預設的搜尋路徑/usr/lib下進行查詢,也就是連結到libc.so.6庫函式中去,這樣就實現了“printf”函式的並達到了連結的目的。
函式庫按照被使用的型別分為靜態函式庫與動態函式庫兩類。
靜態函式庫(Static)
靜態庫在編譯的時候會直接整合到可執行程式當中,所以利用靜態函式庫編譯成的檔案會比較大
當然,靜態庫也有其優點:編譯成功的可執行檔案可以獨立執行**,而不需要再向外部要求讀取函式庫的內容。
關於靜態庫的升級:雖然可執行檔案可以獨立執行,但因為函式庫是直接整合到可執行檔案中,因此若函式庫升級時,整個可執行檔案必須重新編譯才能將新版的函式庫整合到程式當中。即:只要函式庫升級了,所有將此函式庫納入的程式都需要重新編譯。
動態函式庫(Dynamic)
動態庫與靜態庫被整個捕捉到程式中不同:動態函式庫在編譯時並沒有把庫檔案的程式碼加入到可執行檔案中,而是在程式執行時由執行時連結檔案載入庫(即在程式裡面只有一個**“指向”的位置而已,當可執行檔案要使用到函式庫的機制時,程式才會讀取函式庫來使用),所以它的檔案會比較小一點。這類函式庫的字尾名通常為“.so”。gcc在編譯時預設使用動態庫,gcc預設生成的二進位制程式是動態連結的。
這類函式庫所編譯出來的程式不能被獨立執行**,因為當我們使用函式庫的機制時,程式才會去讀取函式庫,所以函式庫檔案必須要存在才行,而且,函式庫的所在目錄也不能改變,因為我們的可執行檔案裡面只有“指標”,當要取用該動態函式庫時,程式會主動去某個路徑下讀取,所以動態函式庫不能隨意移動或刪除。
關於動態庫的升級:雖然這型別的可執行檔案不能被獨立執行,然而由於其具有指向的功能,所以,當函式庫升級後,可執行檔案根本不需要進行重新編譯,因為可執行檔案會直接指向新的函式庫檔案(前期是新舊版本的函式庫檔名必須相同)。
Linux下更傾向於使用動態函式庫,那麼如何判斷某個可執行的二進位制檔案含有什麼動態函式庫呢?
這裡要用到一個命令:ldd
語法:ldd [-vdr] 檔名
功能:
①“-v”:列出所有內容資訊;
②“-d”:重新將資料有丟失的連結點顯示出來;
③“-r”:將ELF有關的錯誤內容顯示出來
舉例說明gcc編譯詳細過程!
(1)新建一個(.c)程式檔案,編寫一段程式程式碼(原始碼):
(2)直接進行編譯:
產生一個編譯成功的可執行檔案a.out,即該檔案內的內容就是機器可識別的語言:
(3)開始執行該可執行檔案a.out:
①該檔案所在位置(絕對路徑):
②相對路徑:
上面兩種執行方式所得結果相同。
在預設狀態下,如果我們直接以gcc編譯原始碼,並且沒有加上任何引數,則執行檔案的檔名會被自動設定為a.out,所以就能直接執行./a.out這個檔案。
那麼,若我們想自己對產生的可執行檔案命名,該如何進行編譯?
如上圖:我在原有的命令之後加上了“-o”選項,該選項作用是將檔案輸出到檔案,然後將可執行檔案命名為ret,最後執行該檔案,得出了相同的結果。
已知,gcc的編譯過程分為四個階段:預處理、編譯、彙編和連結,下面利用原始檔gcct.c詳細說明這四個階段:
(1)預處理:gcc -E gcct.c -o gcct.i
“-o”後面的檔名可隨意指定。進行上面的第一步編譯(成功)後產生了一個檔案gcct.i,開啟它(下圖僅為其中一部分):
(2)編譯:gcc -S gcct.i -o gcct.s
編譯成功之後產生了一個檔案gcct.s,開啟它:
該檔案中的內容就是編譯生成的彙編程式碼。
(3)彙編:gcc -c gcct.s -o gcct.o
編譯成功之後產生了一個檔案gcct.o,開啟它(下圖僅為其中一部分):
此時,其中的內容機器已經可以識別,但是它還不能被執行,因為在原始碼中包含一個函式printf,該函式(庫函式)依賴了作業系統中的函式庫。上面我們已經說過,目標檔案gcct.o要被執行,還應該告訴作業系統函式printf是如何實現的,即將該函式所在函式庫連結過來,所以第四個階段:
(4)連結:gcc gcct.o -o ret
連結成功!下面看一下檔案ret連結的(動態)庫的詳細資訊:
其中libc.so.6,“c”為庫名,“lib”為庫的字首,“.so”為字尾,“6”為版本號。
再想想,如果原始碼檔案不只是一個,而有多個,此時應該如何對程式進行編譯?
下面編寫了兩個程式t1.c和t2.c,用t1.c去呼叫t2.c:
接下來將原始碼編譯成可執行的二進位制檔案並執行它:
【說明】由於原始碼檔案有時並非只有一個檔案,所以無法直接進行編譯。此時,就需要先生成目標檔案,然後再以連結製作成二進位制的可執行檔案。此外,當更新了t2.c這個檔案的內容,則只需重新編譯t2.c來產生新的t2.o檔案,然後再以連結製作出新的可執行檔案即可,而不必重新編譯其他未修改過的原始檔。
如果我們想讓程式在編譯的過程中具有比較好的效能或產生更詳細的資訊,可以加適當的引數:
(1)“-Wall”:產生編譯過程中的所有警告資訊。
(2)“-O”:在編譯的時候,依據操作環境給予優化執行速度,會自動的生成目標檔案,並進行優化。
(3)“-l”:加入某個函式庫。
先看一段原始碼:
直接對其進行編譯,沒有成功:
警告資訊:沒有sin的相關定義參考值。這是因為C語言裡面的sin函式是寫在libm.so這個函式庫中的,而在原始碼中並未將該函式庫功能加進去,所以就需要在編譯與連結的時候將這個函式庫連結進執行檔案裡面。