1、編譯過程
1.預處理:解釋并展開源程序當中的所有的預處理指令,此時生成 *.i 文件。
2.編譯:詞法和語法的分析,生成對應硬件平臺的匯編語言文件,此時生成 *.s 文件。
3.匯編:將匯編語言文件翻譯為對應處理器的二進制機器碼,此時生成 *.o 文件。
4.鏈接:將多個 *.o 文件合并成一個不帶后綴的可執行文件。
gec@ubuntu:~$ gcc hello.c -o hello.i -E
gec@ubuntu:~$ gcc hello.i -o hello.s -S
gec@ubuntu:~$ gcc hello.s -o hello.o -c
gec@ubuntu:~$ gcc hello.o -o hello -lcgcc hello.c -o hello
2.ELF格式
2.1概述
對于上述編譯過程,重點關注最后一步庫文件的鏈接(gcc hello.o -o hello -lc):鏈接實際上是將多個.o文件合并在一起的過程。這些 *.o 文件合并前是 ELF 格式,合并后也是 ELF 格式。
ELF全稱是 Executable and Linkable Format,即可執行可鏈接格式。ELF文件由多個不同的段(section)組成,如下圖所示:
ELF格式的合并,實際上就是將多個文件中各自對應的段合并在一起,形成一個統一的ELF文件。在此過程中,必然需要對各個 *.o 文件中的靜態數據(包括常量)、函數入口的地址做統一分配和管理,這個過程就叫做 重定位,因此未經鏈接的單獨的 *.o 文件又被稱為可重定位文件,經過鏈接處理合并了相同的段的文件稱為可執行文件。
庫的本意是library圖書館,庫文件就是一個由很多 *.o 文件堆積起來的集合。
2.2 相關命令
(1) readelf 可以用來查看 ELF 格式文件的具體細節:
# 查看文件格式頭部信息
gec@ubuntu:~$ readelf -h a.out# 查看各個section信息
gec@ubuntu:~$ readelf -S a.out# 查看符號表
gec@ubuntu:~$ readelf -s a.out
3.庫文件
3.1概述
庫的本意是library圖書館,庫文件就是一個由很多 *.o 文件堆積起來的集合。本質上來說庫是一種可執行代碼的二進制形式,這個文件可以在編譯時由編譯器直接鏈接到可執行程序中,也可以在運行時由操作系統的runtime enviroment根據需要動態加載到內存中。
3.2分類
庫文件分為兩類:靜態庫和動態庫。如:
win32平臺下,靜態庫通常后綴為.lib,動態庫為.dll ;
linux平臺下,靜態庫通常后綴為.a,動態庫為.so 。靜態庫:libx.a
動態庫:liby.so
庫文件的名稱遵循這樣的規范:
lib庫名.后綴
其中,lib是任何庫文件都必須有的前綴,庫名就是庫文件真正的名稱,比如上述例子中兩個庫文件分別叫x和y,在鏈接它們的時候寫成 -lx 和 -ly ,后綴根據靜態庫和動態庫,可以是 .a 或者 .so:
- 靜態庫的后綴:.a (archive,意即檔案)
- 動態庫的后綴:.so (share object,意即共享對象)
注意:不管是靜態庫,還是動態庫,都是可重定位文件 *.o 的集合。
3.3目的
- 模塊化:庫文件將功能模塊化,使得程序結構更加清晰,易于管理和維護。
- 簡化部署:使用庫文件可以簡化軟件的部署過程,因為它們可以在不同的程序之間共享,而不需要重復包含相同的代碼。
- 動態鏈接:動態庫文件允許在程序運行時才鏈接,這樣可以在不重新編譯程序的情況下更新庫,提供了更大的靈活性。
- 減少內存占用:使用動態庫時,由于多個程序可以共享同一份庫文件,因此可以減少每個程序的內存占用。
- 易于更新和維護:庫文件的更新只需要替換原有文件,而不需要重新編譯使用該庫的所有程序,簡化了維護工作。
- 跨平臺兼容性:庫文件可以被設計為跨平臺使用,增加了軟件的可移植性。
總的來說,庫文件的使用是為了提高軟件開發的效率、靈活性和可維護性,同時減少資源的重復占用。
4、靜態庫
1所謂靜態庫,就是在靜態編譯時由編譯器到指定目錄尋找并且進行鏈接,一旦鏈接完成,最終的可執行程序中就包含了該庫文件中的所有有用信息,包括代碼段、數據段等。
靜態鏈接庫在程序編譯時會被鏈接到目標代碼中,目標程序運行時將不再需要改動態庫,移植方便,體積較大,浪費空間和資源,因為所有相關的對象文件與牽涉到庫都被鏈接合成一個可執行文件,這樣導致可執行文件的體積較大。
2.靜態庫的制作
假設功能文件 a.c、b.c 包含了一些通用的程序模塊,可以被其他程序復用,那么可以將它們制作成靜態庫,具體的步驟是:
第一步,制作 *.o 原材料
gec@ubuntu:~$ gcc a.c -o a.o -c
gec@ubuntu:~$ gcc b.c -o b.o -c
第二步,將 *.o 合并成一個靜態庫
gec@ubuntu:~$ ar crs libx.a a.o b.o
可見制作靜態庫非常簡單,制作完成之后,可以用命令 ar 查看庫中所包含的 *.o 文件:
gec@ubuntu:~$ ar -t libx.a
- 靜態庫的常見操作
3.1 查看靜態庫中的 .o 列表
gec@ubuntu:~$ ar t libx.a #(t意即table,以列表方式列出.o文件)
a.o
b.o
3.2 刪除靜態庫中的 .o 文件
gec@ubuntu:~$ ar d libx.a b.o #(d意即delte,刪除掉指定的.o文件)
gec@ubuntu:~$ ar t libx.a
a.o
3.3 向靜態庫增加 .o 文件
gec@ubuntu:~$ ar r libx.a b.o #(r意即replace,添加或替換(重名時)指定的.o文件)
gec@ubuntu:~$ ar t libx.a
a.o
b.o
3.4 提取靜態庫中的 .o 文件
gec@ubuntu:~$ ar x libx.a #(x意即extract,將庫中所有的.o文件釋放出來)
gec@ubuntu:~$ ar x libx.a a.o #(指定釋放庫中的a.o文件)
4.靜態庫的使用
庫文件最大的價值,在于代碼復用。假設在上述庫文件所包含的 *.o 文件中,已經包含了若干函數接口,那么只要能鏈接這個庫,就無需再重復編寫這些接口,直接鏈接即可。
使用靜態庫 要是用靜態庫libadd.a,只需要包含add.h,就可以使用函數add()、sub()。
#include <stdio.h>
#include “add.h”
void main(){
printf(“add(5,4) is %d\n”,add(5,4));
printf(“sub(5,4) is %d\n”,sub(5,4));
}
靜態庫的文件可以放在任意的位置,編譯時只需要找到該庫文件即可。
gcc -c -I /home/xxxx/include -L /home/xxxxx/lib libadd.a test.c
1). 通過-I(是大i)指定對應的頭文件
2). 通過-L制定庫文件的路徑,libadd.a就是要用的靜態庫。
3). 在test.c中要包含靜態庫的頭文件。
總結:
編譯時:gcc a.c liba.a -o project
相當于liba.a 代替了b.c c.c參與編譯
5、動態庫
1.概述
不管是動態庫還是靜態庫,它們都是 *.o 文件的集合。動態庫指的是以.so后綴的庫文件。動態庫在程序編譯時并不會被鏈接到目標代碼中,而是在程序運行時才被載入,因為可執行文件體積較小。有了動態庫,程序的升級會相對比較簡單,比如某個動態庫升級了,只需要更換這個動態庫的文件,而不需要去更換可執行文件。但要注意的是,可執行程序在運行時需要能找到動態庫文件。可執行文件時動態庫的調用者。
在實際應用中,動態庫應用場合要遠多于靜態庫,因為雖然動態庫的運行時裝載特性會使得程序性能有略微的下降,但換來的是不僅僅節省了大量的存儲空間,更重要的是使得主程序和庫松耦合,不互相捆綁,當庫升級的時候,應用程序無需任何改動即可獲得新版庫文件的功能,這極大地提高了程序的靈活性。
2.庫文件命名
靜態庫的名字一般為libxxxx.a,其中xxxx是該lib的名稱;動態庫的名字一般為libxxxx.so.x.y.z,含義如下圖所示:
此處,符號鏈接的作用不是“快捷方式”,而是為了可以讓動態庫在升級版本的時候更加方便地向前兼容。一般而言,完整的動態庫文件名稱是:
lib庫名.so.主版本號.次版本號.修訂版本號
比如: libx.so.1.3.1
當動態庫迭代升級時,其版本號會發生相應的改變。比如下面的版本更迭:
2021年3月08日發布:libx.so.1.0.0
2021年4月02日發布:libx.so.1.0.1
2021年4月23日發布:libx.so.1.0.2
2021年5月18日發布:libx.so.1.0.3
2021年8月09日發布:libx.so.1.1.0
2021年9月12日發布:libx.so.1.1.1
可以看到,修訂版本號的更迭會比較頻繁,次版本號次之,主版本號再次之。為了避免每次版本號的修改而重新編譯,動態庫一般會用一個只帶主版本號的符號鏈接來鏈接程序,如:
gec@ubuntu:~$ ls -l
lrwxrwxrwx 1 root root 15 Jan 16 2020 libbsd.so.0 -> libbsd.so.0.8.7
-rw-r--r-- 1 root root 80104 Jan 16 2020 libbsd.so.0.8.7
gec@ubuntu:~$
這樣一來,未來不管版本號如何變遷,只要主版本號不變,那么用戶鏈接的庫名永遠都是 libbsd.so.0,而無需關心具體某個版本。而如果連主版本號都發生了改變,這一般是因為庫不再向前兼容,比如刪除了某些原有的接口,這種情況下,用戶就需要重新編譯程序。
3.制作庫文件常用的參數
首先需要了解gcc編譯庫要用到一些參數,很重要。
4.制作動態庫
不管是靜態庫還是動態庫,都是用來被其他程序鏈接的一個個功能模塊。與靜態庫一致,制作動態庫的步驟如下:
將 *.c 編譯生成 *.o
將 *.o 編譯成動態庫
把add.c編譯成動態鏈接庫libadd.so
#第一步:將源碼編譯為 *.o
gcc -fPIC -o libadd.o -c add.c
#第二步:將 *.o 編譯為動態庫
gcc -shared -o libadd.so libadd.o
也可以直接使用一條命令
gcc -fPIC -shared -o libadd.so add.c
5.動態庫的使用
動態庫的編譯跟靜態庫并無二致,如:
gec@ubuntu:~$ pwd
/home/gec
gec@ubuntu:~$ ls lib/
libx.so
gec@ubuntu:~$ gcc main.c -o main -L./lib -lx
說明:
-L 選項后面跟著動態庫所在的路徑。
-l 選項后面跟著動態庫的名稱。
運行時鏈接
動態庫的最大特征,就是編譯鏈接后程序并不包含動態庫的代碼,這些程序會在每次運行時,動態地去尋找并定位其所依賴的庫文件中的模塊,這是他們為什么被稱為動態庫的原因。
也就是說,如果程序運行時找不到動態庫,運行就會失敗,例如:
gec@ubuntu:~$ ./main
報錯
出現上述錯誤的原因,就是因為運行程序 main 時,無法找到其所依賴的動態庫 libx.so,解決這個問題,有三種辦法:
1.編譯時預告:
gec@ubuntu:~$ gcc main.c -o main -L. -lx -Wl,-rpath=/home/gec/lib
2.設置環境變量:
gec@ubuntu:~$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/gec/lib
3.將庫文件拷貝到根目錄下的/lib里面
總結
可以通過動態庫(也稱為共享庫或共享對象文件)再構建一個動態庫。在鏈接過程中,一個動態庫可以依賴于其他動態庫或靜態庫。
當你使用編譯器(如gcc或clang)來構建動態庫時,你可以指定其他動態庫作為鏈接時的依賴。這些依賴的庫在運行時會被動態加載。
以下是一個簡單的例子,展示了如何使用gcc來從一個動態庫(libA.so)構建一個依賴于它的新動態庫(libB.so):
編譯和鏈接第一個動態庫(libA.so)
假設你有一個源文件a.c,你可以這樣編譯和鏈接它:
gcc -shared -o libA.so a.c
編譯和鏈接第二個動態庫(libB.so),它依賴于libA.so
假設你有一個源文件b.c,它調用了在libA.so中定義的函數。為了構建libB.so,你需要鏈接到libA.so:
gcc -shared -o libB.so b.c -L. -lA
注意-L.選項告訴鏈接器在當前目錄(.表示當前目錄)中查找庫,而-lA選項告訴鏈接器鏈接到名為libA.so的庫(注意,在-l選項后,庫名通常不包含前綴lib和后綴.so)。