文章目錄
- 💐專欄導讀
- 💐文章導讀
- 🐧什么是庫?
- 🐧為什么要有庫?
- 🐧寫一個自己的庫
- 🐦方法一
- 🐦方法二 靜態庫
- 🐦標準化
- 🐦方法三 動態庫
- 🐦配置動態庫
- 🐱環境變量
- 🐱軟鏈接
- 🐱配置文件
- 🐧靜態庫與動態庫的區別
- 🐧動態庫的運作原理
- 🐔為什么進程可以在運行時加載動態庫?
- 🐔為什么多個進程可以共享一個動態庫
💐專欄導讀
🌸作者簡介:花想云 ,在讀本科生一枚,C/C++領域新星創作者,新星計劃導師,阿里云專家博主,CSDN內容合伙人…致力于 C/C++、Linux 學習。
🌸專欄簡介:本文收錄于 Linux從入門到精通,本專欄主要內容為本專欄主要內容為Linux的系統性學習,專為小白打造的文章專欄。
🌸相關專欄推薦:C語言初階系列、C語言進階系列 、C++系列、數據結構與算法。
💐文章導讀
本章我們將深入學習Linux中動靜態庫的使用及其原理。
🐧什么是庫?
在學習生涯中,我們總是能談到庫,例如C語言標準庫、C++標準庫。那么到底什么是庫呢?
在計算機科學中,術語“庫”
通常指的是庫文件(Library),它是一組預編譯的、可重用的代碼和資源的集合,用于支持軟件開發。庫的目的是為開發人員提供一組常用的功能,以便在應用程序中進行調用,從而避免重復編寫相同的代碼。
庫可以分為兩大類:
-
靜態庫(Static Library): 靜態庫在編譯時被鏈接到應用程序中,形成一個獨立的可執行文件。在程序運行時,
靜態庫的代碼被完全復制到應用程序中,因此應用程序不再依賴于原始的庫文件
。靜態庫的文件擴展名通常是.a
(在Unix/Linux系統中)或.lib
(在Windows系統中)。 -
動態庫(Dynamic Library): 動態庫在運行時加載到內存中,多個應用程序可以共享同一個動態庫的實例。這可以減小應用程序的大小,因為動態庫的代碼只需要存在一份,而且可以在運行時更新。動態庫的文件擴展名通常是
.so
(在Unix/Linux系統中)或.dll
(在Windows系統中)。
在編程中,開發人員通過包含庫的頭文件、鏈接庫文件,以及在代碼中調用庫提供的函數或方法,可以輕松地利用庫的功能。
在我們的Linxu機器上,系統已經為我們預裝了C/C++的頭文件和庫文件。頭文件提供方法說明,庫文件提供方法的實現。頭文件與庫文件是有對應關系的,需要組合在一起使用。
一個可執行程序生成需要經歷四個階段:預處理、編譯、匯編、鏈接
。頭文件在預處理階段被引入,庫文件在鏈接階段被引入。
C/C++庫文件
$ ls /usr/lib64/libc*
/usr/lib64/libc-2.17.so /usr/lib64/libc_nonshared.a /usr/lib64/libcroco-0.6.so.3.0.1 /usr/lib64/libc.so
/usr/lib64/libcap-ng.so.0 /usr/lib64/libcollection.so.2 /usr/lib64/libcrypt-2.17.so /usr/lib64/libc.so.6
/usr/lib64/libcap-ng.so.0.0.0 /usr/lib64/libcollection.so.2.1.1 /usr/lib64/libcrypto.so.10 /usr/lib64/libcupscgi.so.1
/usr/lib64/libcap.so.2 /usr/lib64/libcom_err.so.2 /usr/lib64/libcrypto.so.1.0.2k /usr/lib64/libcupsimage.so.2
/usr/lib64/libcap.so.2.22 /usr/lib64/libcom_err.so.2.1 /usr/lib64/libcryptsetup.so.12 /usr/lib64/libcupsmime.so.1
/usr/lib64/libcgroup.so.1 /usr/lib64/libcpupower.so.0 /usr/lib64/libcryptsetup.so.12.3.0 /usr/lib64/libcupsppdc.so.1
/usr/lib64/libcgroup.so.1.0.41 /usr/lib64/libcpupower.so.0.0.0 /usr/lib64/libcryptsetup.so.4 /usr/lib64/libcups.so.2
/usr/lib64/libcidn-2.17.so /usr/lib64/libcrack.so.2 /usr/lib64/libcryptsetup.so.4.7.0 /usr/lib64/libcurl.so.4
/usr/lib64/libcidn.so /usr/lib64/libcrack.so.2.9.0 /usr/lib64/libcrypt.so /usr/lib64/libcurl.so.4.3.0
/usr/lib64/libcidn.so.1 /usr/lib64/libcroco-0.6.so.3 /usr/lib64/libcrypt.so.1
C/C++頭文件
$ ls /usr/include/stdio.h
/usr/include/stdio.h
$ ls /usr/include/c++/4.8.5/iostream
/usr/include/c++/4.8.5/iostream
🐧為什么要有庫?
引入庫的概念有很多重要的原因,它們有助于提高軟件開發的效率、可維護性和可擴展性。以下是一些主要的原因:
-
代碼重用: 庫提供了一組通用的功能或工具,可以在多個項目中被重復使用。這樣可以避免開發人員重復編寫相同的代碼,提高了開發效率。
-
模塊化開發: 庫使軟件能夠以模塊化的方式構建。通過將不同的功能分解成獨立的庫,開發人員可以更容易地理解和維護代碼。模塊化開發還使得團隊能夠并行工作,每個成員專注于特定的任務或功能。
-
抽象和封裝: 庫提供了對底層實現的抽象,使開發人員可以專注于高層次的問題而不必關心底層的細節。這種抽象和封裝的概念有助于隱藏復雜性,提高代碼的可讀性和可維護性。
-
提高可靠性: 庫經過充分測試和驗證,可以提供高質量的代碼。開發人員使用庫時,可以信任這些已經驗證過的功能,減少了潛在的錯誤和漏洞。
-
快速開發: 使用庫可以加速開發過程。通過利用現成的庫,開發人員可以更快地構建應用程序,而不必從頭開始編寫每一個功能。
-
標準化: 某些庫成為行業或社區標準,提供了一致的接口和實現。這種標準化有助于確保代碼的一致性,同時使得不同項目之間更容易進行集成和交互。
-
可擴展性: 庫使得軟件的架構更具擴展性。通過將不同的模塊組織為庫,可以更容易地添加新功能、升級現有功能或替換特定的實現,而無需修改整個應用程序。
總體而言,引入庫的概念有助于構建更可靠、可維護和可擴展的軟件系統,提高了軟件開發的效率和質量。
🐧寫一個自己的庫
為了更深入的理解庫運作的原理,我們嘗試自己寫一個庫,并交給其他小伙伴使用。
接下來,我們將實現一個加減運算的程序,并將給程序的源代碼與頭文件進行打包,并交給小伙伴——小黑
使用。
$ touch add.c
$ touch add.h
$ touch sub.c
$ touch sub.h
/* add.h */
#pragma once
int add(int a, int b);
/* add.c */
#include "add.h"
int add(int a, int b){return a + b;
}
/* sub.h */
#pragma once
int sub(int a, int b);
/* sub.c */
#include "sub.h"
int sub(int a, int b){return a - b;
}
現在我們已經將計算器的源代碼寫好,現在我們想讓小黑使用我們的成果,倘若我們直接把源文件以及頭文件發給小黑,這種做法肯定是沒問題的。
但是我們又想讓小黑使用我們的成果,又不想讓小黑看到我們的源代碼,現在該怎么辦呢?
🐦方法一
第一種方法是我們可以將源代碼經過預處理、編譯、匯編后形成二進制文件。小黑拿到該二進制文件后,再將它自己寫的程序同樣經過預處理、編譯、匯編形成二進制文件,然后將兩個二進制文件進行鏈接即可。
$ gcc -c *.c
$ ll
total 28
-rw-rw-r-- 1 hxy hxy 60 Feb 28 16:25 add.c
-rw-rw-r-- 1 hxy hxy 38 Feb 28 16:25 add.h
-rw-rw-r-- 1 hxy hxy 1240 Feb 28 16:40 add.o
-rw-rw-r-- 1 hxy hxy 60 Feb 28 16:20 sub.c
-rw-rw-r-- 1 hxy hxy 39 Feb 28 16:19 sub.h
-rw-rw-r-- 1 hxy hxy 1240 Feb 28 16:40 sub.o
drwxrwxr-x 2 hxy hxy 4096 Feb 28 16:40 xiaohei
$ cp *h xiaohei/
$ cp *o xiaohei//*小黑視角*/
ll
total 20
-rw-rw-r-- 1 hxy hxy 38 Feb 28 16:43 add.h
-rw-rw-r-- 1 hxy hxy 1240 Feb 28 16:45 add.o
-rw-rw-r-- 1 hxy hxy 200 Feb 28 16:26 main.c
-rw-rw-r-- 1 hxy hxy 39 Feb 28 16:43 sub.h
-rw-rw-r-- 1 hxy hxy 1240 Feb 28 16:45 sub.o
/*小黑視角*/
$ gcc -c main.c
$ gcc -o test add.o sub.o main.o
$ ls
add.h add.o main.c main.o sub.h sub.o test
$ ./test
10 + 3 = 13
10 - 3 = 7
🐦方法二 靜態庫
方法一中我們需要將許多的 .o
文件以及.h
文件打包給對方,這種做法明顯感覺不是特別優雅。接下來我們就是用靜態庫的方式。
- 先將我們的
.o
文件打包生成一個靜態庫,并發送給小黑;
$ ar -rc libcalculate.a *.o
$ ll
total 32
-rw-rw-r-- 1 hxy hxy 60 Feb 28 16:25 add.c
-rw-rw-r-- 1 hxy hxy 38 Feb 28 16:25 add.h
-rw-rw-r-- 1 hxy hxy 1240 Feb 28 16:40 add.o
-rw-rw-r-- 1 hxy hxy 2688 Feb 28 18:31 libcalculate.a
-rw-rw-r-- 1 hxy hxy 60 Feb 28 16:20 sub.c
-rw-rw-r-- 1 hxy hxy 39 Feb 28 16:19 sub.h
-rw-rw-r-- 1 hxy hxy 1240 Feb 28 16:40 sub.o
drwxrwxr-x 2 hxy hxy 4096 Feb 28 18:23 xiaohei
$ cp libcalculate.a xiaohei/* 小黑視角 */
$ ll
total 16
-rw-rw-r-- 1 hxy hxy 38 Feb 28 18:35 add.h
-rw-rw-r-- 1 hxy hxy 2688 Feb 28 18:33 libcalculate.a
-rw-rw-r-- 1 hxy hxy 200 Feb 28 16:26 main.c
-rw-rw-r-- 1 hxy hxy 39 Feb 28 18:35 sub.h
注意
這里我們需要注意庫的命名規則。庫的命名是以lib為開頭,以.a或.so為結尾
。例如 libcalculate.a
的真實名稱為 calculate
。
在小黑拿到我們的庫文件后,他就可以編譯生成自己的程序了。但是這里有幾個細節需要注意:
- 因為我們的庫是第三方的,編譯器并不知道這個庫的存在,所以我們需要指明庫所在的路徑;
- 同樣,我們需要告訴編譯器該鏈接哪一個庫;
- 同理,我們還需指明頭文件所在的路徑。但是目前頭文件就在當前路徑下,所以可省略;
2.小黑進行編譯鏈接;
/* 小黑視角 */
$ gcc -o test main.c -L . -l calculate -I .
$ ./test
10 + 3 = 13
10 - 3 = 7
-L 選項
:指明庫所在的路徑;-l 選項
: 告訴編譯器鏈接哪一個庫;-I 選項
:告訴編譯器頭文件的位置;
🐦標準化
上面方法二中我們演示了一個庫文件的使用原理。在實際的項目開發中,我們并不會這么隨意潦草。
再以小黑為例:
- 將頭文件全部移至一個目錄下;
- 將庫文件全部移至一個目錄下;
- 將頭文件與庫文件進行打包;
- 將打包好的文件上傳至云端;
$ mkdir lib
$ mkdir include
$ cp *.h include/
$ cp *.a lib
$ tar -czf calcuate.tgz include lib
遠在海外的小黑想用我們寫好的庫,于是在云端將壓縮包下載到了本地;
/* 小黑視角 */
$ ll
total 8
-rw-rw-r-- 1 hxy hxy 788 Feb 28 19:01 calcuate.tgz
-rw-rw-r-- 1 hxy hxy 200 Feb 28 16:26 main.c
小黑將它進行解壓看到了頭文件與庫文件;
$ tar xzf calcuate.tgz
$ ll
total 16
-rw-rw-r-- 1 hxy hxy 788 Feb 28 19:01 calcuate.tgz
drwxrwxr-x 2 hxy hxy 4096 Feb 28 18:56 include
drwxrwxr-x 2 hxy hxy 4096 Feb 28 18:56 lib
-rw-rw-r-- 1 hxy hxy 200 Feb 28 16:26 main.c
最后小黑進行了編譯鏈接等一系列操作,成功運行了自己的程序;
$ gcc -o test main.c -I ./include -L ./lib -l calculate
$ ./test
10 + 3 = 13
10 - 3 = 7
以后的小黑會經常用到這個庫,但是他覺得每次都要寫這么長的指令有些麻煩。于是他將這個庫的頭文件全部移至系統的/usr/include目錄下;將庫文件移至/usr/lib目錄下;
$ sudo cp include/*.h /usr/include/
$ sudo cp lib/* /lib64/
以后他每次使用這個庫時,編譯器會自動在這兩個目錄下尋找所程序所依賴的頭文件與庫文件;
$ gcc -o test main.c -lcalculate
$ ./test
10 + 3 = 13
10 - 3 = 7
🐦方法三 動態庫
以上我們嘗試將自己的源文件制作為一個靜態庫供小黑使用,接下來我們在嘗試制作一個動態庫。
$ gcc -fPIC -c add.c sub.c
- fPIC:產生位置無關碼(position independent code);
gcc -shared -o libcalculate.so *.o
- shared: 表示生成共享庫格式;
接著我們把生成的.so
文件放在lib
目錄下,將.h
文件放到include
目錄下,并打包發給小黑(重復之前的操作)。
$ rm lib/libcalculate.a
$ rm calcuate.tgz
$ cp *.so lib
$ tar -czf calculate.tgz lib include
$ cp calculate.tgz xiaohei/
小黑拿到壓縮文件,解壓后得到lib
與include
于是用我們的庫來鏈接自己的程序。
/*小黑視角*/
$ tar xzf calculate.tgz
$ ll
total 16
-rw-rw-r-- 1 hxy hxy 2343 Feb 29 16:39 calculate.tgz
drwxrwxr-x 2 hxy hxy 4096 Feb 28 18:56 include
drwxrwxr-x 2 hxy hxy 4096 Feb 29 16:38 lib
-rw-rw-r-- 1 hxy hxy 200 Feb 28 16:26 main.c
$
$ gcc -o test main.c -I include -L lib -lcalculate
$ ll
total 28
-rw-rw-r-- 1 hxy hxy 2343 Feb 29 16:39 calculate.tgz
drwxrwxr-x 2 hxy hxy 4096 Feb 28 18:56 include
drwxrwxr-x 2 hxy hxy 4096 Feb 29 16:38 lib
-rw-rw-r-- 1 hxy hxy 200 Feb 28 16:26 main.c
-rwxrwxr-x 1 hxy hxy 8432 Feb 29 16:49 test
$
$ ./test
./test: error while loading shared libraries: libcalculate.so: cannot open shared object file: No such file or directory
小黑發現了事情的不妙,心想剛才不是還好好的嗎?怎么運行時提示找不到庫文件呢?
原來是因為在程序運行時,calculate.so
并沒有在系統的默認路徑下,所以OS找不到!那么如何才能讓OS找到我們的庫呢?答案是需要我們自己來配置。
🐦配置動態庫
配置動態庫有三種方法:
- 環境變量:LD_LIBRARY_PATH (臨時方案);
- 軟鏈接方案;
- 配置文件方案
🐱環境變量
導入環境變量LD_LIBRARY_PATH:
$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/..../lib/ # 你的.so文件存放路徑
查看環境變量LD_LIBRARY_PATH:
$ echo $LD_LIBRARY_PATH
:/usr/local/protobuf/lib/:/home/hxy/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/..../lib/ # 你的.so
現在再來運行程序就可以成功運行了:
$ ./test
10 + 3 = 13
10 - 3 = 7
🐱軟鏈接
上面我們說在程序運行時,calculate.so并沒有在系統的默認路徑下,所以OS找不到我們的庫,那么這個默認路徑在哪里呢?
# 一般在這兩個路徑下
$ /lib
$ /lib64/
所以我們直接將庫文件移動到這兩個路徑下也可以,但是還有比較優雅一點的方案,那就是為我們的庫文件建立軟鏈接。
$ sudo ln -s lib/libcalculate.so /lib64/libcalculate.so
$ ./test
10 + 3 = 13
10 - 3 = 7
🐱配置文件
- 在系統的 /etc/ld.so.conf.d/ 目錄下創建一個配置文件;
$ sudo touch /etc/ld.so.conf.d/calculate.conf
- 將動態庫所在路徑寫入配置文件;
$ sudo vim /etc/ld.so.conf.d/calculate.conf
$ sudo cat /etc/ld.so.conf.d/calculate.conf
/home/.../lib # 你的.so文件路徑
- 讓配置文件生效;
$ sudo ldconfig
現在小黑的程序也能成功運行了;
$ ./test
10 + 3 = 13
10 - 3 = 7
🐧靜態庫與動態庫的區別
以上我們通過制作一個自己的靜態庫,對庫文件有了基礎的了解。那么庫為何又要分為靜態庫與動態庫呢?二者有何區別?
-
鏈接時機和方式:
- 靜態庫: 在編譯時被鏈接到目標程序中,鏈接器將庫的代碼和數據拷貝到最終的可執行文件中。因此,可執行文件在運行時獨立于庫文件。
- 動態庫: 在編譯時并不直接鏈接到目標程序,而是在運行時由操作系統動態加載到內存中。動態庫的鏈接發生在程序啟動時(靜態加載)或在運行時(動態加載)。
-
文件大小和內存占用:
- 靜態庫: 鏈接時會將庫的代碼和數據完全復制到目標程序中,可能導致可執行文件較大。每個使用該庫的可執行文件都包含一份庫的拷貝。
- 動態庫: 多個程序可以共享同一個動態庫的實例,因此相同的庫只需要在內存中存在一份,可以減小程序的大小。
-
更新和維護:
- 靜態庫: 如果庫的代碼或數據發生變化,需要重新編譯并重新鏈接所有使用該庫的程序。每個程序都需要更新以包含最新版本的庫。
- 動態庫: 如果庫的代碼或數據發生變化,只需要替換庫文件而無需重新編譯和鏈接使用該庫的程序。這使得動態庫更容易更新和維護。
-
跨平臺兼容性:
- 靜態庫: 可執行文件與庫的鏈接是在編譯時完成的,因此在不同平臺上可能需要不同版本的庫。
- 動態庫: 由于動態庫的加載是在運行時由操作系統完成的,因此相同的動態庫文件可以在多個平臺上使用。
-
運行時靈活性:
- 靜態庫: 執行文件在編譯時固定了對靜態庫的依賴,無法在運行時更改。
- 動態庫: 可以在運行時加載或替換動態庫,這使得系統更加靈活。
🐧動態庫的運作原理
🐔為什么進程可以在運行時加載動態庫?
我們知道每個程序在運行時就變成一個進程,一個進程擁有自己的虛擬地址空間。
在程序運行時,我們只需要將庫加載到內存當中,經過頁表
映射到進程的地址空間中,我們的代碼執行庫中的方法就依舊還是在自己的地址空間中進行函數跳轉。
🐔為什么多個進程可以共享一個動態庫
當多個進程同時運行時,按照同樣的方式,將庫中的地址映射到每個進程的地址空間中,那么如果每個程序使用的地址都是相同的,不會產生沖突嗎?
還記得我們在用 gcc
生成動態庫時用到的參數 - fPIC
嗎?
-fPIC
是 GCC 編譯器選項,用于生成位置無關碼(Position Independent Code,PIC)。位置無關碼是一種在內存中加載時不依賴于特定內存地址的機器碼,通常用于共享庫(動態鏈接庫)的編譯。
具體來說,使用 -fPIC
選項的目的是允許將生成的目標文件用于共享庫,而這些庫可以被多個進程加載到內存的不同地址上,而不會發生地址沖突。
它主要特點包括:
-
位置獨立性: 生成的代碼不依賴于特定的內存地址,可以在不同的內存地址空間中運行。這對于動態鏈接庫是必要的,因為它們可能在不同的進程中加載并映射到不同的地址。
-
全局偏移表(Global Offset Table,GOT): 在運行時,PIC 代碼使用全局偏移表,其中包含指向全局和共享庫中的符號的指針。這些指針在加載庫時進行重定位,以便正確地找到符號的位置。
-
避免絕對地址: 使用相對尋址或基于 GOT 的尋址,而不是絕對地址。這使得代碼更容易在不同的地址空間中重定位。
本章的內容到這里就結束了!如果覺得對你有所幫助的話,歡迎三連~