靜態鏈接和動態鏈接


1.基礎知識


  程序由源代碼變成可執行文件,一般可以分解為四個步驟,分別是:

    [1]預處理(Prepressing):預處理過程主要處理源代碼中以“#”開始的預編譯指令;

    [2]編譯(Compilation) :編譯過程把預處理完成的文件進行詞法、語法、語義等分析並產生相應的匯編代碼文件;

    [3]匯編(Assembly)    :匯編過程將匯編代碼文件翻譯成機器可以執行的目標文件;

    [4]鏈接(Linking)     :鏈接過程將匯編生成的目標文件集合相連接並生成最終的可執行文件。

  鏈接器在鏈接靜態鏈接庫的時候是以目標文件(.o)為單位的,如果該靜態庫里的某些方法沒有任何地方調用,則這些沒有被調用到的方法或變量將會被丟棄掉,不會被鏈接到目標程序中,這樣做可以大大減小生成二進制文件的體積。

  動態鏈接在程序運行時才將它們鏈接在一起形成一個完整的程序,而不是像靜態鏈接一樣把所有程序模塊都鏈接成一個單獨的可執行文件。此時在程序的鏈接階段時,鏈接器只是拷貝了一些重定位和符號信息。在程序加載(execve)時才會解析so文件中代碼和數據的引用。某個程序在運行中要調用某個動態鏈接庫函數的時候,操作系統首先會查看所有正在運行的程序,看在內存里是否已有此庫函數的拷貝了。如果有,則讓其共享那一個拷貝;只有沒有才鏈接載入。在程序運行的時候,被調用的動態鏈接庫函數被安置在內存的某個地方,所有調用它的程序將指向這個代碼段。因此,這些代碼必須使用相對地址,而不是絕對地址。在編譯的時候,我們需要告訴編譯器,這些對象文件是用來做動態鏈接庫的,所以要用地址無關代碼(Position Independent Code (PIC))。

  動態鏈接庫的加載方式有兩種:隱式加載和顯示加載。linux下進行連接的缺省操作是首先連接動態庫,也就是說,如果同時存在靜態和動態庫,不特別指定的話,將與動態庫相連接。

2.庫的搜索路徑


  庫的搜索路徑,包括編譯時的搜索(.a和.so)和運行時的搜索(.so)。所以雖然使用-L可以鏈接so編譯過程序,但是運行時並不會從-L指定的目錄搜索從而導致可能運行時找不到文件。

  【1】編譯鏈接庫時的搜索路徑順序:

    [1]-L選項指定的目錄,例如:g++ -o test_fun test_fun.c -L. -lfun

    [2]-Wl,rpath指定的目錄。例如:gcc -o foo foo.c -L. -lfoo -Wl,-rpath=./

    [3]環境變量LD_LIBRARY_PATH中設置的目錄。

    [4]/etc/ld.so.cache文件中緩存的文件位置。

    [5]默認的/usr/lib或者/usr/lib64。

  【2】程序運行時搜索動態庫文件的順序:

    [1]-Wl,rpath指定的目錄。例如:gcc -o foo foo.c -L. -lfoo -Wl,-rpath=./

    [2]環境變量LD_LIBRARY_PATH中設置的目錄。

    [3]/etc/ld.so.cache文件中緩存的文件位置。

    [4]默認的/usr/lib或者/usr/lib64。

  【3】ldconfig

    ldconfig命令是在默認搜尋目錄(/lib和/usr/lib)以及動態庫配置文件/etc/ld.so.conf內所列的目錄下,搜索出可共享的動態鏈接庫(lib*.so*),進而創建出動態裝入程序(ld.so)所需的連接和緩存文件,緩存文件默認為 /etc/ld.so.cache,此文件保存已排好序的動態鏈接庫名字列表。程序連接的時候首先從這個緩存文件里邊查找,然后再到ld.so.conf的路徑里邊去詳細找。

    添加動態鏈接庫的方法:

      [1]往/lib和/usr/lib里面加東西。此時不用修改/etc/ld.so.conf文件的,但是添加完后必須調用ldconfig,不然添加的library會找不到。另外如果是加到/lib64或者/usr/lib64,則不執行ldconfig也可以找到。

      [2]往普通目錄添加library。此時一定要修改/etc/ld.so.conf文件,往該文件追加library所在的路徑,然后調用ldconfig命令。

      [3]設置環境變量LD_LIBRARY_PATH。因為ldconfig的作用和這個環境變量無關,所以不用執行ldconfig。例如export LD_LIBRARY_PATH=/usr/local/test

