linux下so動態庫一些不為人知的秘密


linux 下有動態庫和靜態庫,動態庫以.so為擴展名,靜態庫以.a為擴展名。二者都使用廣泛。本文主要講動態庫方面知識。

   基本上每一個linux 程序都至少會有一個動態庫,查看某個程序使用了那些動態庫,使用ldd命令查看

  1. # ldd /bin/ls
  2. linux-vdso.so.1 => (0x00007fff597ff000)
  3. libselinux.so.1 => /lib64/libselinux.so.1 (0x00000036c2e00000)
  4. librt.so.1 => /lib64/librt.so.1 (0x00000036c2200000)
  5. libcap.so.2 => /lib64/libcap.so.2 (0x00000036c4a00000)
  6. libacl.so.1 => /lib64/libacl.so.1 (0x00000036d0600000)
  7. libc.so.6 => /lib64/libc.so.6 (0x00000036c1200000)
  8. libdl.so.2 => /lib64/libdl.so.2 (0x00000036c1600000)
  9. /lib64/ld-linux-x86-64.so.2 (0x00000036c0e00000)
  10. libpthread.so.0 => /lib64/libpthread.so.0 (0x00000036c1a00000)
  11. libattr.so.1 => /lib64/libattr.so.1 (0x00000036cf600000)

   這么多so,是的。使用ldd顯示的so,並不是所有so都是需要使用的,下面舉個例子

main.cpp

#include <stdio.h>
#include <iostream>
#include <string>

using namespace std;

int main ()
{
   cout << "test" << endl;
   return 0;
}

   使用缺省參數編譯結果

  1. # g++ -o demo main.cpp
  2. # ldd demo
  3.     linux-vdso.so.1 => (0x00007fffcd1ff000)
  4.         libstdc++.so.6 => /usr/lib64/libstdc++.so.6 (0x00007f4d02f69000)
  5.         libm.so.6 => /lib64/libm.so.6 (0x00000036c1e00000)
  6.         libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00000036c7e00000)
  7.         libc.so.6 => /lib64/libc.so.6 (0x00000036c1200000)
  8. /lib64/ld-linux-x86-64.so.2 (0x00000036c0e00000)

   如果我鏈接一些so,但是程序並不用到這些so,又是什么情況呢,下面我加入鏈接壓縮庫,數學庫,線程庫

  1. # g++ -o demo -lz -lm -lrt main.cpp
  2. # ldd demo
  3.         linux-vdso.so.1 => (0x00007fff0f7fc000)
  4. libz.so.1 => /lib64/libz.so.1 (0x00000036c2600000)
  5. librt.so.1 => /lib64/librt.so.1 (0x00000036c2200000)
  6.         libstdc++.so.6 => /usr/lib64/libstdc++.so.6 (0x00007ff6ab70d000)
  7. libm.so.6 => /lib64/libm.so.6 (0x00000036c1e00000)
  8.         libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00000036c7e00000)
  9.         libc.so.6 => /lib64/libc.so.6 (0x00000036c1200000)
  10.         libpthread.so.0 => /lib64/libpthread.so.0 (0x00000036c1a00000)
  11. /lib64/ld-linux-x86-64.so.2 (0x00000036c0e00000)

  看看,雖然沒有用到,但是一樣有鏈接進來,那看看程序啟動時候有沒有去加載它們呢

  1. # strace ./demo
  2.     execve("./demo", ["./demo"], [/* 30 vars */]) = 0
  3. ... = 0
  4. open("/lib64/libz.so.1", O_RDONLY) = 3
  5. ...
  6. close(3) = 0
  7. open("/lib64/librt.so.1", O_RDONLY) = 3
  8. ...
  9. close(3) = 0
  10. open("/usr/lib64/libstdc++.so.6", O_RDONLY) = 3
  11. ...
  12. close(3) = 0
  13. open("/lib64/libm.so.6", O_RDONLY) = 3
  14. ...
  15. close(3) = 0
  16. open("/lib64/libgcc_s.so.1", O_RDONLY) = 3
  17. ...
  18. close(3) = 0
  19. open("/lib64/libc.so.6", O_RDONLY) = 3
  20. ...
  21. close(3) = 0
  22. open("/lib64/libpthread.so.0", O_RDONLY) = 3
  23. ...
  24. close(3) = 0
  25. ...

  看,有加載,所以必定會影響進程啟動速度,所以我們最后不要把無用的so編譯進來,這里會有什么影響呢?

   大家知不知道linux從程序(program或對象)變成進程(process或進程),要經過哪些步驟呢,這里如果詳細的說,估計要另開一篇文章。簡單的說分三步:

    1、fork進程,在內核創建進程相關內核項,加載進程可執行文件;

    2、查找依賴的so,一一加載映射虛擬地址

    3、初始化程序變量。

  可以看到,第二步中dll依賴越多,進程啟動越慢,並且發布程序的時候,這些鏈接但沒有使用的so,同樣要一起跟着發布,否則進程啟動時候,會失敗,找不到對應的so。所以我們不能像上面那樣,把一些毫無意義的so鏈接進來,浪費資源。但是開發人員寫makefile 一般有沒有那么細心,圖省事方便,那么有什么好的辦法呢。繼續看下去,下面會給你解決方法。

  先使用 ldd -u demo 查看不需要鏈接的so,看下面,一面了然,無用的so全部暴露出來了吧

  1. # ldd -u demo
  2. Unused direct dependencies:
  3. /lib64/libz.so.1
  4. /lib64/librt.so.1
  5. /lib64/libm.so.6
  6. /lib64/libgcc_s.so.1

  使用 -Wl,--as-needed 編譯選項

  1. # g++ -Wl,--as-needed -o demo -lz -lm -lrt main.cpp
  2. # ldd demo
  3.         linux-vdso.so.1 => (0x00007fffebfff000)
  4.         libstdc++.so.6 => /usr/lib64/libstdc++.so.6 (0x00007ff665c05000)
  5.         libc.so.6 => /lib64/libc.so.6 (0x00000036c1200000)
  6.         libm.so.6 => /lib64/libm.so.6 (0x00000036c1e00000)
  7. /lib64/ld-linux-x86-64.so.2 (0x00000036c0e00000)
  8.         libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00000036c7e00000)
  9. # ldd -u demo
  10. Unused direct dependencies:


  呵呵,辦法很簡單省事吧,本文主要講so依賴的一些問題,下一篇將介紹so的路徑方面一些不為人知的小秘密

我們知道linux鏈接so 有兩種途徑:顯示和隱式。所謂顯示就是程序主動調用dlopen打開相關so;這里需要補充的是,如果使用顯示鏈接,上篇文章討論的那些問題都不存在。首先,dlopen的so使用ldd是查看不到的。其次,使用dlopen打開的so並不是在進程啟動時候加載映射的,而是當進程運行到調用dlopen代碼地方才加載該so,也就是說,如果每個進程顯示鏈接a.so;但是如果發布該程序時候忘記附帶發布該a.so,程序仍然能夠正常啟動,甚至如果運行邏輯沒有觸發運行到調用dlopen函數代碼地方。該程序還能正常運行,即使沒有a.so.
既然顯示加載這么多優點,那么為什么實際生產中很少碼農使用它呢, 主要原因還是起使用不是很方便,需要開發人員多寫不少代碼。所以不被大多數碼農使用,還有一個重要原因應該是能提前發現錯誤,在部署的時候就能發現缺少哪些so,而不是等到實際上限運行的時候才發現缺東少西。
下面舉個工作中最常碰到的問題,來引申出本篇內容吧。
寫一個最簡單的so, tmp.cpp

int test()
{
     return 20;
}


  編譯=>鏈接=》運行, 下面main.cpp 內容請參見上一篇文章。
[stevenrao]$ g++ -fPIC -c tmp.cpp
[stevenrao]$ g++ -shared -o libtmp.so tmp.o
[stevenrao]$ mv libtmp.so /tmp/
[stevenrao]$ g++ -o demo -L/tmp -ltmp main.cpp
[stevenrao]$ ./demo
./demo: error while loading shared libraries: libtmp.so: cannot open shared object file: No such file or directory
[stevenrao]$ ldd demo
linux-vdso.so.1 =>  (0x00007fff7fdc1000)
        libtmp.so => not found
   這個錯誤是最常見的錯誤了。運行程序的時候找不到依賴的so。一般人使用方法是修改LD_LIBRARY_PATH這個環境變量
   export LD_LIBRARY_PATH=/tmp
[stevenrao]$ ./demo
test
   這樣就OK了, 不過這樣export 只對當前shell有效,當另開一個shell時候,又要重新設置。可以把export LD_LIBRARY_PATH=/tmp 語句寫到 ~/.bashrc中,這樣就對當前用戶有效了,寫到/etc/bashrc中就對所有用戶有效了。
   前面鏈接時候使用 -L/tmp/ -ltmp 是一種設置相對路徑方法,還有一種絕對路徑鏈接方法
[stevenrao]$ g++ -o demo  /tmp/libtmp.so main.cpp
[stevenrao]$ ./demo
  test
[stevenrao]$ ldd demo
        linux-vdso.so.1 =>  (0x00007fff083ff000)
        /tmp/libtmp.so (0x00007f53ed30f000) 
   絕對路徑雖然申請設置環境變量步驟,但是缺陷也是致命的,這個so必須放在絕對路徑下,不能放到其他地方,這樣給部署帶來很大麻煩。所以應該禁止使用絕對路徑鏈接so。
   搜索路徑分兩種,一種是鏈接時候的搜索路徑,一種是運行時期的搜索路徑。像前面提到的 -L/tmp/ 是屬於鏈接時期的搜索路徑,即給ld程序提供的編譯鏈接時候尋找動態庫路徑;而 LD_LIBRARY_PATH則既屬於鏈接期搜索路徑,又屬於運行時期的搜索路徑。
   這里需要介紹鏈-rpath鏈接選項,它是指定運行時候都使用的搜索路徑。聰明的同學馬上就想到,運行時搜索路徑,那它記錄在哪兒呢。也像. LD_LIBRARY_PATH那樣,每部署一台機器就需要配一下嗎。呵呵,不需要..,因為它已經被硬編碼到可執行文件內部了。看看下面演示
[stevenrao] $ g++ -o demo -L /tmp/ -ltmp main.cpp
[stevenrao] $ ./demo
./demo: error while loading shared libraries: libtmp.so: cannot open shared object file: No such file or directory
[stevenrao] $ g++ -o demo -Wl,-rpath /tmp/ -L/tmp/ -ltmp main.cpp
[stevenrao] $ ./demo
test
[stevenrao] $ readelf -d demo
Dynamic section at offset 0xc58 contains 26 entries:
  Tag        Type                         Name/Value
0x0000000000000001 (NEEDED)             Shared library: [libtmp.so]
0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
0x000000000000000f (RPATH)              Library rpath: [/tmp/]
0x000000000000001d (RUNPATH)            Library runpath: [/tmp/]
   看看是吧,編譯到elf文件內部了,路徑和程序深深的耦合到一起
   還有一個類似於-path,叫LD_RUN_PATH環境變量, 它也是把路徑編譯進可執行文件內,不同的是它只設置RPATH。
[stevenrao] $ g++ -o demo -L /tmp/  -ltmp main.cpp
[stevenrao] $ readelf -d demo
Dynamic section at offset 0xb98 contains 25 entries:
  Tag        Type                         Name/Value
0x0000000000000001 (NEEDED)             Shared library: [libtmp.so]
....
0x000000000000000f (RPATH)              Library rpath: [/tmp/]
  另外還可以通過配置/etc/ld.so.conf,在其中加入一行
  /tmp/
  這個配置項也是只對運行期有效,並且是全局用戶都生效,需要root權限修改,修改完后需要使用命令ldconfig 將 /etc/ld.so.conf 加載到ld.so.cache中,避免重啟系統就可以立即生效。
  除了前面介紹的那些搜索路徑外,還有缺省搜索路徑/usr/lib/ /lib/ 目錄,可以通過-z nodefaultlib編譯選項禁止搜索缺省路徑。
  [stevenrao] $ g++ -o demo -z nodefaultlib  -L/tmp -ltmp main.cpp
  [stevenrao] $  ./demo
   ./demo: error while loading shared libraries: libstdc++.so.6: cannot open shared object file
  這么多搜索路徑,他們有個先后順序如下
  1、RUMPATH 優先級最高
  2、RPATH   其次
  3、LD_LIBRARY_PATH
  4、/etc/ld.so.cache
  5、/usr/lib/ /lib/
  查看一個程序搜索其各個動態庫另一個簡單的辦法是使用 LD_DEBUG這個環境變量;
  [stevenrao] $ export LD_DEBUG=libs
  [stevenrao] $ ./demo

注:本文轉載自http://blog.csdn.net/suwei19870312/article/details/20045281


免責聲明!

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



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