1.什么是庫
在計算機編程中,庫(library)是一個預先編寫的代碼集合,包含了可以被其他程序調用的函數、類、變量和資源。庫的主要目的是為了簡化編程過程,提供常用功能的實現,促進代碼重用,從而減少開發時間和提高軟件的可靠性。
在實踐中,我們一定會使用到別人的庫,如你在c語言時期一定會調用到c標準庫。除了標準庫,我們還可以使用第三方庫,無論使用那種庫都是為了節省我們的時間,不在需要我們自己來造“輪子”。
所有的庫都可以從兩個方面來認識:
創建者使用者提問:使用者在使用庫時,是否能知道該庫的源代碼呢?回答:在不逆向的情況下,使用者是無法得知庫的源代碼的,這也就牽扯到了,庫的第二個屬性隱藏源代碼。我們都知道,形成可執行文件的步驟有4步:預處理:頭文件展開、去注釋、宏替換、條件編譯等,生成.i文件。編譯:語法分析、語義分析、符號匯總等,檢查無誤后將代碼翻譯成匯編指令,生成.s文件,匯編:將匯編指令轉化成二進制指令,生成.o文件。鏈接:將生成的各個.o文件進行鏈接,生成可執行程序。
而庫就是所有.o文件用特定的方式,進行打包,形成的一個文件。
注意庫文件需要配合對應的頭文件進行使用,頭文件不必隱藏
當我們在main.c中使用對應的功能函數:

當有許多不同的源文件去調用這些功能函數時,那要的話,功能函數會被重復的進行預處理、編譯、匯編的操作,各自生成.o文件,然后我們的源文件再和這些功能函數一起生成可執行文件。
如此操作會有許多重復的操作,我們完全可以提前讓功能函數變成一個個.o文件,再去和源文件進行鏈接,但是我們還要考慮到,可能會存在非常多的.o文件,為了減輕我們的操作,我們可以對這些.o文件進行打包,這樣的文件我們稱為庫。
如此一來,庫的本質就是若干個目標文件的集合,每一個目標文件都包含了由源碼編譯生成的二進制代碼,在保證使用的同時,還有很好的隱藏性
1.2 認識動靜態庫
動靜態庫是編程中常用的兩種庫類型,用于封裝和重用代碼。它們在鏈接、加載和使用方面存在顯著的差異。
1.2.1 動態庫
動態庫(或共享庫)是在運行時加載的庫,通常以 .so(linux)或 .dll(windows)文件格式存在。動態庫的代碼不被復制到可執行文件中,而是在運行時由操作系統加載。
我們先來寫一段簡單的代碼:
代碼語言:JavaScript代碼運行次數:0運行復制
#include <stdio.h>int main(){ printf("hello world!!!!n"); return 0;}//生成可執行文件mybin
編譯成功后,我們使用ldd指令來查看其所鏈接的動態庫
ldd語法:
代碼語言:javascript代碼運行次數:0運行復制
ldd filename
功能:
代碼語言:javascript代碼運行次數:0運行復制
ubuntu@VM-20-9-ubuntu:~/libraryTest$ ldd mybin linux-vdso.so.1 (0x00007ffc9c4db000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd8df175000)/lib64/ld-linux-x86-64.so.2 (0x00007fd8df3ab000)
libc.so 就是該程序所依賴的動態庫,那么我們一個如何識別一個動態庫的名字呢?
也就是說,這個動態庫的名字就是c,沒錯這就是c運行庫。
1.2.2 靜態庫
靜態庫是將一組對象文件(.o 文件)打包成一個庫文件(通常為 .a 后綴),在編譯時將其鏈接到最終生成的可執行文件中。鏈接過程是在編譯階段完成的,庫的代碼被復制到可執行文件中。
正常情況下,gcc默認都是連接的動態庫,如果需要進行靜態連接要特別指定。
代碼語言:javascript代碼運行次數:0運行復制
gcc -o StaticBin libTest.c -staticubuntu@VM-20-9-ubuntu:~/libraryTest$ ls -ltotal 904-rw-rw-r-- 1 ubuntu ubuntu 82 Oct 29 11:40 libTest.c-rw-rw-r-- 1 ubuntu ubuntu 74 Oct 29 11:41 makefile-rwxrwxr-x 1 ubuntu ubuntu 15952 Oct 29 11:41 mybin-rwxrwxr-x 1 ubuntu ubuntu 900344 Oct 29 14:49 staticBin
可以發現靜態連接的可執行程序所占的空間大小遠大于動態連接的。
使用ldd觀察是否有依賴的庫
代碼語言:javascript代碼運行次數:0運行復制
ubuntu@VM-20-9-ubuntu:~/libraryTest$ ldd staticBin not a dynamic executable
發現靜態鏈接生成的可執行程序不依賴其他庫文件。
使用file查看:
代碼語言:javascript代碼運行次數:0運行復制
ubuntu@VM-20-9-ubuntu:~/libraryTest$ file staticBin staticBin: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=d4a39b68ac04f9fd8b37bd8c17ce42a8b27ad8c2, for GNU/Linux 3.2.0, not stripped
發現是:statically linked
如果你是centos用戶,可能會遇到靜態庫未安裝的情況,只需要搜索一下安裝方法即可。
2.封裝動靜態庫2.1 封裝動態庫
先寫好需要封裝的代碼:
add.h:
代碼語言:javascript代碼運行次數:0運行復制
#pragma onceint add(int a,int b);
sub.h:
代碼語言:javascript代碼運行次數:0運行復制
#pragma onceint sub(int a,int b);
add.c:
代碼語言:javascript代碼運行次數:0運行復制
#include "add.h"int add(int a,int b){ return a+b;}
sub.c:
代碼語言:javascript代碼運行次數:0運行復制
#include "sub.h"int sub(int a,int b){ return a-b;}
dynLibTest.c:
代碼語言:javascript代碼運行次數:0運行復制
#include <stdio.h>#include "add.h"#include "sub.h"int main(){ int a = 100; int b = 200; printf("%d + %d = %dn",a,b,add(a,b)); printf("%d - %d = %dn",a,b,sub(a,b)); return 0;}
然后我們還需要知道位置無關碼的概念(position-Independent Code,PIC)
位置無關碼(Position-Independent Code,PIC)是一種編譯代碼的方式,使得生成的代碼可以在內存的任意位置執行,而不需要修改代碼中的地址。這種特性在動態鏈接庫和共享庫中非常重要,因為它們可以被多個進程共享,并在加載時被放置到不同的內存地址。
為了實現位置無關,編譯器在生成代碼時使用相對地址而不是絕對地址。例如,在訪問全局變量時,編譯器不會生成直接訪問變量的絕對地址的代碼,而是使用相對于當前指令位置的偏移量。這樣,無論代碼被加載到哪個地址,訪問都可以通過相對計算來實現。
位置無關碼是一種重要的編程技術,尤其在動態鏈接和共享庫中具有廣泛的應用。它提供了靈活性和內存使用效率,同時也增強了程序的安全性。
如果我要實現位置無關碼可以在gcc后面加上-fPIC選項
代碼語言:javascript代碼運行次數:0運行復制
gcc -fPIC -c add.cgcc -fPIC -c sub.c

