GCC 指令詳解及動態庫、靜態庫的使用方法
一、GCC
1.1 GCC 介紹
GCC 是 Linux 下的編譯工具集,是「GNU Compiler Collection」的縮寫,包含 gcc、g++ 等編譯器。這個工具集不僅包含編譯器,還包含其他工具集,例如 ar、nm 等。
GCC 工具集不僅能編譯 C/C++ 語言,其他例如 Objective-C、Pascal、Fortran、Java、Ada 等語言均能進行編譯。GCC 還可以根據不同的硬件平臺進行編譯,即能進行交叉編譯,在 A 平臺上編譯 B 平臺的程序,支持常見的 X86、ARM、PowerPC、mips 等,以及 Linux、Windows 等軟件平臺。
1.2 安裝 GCC
首先,查看 gcc 是否安裝:
# 查看 gcc 版本 $ gcc -v $ gcc --version # 查看 g++ 版本 $ g++ -v $ g++ --version
如果在輸入指令後可以獲取到 gcc 版本,那麼就表明你的 Linux 中已經安裝瞭 gcc:
如果沒有安裝,則可按照如下方法安裝 gcc:
# centos $ sudo yum update # 更新本地的軟件下載列表, 得到最新的下載地址 $ sudo yum install gcc g++ # 通過下載列表中提供的地址下載安裝包, 並安裝
1.3 GCC 工作流程
1.3.1 一般使用流程
首先準備一個 C 語言代碼,並命名為 test.c:
#include <stdio.h> #define MAX 3 int main() { int i; for (i = 1; i <= MAX; i++) { printf("Hello World\n"); // 輸出 Hello World } return 0; }
一般情況下,我們可以直接通過 $ gcc test.c -o test
編譯 test.c,並通過$ ./test
指令運行生成的可執行文件:
-o
:output,是 gcc 編譯器的可選參數,用於指定輸出文件名及路徑,默認輸出到當前路徑下。下圖展示瞭如何通過 -o 參數修改輸出路徑:
或者不使用 -o 參數,則生成一個默認名稱的可執行文件 a.out:
實際上,GCC 編譯器在對程序進行編譯的時候,分為瞭四個步驟:
-
預處理(Pre-Processing):
- 在這個階段主要做瞭三件事:展開頭文件 、宏替換 、去掉註釋行
- 結果得到的還是一個 C 程序,通常是以 .i 作為文件擴展名
-
編譯(Compiling) :
- 在這個階段中,gcc 首先要檢查代碼的規范性、是否有語法錯誤等,以確定代碼實際要做的工作
- 在檢查無誤後,gcc 把代碼編譯成匯編代碼,得到一個以 .s 作為文件拓展名的匯編文件。
匯編(Assembling):
+ 匯編階段是把編譯階段生成的 .s 文件轉化成目標文件 + 最終得到一個以 .o 結尾的二進制文件
-
鏈接(Linking):這個階段需要 GCC 調用鏈接器對程序需要調用的庫進行鏈接,最終得到一個可執行的二進制文件
而 GCC 的編譯器可以將這 4 個步驟合並成一個,這也就是為什麼我們使用$ gcc test.c -o test
就可以直接生成可執行文件 test 的原因。下面我們對這 4 個步驟做個詳細的介紹。
1.3.2 詳細的工作流程
1.3.2.1 預處理
# 通過添加參數 -E 生成預處理後的 C 文件 test.i # 必須通過 -o 參數指定輸出的文件名 $ gcc -E test.c -o test.i
讓我們來觀察一下 test.i 中的代碼內容(太長瞭,隻觀察 main 函數中的替換情況):
int main() { int i; for (i = 1; i <= 3; i++) { printf("Hello World\n"); } return 0; }
通過分析 test.i 可以發現:
- 宏定義 MAX 被替換為瞭相應的值 3
- 註釋「// 輸出 Hello World」也被去掉瞭
1.3.2.2 編譯
# 通過添加參數 -S 將 test.i 轉換為匯編文件 test.s(默認生成 .s 文件) $ gcc -S test.i $ gcc -S test.i -o test.s # 寫法二
1.3.2.3 匯編
# 通過匯編得到二進制文件 test.o(默認生成 .o 文件,object) $ gcc -c test.s $ gcc -c test.s -o test.o # 寫法二
1.3.2.4 鏈接
# 通過鏈接得到可執行文件 test $ gcc test.o -o test
在成功生成 test.o 文件後,就進入瞭鏈接階段。在這裡涉及到一個重要的概念:函數庫。
在 test.c 的代碼中,我們通過print()
函數打印 Hello World 語句;但是在這段程序中並沒有定義 printf 的函數實現,且在預編譯中包含進去的「stdio.h」中也隻有該函數的聲明extern int printf (const char *__restrict __format, ...);
,而沒有定義函數的實現,那麼是在哪裡實現的呢?
答案就是:系統把這些函數實現都做到瞭名為 libc.so.6 的庫文件中去瞭,在沒有特別指定時,gcc 會到系統默認的搜索路徑 /usr/lib64 下進行查找,也就是鏈接到 libc.so.6 庫函數中去,這樣就有函數 printf 的實現瞭,而這也就是鏈接的作用。
而函數庫一般分為靜態庫和動態庫兩種:
- 靜態庫是指在編譯鏈接時,把庫文件的代碼全部加入到可執行文件中,因此生成的文件比較大,但在運行時也就不需要庫文件瞭。在 Linux 中靜態庫一般以 .a 作為後綴。
- 動態庫與之相反,在編譯鏈接時並沒有把庫文件的代碼加入到可執行文件中,而是在程序執行時鏈接文件加載庫,這樣就可以節省系統的開銷。在 Linux 中動態庫一般以 .so 作為後綴。
如前面所述的 libc.so.6 就是動態庫,gcc 在編譯時默認使用動態庫。完成瞭鏈接之後,gcc 就可以生成可執行文件瞭。
有關動態庫和靜態庫的詳細介紹,將在下文進行具體講解。
1.3.2.5 總結
最後,通過一張圖來總結一下上述流程:
在 Linux 下使用 GCC 編譯器編譯單個文件十分簡單,直接使用
$ gcc test.c
(test.c 為要編譯的 C 語言的源文件),GCC 會自動生成文件名為 a.out 的可執行文件(也可以通過參數 -o 指定生成的文件名);也就是通過一個簡單的命令就可以將上邊提到的 4 個步驟全部執行完畢瞭;但是如果想要單步執行也是沒問題的。
1.4 GCC 常用參數
下面的表格中列出瞭一些常用的 gcc 參數,這些參數在 gcc 命令中沒有位置要求,隻需要編譯程序的時候將需要的參數指定出來即可。
gcc 編譯選項 | 解釋說明 |
---|---|
-E | 預處理,主要是進行宏展開等步驟,生成 test.i |
-S | 編譯指定的源文件,但是不進行匯編,生成 test.s |
-c | 編譯、匯編源文件,但是不進行鏈接,生成 test.o |
-o | 指定鏈接的文件名及路徑 |
-g | 在編譯的時候,生成調試信息,該程序可以被調試器調試 |
-D | 在程序編譯的時候,指定一個宏 |
-std | 指定 C 方言,如 -std=c99。gcc 默認的方言是 GNU C |
-l | 在程序編譯的時候,指定使用的庫(庫的名字一定要掐頭去尾,如 libtest.so 變為 test) |
-L | 在程序編譯的時候,指定使用的庫的路徑 |
-fpic | 生成與位置無關的代碼 |
-shared | 生成共享目標文件,通常用在建立動態庫時 |
1.4.1 指定一個宏(-D)
在程序中我們可以通過使用#define
定義一個宏,也可以通過宏控制某段代碼是否能夠被執行。
#include <stdio.h> int main() { int num = 60; printf("num = %d\n", num); #ifdef DEBUG printf("定義瞭 DEBUG 宏, num++\n"); num++; #else printf("未定義 DEBUG 宏, num--\n"); num--; #endif printf("num = %d\n", num); return 0; }
由於我們在程序中並沒有定義 DEBUG 宏,所以第 8~9 行的代碼就不會被執行:
那麼如何才能夠在程序中不定義 DEBUG 宏的情況下執行第 8~9 行的代碼呢?答案是通過 -D 參數:
需要註意的是,-D 參數必須在生成 test.o 前使用(鏈接前)。如下所示,是無效的:
說瞭這麼多,-D 參數有什麼用呢?下面我們簡單敘述一下 -D 參數的應用場景。
1.4.1.1 應用場景一
在發佈程序的時候,一般都會要求將程序中所有的 log 輸出去掉,如果不去掉會影響程序的執行效率,很顯然刪除這些打印 log 的源代碼是一件很麻煩的事情,解決方案是這樣的:
- 將所有的打印 log 的代碼都寫到一個宏判定中,可以模仿上邊的例子;
- 在調試程序的時候指定 -D,就會有 log 輸出;
- 在發佈程序的時候不指定 -D,log 就不會輸出;
1.4.1.2 應用場景二
或者,你編寫的一個軟件,某個付費功能隻對已付費的用戶 A 開放,但不對白嫖的用戶 B 開放,其中一種解決方法是:
- 每個用戶對應一個維護分支,用戶 A 對應 project_1 分支包含付費功能的代碼,用戶 B 對應的 project_2 分支不包含付費功能的代碼。
- 當用戶 B 付費訂閱時,再將付費項目的代碼拷貝到 project_2 中
如果再來一個用戶 C 呢?有沒有感覺很麻煩的樣子?那麼我們完全可以這樣做:
#include <stdio.h> int main() { #ifdef CHARGE //付費用戶執行流程 printf("該用戶已付費,執行付費功\n"); #else //白嫖用戶執行流程 printf("白嫖用戶,拒絕執行付費功能\n"); #endif printf("公共功能\n"); return 0; }
在編譯付費用戶的時候,添加 -D CHARGE 參數;編譯白嫖用戶,則不添加。這樣的話,不管來多少用戶,都隻需要維護一個分支即可。
1.4.2 指定 C 方言(-std)
對於如下 C 語言代碼:
#include <stdio.h> int main() { for (int i = 1; i <= 3; i++) { printf("i = %d\n", i); } return 0; }
在編譯時是會報錯的:
但如果我們加上 -std=c99,就可以瞭:
二、靜態庫和動態庫
2.1 掃盲
庫是「已經寫好的、供使用的」可復用代碼,每個程序都要依賴很多基礎的底層庫。
從本質上,庫是一種可執行代碼的二進制形式,可以被操作系統載入內存執行。程序中調用的庫有兩種「靜態庫和動態庫」,所謂的「靜態、動態」指的是鏈接的過程。
2.2 靜態庫
2.2.1 靜態庫簡介
在 Linux 中靜態庫以 lib 作為前綴、以 .a 作為後綴,形如 libxxx.a(其中的 xxx 是庫的名字,自己指定即可)。靜態庫以之所以稱之為「靜態庫」,是因為在鏈接階段,會將匯編生成的目標文件 .o 與引用的庫一起鏈接到可執行文件中,對應的鏈接方式稱為靜態鏈接。
2.2.2 靜態庫的生成
在 Linux 中靜態庫由程序 ar 生成。生成靜態庫,需要先對源文件進行匯編操作得到二進制格式的目標文件(以 .o 結尾的文件),然後再通過 ar 工具將目標文件打包就可以得到靜態庫文件瞭。
使用 ar 工具創建靜態庫的一般格式為$ ar -rcs libxxx.a 若幹原材料(.o文件)
:
2.2.3 靜態庫的制作舉例
在某目錄中有如下源文件,用來實現一個簡單的計算器。
add.c
#include <stdio.h> int add(int a, int b) { return a + b; }
sub.c
#include <stdio.h> int subtract(int a, int b) { return a - b; }
mult.c
#include <stdio.h> int multiply(int a, int b) { return a * b; }
具體操作步驟如下:
# 第一步:將源文件 add.c、sub.c、mult.c 進行匯編,得到二進制目標文件 add.o、sub.o、mult.o $ gcc -c add.c sub.c mult.c # 第二步:將生成的目標文件通過 ar 工具打包生成靜態庫 $ ar rcs libcalc.a add.o sub.o mult.o
2.2.4 靜態庫的使用
定義 main 函數如下所示:
main.c
#include <stdio.h> int main() { int a = 20; int b = 12; printf("a = %d, b = %d\n", a, b); printf("a + b = %d\n", add(a, b)); printf("a - b = %d\n", subtract(a, b)); printf("a * b = %d\n", multiply(a, b)); return 0; }
並將靜態庫 libcalc.a 置於同級目錄下:
通過指令$ gcc main.c -o main -L ./ -l calc
編譯 main.c 文件,並鏈接靜態庫 libcalc.a:
- -L:指定使用的庫的路徑(因為在同一級目錄下,所以可以直接用瞭
./
,或者使用絕對路徑也是可以的) - -l:指定使用的庫(庫的名字一定要掐頭去尾。如:libcalc.a 變為 calc)
編譯結果會提示三個 warning,這是由於沒有定義這些函數導致的,先暫時不用管。
運行 main 結果如下:
我們思考這麼一個問題:由於靜態庫是我們自己制作的,其所包含的函數我們很清楚,直接鏈接並使用即可。但如果別人想要使用呢?他們可不清楚靜態庫中的函數該如何調用,所以我們有必要提供一個頭文件,這樣將靜態庫及頭文件交給其他人時,他們知道該如何用瞭。
head.h
#ifndef _HEAD_H_ #define _HEAD_H_ int add(int a, int b); int subtract(int a, int b); int multiply(int a, int b); #endif
還記得之前的報錯嗎?現在有瞭頭文件就要使用起來。
main.c
#include <stdio.h> #include "head.h" int main() { int a = 20; int b = 12; printf("a = %d, b = %d\n", a, b); printf("a + b = %d\n", add(a, b)); printf("a - b = %d\n", subtract(a, b)); printf("a * b = %d\n", multiply(a, b)); return 0; }
編譯、鏈接、運行,一氣呵成:
2.2.5 ar 命令參數介紹
制作靜態庫時所使用的指令$ ar rcs libcalc.a add.o sub.o mult.o div.o
共有三個參數:
-
-c:創建一個庫,不管庫是否存在,都將創建。這個很好理解,就不做過多的解釋瞭。
-
-r:在庫中插入(替換)模塊 。默認新的成員添加在庫的結尾處,如果模塊名已經在庫中存在,則替換同名的模塊。
-
-s:創建目標文件索引,這在創建較大的庫時能加快時間。
參數 -r 的詳細解釋
假設現在有瞭新的需求,需要靜態庫 libcalc.a 提供除法運算的功能模塊,該怎麼操作呢?
首先我們需要新建一個除法運算的源文件 div.c:
#include <stdio.h> double divide(int a, int b) { return (double)a / b; }
並通過匯編操作生成目標文件 div.o。
接下來我們可以通過 -r 參數將除法運算的模塊添加到靜態庫中:$ ar -r libcalc.a div.o
。
並且要在 head.h 中增加對除法運算的聲明:
#ifndef _HEAD_H_ #define _HEAD_H_ // Other double divide(int a, int b); #endif
參數 -s 的詳細解釋
在獲取一個靜態庫的時候,我們可以通過$ nm -s libcalc.a
來顯示庫文件中的索引表:
而索引的生成就要歸功於 -s 參數瞭。
如果不需要創建索引,可改成 -S 參數。
如果 libcalc.a 缺少索引,可以使用
$ ranlib libcalc.a
指令添加。
2.2.6 其他命令介紹
# 顯示庫文件中有哪些目標文件,隻顯示名稱 $ ar t libcalc.a # 顯示庫文件中有哪些目標文件,顯示文件名、時間、大小等詳細信息 $ ar tv libcalc.a # 顯示庫文件中的索引表 $ nm -s libcalc.a # 為庫文件創建索引表 $ ranlib libcalc.a
2.3 動態庫
2.3.1 動態庫簡介
在 Linux 中動態庫以 lib 作為前綴、以 .so 作為後綴,形如 libxxx.so(其中的 xxx 是庫的名字,自己指定即可)。相比於靜態庫,使用動態庫的程序,在程序編譯時並不會鏈接到目標代碼中,而是在運行時才被載入。不同的應用程序如果調用相同的庫,那麼在內存中隻需要有一份該共享庫的實例,避免瞭空間浪費問題。同時也解決瞭靜態庫對程序的更新的依賴,用戶隻需更新動態庫即可。
2.3.2 動態庫的生成
生成動態庫是直接使用 gcc 命令,並且需要添加 -fpic 以及 -shared 參數:
- -fpic 參數的作用是使得 gcc 生成的代碼是與位置無關的,也就是使用相對位置。
- -shared 參數的作用是告訴編譯器生成一個動態鏈接庫。
2.3.3 動態庫的制作舉例
還是以上述程序 add.c、sub.c、mult.c 為例:
# 第一步:將源文件 add.c、sub.c、mult.c 進行匯編,得到二進制目標文件 add.o、sub.o、mult.o $ gcc -c -fpic add.c sub.c mult.c # 第二步:將得到的 .o 文件打包成動態庫 $ gcc -shared add.o sub.o mult.o -o libcalc.so # 第三步:發佈動態庫和頭文件 1. 提供頭文件 head.h 2. 提供動態庫 libcalc.so
至於為什麼需要提供頭文件,在講解靜態庫時已經做瞭說明,此處不再贅述。
2.3.4 動態庫的使用
head.h
#ifndef _HEAD_H_ #define _HEAD_H_ int add(int a, int b); int subtract(int a, int b); int multiply(int a, int b); #endif
main.c
#include <stdio.h> #include "head.h" int main() { int a = 20; int b = 12; printf("a = %d, b = %d\n", a, b); printf("a + b = %d\n", add(a, b)); printf("a - b = %d\n", subtract(a, b)); printf("a * b = %d\n", multiply(a, b)); return 0; }
和靜態庫的鏈接方式一樣,都是通過指令$ gcc main.c -o main -L ./ -l calc
來進行鏈接庫操作。
gcc 通過指定的動態庫信息生成瞭可執行程序 main,但是可執行程序運行卻提示無法加載到動態庫:
./main: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory
這是怎麼回事呢?
2.3.5 解決動態庫加載失敗的問題
首先來看一下不同庫的工作原理:
- 靜態庫如何被加載:
- 在程序編譯的最後一個階段也就是鏈接階段,提供的靜態庫會被打包到可執行程序中。
- 當可執行程序被執行,靜態庫中的代碼也會一並被加載到內存中,因此不會出現靜態庫找不到無法被加載的問題。
- 動態庫如何被加載:
- 在程序編譯的最後一個階段也就是鏈接階段,在 gcc 命令中雖然指定瞭庫路徑,但是這個路徑並沒有被記錄到可執行程序中,隻是檢查瞭這個路徑下的庫文件是否存在。同樣對應的動態庫文件也沒有被打包到可執行程序中,隻是在可執行程序中記錄瞭庫的名字。
- 當可執行程序被執行起來之後:
- 程序會先檢測所需的動態庫是否可以被加載,加載不到就會提示上邊的錯誤信息。
- 當動態庫中的函數在程序中被調用瞭,這個時候動態庫才加載到內存,如果不被調用就不加載。
動態庫的檢測和內存加載操作都是由動態鏈接器來完成的
動態鏈接器是一個獨立於應用程序的進程,屬於操作系統。當用戶的程序需要加載動態庫的時候動態連接器就開始工作瞭,很顯然動態連接器根本就不知道用戶通過 gcc 編譯程序的時候通過參數 -L 指定的路徑。
那麼動態鏈接器是如何搜索某一個動態庫的呢,在它內部有一個默認的搜索順序,按照優先級從高到低的順序分別是:
-
可執行文件內部的 DT_RPATH 段。
-
系統的環境變量 LD_LIBRARY_PATH。
-
系統動態庫的緩存文件 /etc/ld.so.cache。
-
存儲「靜態庫 / 動態庫」的系統目錄 /lib、/usr/lib 等。
按照以上四個順序,依次搜索,找到之後結束遍歷。若檢索到最終還是沒找到,那麼動態連接器就會提示動態庫找不到的錯誤信息。一般情況下,我們都是通過修改系統的環境變量的方式設置動態庫的地址。
將動態庫路徑追加到環境變量 LD_LIBRARY_PATH 中:$ LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:動態庫的絕對路徑
比如,我所需要的動態庫的絕對路徑為 /mnt/hgfs/SharedFolders/DynamicLibrary,那麼:
$ LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/mnt/hgfs/SharedFolders/DynamicLibrary
這樣的話,我在運行 main,就不會報錯瞭。
但是通過這種方式設置的環境變量盡在當前的終端中有效,那麼怎樣才能讓這個設置永久生效呢?
通過指令$ vim ~/.bashrc
打開並修改該文件:
修改後,使用$ source ~/.bashrc
使修改立即生效。
經過上述操作,就不用每次開啟終端都需要修改環境變量瞭。當然這種永久生效的方式僅適用於動態庫路徑唯一的情況,如果你每次使用的動態庫都在不同的位置,那麼這麼設置也沒啥用😂
2.4 動態庫與靜態庫的比較
2.4.1 靜態庫的特點
- 靜態庫對函數庫的鏈接是在編譯期完成的。
- 靜態庫在程序編譯時會鏈接到目標代碼中,因此使可執行文件變大。
- 當鏈接好靜態庫後,在程序運行時就不需要靜態庫瞭。
- 對程序的更新、部署與發佈不方便,需要全量更新。
- 如果某一個靜態庫更新瞭,所有使用它的應用程序都需要重新編譯、發佈給用戶。
2.4.2 動態庫的特點
- 動態庫把對一些庫函數的鏈接載入推遲到程序運行時期。
- 可以實現進程之間的資源共享,因此動態庫也稱為共享庫。
- 將一些程序升級變得簡單,不需要重新編譯,屬於增量更新。
2.5 使用庫的目的
在項目中使用庫一般有兩個目的:
- 為瞭使程序更加簡潔不需要在項目中維護太多的源文件。
- 另一方面是為瞭源代碼保密,畢竟不是所有人都想把自己編寫的程序開源出來。
當我們拿到瞭庫文件(動態庫、靜態庫)之後要想使用還必須有這些庫中提供的 API 函數的聲明,也就是頭文件,把這些都添加到項目中,就可以快樂的寫代碼瞭。
參考資料
- GCC | 愛編程的大丙 (subingwen.cn)
- GCC編譯的四個階段
- Linux 靜態庫和動態庫 | 愛編程的大丙 (subingwen.cn)
- linux命令之ar—創建靜態庫.a文件
- 靜態庫和動態庫 – 簡書 (jianshu.com)
- linux中 ldd命令簡介
- collect2: error: ld returned 1 exit status(解決方案大總結)
到此這篇關於GCC 指令詳解及動態庫、靜態庫的使用方法的文章就介紹到這瞭,更多相關GCC 指令使用動態庫、靜態庫內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- C語言編程gcc如何生成靜態庫.a和動態庫.so示例詳解
- C語言程序環境中的預處理詳解
- C語言中的程序環境與預處理詳情
- C語言中#define在多行宏定義出錯的原因及分析
- C語言詳細分析宏定義與預處理命令的應用