Linux中的動態庫和靜態庫(.a/.la/.so/.o)

在windows下,一般可以通過文件的後綴名來識別文件的類型。在Linux下大致上也是可以的。但是要明確的一點是,在linux下,文件的後綴與文件的類型是沒有必然的聯繫的。這只是約定俗稱的習慣罷了。

在linux 下進行C/C++開發,一般都是使用的gcc編譯器,所以本文的講解以gcc爲主。

  • .o文件,即目標文件。一般通過.c或者.cpp文件編譯而來,相當於VC編譯出來的obj文件

  • .so文件,shared object 共享庫(對象),相當於windows下的dll。

  • .a文件,archive 歸檔包,即靜態庫。其實質是多個.o文件打包的結果,相當於VC下的.lib文件

  • .la文件,libtool archive 文件,是libtool自動生成的共享庫文件。

下面對這四種文件進行逐個說明。

 

C/C++程序編譯的過程

先說一下C/C++編譯的幾個過程。

  1. 預處理,展開頭文件,宏定義,條件編譯處理等。通過gcc -E source.c -o source.i或者cpp source.c生成。

  2. 編譯。這裏是一個狹義的編譯意義,指的是將預處理後的文件翻譯成彙編代碼的過程。通過gcc -S source.i生成。默認生成source.s文件。

  3. 彙編。彙編即將上一步生成的彙編代碼翻譯成對應的二進制機器碼的過程。通過gcc -c source.s來生成source.o文件。

  4. 鏈接。鏈接是將生成目標文件和其引用的各種符號等生成一個完整的可執行程序的過程。鏈接的時候會進行虛擬內存的重定向操作。

上面四個步驟就是C/C++程序編譯的幾個基本步驟。前面三個步驟都是很簡單,大多時候會合併爲一個步驟。只有第四個步驟鏈接是複雜一點的。很多時候我們編譯比較大的項目,報錯的往往是在鏈接的時候缺少某些庫,或者某些符號找不到定義,重定義等

 

.o文件(目標文件)

.o文件就是C/C++源碼編譯的結果。即上面所說的C/C++編譯過程中的前三步。一般開發中很少將這三步分開來做,通常的做法是一步生成。

這裏舉個例子,我們來寫一個函數int atoi(const char* str)。

頭文件atoi.h

.#ifndef ATOI_H

.#define ATOI_H

int atoi(const char* str);

.#endif //! ATOI_H

源文件atoi.c

.#include <stdio.h>

.#include "atoi.h"

int atoi(const char* str)

{

int ret = 0;

if(str != NULL){

sscanf(str,"%d",&ret);

}

return ret;

}

創建atoi.o

直接使用命令gcc -c atoi.c -o atoi.o或gcc -c atoi.c來生成目標文件。

上面我們函數中調用了sscanf這個C標準庫中的函數,那麼它在.o文件中會記錄這個存在,我們可以使用readelf工具來查看一下。

o@Neo-kylin:~/lib_a_so$ ls

atoi.c atoi.h

o@Neo-kylin:~/lib_a_so$ gcc -c atoi.c

o@Neo-kylin:~/lib_a_so$ ll

總用量 20

drwxr-xr-x 2 o o 4096 10月 10 15:11 ./

drwxrwxr-x 5 1000 1000 4096 10月 10 14:32 ../

-rw-rw-r-- 1 o o 140 10月 10 15:07 atoi.c

-rw-rw-r-- 1 o o 75 10月 10 15:07 atoi.h

-rw-rw-r-- 1 o o 1536 10月 10 15:11 atoi.o

o@Neo-kylin:~/lib_a_so$ readelf -s atoi.o

 

Symbol table '.symtab' contains 11 entries:

Num: Value Size Type Bind Vis Ndx Name

0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND

1: 0000000000000000 0 FILE LOCAL DEFAULT ABS atoi.c

2: 0000000000000000 0 SECTION LOCAL DEFAULT 1

3: 0000000000000000 0 SECTION LOCAL DEFAULT 3

4: 0000000000000000 0 SECTION LOCAL DEFAULT 4

5: 0000000000000000 0 SECTION LOCAL DEFAULT 5

6: 0000000000000000 0 SECTION LOCAL DEFAULT 7

7: 0000000000000000 0 SECTION LOCAL DEFAULT 8

8: 0000000000000000 0 SECTION LOCAL DEFAULT 6

9: 0000000000000000 60 FUNC GLOBAL DEFAULT 1 atoi

10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND __isoc99_sscanf

