📘前言
書接上文,我們已經學習了 Linux 中的編輯器 vim 的相關使用方法,現在已經能直接在 Linux 中編寫C/C++代碼,有了代碼之后就要嘗試去編譯并運行它,此時就可以學習一下 Linux 中的編譯器 gcc/g++ 了,我們一般使用 gcc 編譯C語言,g++ 編譯C++(當然 g++ 也可編譯C語言),這兩個編譯器我們可以當作一個來學習,因為它們的命令選項都是通用的,只是編譯對象不同。除了編譯器相關介紹外,本文還會庫、自動化構建工具、提權等知識,一起來看看吧
?📘正文
📖gcc/g++ 命令
在接下來的學習中,我們以 gcc 為例,因為兩者選項都是通用的,所以也就相當于間接學習了 g++ ,這個編譯器上手還是很簡單的,選項也不是很多
注意: 如果命令失效,很有可能是沒有下載 gcc/g++ ,需要自行下載安裝 gcc 與 g++
📃-o 目標文件
gcc 源文件 默認會將代碼編譯鏈接并生成可執行文件 a.out ,當然前提是代碼沒問題,所以這樣看來編譯一個文件還是很簡單的
$ gcc 源文件 //直接編譯源文件,生成默認可執行文件為 a.out
?可能有的人不想讓它生成默認的?a.out
?,想生成為指定文件,沒有問題,直接通過?-o
?選項就能實現
注意:-o
?選項后面必須緊跟生成的目標文件,這個選項可以放在源文件后面,也可以放在前面
$ gcc test.c -o OK //編譯生成文件為 OK
$ gcc -o OK test.c //這種寫法也是可以的
?在我們使用?gcc/g++
?時,都可以通過?-o
?選項生成指定文件?
📃-E 預處理
在C語言學習階段,我們學習了源文件變成可執行文件的過程,即預處理-編譯-匯編-鏈接,當時因為沒有學習Linux,沒法很好的展示各個環節的現象,今天可以來詳細看看
首先是第一步:預處理,又稱預編譯
會進行頭文件展開、刪除注釋、替換宏、執行條件編譯等操作
目的是生成一個純粹的C代碼程序
經過預處理后的文件后綴為 .i
我們可以直接通過 gcc 中的 -E 命令,使編譯器在執行完預處理后停下來,配合 -o 生成指定文件,這樣我們就可以觀察到上面所提到的這些現象了。
$ gcc -E test.c -o test.i //預處理后的文件后綴為 .i 此時仍然是C語言
📃-S 編譯
下面進入第二個步驟:編譯
進行語法分析、詞法分析、語義分析、符號匯總等,然后將合法的代碼轉為匯編代碼
編譯目的是生成匯編代碼
編譯后生成的文件后綴為 .s
編譯階段比較重要的一步就是符號匯總,它會各種符號匯總起來,方便后續符號表的形成,符號表用于各種函數間的相互調用
我們可以通過 -S 選項,使 gcc 在執行完編譯階段后就停下來,配合 -o 生成文件 test.s
$ gcc -S test.c -o test.s //可以直接從 test.c 開始執行,也可以從上一步中的 test.i 執行
?📃-c 匯編
接下來進入第三步:匯編
主要任務是將匯編代碼轉為二進制,并生成符號表
二進制文件的格式是 elf ,此時 vim 查看為亂碼
生成的文件后綴為 .o
因為計算機只能看懂二進制,所以將代碼轉為二進制是必須進行的操作,除此之外,還有一個重要步驟:生成符號表
關于符號表
這個東西相當于函數獨一無二的地址,在Linux 中,C語言的符號表比較簡單,通常是 _函數名,比如 _Add ;C++更詳細一些,通常為 _Z函數名長度+函數名+參數1+參數2 ,比如常見的 Add 函數,生成的符號表為 _Z3Addii ,這里的參數是兩個整型,這也是C++支持重載,而C語言不支持重載的根本原因,畢竟C語言中兩個重名的函數生成的符號表是完全一樣的,區分不了
可以通過 -c 選項使 gcc 在執行完匯編階段后就停下來,指定保存文件為 test.o
查看生成的 test.o 文件,可以用 readelf 這個工具,缺失的可以去下載
$ gcc -c test.c -o test.o //從源文件重新開始編譯,生成 test.o 二進制文件
$ gcc -c test.s -o test.o //從上一步中生成的 test.s 文件開始編譯,兩者效果是一樣的//關于查看 elf 格式的文件
$ readelf -a test.o //可以通過軟件,觀察到符號表等信息
📃gcc 鏈接
下面是最后一步:鏈接
進行合并段表、將符號表進行合并和重定位等
將程序運行所需的各種函數鏈接起來,包括與庫函數的鏈接,Linux 中一般是動態鏈接,鏈接后生成可執行文件,此時的文件也是 elf 的格式
gcc 默認生成的可執行文件為 a.out,我們可以指定生成任意文件
$ gcc test.c -o myfile //生成可執行文件為 myfile
$ gcc test.o -o myfile //繼上一次生成的二進制文件執行鏈接,也是沒有問題的
?📃小結
關于各個命令選項可以巧記為?ESc
?這是鍵盤上的一個鍵,忘記了可以看看
還有各個選項對應生成的文件后綴為?iso
📖庫
1. 標準庫的位置:
-
標準庫頭文件:標準庫的頭文件(如
stdio.h
、string.h
、stdlib.h
等)通常存放在/usr/include
目錄下。這些頭文件提供了庫函數的聲明和宏定義,函數的具體實現都在庫中了。它們是編寫 C 程序時必須包含的文件。 -
標準庫的實現:標準庫的實際實現(即函數的定義和邏輯)并不在
/usr/include
目錄下,而是通常存放在系統的庫目錄中,比如/lib
或/usr/lib
。這些庫可能是 靜態庫(以.a
為擴展名)或 動態庫(以.so
為擴展名)
📃動態庫
動態庫 即通過 動態鏈接 的庫,動態庫 又稱 共享庫,因為 動態庫 中的內容是被所有程序共享的,簡言之 動態庫 中的代碼只需要存在一份,程序需要使用時,直接通過對應位置調用就行了
Linux 中默認使用 動態鏈接 的方式,我們可以通過指令 ldd 最終生成的文件 來查看最終生成文件的鏈接情況
$ ldd 最終生成的文件 //查看文件的鏈接情況
?libXXX.so 是動態鏈接的標志
其中 lib 是前綴
.so 是后綴
去掉前綴與后綴,就是最終調用的庫
舉例:libc.so 去掉前綴與后綴,最終為 c ,可以看出文件最終調用的是C語言共享庫,即 動態鏈接
動態鏈接 主要依賴不同函數在庫中的位置信息進行調用,只有一份代碼庫,比較節省空間
我們還可以通過 file 命令查看文件詳細信息
$ file 最終生成的文件 //查看文件的詳細情況
?📃靜態庫
除了 動態庫 外,還有 靜態庫 ,采用 靜態鏈接 的方式;靜態鏈接 不同與 動態鏈接 共享的方式,如果程序調用 靜態庫 ,會將自己所需要的代碼 拷貝至程序中 ,完成拷貝后,后續不需要再調用 靜態庫
如果想采用 靜態鏈接 鏈接的方式編譯程序,需要在編譯時加上 -static 選項,當然前提是得有 靜態庫,沒有的可以通過 yum install -y glibc-static 下載 靜態庫
當然我們也可以通過 ldd 最終生成的文件 查看是否為 靜態鏈接
$ yum install -y glibc-static //下載靜態庫
$ gcc test.c -o myfile-static -static //采取靜態鏈接的方式編譯程序
$ ldd 最終生成的文件 //查看文件的鏈接方式
動態庫 vs 靜態庫的優缺點對比:
區別 | 動態庫 | 靜態庫 |
---|---|---|
調用方式 | 通過函數位置(動態鏈接)進行調用 | 直接將需要的函數拷貝至程序中(靜態鏈接) |
依賴性(運行時) | 需要依賴于動態庫文件,運行時必須能找到對應的 .so 文件 | 不依賴外部庫,程序可以獨立運行 |
空間占用 | 共享動態庫中的代碼,多個程序共享同一個庫,節省空間 | 每個程序都包含庫代碼,導致文件較大 |
加載速度 | 調用時需要加載庫并進行鏈接,加載速度慢 | 直接運行,程序中已經包含了庫的代碼,加載速度快 |
更新 | 更新庫時,無需重新編譯程序,方便管理和維護 | 更新庫時需要重新編譯程序,管理較為繁瑣 |
版本兼容性 | 可能會遇到版本不兼容問題(“DLL Hell”) | 一旦編譯完成,不受庫版本變化影響 |
內存占用 | 多個程序可以共享同一個庫文件,節省內存 | 每個程序都占用一份內存空間 |
動態庫的優點:
-
共享代碼:動態庫可以在多個程序之間共享,節省磁盤空間和內存。對于大型程序和多個程序共享同一個庫的情況,動態庫非常有用。
-
程序小巧:因為動態庫不包含在每個可執行文件中,所以生成的程序文件較小。
-
更新簡便:如果庫的功能有更新,只需要替換庫文件,無需重新編譯所有依賴這個庫的程序。這使得系統升級和維護更加方便。
-
內存共享:多個程序運行時,可以共享動態庫中的代碼和數據,節省內存。
動態庫的缺點:
-
加載速度較慢:由于程序在運行時需要加載和鏈接動態庫,調用速度相對較慢,特別是在頻繁調用庫函數的情況下。
-
運行時依賴性:程序需要在運行時找到并加載正確的動態庫版本。如果缺少動態庫或版本不兼容,程序可能無法正常運行(例如,缺少
.so
文件)。 -
版本問題:如果系統中多個程序依賴于同一個動態庫,而庫的版本發生變化時,可能會導致“版本不兼容”(DLL Hell)的問題。
靜態庫的優點:
-
獨立性:靜態庫在編譯時就已鏈接到可執行文件中,程序不依賴外部的庫文件,減少了運行時的復雜性。
-
加載速度快:靜態庫的代碼已經包含在程序中,程序啟動時不需要額外加載庫,加載速度較快。
-
無需擔心版本問題:由于靜態庫在編譯時就已經鏈接到程序,程序和庫的版本不會再發生兼容性問題。
靜態庫的缺點:
-
空間占用大:每個程序都需要包含靜態庫的副本,因此生成的可執行文件較大,浪費存儲空間。
-
更新麻煩:如果需要更新庫,必須重新編譯程序,這對于大型項目或多個依賴同一庫的項目來說,管理和更新較為麻煩。
-
內存占用多:每個運行的程序都加載靜態庫的代碼,占用更多內存,而動態庫則可以被多個程序共享內存。
總結:
-
動態庫適用于:
-
需要多個程序共享同一份代碼庫的場景,尤其是在內存和磁盤空間有限的情況下。
-
程序開發周期較長,庫需要經常更新,且更新后不想重新編譯所有依賴程序的情況。
-
對更新靈活性要求較高,且能夠接受可能出現的加載速度和依賴問題。
-
-
靜態庫適用于:
-
對程序啟動速度要求較高,且不依賴外部庫的場景。
-
程序體積可以接受,且不需要頻繁更新庫的情況。
-
獨立部署的應用程序,不想擔心外部庫的兼容性問題。
-
選擇使用動態庫還是靜態庫,通常要根據具體項目的需求、系統資源以及維護成本來決定。如果項目中有多個依賴共享的庫文件,動態庫往往是更好的選擇;而如果項目需要更高的執行效率或獨立性,靜態庫可能更適合。