<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
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 等軟體平臺。
首先,檢視 gcc 是否安裝:
# 檢視 gcc 版本 $ gcc -v $ gcc --version # 檢視 g++ 版本 $ g++ -v $ g++ --version
如果在輸入指令後可以獲取到 gcc 版本,那麼就表明你的 Linux 中已經安裝了 gcc:
如果沒有安裝,則可按照如下方法安裝 gcc:
# centos $ sudo yum update # 更新原生的軟體下載列表, 得到最新的下載地址 $ sudo yum install gcc g++ # 通過下載列表中提供的地址下載安裝包, 並安裝
首先準備一個 C 語言程式碼,並命名為 test.c:
#include <stdio.h> #define MAX 3 int main() { int i; for (i = 1; i <= MAX; i++) { printf("Hello Worldn"); // 輸出 Hello World } return 0; }
一般情況下,我們可以直接通過 $ gcc test.c -o test
編譯 test.c,並通過$ ./test
指令執行生成的可執行檔案:
-o
:output,是 gcc 編譯器的可選引數,用於指定輸出檔名及路徑,預設輸出到當前路徑下。下圖展示瞭如何通過 -o 引數修改輸出路徑:
或者不使用 -o 引數,則生成一個預設名稱的可執行檔案 a.out:
實際上,GCC 編譯器在對程式進行編譯的時候,分為了四個步驟:
預處理(Pre-Processing):
編譯(Compiling) :
組合(Assembling):
+ 組合階段是把編譯階段生成的 .s 檔案轉化成目標檔案 + 最終得到一個以 .o 結尾的二進位制檔案
連結(Linking):這個階段需要 GCC 呼叫連結器對程式需要呼叫的庫進行連結,最終得到一個可執行的二進位制檔案
而 GCC 的編譯器可以將這 4 個步驟合併成一個,這也就是為什麼我們使用$ gcc test.c -o test
就可以直接生成可執行檔案 test 的原因。下面我們對這 4 個步驟做個詳細的介紹。
# 通過新增引數 -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 Worldn"); } return 0; }
通過分析 test.i 可以發現:
# 通過新增引數 -S 將 test.i 轉換為組合檔案 test.s(預設生成 .s 檔案) $ gcc -S test.i $ gcc -S test.i -o test.s # 寫法二
# 通過組合得到二進位制檔案 test.o(預設生成 .o 檔案,object) $ gcc -c test.s $ gcc -c test.s -o test.o # 寫法二
# 通過連結得到可執行檔案 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 的實現了,而這也就是連結的作用。
而函數庫一般分為靜態庫和動態庫兩種:
如前面所述的 libc.so.6 就是動態庫,gcc 在編譯時預設使用動態庫。完成了連結之後,gcc 就可以生成可執行檔案了。
有關動態庫和靜態庫的詳細介紹,將在下文進行具體講解。
最後,通過一張圖來總結一下上述流程:
在 Linux 下使用 GCC 編譯器編譯單個檔案十分簡單,直接使用
$ gcc test.c
(test.c 為要編譯的 C 語言的原始檔),GCC 會自動生成檔名為 a.out 的可執行檔案(也可以通過引數 -o 指定生成的檔名);也就是通過一個簡單的命令就可以將上邊提到的 4 個步驟全部執行完畢了;但是如果想要單步執行也是沒問題的。
下面的表格中列出了一些常用的 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 | 生成共用目標檔案,通常用在建立動態庫時 |
在程式中我們可以通過使用#define
定義一個宏,也可以通過宏控制某段程式碼是否能夠被執行。
#include <stdio.h> int main() { int num = 60; printf("num = %dn", num); #ifdef DEBUG printf("定義了 DEBUG 宏, num++n"); num++; #else printf("未定義 DEBUG 宏, num--n"); num--; #endif printf("num = %dn", num); return 0; }
由於我們在程式中並沒有定義 DEBUG 宏,所以第 8~9 行的程式碼就不會被執行:
那麼如何才能夠在程式中不定義 DEBUG 宏的情況下執行第 8~9 行的程式碼呢?答案是通過 -D 引數:
需要注意的是,-D 引數必須在生成 test.o 前使用(連結前)。如下所示,是無效的:
說了這麼多,-D 引數有什麼用呢?下面我們簡單敘述一下 -D 引數的應用場景。
在釋出程式的時候,一般都會要求將程式中所有的 log 輸出去掉,如果不去掉會影響程式的執行效率,很顯然刪除這些列印 log 的原始碼是一件很麻煩的事情,解決方案是這樣的:
或者,你編寫的一個軟體,某個付費功能只對已付費的使用者 A 開放,但不對白嫖的使用者 B 開放,其中一種解決方法是:
如果再來一個使用者 C 呢?有沒有感覺很麻煩的樣子?那麼我們完全可以這樣做:
#include <stdio.h> int main() { #ifdef CHARGE //付費使用者執行流程 printf("該使用者已付費,執行付費功n"); #else //白嫖使用者執行流程 printf("白嫖使用者,拒絕執行付費功能n"); #endif printf("公共功能n"); return 0; }
在編譯付費使用者的時候,新增 -D CHARGE 引數;編譯白嫖使用者,則不新增。這樣的話,不管來多少使用者,都只需要維護一個分支即可。
對於如下 C 語言程式碼:
#include <stdio.h> int main() { for (int i = 1; i <= 3; i++) { printf("i = %dn", i); } return 0; }
在編譯時是會報錯的:
但如果我們加上 -std=c99,就可以了:
庫是「已經寫好的、供使用的」可複用程式碼,每個程式都要依賴很多基礎的底層庫。
從本質上,庫是一種可執行程式碼的二進位制形式,可以被作業系統載入記憶體執行。程式中呼叫的庫有兩種「靜態庫和動態庫」,所謂的「靜態、動態」指的是連結的過程。
在 Linux 中靜態庫以 lib 作為字首、以 .a 作為字尾,形如 libxxx.a(其中的 xxx 是庫的名字,自己指定即可)。靜態庫以之所以稱之為「靜態庫」,是因為在連結階段,會將組合生成的目標檔案 .o 與參照的庫一起連結到可執行檔案中,對應的連結方式稱為靜態連結。
在 Linux 中靜態庫由程式 ar 生成。生成靜態庫,需要先對原始檔進行組合操作得到二進位制格式的目標檔案(以 .o 結尾的檔案),然後再通過 ar 工具將目標檔案打包就可以得到靜態庫檔案了。
使用 ar 工具建立靜態庫的一般格式為$ ar -rcs libxxx.a 若干原材料(.o檔案)
:
在某目錄中有如下原始檔,用來實現一個簡單的計算器。
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
定義 main 函數如下所示:
main.c
#include <stdio.h> int main() { int a = 20; int b = 12; printf("a = %d, b = %dn", a, b); printf("a + b = %dn", add(a, b)); printf("a - b = %dn", subtract(a, b)); printf("a * b = %dn", multiply(a, b)); return 0; }
並將靜態庫 libcalc.a 置於同級目錄下:
通過指令$ gcc main.c -o main -L ./ -l calc
編譯 main.c 檔案,並連結靜態庫 libcalc.a:
./
,或者使用絕對路徑也是可以的)編譯結果會提示三個 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 = %dn", a, b); printf("a + b = %dn", add(a, b)); printf("a - b = %dn", subtract(a, b)); printf("a * b = %dn", multiply(a, b)); return 0; }
編譯、連結、執行,一氣呵成:
製作靜態庫時所使用的指令$ 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
指令新增。
# 顯示庫檔案中有哪些目標檔案,只顯示名稱 $ ar t libcalc.a # 顯示庫檔案中有哪些目標檔案,顯示檔名、時間、大小等詳細資訊 $ ar tv libcalc.a # 顯示庫檔案中的索引表 $ nm -s libcalc.a # 為庫檔案建立索引表 $ ranlib libcalc.a
在 Linux 中動態庫以 lib 作為字首、以 .so 作為字尾,形如 libxxx.so(其中的 xxx 是庫的名字,自己指定即可)。相比於靜態庫,使用動態庫的程式,在程式編譯時並不會連結到目的碼中,而是在執行時才被載入。不同的應用程式如果呼叫相同的庫,那麼在記憶體中只需要有一份該共用庫的範例,避免了空間浪費問題。同時也解決了靜態庫對程式的更新的依賴,使用者只需更新動態庫即可。
生成動態庫是直接使用 gcc 命令,並且需要新增 -fpic 以及 -shared 引數:
還是以上述程式 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
至於為什麼需要提供標頭檔案,在講解靜態庫時已經做了說明,此處不再贅述。
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 = %dn", a, b); printf("a + b = %dn", add(a, b)); printf("a - b = %dn", subtract(a, b)); printf("a * b = %dn", 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
這是怎麼回事呢?
首先來看一下不同庫的工作原理:
動態庫的檢測和記憶體載入操作都是由動態連結器來完成的
動態連結器是一個獨立於應用程式的程序,屬於作業系統。當用戶的程式需要載入動態庫的時候動態聯結器就開始工作了,很顯然動態聯結器根本就不知道使用者通過 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
使修改立即生效。
經過上述操作,就不用每次開啟終端都需要修改環境變數了。當然這種永久生效的方式僅適用於動態庫路徑唯一的情況,如果你每次使用的動態庫都在不同的位置,那麼這麼設定也沒啥用
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45