我們也知道不加-fPIC一樣可以生成.o文件。但是它們之間還是很有區別的。
位置無關碼對于gcc:
fPIC作用于編譯階段,告訴編譯器于位置無關的代碼,此時產生的代碼中沒有絕對地址,全部都使用相對地址,從而代碼可以被加載到內存的任意位置可以正確的執行。這正是共享庫被加載時,在內存的位置不是不是固定的。如果不加-fPIC選項,則加載.so文件的代碼時,代碼段引用的數據對象需要重定位,這個重定位會修改代碼段地內容,這就會造成每一個使用這個.so文件代碼段的內核里都會生成這個.so文件代碼的拷貝,并且每一個拷貝都不一樣,這就是使得內存的消耗變大。為此我們總是會用-fPIC來生成.so文件,但是不會用點-fPIC來生成.a靜態文件。我們當然可以不用-fPIC來生成.so文件,只是這樣的話.so文件必須要在加載到用戶的地址空間時重定向所有表目。使用**-shard**將文件打包為動態庫代碼語言:javascript代碼運行次數:0運行復制
gcc -shared -o libmyc.so add.o sub.o
通過readelf -S 來查看庫的信息,如偏移量offset。

2.1.1 組織頭文件和庫文件
為了方便我們后續的操作,我會寫一個makefile來組織頭文件和庫文件。
代碼語言:javascript代碼運行次數:0運行復制
.PHONY:allall:libmyc.solibmyc.so:add.o sub.ogcc -shared add.o sub.o -o libmyc.soadd.o:add.cgcc -fPIC -c add.c -o add.osub.o:sub.cgcc -fPIC -c sub.c -o sub.o.PHONY:outputoutput:mkdir -p mylib/libmkdir -p mylib/includecp -rf *.h mylib/includecp -rf *.so mylib/lib.PHONY:cleanclean:rm -rf *.o mylib *.a *.so output

2.1.2 動態庫的使用
當我們把當前目錄的頭文件和目標文件刪除后
代碼語言:javascript代碼運行次數:0運行復制
ubuntu@VM-20-9-ubuntu:~/libraryTest$ rm -rf *.o *.h
此時如果我們直接使用gcc來編譯會發生什么呢?
沒錯,肯定會報錯說,找不到頭文件啦。
代碼語言:javascript代碼運行次數:0運行復制
ubuntu@VM-20-9-ubuntu:~/libraryTest$ gcc dynLibTest.c dynLibTest.c:2:10: fatal error: add.h: No such file or directory 2 | #include "add.h" | ^~~~~~~compilation terminated.
為了讓編譯器能夠找到我們的頭文件,需要我們加上-I 頭文件所在路徑
代碼語言:javascript代碼運行次數:0運行復制
gcc -I./mylib/include dynLibTest.c
現在的錯誤就是,找不到目標函數了,因為我們沒有給編譯器指明在哪里,它肯定就找不到了。
代碼語言:javascript代碼運行次數:0運行復制
ubuntu@VM-20-9-ubuntu:~/libraryTest$ gcc -I./mylib/include dynLibTest.c/usr/bin/ld: /tmp/ccOkC8RY.o: in function `main':dynLibTest.c:(.text+0x25): undefined reference to `add'/usr/bin/ld: dynLibTest.c:(.text+0x52): undefined reference to `sub'collect2: error: ld returned 1 exit status
所有我們還需要加上`-L庫文件所在路徑 l庫名
代碼語言:javascript代碼運行次數:0運行復制
gcc -I./mylib/include -L./mylib/lib dynLibTest.c -lmyc
‘-I’:指定頭文件搜索路徑?!狶:指定庫文件搜索路徑。-l:指明需要鏈接庫文件路徑下的哪一個庫。
然后我們就可以得到一個可執行文件了,但是如果你去執行它就會發現,它居然不能執行?。?!
代碼語言:javascript代碼運行次數:0運行復制
ubuntu@VM-20-9-ubuntu:~/libraryTest$ ./a.out ./a.out: error while loading shared libraries: libmyc.so: cannot open shared object file: No such file or directory
為什么會這樣?
算了,先用ldd看看它依賴的動態庫的吧。

居然沒有連到,豈有此理,辛苦打了一連串指令竟然完全沒作用。
其實不是這樣啦,因為是動態庫,在執行時我們仍然需要知道動態庫的位置在哪。
對于動態庫,編譯時會搜索庫的路徑,運行時也會搜索庫的路徑。
為了解決這個問題,我們有4種解決方案:
直接將庫進行安裝(拷貝)到系統庫當中。(最傻瓜操作)將不在系統默認庫搜索路徑下的庫路徑,添加到LD_LIBRARY_PATH3.使用idconfig指令。
拷貝到系統目錄
代碼語言:javascript代碼運行次數:0運行復制
ubuntu@VM-20-9-ubuntu:~/libraryTest$ sudo cp mylib/lib/libmyc.so /lib/x86_64-linux-gnu/ubuntu@VM-20-9-ubuntu:~/libraryTest$ ldd a.out linux-vdso.so.1 (0x00007fffc4f54000)libmyc.so => /lib/x86_64-linux-gnu/libmyc.so (0x00007f35c8f9c000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f35c8d73000)/lib64/ld-linux-x86-64.so.2 (0x00007f35c8fae000)
這樣雖然簡單,但是由于我們自己書寫的庫,大概率是不成熟的,這樣會污染系統庫目錄。
更改環境變量
代碼語言:javascript代碼運行次數:0運行復制
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:home/ubuntu/libraryTest/mylib/lib
ldd查看:
代碼語言:javascript代碼運行次數:0運行復制
ubuntu@VM-20-9-ubuntu:~/libraryTest$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:home/ubuntu/libraryTest/mylib/libubuntu@VM-20-9-ubuntu:~/libraryTest$ ldd a.outlinux-vdso.so.1 (0x00007fff8fdee000)libmyc.so (0x00007f85aaf99000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f85aad6a000)/lib64/ld-linux-x86-64.so.2 (0x00007f85aafa5000)
該方法為臨時方法,在系統重新登入時就失效了。
使用Idconfig指令
/etc/ld.so.conf.d/目錄下的文件用來指定動態庫搜索路徑。這些文件被包含在/etc/ld.so.conf文件中,ldconfig命令會在默認搜尋目錄(/lib和/usr/lib)以及動態庫配置文件/etc/ld.so.conf內所列的目錄下,搜索可共享的動態鏈接庫,并創建出動態裝入程序(ld.so)所需的連接和緩存文件。
.conf文件用來存儲各種文件的路徑,我們只要把我們自己寫的第三方庫的路徑存放進去,程序運行時就會去里面找了。
代碼語言:javascript代碼運行次數:0運行復制
echo /home/ubuntu/libraryTest/mylib/lib > libmyc.conf
然后我們需要將,.conf文件拷貝到/etc/ld.so.conf.d/下。

代碼語言:javascript代碼運行次數:0運行復制
sudo cp libmyc.conf /etc/ld.so.conf.d/
此時如果我們直接ldd是無法找到的,我們來需要更新一下。
代碼語言:javascript代碼運行次數:0運行復制
sudo ldconfig
然后就可以了

那么動態庫就先到這里,下面我們開始靜態庫的講解。
2.2 封裝靜態庫
靜態庫的操作會比動態庫的更為簡單。
代碼的話,依然是上面的那些代碼。
代碼語言:javascript代碼運行次數:0運行復制
add.c sub.c add.h sub.h staLibTest.c
staLibTest.c里面的代碼和dynLibTest.c
首先我們需要把所有的.c源文件都編譯為目標文件。(此時不在需要位置無關碼)
代碼語言:javascript代碼運行次數:0運行復制
gcc -c add.cgcc -c sub.c
我們可以直接將主程序和這些.o文件進行編譯。
代碼語言:javascript代碼運行次數:0運行復制
ubuntu@VM-20-9-ubuntu:~/libraryTest$ gcc -o staticBin staLibTest.c add.o sub.oubuntu@VM-20-9-ubuntu:~/libraryTest$ ./staticBin 100 + 200 = 300100 - 200 = -100
正常編譯,不過目標文件少的情況下還好,如果有很多的目標文件,我們就有點吃力了。為此我們可以打一個包。
使用ar指令將目標文件打包為靜態庫:
語法:
代碼語言:javascript代碼運行次數:0運行復制
ar [選項] [庫名] [依賴文件]
主要功能:
r:插入文件。如果目標文件已經存在,則替換它。c:創建一個新的庫,如果庫文件不存在。s:創建索引,以加快鏈接過程。t:列出庫中包含的文件。x:從庫中提取指定的文件。d:從庫中刪除指定的文件。v:查看庫的信息舉例:將add.o和sub.o打包為靜態庫代碼語言:javascript代碼運行次數:0運行復制
ar -rc libmyc.a add.o sub.o
利用-t -v選項來查看靜態庫的文件以及信息。
代碼語言:javascript代碼運行次數:0運行復制
ubuntu@VM-20-9-ubuntu:~/libraryTest$ ar -tv libmyc.arw-r--r-- 0/0 1224 Jan 1 08:00 1970 add.orw-r--r-- 0/0 1224 Jan 1 08:00 1970 sub.o
和動態庫時一樣,我們現在利用makefile來完成這些操作
代碼語言:javascript代碼運行次數:0運行復制
libmyc.a:add.o sub.oar -rc libmyc.a add.o sub.oadd.o:add.cgcc -o add.o -c add.csub.o:sub.cgcc -o sub.o -c sub.c.PHONY:outputoutput:mkdir -p mylibs/libmkdir -p mylibs/includecp -rf *.h mylibs/includecp -rf *.a mylibs/lib.PHONY:cleanclean:rm -rf *.o mylibs libmyc.a

2.2.1 使用靜態庫‘-I’:指定頭文件搜索路徑。—L:指定庫文件搜索路徑。-l:指明需要鏈接庫文件路徑下的哪一個庫。
在動態庫的篇章中,我們就已經了解了指定庫的路徑了。
在靜態庫也同理。
代碼語言:javascript代碼運行次數:0運行復制
gcc staLibTest.c -I./mybins/include -L./mylibs/lib -lmyc
編譯完后就可以直接運行了。
代碼語言:javascript代碼運行次數:0運行復制
ubuntu@VM-20-9-ubuntu:~/libraryTest$ gcc staLibTest.c -I./mybins/include -L./mylibs/lib -lmyc -o testbin -staticubuntu@VM-20-9-ubuntu:~/libraryTest$ ./testbin 100 + 200 = 300100 - 200 = -100
同樣的,如果你覺得沒錯都顯示指定庫連接很麻煩,可以把目標文件拷貝到系統庫當中。
3. 動靜態庫小知識3.1 gcc對動靜態的優先級
如果我們同時提供動態庫和靜態庫,gcc會默認使用動態庫。如果我們非要靜態鏈接,必須使用static指定。如果我們只提供靜態庫,那我們的程序只能對該庫進行靜態鏈接,但是程序不一定整體是靜態鏈接的。如果我們只提供動態庫,默認只能動態連接,非要靜態鏈接的話會報錯。
3.2 動靜態庫的區別
特性
靜態庫
動態庫
文件后綴
.a
.so (Linux), .dll (Windows)
鏈接方式
編譯時鏈接
運行時鏈接
可執行文件大小
較大(包含所有庫代碼)
較?。ㄖ话茫?/p>
外部依賴
無
需要在運行時提供
更新方式
需要重新編譯所有依賴的程序
只需替換庫文件
性能
加載速度快
加載速度相對較慢
共享性
不支持多個進程共享
支持多個進程共享
4.總結
動靜態庫各有優缺點,選擇使用哪種庫通常取決于具體的項目需求、資源限制和開發環境。靜態庫適用于對依賴性和更新不敏感的應用,而動態庫則更靈活,適合需要頻繁更新和共享代碼的場景。在實際開發中,合理選擇和使用這兩種庫能夠提高代碼的復用性和維護性。