這就是.o文件了。它保存了編譯的時候引用到的符號(函數,全局變量等),這些符號,在鏈接的時候需要使用到。

使用atoi.o

使用atoi.o的地方有很多,就不一一列舉了。這裏先不說怎麼用,後面生成.a文件的時候用到了。


.a文件(靜態庫文件)

靜態庫是多個.o文件的打包的結果,前面已經說過了,其實不一定非要多個文件,一個.o文件也可以打包爲.a文件。

這一步使用ar工具來操作。ar工具是用來創建, 修改和提取archives歸檔文件的工具,具體使用可以看manpages。

ar [emulation options] [-]{dmpqrstx}[abcfilNoPsSuvV] [member-name] [count] archive-file file...

這個工具的作用看起來很簡單,但是其是很強大,且參數的設置很複雜的。這裏不是爲了介紹這個工具,不細說了。

創建atoi.a

我們先使用上面生成的atoi.o文件來生成一個atoi.a文件。

o@Neo-kylin:~/lib_a_so$ ls

atoi.c atoi.h atoi.o

o@Neo-kylin:~/lib_a_so$ ar -r atoi.a atoi.o

ar: creating atoi.a

o@Neo-kylin:~/lib_a_so$ ll

總用量 24

drwxr-xr-x 2 o o 4096 10月 10 15:35 ./

drwxrwxr-x 5 1000 1000 4096 10月 10 14:32 ../

-rw-rw-r-- 1 o o 1678 10月 10 15:35 atoi.a

-rw-rw-r-- 1 o o 140 10月 10 15:07 atoi.c

-rw-rw-r-- 1 o o 75 10月 10 15:07 atoi.h

-rw-rw-r-- 1 o o 1536 10月 10 15:11 atoi.o

-r參數的意思是替換已存在的或插入新的文件到archive包中。

使用atoi.a

創建了atoi.a文件後,我們就可以來使用它了。這裏我們寫一個main函數來調用atoi。

main.c文件

int main()

{

return atoi("5");

}

這一次我們先把main.c編譯爲main.o文件。

o@Neo-kylin:~/lib_a_so$ ls

atoi.a atoi.c atoi.h atoi.o main.c

o@Neo-kylin:~/lib_a_so$ gcc -c main.c

o@Neo-kylin:~/lib_a_so$ ls

atoi.a atoi.c atoi.h atoi.o main.c main.o

然後使用ld程序來鏈接main.o和atoi.a

o@Neo-kylin:~/lib_a_so$ ld main.o atoi.a -o main

ld: warning: cannot find entry symbol _start; defaulting to 00000000004000b0

atoi.a(atoi.o): In function `atoi':

atoi.c:(.text+0x33): undefined reference to `__isoc99_sscanf'`

上面報了一個錯誤,原因是在atoi函數中使用未定義的引用 __isoc99_sscanf,這個問題我們可以通過鏈接上libc.a或者libc.so來解決這個問題。通常的情況下,都是鏈接libc.so來解決的,如果使用glibc的靜態庫,那麼你也必須將你的程序開源,不然這應該算是違反GPL協議的約定。

o@Neo-kylin:~/lib_a_so$ ld main.o atoi.a /lib64/libc.so.6 -o main

ld: warning: cannot find entry symbol _start; defaulting to 0000000000400288

這裏又報了一個警告,是沒有發現_start符號的意思。這是因爲沒有發現程序主入口點的原因。在C語言中,程序的入口函數是main,但是在彙編中,程序的主入口函數是_start。

這裏我們可以把main.c文件中的main函數改爲_start函數,然後再編譯爲main.o再鏈接就沒有問題了。但是這不是正確的做法,這樣做雖然使用ld來鏈接是不會報錯了,但是程序是運行不了的。會報錯誤

o@Neo-kylin:~/lib_a_so$ ld main.o atoi.a /lib64/libc.so.6 -o main

o@Neo-kylin:~/lib_a_so$ ./main

-bash: ./main: /lib/ld64.so.1: bad ELF interpreter: 沒有那個文件或目錄

正確的做法是鏈接上crt0.o、crti.o、crtn.o等很多個文件就行了,不同的機器,需要鏈接的文件的位置可能不一樣。這個參數可能非常長,普通人記不住。

這個是可以怎麼得到呢?我肯定不知道這些文件都在什麼位置,但是gcc編譯環境知道,我們可以使用gcc來獲取。

o@Neo-kylin:~/lib_a_so$ gcc -v -o main main.o atoi.a

使用內建 specs。

目標:x86_64-redhat-linux

配置爲:../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-languages=c,c++,objc,obj-c++,java,fortran,ada --enable-java-awt=gtk --disable-dssi --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-1.5.0.0/jre --enable-libgcj-multifile --enable-java-maintainer-mode --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --disable-libjava-multilib --with-ppl --with-cloog --with-tune=generic --with-arch_32=i686 --build=x86_64-redhat-linux

線程模型:posix

gcc 版本 4.4.7 20120313 (Red Hat 4.4.7-16) (GCC)

COMPILER_PATH=/usr/libexec/gcc/x86_64-redhat-linux/4.4.7/:/usr/libexec/gcc/x86_64-redhat-linux/4.4.7/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/:/usr/lib/gcc/x86_64-redhat-linux/:/usr/libexec/gcc/x86_64-redhat-linux/4.4.7/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/:/usr/lib/gcc/x86_64-redhat-linux/

LIBRARY_PATH=/usr/lib/gcc/x86_64-redhat-linux/4.4.7/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/:/lib/../lib64/:/usr/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../:/lib/:/usr/lib/

COLLECT_GCC_OPTIONS='-v' '-o' 'main' '-mtune=generic'

/usr/libexec/gcc/x86_64-redhat-linux/4.4.7/collect2 --eh-frame-hdr --build-id -m elf_x86_64 --hash-style=gnu -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o main /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crti.o /usr/lib/gcc/x86_64-redhat-linux/4.4.7/crtbegin.o -L/usr/lib/gcc/x86_64-redhat-linux/4.4.7 -L/usr/lib/gcc/x86_64-redhat-linux/4.4.7 -L/usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64 -L/lib/../lib64 -L/usr/lib/../lib64 -L/usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../.. main.o atoi.a -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-redhat-linux/4.4.7/crtend.o /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crtn.o

編譯之後我們可以來看以下程序運行結果是否正確。

o@Neo-kylin:~/lib_a_so$ ./main

o@Neo-kylin:~/lib_a_so$ echo $?

5

結果爲5,與預期一致。

.so文件(共享庫文件)

共享庫文件和windows下的dll文件(dynamic link library)的概念是一樣的,都是在程序運行的時候進行動態鏈接,供程序調用的。

在linux 下可以使用ldd命令來查看某個可執行文件需要鏈接哪些共享庫(動態庫),並可以確定這些要鏈接的共享庫在本機中的位置。

o@Neo-kylin:~/lib_a_so$ ldd main

linux-vdso.so.1 => (0x00007fffab1ff000)

libc.so.6 => /lib64/libc.so.6 (0x000000305d800000)

/lib64/ld-linux-x86-64.so.2 (0x000000305d000000)

這裏要說以下動態庫的查找路徑。對於程序需要鏈接的動態庫xxx.so,如果它在當前目錄下有,那麼鏈接當前目錄下的。如果沒有,那麼就鏈接系統/etc/ld.so.cache(可通過ldconfig來更新)文件中查找xxx.so的路徑,如果都沒有,那麼就會報錯啦。

我們在當前目錄創建一個libc.so.6文件,然後再使用ldd看一下。

o@Neo-kylin:~/lib_a_so$ touch libc.so.6 && chmod +x libc.so.6

o@Neo-kylin:~/lib_a_so$ ls -l libc.so.6

-rwxrwxr-x 1 o o 0 10月 10 17:15 libc.so.6

o@Neo-kylin:~/lib_a_so$ ldd main

./main: error while loading shared libraries: ./libc.so.6: file too short

可以看到,這時候是鏈接的當前目錄下的libc.so.6這個文件,很可惜,出錯了。

