jni加载第三方so_Linux的so文件到底是干嘛的?浅析Linux的动态链接库
HelloWorld背后的故事:在Linux上編譯C語言程序 我們分析了Hello World是如何編譯的,即使一個(gè)非常簡單的程序,也需要依賴C標(biāo)準(zhǔn)庫和系統(tǒng)庫,鏈接其實(shí)就是把其他第三方庫和自己源代碼生成的二進(jìn)制目標(biāo)文件融合在一起的過程。經(jīng)過鏈接之后,那些第三方庫中定義的函數(shù)就能被調(diào)用執(zhí)行了。早期的一些操作系統(tǒng)一般使用靜態(tài)鏈接的方式,現(xiàn)在基本上都在使用動(dòng)態(tài)鏈接的方式。
靜態(tài)鏈接和動(dòng)態(tài)鏈接
雖然靜態(tài)鏈接和動(dòng)態(tài)鏈接都能生成可執(zhí)行文件,但兩者的代價(jià)差異很大。下面這張圖可以很形象地演示了動(dòng)態(tài)鏈接和靜態(tài)鏈接的區(qū)別:
動(dòng)態(tài)鏈接 v.s 靜態(tài)鏈接
左側(cè)的人就像是一個(gè)動(dòng)態(tài)鏈接的可執(zhí)行文件,右側(cè)的海象是一個(gè)靜態(tài)鏈接的可執(zhí)行文件。比起人,海象臃腫得多,那是因?yàn)殪o態(tài)鏈接在鏈接的時(shí)候,就把所依賴的第三方庫函數(shù)都打包到了一起,導(dǎo)致最終的可執(zhí)行文件非常大。而動(dòng)態(tài)鏈接在鏈接的時(shí)候并不將那些庫文件直接拿過來,而是在運(yùn)行時(shí),發(fā)現(xiàn)用到某些庫中的某些函數(shù)時(shí),再從這些第三方庫中讀取自己所需的方法。
我們把編譯后但是還未鏈接的二進(jìn)制機(jī)器碼文件稱為目標(biāo)文件(Object File),那些第三方庫是其他人編譯打包好的目標(biāo)文件,這些庫里面包含了一些函數(shù),我們可以直接調(diào)用而不用自己動(dòng)手寫一遍。在編譯構(gòu)建自己的可執(zhí)行文件時(shí),使用靜態(tài)鏈接的方式,其實(shí)就是將所需的靜態(tài)庫與目標(biāo)文件打包到一起。最終的可執(zhí)行文件除了有自己的程序外,還包含了這些第三方的靜態(tài)庫,可執(zhí)行文件比較臃腫。相比而言,動(dòng)態(tài)鏈接不將所有的第三方庫都打包到最終的可執(zhí)行文件上,而是只記錄用到了哪些動(dòng)態(tài)鏈接庫,在運(yùn)行時(shí)才將那些第三方庫裝載(Load)進(jìn)來。裝載是指將磁盤上的程序和數(shù)據(jù)加載到內(nèi)存上。例如下圖中的Program 1,系統(tǒng)首先加載Program 1,發(fā)現(xiàn)它依賴libx.so后才去加載libx.so。
靜態(tài)鏈接(Static Link)和動(dòng)態(tài)鏈接(Dynamic Link)
所以,靜態(tài)鏈接就像GIF圖中的海象,把所需的東西都帶在了身上。動(dòng)態(tài)鏈接只把精簡后的內(nèi)容帶在自己身上,需要什么,運(yùn)行的時(shí)候再去拿。
不同操作系統(tǒng)的動(dòng)態(tài)鏈接庫文件格式稍有不同,Linux稱之為共享目標(biāo)文件(Shared Object),文件后綴為.so,Windows的動(dòng)態(tài)鏈接庫(Dynamic Link Library)文件后綴為.dll。
地址無關(guān)
無論何種操作系統(tǒng)上,使用動(dòng)態(tài)鏈接生成的目標(biāo)文件中凡是涉及第三方庫的函數(shù)調(diào)用都是地址無關(guān)的。假如我們自己編寫的程序名為Program 1,Program 1中調(diào)用了C標(biāo)準(zhǔn)庫的printf(),在生成的目標(biāo)文件中,不會(huì)立即確定printf()的具體地址,而是在運(yùn)行時(shí)去裝載這個(gè)函數(shù),在裝載階段確定printf()的地址。這里提到的地址指的是進(jìn)程在內(nèi)存上的虛擬地址。動(dòng)態(tài)鏈接庫的函數(shù)地址在編譯時(shí)是不確定的,在裝載時(shí),裝載器根據(jù)當(dāng)前地址空間情況,動(dòng)態(tài)地分配一塊虛擬地址空間。
而靜態(tài)鏈接庫其實(shí)是在編譯時(shí)就確定了庫函數(shù)地址。比如,我們使用了printf()函數(shù),printf()函數(shù)對(duì)應(yīng)有一個(gè)目標(biāo)文件printf.o,靜態(tài)鏈接時(shí),會(huì)把printf.o鏈接打包到可執(zhí)行文件中。在可執(zhí)行文件中,printf()函數(shù)相對(duì)于文件頭的偏移量是確定的,所以說它的地址在編譯鏈接后就是確定的。
動(dòng)態(tài)鏈接的優(yōu)缺點(diǎn)
相比之下,動(dòng)態(tài)鏈接主要有以下好處:
- 多個(gè)可執(zhí)行文件可以共享使用系統(tǒng)中的共享庫。每個(gè)可執(zhí)行文件都更小,占用的磁盤空間也相對(duì)比較小。而靜態(tài)鏈接把所依賴的庫打包進(jìn)可執(zhí)行文件,假如printf()被其他程序使用了上千次,就要被打包到上千個(gè)可執(zhí)行文件中,這樣會(huì)占用了大量磁盤空間。
- 共享庫的之間隔離決定了共享庫可以進(jìn)行小版本的代碼升級(jí),重新編譯并部署到操作系統(tǒng)上,并不影響它被可執(zhí)行文件調(diào)用。靜態(tài)鏈接庫的任何函數(shù)有了改動(dòng),除了靜態(tài)鏈接庫本身需要重新編譯構(gòu)建,依賴這個(gè)函數(shù)的所有可執(zhí)行文件都需要重新編譯構(gòu)建一遍。
當(dāng)然,共享庫也有缺點(diǎn):
- 如果將一份目標(biāo)文件移植到一個(gè)新的操作系統(tǒng)上,而新的操作系統(tǒng)缺少相應(yīng)的共享庫,程序?qū)o法運(yùn)行,必須在操作系統(tǒng)上安裝好相應(yīng)的庫才行。
- 共享庫必須按照一定的開發(fā)和升級(jí)規(guī)則升級(jí),不能突然重構(gòu)所有的接口,且新庫文件直接覆蓋老庫文件,否則程序?qū)o法運(yùn)行。
ldd命令查看動(dòng)態(tài)鏈接庫依賴
在Linux上,動(dòng)態(tài)鏈接庫有默認(rèn)的部署位置,很多重要的庫放在了系統(tǒng)的/lib和/usr/lib兩個(gè)路徑下。一些常用的Linux命令非常依賴/lib和/usr/lib64下面的各個(gè)庫,比如:scp、rm、cp、mv等Linux下常用的命令非常依賴/lib和/usr/lib64下的各個(gè)庫。不小心刪除了這些路徑,可能導(dǎo)致系統(tǒng)的很多命令和工具都無法繼續(xù)使用。
我們可以用ldd命令查看某個(gè)可執(zhí)行文件依賴了哪些動(dòng)態(tài)鏈接庫。
# on Ubuntu 16.04 x86_64$ ldd /bin/ls linux-vdso.so.1 => (0x00007ffcd3dd9000) libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f4547151000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4546d87000) libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007f4546b17000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f4546913000) /lib64/ld-linux-x86-64.so.2 (0x00007f4547373000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f45466f6000)復(fù)制代碼可以看到,我們經(jīng)常使用的ls命令依賴了不少庫,包括了C語言標(biāo)準(zhǔn)庫libc.so。
如果某個(gè)Linux的程序報(bào)錯(cuò)提示缺少某個(gè)庫,可以用ldd命令可以用來檢查這個(gè)程序依賴了哪些庫,是否能在磁盤某個(gè)路徑下找到.so文件。如果找不到,需要使用環(huán)境變量LD_LIBRARY_PATH來調(diào)整,下文將介紹環(huán)境變量LD_LIBRARY_PATH。
SONAME文件命名規(guī)則
so文件后面往往跟著很多數(shù)字,這表示了不同的版本。so文件命名規(guī)則被稱為SONAME:
libname.so.x.y.zlib是前綴,這是一個(gè)約定俗成的規(guī)則。x為主版本號(hào)(Major Version),y為次版本號(hào)(Minor Version),z為發(fā)布版本號(hào)(Release Version)。
- Major Version表示重大升級(jí),不同Major Version之間的庫是不兼容的。Major Version升級(jí)后,或者依賴舊Major Version的程序需要更新代碼,重新編譯,才可以在新的Major Version上運(yùn)行;或者操作系統(tǒng)保留舊Major Version,使得老程序依然能運(yùn)行。
- Minor Version表示增量更新,一般是增加了一些新接口,原來的接口不變。所以,在Major Version相同的情況下,Minor Version從高到低是兼容的。
- Release Version表示庫的一些bug修復(fù),性能改進(jìn)等,不添加任何新的接口,不改變?cè)瓉淼慕涌凇?/li>
但是我們剛剛看到的.so只有一個(gè)Major Version,因?yàn)檫@是一個(gè)軟連接,libname.so.x軟連接到了libname.so.x.y.z文件上。
$ ls -l /lib/x86_64-linux-gnu/libpcre.so.3/lib/x86_64-linux-gnu/libpcre.so.3 -> libpcre.so.3.13.2因?yàn)椴煌腗ajor Version之間不兼容,而Minor Version和Release Version都是向下兼容的,軟連接會(huì)指向Major Version相同,Minor Version和Release Version最高的.so文件上。
動(dòng)態(tài)鏈接庫查找過程
剛才提到,Linux的動(dòng)態(tài)鏈接庫絕大多數(shù)都在/lib和/usr/lib下,操作系統(tǒng)也會(huì)默認(rèn)去這兩個(gè)路徑下搜索動(dòng)態(tài)鏈接庫。另外,/etc/ld.so.conf文件里可以配置路徑,/etc/ld.so.conf文件會(huì)告訴操作系統(tǒng)去哪些路徑下搜索動(dòng)態(tài)鏈接庫。這些位置的動(dòng)態(tài)鏈接庫很多,如果鏈接器每次都去這些路徑遍歷一遍,非常耗時(shí),Linux提供了ldconfig工具,這個(gè)工具會(huì)對(duì)這些路徑的動(dòng)態(tài)鏈接庫按照SONAME規(guī)則創(chuàng)建軟連接,同時(shí)也會(huì)生成一個(gè)緩存Cache到/etc/ld.so.cache文件里,鏈接器根據(jù)緩存可以更快地查找到各個(gè).so文件。每次在/lib和/usr/lib這些路徑下安裝了新的庫,或者更改了/etc/ld.so.conf文件,都需要調(diào)用ldconfig命令來做一次更新,重新生成軟連接和Cache。但是/etc/ld.so.conf文件和ldconfig命令最好使用root賬戶操作。非root用戶可以在某個(gè)路徑下安裝庫文件,并將這個(gè)路徑添加到/etc/ld.so.conf文件下,再由root用戶調(diào)用一下ldconfig。
對(duì)于非root用戶,另一種方法是使用LD_LIBRARY_PATH環(huán)境變量。LD_LIBRARY_PATH存放著若干路徑。鏈接器會(huì)去這些路徑下查找?guī)臁7莚oot可以將某個(gè)庫安裝在了一個(gè)非root權(quán)限的路徑下,再將其添加到環(huán)境變量中。
動(dòng)態(tài)鏈接庫的查找先后順序?yàn)?#xff1a;
- LD_LIBRARY_PATH環(huán)境變量中的路徑
- /etc/ld.so.cache緩存文件
- /usr/lib和/lib
比如,我們把CUDA安裝到/opt下面,我們可以使用下面的命令將CUDA添加到環(huán)境變量里。
export LD_LIBRARY_PATH=/opt/cuda/cuda-toolkit/lib64:$LD_LIBRARY_PATH如果在執(zhí)行某個(gè)具體程序前先執(zhí)行上面的命令,那么這個(gè)程序?qū)⑹褂眠@個(gè)路徑下的CUDA;如果將這行添加到了.bashrc文件,那么該用戶一登錄就會(huì)執(zhí)行這行命令,因此該用戶的所有程序也都將使用這個(gè)路徑下的CUDA。當(dāng)同一個(gè)動(dòng)態(tài)鏈接庫有多個(gè)不同版本的.so文件時(shí),可以將它們安裝到不同的路徑下面,然后使用LD_LIBRARY_PATH環(huán)境變量來控制使用哪個(gè)庫。這種比較適合在多人共享的服務(wù)器上使用不同版本的庫,比如CUDA這種版本變化較快,且深度學(xué)習(xí)程序又高度依賴的庫。
除了LD_LIBRARY_PATH環(huán)境變量外,還有一個(gè)LD_PRELOAD環(huán)境變量。LD_PRELOAD的查找順序比LD_LIBRARY_PATH還要優(yōu)先。LD_PRELOAD里是具體的目標(biāo)文件列表(A list of shared objects);LD_LIBRARY_PATH是目錄列表(A list of directories)。
GCC編譯選項(xiàng)
使用GCC編譯鏈接時(shí),有兩個(gè)參數(shù)需要注意,一個(gè)是-l(小寫的L),一個(gè)是-L(大寫的L)。我們前面曾提到,Linux有個(gè)約定速成的規(guī)則,假如庫名是name,那么動(dòng)態(tài)鏈接庫文件名就是libname.so。在使用GCC編譯鏈接時(shí),-lname來告訴GCC使用哪個(gè)庫。鏈接時(shí),GCC的鏈接器ld就會(huì)前往LD_LIBRARY_PATH環(huán)境變量、/etc/ld.so.cache緩存文件和/usr/lib和/lib目錄下去查找libname.so。我們也可以用-L/path/to/library的方式,讓鏈接器ld去/path/to/library路徑下去找?guī)煳募?/p>
如果動(dòng)態(tài)鏈接庫文件在/path/to/library,庫名叫name,編譯鏈接的方式如下:
$ gcc -L/path/to/library -lname myfile.c總結(jié)
以上是生活随笔為你收集整理的jni加载第三方so_Linux的so文件到底是干嘛的?浅析Linux的动态链接库的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: git gui here如何汉化_你不知
- 下一篇: 云鲸扫拖一体机器人说明书_比老公更好用的