Linux的so文件到底是幹嘛的?淺析Linux的動態連結庫

2020-09-12 皮皮魯的科技星球

我們分析了Hello World是如何編譯的,即使一個非常簡單的程序,也需要依賴C標準庫和系統庫,連結其實就是把其他第三方庫和自己原始碼生成的二進位目標文件融合在一起的過程。經過連結之後,那些第三方庫中定義的函數就能被調用執行了。早期的一些作業系統一般使用靜態連結的方式,現在基本上都在使用動態連結的方式。

靜態連結和動態連結

雖然靜態連結和動態連結都能生成可執行文件,但兩者的代價差異很大。下面這張圖可以很形象地演示了動態連結和靜態連結的區別:

動態連結 v.s 靜態連結

左側的人就像是一個動態連結的可執行文件,右側的海象是一個靜態連結的可執行文件。比起人,海象臃腫得多,那是因為靜態連結在連結的時候,就把所依賴的第三方庫函數都打包到了一起,導致最終的可執行文件非常大。而動態連結在連結的時候並不將那些庫文件直接拿過來,而是在運行時,發現用到某些庫中的某些函數時,再從這些第三方庫中讀取自己所需的方法。

我們把編譯後但是還未連結的二進位機器碼文件稱為目標文件(Object File),那些第三方庫是其他人編譯打包好的目標文件,這些庫裡面包含了一些函數,我們可以直接調用而不用自己動手寫一遍。在編譯構建自己的可執行文件時,使用靜態連結的方式,其實就是將所需的靜態庫與目標文件打包到一起。最終的可執行文件除了有自己的程序外,還包含了這些第三方的靜態庫,可執行文件比較臃腫。相比而言,動態連結不將所有的第三方庫都打包到最終的可執行文件上,而是只記錄用到了哪些動態連結庫,在運行時才將那些第三方庫裝載(Load)進來。裝載是指將磁碟上的程序和數據加載到內存上。例如下圖中的Program 1,系統首先加載Program 1,發現它依賴libx.so後才去加載libx.so。

靜態連結(Static Link)和動態連結(Dynamic Link)

所以,靜態連結就像GIF圖中的海象,把所需的東西都帶在了身上。動態連結只把精簡後的內容帶在自己身上,需要什麼,運行的時候再去拿。

不同作業系統的動態連結庫文件格式稍有不同,Linux稱之為共享目標文件(Shared Object),文件後綴為.so,Windows的動態連結庫(Dynamic Link Library)文件後綴為.dll。

地址無關

無論何種作業系統上,使用動態連結生成的目標文件中凡是涉及第三方庫的函數調用都是地址無關的。假如我們自己編寫的程序名為Program 1,Program 1中調用了C標準庫的printf(),在生成的目標文件中,不會立即確定printf()的具體地址,而是在運行時去裝載這個函數,在裝載階段確定printf()的地址。這裡提到的地址指的是進程在內存上的虛擬地址。動態連結庫的函數地址在編譯時是不確定的,在裝載時,裝載器根據當前地址空間情況,動態地分配一塊虛擬地址空間。

而靜態連結庫其實是在編譯時就確定了庫函數地址。比如,我們使用了printf()函數,printf()函數對應有一個目標文件printf.o,靜態連結時,會把printf.o連結打包到可執行文件中。在可執行文件中,printf()函數相對於文件頭的偏移量是確定的,所以說它的地址在編譯連結後就是確定的。

動態連結的優缺點

相比之下,動態連結主要有以下好處:

  • 多個可執行文件可以共享使用系統中的共享庫。每個可執行文件都更小,佔用的磁碟空間也相對比較小。而靜態連結把所依賴的庫打包進可執行文件,假如printf()被其他程序使用了上千次,就要被打包到上千個可執行文件中,這樣會佔用了大量磁碟空間。
  • 共享庫的之間隔離決定了共享庫可以進行小版本的代碼升級,重新編譯並部署到作業系統上,並不影響它被可執行文件調用。靜態連結庫的任何函數有了改動,除了靜態連結庫本身需要重新編譯構建,依賴這個函數的所有可執行文件都需要重新編譯構建一遍。

當然,共享庫也有缺點:

  • 如果將一份目標文件移植到一個新的作業系統上,而新的作業系統缺少相應的共享庫,程序將無法運行,必須在作業系統上安裝好相應的庫才行。
  • 共享庫必須按照一定的開發和升級規則升級,不能突然重構所有的接口,且新庫文件直接覆蓋老庫文件,否則程序將無法運行。

ldd命令查看動態連結庫依賴