其實在鏈接的時候,我們可以通過-Wl,-rpath=sopath來指定運行時加載動態庫的路徑。這樣做的好處是可以把一些動態庫的位置信息不加入到/etc/ld.so.cache中,已經避免和系統已有動態庫產生衝突的情況。(例如目標機器的glibc庫版本太低,而編譯程序的時候使用的高版本的,而出現”libc.so.6: version `GLIBC_2.14’ not found”類似的錯誤的情況)

注: -Wl: 表示後面的參數將傳給link程序ld,gcc編譯時候的鏈接實際上是調用ld進行的.

創建atoi.so

這裏還是使用前面創建的atoi.c文件創建atoi.so文件。實際上我們這裏創建atoi.so.1文件,文件名後面的.1代表的是版本號。動態庫因爲使用的時候是動態鏈接的,而不是直接鏈接到目標程序文件中的。所以可能同時存在多個版本的情況,一般都會指定版本號。

通常使用libxxx.so.主版本號.副版本號的形式來命名。

o@Neo-kylin:~/lib_a_so$ gcc -shared -o atoi.so.1 atoi.c

/usr/bin/ld: /tmp/ccLK0pLC.o: relocation R_X86_64_32 against `.rodata' can not be used when making a shared object; recompile with -fPIC

/tmp/ccLK0pLC.o: could not read symbols: Bad value

collect2: ld 返回 1

o@Neo-kylin:~/lib_a_so$ gcc -fPIC -shared -o atoi.so.1 atoi.c

o@Neo-kylin:~/lib_a_so$ ls

atoi.a atoi.c atoi.h atoi.o atoi.so.1 main.c main.o

-share該選項指定生成動態連接庫(讓連接器生成T類型的導出符號表,有時候也生成弱連接W類型的導出符號,後面介紹nm工具的時候再說),不用該標誌外部程序無法連接。相當於一個可執行文件。

-fPIC表示編譯爲位置獨立的代碼,不用此選項的話編譯後的代碼是位置相關的所以動態載入時是通過代碼拷貝的方式來滿足不同進程的需要,而不能達到真正代碼段共享的目的。

第一次沒有指定-fPIC的時候出錯了,原因是針對可遷移R_X86_64_32平臺,只讀數據段’.rodata’不能創建成共享對象,原因是在動態鏈接動態庫的時候,如果沒有編譯成位置無關代碼,那麼鏈接的時候可能因爲某些代碼的位置具有相關性,而在執行時出現錯誤。可執行文件在鏈接時就知道每一行代碼、每一個變量會被放到線性地址空間的什麼位置,因此這些地址可以都作爲常數寫到代碼裏面。對於動態庫,只有加載的時候才知道。

如果代碼中沒有隻讀數據段,那麼就不會有這個問題。例如

o@Neo-kylin:~/lib_a_so$ cat >val.c

int a= 100;

o@Neo-kylin:~/lib_a_so$ gcc -shared -o val.so val.c

使用atoi.so

使用.so文件的形式和使用.a也差不多,也是使用ld來進行鏈接。因爲這過於複雜,還是使用gcc來做這個操作(實際上gcc也是使用的ld)。

o@Neo-kylin:~/lib_a_so$ gcc -o main main.o atoi.so.1

o@Neo-kylin:~/lib_a_so$ ldd main

linux-vdso.so.1 => (0x00007fff56eaf000)

atoi.so.1 => not found

libc.so.6 => /lib64/libc.so.6 (0x000000305d800000)

/lib64/ld-linux-x86-64.so.2 (0x000000305d000000)

o@Neo-kylin:~/lib_a_so$ ./main

./main: error while loading shared libraries: atoi.so.1: cannot open shared object file: No such file or directory

上面執行的時候報錯,意思是找不到atoi.so.1這個文件。原因是共享庫的查找目錄沒有當前目錄,我們可以添加環境變量LD_LIBRARY_PATH來使系統動態載入器 (dynamic linker/loader)在當前目錄也查找。

o@Neo-kylin:~/lib_a_so$ export LD_LIBRARY_PATH=.

o@Neo-kylin:~/lib_a_so$ ldd main

linux-vdso.so.1 => (0x00007fff08fff000)

atoi.so.1 => ./atoi.so.1 (0x00007f9ed7ac6000)

libc.so.6 => /lib64/libc.so.6 (0x000000305d800000)

/lib64/ld-linux-x86-64.so.2 (0x000000305d000000)

o@Neo-kylin:~/lib_a_so$ ./main

o@Neo-kylin:~/lib_a_so$ echo $?

5

還有一種辦法,比添加環境變量更好使,也更具有可移植性,那就是編譯的時候指定運行的時候共享庫的加載路徑。gcc使用-Wl,-rpath=sopath來指定,其中sopath是共享庫放置的路徑(可以是絕對路徑,也可以是相對路徑)。

o@Neo-kylin:~/lib_a_so$ gcc -o main main.o -Wl,-rpath=. atoi.so.1

o@Neo-kylin:~/lib_a_so$ ldd main

linux-vdso.so.1 => (0x00007fff457ff000)

atoi.so.1 => ./atoi.so.1 (0x00007fb946d56000)

libc.so.6 => /lib64/libc.so.6 (0x000000305d800000)

/lib64/ld-linux-x86-64.so.2 (0x000000305d000000)

o@Neo-kylin:~/lib_a_so$ ./main

o@Neo-kylin:~/lib_a_so$ echo $?

5

動態庫還可以通過dlopen/dlsym等來使用,這裏就不介紹了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章