3.庫相關命令


  readelf -s libfun.so

    該命令可以打印出靜態庫或動態庫的符號。

  lsof libfun.so

    該命令可以打印出正在使用libfun.so的進程。

4.動態鏈接庫


  【1】隱式方式使用動態庫

    創建庫源文件,編譯.so文件:g++ -fPIC -shared -o libfun.so fun.c

    引用庫:g++ -o test_fun test_fun.c -L. -lfun

    此時可以生成可執行文件test_fun,但是執行的時候會報錯:libfun.so: cannot open shared object file: No such file or directory。這是因為程序在運行時,會在/usr/lib和/lib等目錄中查找需要的動態庫文件,如果找不到就會報錯,而上面用-L. -lfun指定so路徑和文件只是可以編譯通過。

  【2】顯示方式使用動態庫

    LINUX下使用動態鏈接庫,源程序需要包含dlfcn.h頭文件,此文件定義了調用動態鏈接庫的函數的原型。

    相關函數:

      void *dlopen (const char *filename, int flag);

        該函數打開指定名字(filename)的動態鏈接庫,並返回操作句柄。如果filename不是以'/'開頭,則按照先后順序從目錄中查找文件:LD_LIBRARY,/etc/ld.so.cache,/lib,/usr/lib。

        flag參數指定在什么時候解決未定義的符號,RTLD_LAZY表示在動態鏈接庫的函數代碼執行時解決,RTLD_NOW表示在dlopen返回前就解決所有未定義的符號,一旦未解決,dlopen將返回錯誤。

        例如dlopen("abc/libfun.so",  RTLD_NOW ),則首先查看/usr/local/test/abc/libfun.so是否存在,最后查看/lib/abc/libfun.so是否存在。

      void *dlsym(void *handle, char *symbol);

        該函數用來獲取符號的地址。符號包括函數、全局變量等。通過此符號可以調用動態庫的函數。

      int dlclose (void *handle);

         關閉動態鏈接庫,其實就是減計數,只有當此動態鏈接庫的使用計數為0時,才會真正被系統卸載。

    例子:這里要注意的是如果使用g++編譯成so時,此時的符號就不是fun,而是c++規則的符號(例如:_Z3funv),所以要查找符號"fun"時,需要在共享庫的源文件中加上extern “C”,這樣編譯出的符號就是c語言格式的。

      

   【3】so的更新替換

    針對未被加載的so,利用復制命令(cp new.so old.so)即可直接完成靜態替換,新so在下次加載時生效,也就是需要重啟程序才可以讓新的so生效。

    如果當前的so已經被程序加載,此時直接用cp會導致程序崩潰,正確方法是先把舊的so刪除,然后再把新的so復制過去,當前這種方法也需要重啟程序,新的so才會被重新加載。

    直接覆蓋會導致崩潰的原因:linux中使用了內存映像和需求分頁機制,需要確保正在運行程序的鏡像不被破壞,因此內核在啟動程序后會鎖定鏡像的inode。跟蹤cp命令的執行流程為:

      

    可以看到cp命令的替換操作會破壞系統訪問原so的索引節點的inode,inode包含了文件的元信息,如文件字節數、擁有者ID、讀寫執行權限等。系統以inode標識程序加載的so,不再關心文件名,因此修改so的名稱並未改變對應inode,因此程序可以繼續正常運行;刪除SO只是無法查看到文件,系統直到程序釋放so后才真正刪除so和inode,因此程序也可以繼續正常運行;但是在直接復制替換時,新so將會繼承原so的inode,導致inode被破壞,從而導致程序崩潰。

    如果替換正在運行的bin文件,此時cp會報錯"text file busy",說明系統對bin文件有特殊保護,不會出現inode被破壞的情況。但是卻沒有對so文件做特殊保護,所以直接cp會崩潰。

 

 

  

    

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM