靜態庫
有時候需要把一組代碼編譯成一個庫,這個庫在很多項目中都要用到,例如libc就是這樣一個庫, 我們在不同的程序中都會用到libc中的庫函數(例如printf),也會用到libc中的變量(例如以后 要講到的environ變量)。本文將介紹怎么創建這樣一個庫。
這些文件的目錄結構是:
$ tree
.
|-- main.c
`-- stack
|-- is_empty.c
|-- pop.c
|-- push.c
|-- stack.c
`-- stack.h
1 directory, 6 files
我們把stack.c、push.c、pop.c、is_empty.c編譯成目標文件:
$ gcc -c stack/stack.c stack/push.c stack/pop.c stack/is_empty.c
然后打包成一個靜態庫libstack.a:
$ ar rs libstack.a stack.o push.o pop.o is_empty.o
ar: creating libstack.a
庫文件名都是以lib開頭的,靜態庫以.a作為后綴,表示Archive。ar命令類似於tar命令,起一個打包的作用,但是把目標文件打包成靜態庫只能用ar命令而不能用tar命令。選項r表示將后面的文件列表添加到文件包,如果文件包不存在就創建它,如果文件包中已有同名文件就替換成新的。s是專用於生成靜態庫的,表示為靜態庫創建索引,這個索引被鏈接器使用。ranlib命令也可以為靜態庫創建索引,以上命令等價於:
$ ar r libstack.a stack.o push.o pop.o is_empty.o
$ ranlib libstack.a
然后我們把libstack.a和main.c編譯鏈接在一起:
$ gcc main.c -L. -lstack -Istack -o main
-L選項告訴編譯器去哪里找需要的庫文件,-L.表示在當前目錄找。-lstack告訴編譯器要鏈 接libstack庫,-I選項告訴編譯器去哪里找頭文件。注意,即使庫文件就在當前目錄,編譯器默認 也不會去找的,所以-L.選項不能少。編譯器默認會找的目錄可以用-print-search-dirs選項查看。編譯器會在這些搜索路徑以及-L選項指定的路徑中查找用-l選項指定的庫,比如-lstack,編譯器會首先找有沒有共享庫libstack.so,如果有就鏈接它,如果沒有就找有沒有靜態庫libstack.a,如果有就鏈接它。所以編譯器是優先考慮共享庫的,如果希望編譯器只鏈接靜態庫,可以指定-static選項。
動態庫(共享庫)
組成共享庫的目標文件和一般的目標文件有所不同,在編譯時要加-fPIC選項,例如:
$ gcc -c -fPIC stack/stack.c stack/push.c stack/pop.c stack/is_empty.c
-f后面跟一些編譯選項,PIC是其中一種,表示生成位置無關代碼(Position Independent Code)。
現在把main.c和共享庫編譯鏈接在一起,然后運行:
$ gcc main.c -g -L. -lstack -Istack -o main
$ ./main
./main: error while loading shared libraries: libstack.so: cannot open shared object file: No such file or directory
結果出乎意料,編譯的時候沒問題,由於指定了-L.選項,編譯器可以在當前目錄下找到libstack.so,而運行時卻說找不到libstack.so。那么運行時在哪些路徑下找共享庫呢?我們先用ldd命令查看可執行文件依賴於哪些共享庫:
$ ldd main
linux-gate.so.1 => (0xb7f5c000)
libstack.so => not found
libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7dcf000)
/lib/ld-linux.so.2 (0xb7f42000)
ldd模擬運行一遍main,在運行過程中做動態鏈接,從而得知這個可執行文件依賴於哪些共享庫,每個共享庫都在什么路徑下,加載到進程地址空間的什么地址。/lib/ld-linux.so.2是動態鏈接器,它的路徑是在編譯鏈接時指定的,gcc在做鏈接時用dynamic-linker指定動態鏈接器的路徑,它也像其它共享庫一樣加載到進程的地址空間中。libc.so.6的路徑/lib/tls/i686/cmov/libc.so.6是由動態鏈接器ld-linux.so.2在做動態鏈接時搜索到的,而libstack.so的路徑沒有找到。linux-gate.so.1這個共享庫其實並不存在於文件系統中,它是由內核虛擬出來的共享庫,所以它沒有對應的路徑,它負責處理系統調用。總之,共享庫的搜索路徑由動態鏈接器決定,從ld.so(8)的Man Page可以查到共享庫路徑的搜索順序:
- 首先在環境變量LD_LIBRARY_PATH所記錄的路徑中查找。
- 然后從緩存文件/etc/ld.so.cache中查找。這個緩存文件由ldconfig命令讀取配置文 件/etc/ld.so.conf之后生成,稍后詳細解釋。
- 如果上述步驟都找不到,則到默認的系統路徑中查找,先是/usr/lib然后是/lib。
先試試第一種方法,在運行main時通過環境變量LD_LIBRARY_PATH把當前目錄添加到共享庫的搜索路徑:
$ LD_LIBRARY_PATH=. ./main
這種方法只適合在開發中臨時用一下,通常LD_LIBRARY_PATH是不推薦使用的,盡量不要設置這個環境變量,理由可以參考Why LD_LIBRARY_PATH is bad。
再試試第二種方法,這是最常用的方法。把libstack.so所在目錄的絕對路徑(比如/home/akaedu/somedir)添加到/etc/ld.so.conf中(該文件中每個路徑占一行),然后運行ldconfig:
$ sudo ldconfig -v
ldconfig命令除了處理/etc/ld.so.conf中配置的目錄之外,還處理一些默認目錄,如/lib、/usr/lib等,處理之后生成/etc/ld.so.cache緩存文件,動態鏈接器就從這個緩存中搜索共享庫。現在再用ldd命令查看,libstack.so就能找到了:
$ ldd main
linux-gate.so.1 => (0xb809c000)
libstack.so => /home/akaedu/somedir/libstack.so (0xb806a000)
libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7f0c000)
/lib/ld-linux.so.2 (0xb8082000)
第三種方法就是把libstack.so拷到/usr/lib或/lib目錄,這樣可以確保動態鏈接器能找到這個共享庫。
其實還有第四種方法,在編譯可執行文件main的時候就把libstack.so的路徑寫死在可執行文件中:
$ gcc main.c -g -L. -lstack -Istack -o main -Wl,rpath,/home/akaedu/somedir
-Wl,-rpath,/home/akaedu/somedir表示-rpath /home/akaedu/somedir是由gcc傳遞給鏈接器的選項。可以看到readelf的結果多了一條rpath記錄:
$ readelf -a main
...
Dynamic section at offset 0xf10 contains 23 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library:
[libstack.so]
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000f (RPATH) Library rpath: [/home/akaedu/somedir]
...
還可以看出,可執行文件運行時需要哪些共享庫也都記錄在.dynamic段中。當然rpath這種辦法也是不推薦的,把共享庫的路徑定死了,失去了靈活性。
甚至還可以這樣寫:
$ gcc -o main main.c -g -L. -lstack -Istack ./stack/libstack.so
參考資料:[linux_C編程一站式學習](https://book.douban.com/subject/4141733/)