在Linux上,動態連結庫有默認的部署位置,很多重要的庫放在了系統的/lib和/usr/lib兩個路徑下。一些常用的Linux命令非常依賴/lib和/usr/lib64下面的各個庫,比如:scp、rm、cp、mv等Linux下常用的命令非常依賴/lib和/usr/lib64下的各個庫。不小心刪除了這些路徑,可能導致系統的很多命令和工具都無法繼續使用。

我們可以用ldd命令查看某個可執行文件依賴了哪些動態連結庫。

# 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)複製代碼

可以看到,我們經常使用的ls命令依賴了不少庫,包括了C語言標準庫libc.so。

如果某個Linux的程序報錯提示缺少某個庫,可以用ldd命令可以用來檢查這個程序依賴了哪些庫,是否能在磁碟某個路徑下找到.so文件。如果找不到,需要使用環境變量LD_LIBRARY_PATH來調整,下文將介紹環境變量LD_LIBRARY_PATH。

SONAME文件命名規則

so文件後面往往跟著很多數字,這表示了不同的版本。so文件命名規則被稱為SONAME:

libname.so.x.y.z

lib是前綴,這是一個約定俗成的規則。x為主版本號(Major Version),y為次版本號(Minor Version),z為發布版本號(Release Version)。

  • Major Version表示重大升級,不同Major Version之間的庫是不兼容的。Major Version升級後,或者依賴舊Major Version的程序需要更新代碼,重新編譯,才可以在新的Major Version上運行;或者作業系統保留舊Major Version,使得老程序依然能運行。
  • Minor Version表示增量更新,一般是增加了一些新接口,原來的接口不變。所以,在Major Version相同的情況下,Minor Version從高到低是兼容的。
  • Release Version表示庫的一些bug修復,性能改進等,不添加任何新的接口,不改變原來的接口。

但是我們剛剛看到的.so只有一個Major Version,因為這是一個軟連接,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

因為不同的Major Version之間不兼容,而Minor Version和Release Version都是向下兼容的,軟連接會指向Major Version相同,Minor Version和Release Version最高的.so文件上。

動態連結庫查找過程

剛才提到,Linux的動態連結庫絕大多數都在/lib和/usr/lib下,作業系統也會默認去這兩個路徑下搜索動態連結庫。另外,/etc/ld.so.conf文件裡可以配置路徑,/etc/ld.so.conf文件會告訴作業系統去哪些路徑下搜索動態連結庫。這些位置的動態連結庫很多,如果連結器每次都去這些路徑遍歷一遍,非常耗時,Linux提供了ldconfig工具,這個工具會對這些路徑的動態連結庫按照SONAME規則創建軟連接,同時也會生成一個緩存Cache到/etc/ld.so.cache文件裡,連結器根據緩存可以更快地查找到各個.so文件。每次在/lib和/usr/lib這些路徑下安裝了新的庫,或者更改了/etc/ld.so.conf文件,都需要調用ldconfig命令來做一次更新,重新生成軟連接和Cache。但是/etc/ld.so.conf文件和ldconfig命令最好使用root帳戶操作。非root用戶可以在某個路徑下安裝庫文件,並將這個路徑添加到/etc/ld.so.conf文件下,再由root用戶調用一下ldconfig。

對於非root用戶,另一種方法是使用LD_LIBRARY_PATH環境變量。LD_LIBRARY_PATH存放著若干路徑。連結器會去這些路徑下查找庫。非root可以將某個庫安裝在了一個非root權限的路徑下,再將其添加到環境變量中。

動態連結庫的查找先後順序為:

  • LD_LIBRARY_PATH環境變量中的路徑
  • /etc/ld.so.cache緩存文件
  • /usr/lib和/lib

比如,我們把CUDA安裝到/opt下面,我們可以使用下面的命令將CUDA添加到環境變量裡。

export LD_LIBRARY_PATH=/opt/cuda/cuda-toolkit/lib64:$LD_LIBRARY_PATH

如果在執行某個具體程序前先執行上面的命令,那麼這個程序將使用這個路徑下的CUDA;如果將這行添加到了.bashrc文件,那麼該用戶一登錄就會執行這行命令,因此該用戶的所有程序也都將使用這個路徑下的CUDA。當同一個動態連結庫有多個不同版本的.so文件時,可以將它們安裝到不同的路徑下面,然後使用LD_LIBRARY_PATH環境變量來控制使用哪個庫。這種比較適合在多人共享的伺服器上使用不同版本的庫,比如CUDA這種版本變化較快,且深度學習程序又高度依賴的庫。

除了LD_LIBRARY_PATH環境變量外,還有一個LD_PRELOAD環境變量。LD_PRELOAD的查找順序比LD_LIBRARY_PATH還要優先。LD_PRELOAD裡是具體的目標文件列表(A list of shared objects);LD_LIBRARY_PATH是目錄列表(A list of directories)。

GCC編譯選項

使用GCC編譯連結時,有兩個參數需要注意,一個是-l(小寫的L),一個是-L(大寫的L)。我們前面曾提到,Linux有個約定速成的規則,假如庫名是name,那麼動態連結庫文件名就是libname.so。在使用GCC編譯連結時,-lname來告訴GCC使用哪個庫。連結時,GCC的連結器ld就會前往LD_LIBRARY_PATH環境變量、/etc/ld.so.cache緩存文件和/usr/lib和/lib目錄下去查找libname.so。我們也可以用-L/path/to/library的方式,讓連結器ld去/path/to/library路徑下去找庫文件。

如果動態連結庫文件在/path/to/library,庫名叫name,編譯連結的方式如下:

$ gcc -L/path/to/library -lname myfile.c

相關焦點

  • Linux下動態庫(.so)和靜態庫(.a) 的區別
    >這類庫的名字一般是libxxx.so;相對於靜態函數庫,動態函數庫在編譯的時候 並沒有被編譯進目標代碼中,你的程序執行到相關函數時才調用該函數庫裡的相應函數,因此動態函數庫所產生的可執行文件比較小。動態函數庫的改變並不影響你的程序,所以動態函數庫的升級比較方便不同的UNIX系統,連結動態庫方法,實現細節不一樣編譯PIC型.o中間文件的方法一般是採用C語言編譯器的-KPIC或者-fpic選項,
  • C語言學習篇(31)——linux中製作動態連結庫
    gcc工具鏈和windows中keill armcc工具鏈製作我們靜態連結庫,今天我們繼續來說說如何在linux gcc製作動態連結庫。如果大家真的感興趣可以參考下這篇文章:https://bbs.csdn.net/topics/392392689製作動態連結庫需要說明的在linux中動態連結庫的後綴名是.so,而在Windows系統的則是.dll。
  • Linux共享庫概述
    對於Linux系統,這個loader的名字是/lib/ld-linux.so.X(X是版本號)。這個loader啟動後,反過來就會load所有的其他本程序要使用的共享函數庫。到底在哪些目錄裡查找共享函數庫呢?這些定義預設的是放在/etc/ld.so.conf文件裡面,我們可以修改這個文件,加入我們自己的一些特殊的路徑要求。
  • Linux庫概念,動態庫和靜態庫的製作,如何移植第三方庫
    不同之處僅僅在於其名字上,也就是「靜態」和「動態」。二者均以文件的形式存在,其本質上是一種可執行代碼的二進位格式,可以被載入內存中執行。 無論是動態連結庫還是靜態連結庫,它們無非是向其調用者提供變量、函數和類。
  • 如何生成linux下的動態庫和靜態庫?一篇文章帶你讀懂「庫」
    一、什麼是庫?在windows平臺和linux平臺下都大量存在著庫。一般是軟體作者為了發布方便、替換方便或二次開發目的,而發布的一組可以單獨與應用程式進行compile time或runtime連結的二進位可重定位目標碼文件。
  • Linux有問必答:如何查看Linux上程序或進程用到的庫
    Linux有問必答:如何查看Linux上程序或進程用到的庫 我想知道當我調用一個特定的可執行文件在運行時載入了哪些共享庫。是否有方法可以明確Linux上可執行程序或運行進程的共享庫依賴關係?
  • Linux環境下C++文件的各種形式組合動態庫.so
    1、 純cpp文件打包動態庫將所有cpp文件和所需要的頭文件放在同一文件夾,然後執行下面命令gcc -shared -fpic *.c -o xxx.so;g++ -std=c++17 -shared -fpic *.cpp -o xxx.so;[C++17
  • c編譯器so easy,gcc c編譯器生成、使用動靜態庫
    /libc.so.6 (0×40021000)/lib/ld-linux.so.2=》 /lib/ld- linux.so.2 (0×40000000)可以看到ln命令依賴於libc庫和ld-linux庫7.可執行程序在執行的時候如何定位共享庫文件
  • linux靜態庫和動態庫分析
    4.庫文件是如何產生的在linux下  靜態庫的後綴是.a,它的產生分兩步  Step 1.由源文件編譯生成一堆.o,每個.o裡都包含這個編譯單元的符號表  Step 2.ar命令將很多.o轉換成.a,成文靜態庫  動態庫的後綴是.so,它由gcc加特定參數編譯產生。
  • 靜態連結和動態連結對比簡析
    簡介在Linux環境下進行開發工作,代碼要經過編譯連結生成二進位可執行文件,才能被CPU識別並執行;程序的編譯過程可以參考另外一篇文章《linux程序編譯過程簡析》;連結過程分為兩種,靜態連結和動態連結;
  • Linux常見的持久化後門匯總
    https://github.com/gaffe23/linux-inject/衍生的另外一個技巧 「linux一種無文件後門技巧「文章參考連結https://kevien.github.io/2018/02/20/linux%E4%B8%80%E7%A7%8D%E6%97%A0%E6%96%87%E4%BB%B6%E5%90%8E%E9%
  • 詳解 gcc 編譯、連結原理—揭開應用程式運行背後的奧秘
    編譯期連結多個 .o 文件可以通過連結器(ld)被打包在一起,組合成庫文件。庫文件又分為靜態庫(.a 文件)和共享庫(.so 文件)。什麼是 ld 呢?它本身也是可執行文件,屬於 GNU 的一部分,將一堆目標文件通過符號表連結成最終的目標文件、庫文件和可執行文件。
  • Linux 掛載 NTFS / exFAT 格式硬碟
    exfat-utils-1.3.0-1.el6.x86_64.rpm出現的問題安裝過程中如果提示:1/sbin/mount.ntfs-3g: symbol lookup error: /sbin/mount.ntfs-3g: undefined symbol: ntfs_xattr_build_mapping說明 ntfs-3g 動態態庫版本過低
  • Linux下C/C++編譯器gcc使用簡介
    -l參數和-L參數-l參數就是用來指定程序要連結的庫,-l參數緊接著就是庫名,那麼庫名跟真正的庫文件名有什麼關係呢?就拿數學庫來說,他的庫名是m,他的庫文件名是libm.so,很容易看出,把庫文件名的頭lib和尾.so去掉就是庫名了。
  • ARM Linux根文件系統Root Filesystem的製作
    /bin、/usr/bin、/usr/sbin、/sbin是編譯Busybox這個Shell時候就有的,用於存放二進位可執行文件,就不多解釋了。/lib用於存放動態連結庫。網上很多文章都說靜態編譯Busybox,可以省去建庫的麻煩過程。這樣做只能讓Busybox啟動,我們自己寫的,或者是編譯的軟體包還是需要動態庫的。
  • 基於busybox的嵌入式Linux根文件系統的的製作方法
    本文引用地址:http://www.eepw.com.cn/article/148616.htm  1 根文件  Linux要在一個分區上存放系統啟動所必需的文件,如內核映像文件、內核啟動後運行的第一個程序、給用戶提供操作界面的Shell程序、應用程式所依賴的庫等,這些必需、基本的文件合稱為根文件系統,它們存放在一個分區中。
  • linux目錄結構和文件屬性管理
    /usr-用戶程序包含二進位文件、庫文件、文檔和二級程序的原始碼。/usr/bin中包含用戶程序的二進位文件。如果你在/bin中找不到用戶二進位文件,到/usr/bin目錄看看。/boot -引導加載程序包含引導加載程序相關的文件。/lib -系統庫包含支持位於/lib和/sbin下的二進位文件的庫文件。
  • Linux提權備忘錄
    如果不是因為真的喜歡你,我糾纏你幹嘛,我又何必那麼卑微,那麼不堪,甚至不停修改自己的底線,到最後變得一文不值。。。利用LD_PRELOAD:如果在 sudoers 文件(/etc/sudoers)中明確定義了LD_PRELOAD的內容:Defaults env_keep += LD_PRELOAD可以使用以下代碼,編譯共享連結庫 gcc -fPIC -shared -o shell.so shell.c -nostartfiles//shell.c
  • 淺析gcc、arm-linux-gcc和arm-elf-gcc的關係
    Glibc:包含了主要的 c 庫,這個庫提供了基本的例程,用於分配內存,搜索目錄,讀寫文件,字符串處理等等。kernel 和 bootloader不需要這個庫的支持。舉例描述下上面 3 個包是如何進行運作的。有一個 c 源文件 test.c 源碼如下:[plain]view plaincopyprint?
  • linux進程調度淺析
    進程調度是對TASK_RUNNING狀態的進程進行調度(參見《linux進程狀態淺析》)。如果進程不可執行(正在睡眠或其他),那麼它跟進程調度沒多大關係。 所以,如果你的系統負載非常低,盼星星盼月亮才出現一個可執行狀態的進程。那麼進程調度也就不會太重要。哪個進程可執行,就讓它執行去,沒有什麼需要多考慮的。