天天看點

Linux系統基礎開發技術1:建構Linux 庫檔案

Author:gnuhpc

WebSite:blog.csdn.net/gnuhpc

實驗環境:Ubuntu Linux 10.04 32bit

1.庫檔案簡介

庫檔案是一個包含了編譯後代碼、資料的檔案,用于與程式其他代碼連編,它可以使得程式子產品化、編譯速度更快,并且易于更新。庫檔案分為三種(實質為兩種,在随後兩句話有解釋):靜态庫(在程式之前就已經裝載進其中了)、共享庫(在程式啟動之時加載進去,在程式直接共享)、動态加載庫(dynamically loaded,DL)(在程式運作中任何時候都可以被加載程序式中使用,事實上DL并非是一個完全不同的庫類型,共享庫可以用作DL而被動态加載(靜态庫在Linux貌似無法用dlopen加載)。注意有些人使用dynamically linked libraries (DLLs)來指代共享庫,有些人使用DLL這個詞來形容任何可以被用作DL的庫檔案,這個請區分對待。

在具體使用中,我們應該多使用共享庫,這使得使用者可以獨立于使用該庫檔案的程式而更新庫。DL的确非常有用,但有時候我們可能并不需要那些靈活性,而對于靜态庫,由于更新起來實在費勁,我們一般不使用。

2.靜态庫的建立

靜态庫就是一堆普通的目标檔案(object file),習慣上靜态庫以.a為字尾,這是使用ar指令生成的。靜态庫允許使用者不用重新編譯代碼就可以連結程式,以節省重新編譯的時間,其實這個時間已經在強大的機器配置和快速的編譯器中顯得微不足道了,這個常常用來提供程式而不是源代碼。速度上,靜态ELF(Executable and Linking Format)庫檔案比共享庫或者動态加載庫快1%-5%,但實際上常常因為其他因素而并不一定快。

我們寫主檔案prog.c:

1: #include

2: void ctest1(int *);

3: void ctest2(int *);

4:

5: int main()

6: {

7: int x;

8: ctest1(&x);

9: printf("Valx=%dn",x);

10:

11: return 0;

12: }

13: 然後寫這兩個函數的實作:

ctest1.c

1: void ctest1(int *i)

2: {

3: *i=5;

4: } ctest2.c

1: void ctest2(int *i)

3: *i=100;

4: }我們首先編譯這兩個函數實作的源檔案:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ gcc -Wall -c ctest1.c ctest2.c

gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ ls

ctest1.c ctest1.o ctest2.c ctest2.o prog.c

然後建立靜态庫libctest.a:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ ar -cvq libctest.a ctest1.o ctest2.o

a - ctest1.o

a - ctest2.o

我們檢視一下這個庫中的檔案:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ ar -t libctest.a

ctest1.o

ctest2.o

此時我們可以編譯我們的程式了,注意-l選項,後邊的參數是去掉lib和.a的部分,并且需要放在要編譯的檔案名之後,否則會報錯。:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ gcc -o test prog.c -L./ –lctest

ctest1.c ctest1.o ctest2.c ctest2.o libctest.a prog.c test

gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ ./test

Valx=5

3.共享庫的建立

共享庫是在程式啟動時加載的庫檔案。當共享庫加載完畢後所有啟動的起來的程式都将使用新的共享庫。在建立共享庫之前,還需要了解一些知識:

命名規則:

每一個共享庫都有一個soname,一般都形如libname.so.versionNumber,其中versionNumber每當接口發生改變時都要增加,一個完全的soname的字首應該是它所在目錄,在一個實際系統中,一個完整的soname隻是共享庫檔案的real name的符号連結。程式運作時在内部列出所需的共享庫時使用的就是soname。

每一個共享庫也有一個real name,這是包含實際代碼的檔案名,real name使用soname為字首,并且在後邊添加一些資訊,一般都形如soname.MinorNumber.ReleaseNumber。 最後的releaseNumber可有可無。這個是生成共享庫時實際檔案的名稱。

同時,在編譯器要求使用一個共享庫時使用的名字稱為linker name,一般都是去掉版本号的soname,用于gcc中-lname這樣的選項的編譯。

這幾個名字的關系:你在建立實際庫檔案中指定libreadline.so.3.0為real name ,并且使用符号連結建立soname ->libreadline.so.3和linker name-> /usr/lib/libreadline.so。

放置位置:

GNU标準推薦将所有預設的庫安裝在/usr/local/lib,這指的是開發者源代碼預設的位置。

FHS指出大多數的庫檔案應該放在/usr/lib,而啟動所需的庫則應該放在/lib中,而非系統庫應該放在/usr/local/lib。這指的是發行版預設的位置,這兩個标準并沒有沖突。

共享庫的主要有三個步驟:

建立目标代碼。

建立庫。

使用符号連結建立預設版本的共享庫(可選)。

現在我們舉個例子來說明,首先我們編譯源代碼,使用-fPIC選項生成共享庫所需的位置獨立代碼(position-independent code (PIC)):

gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ gcc -Wall -fPIC -c *.c

gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ ls

ctest1.c ctest1.o ctest2.c ctest2.o prog.c prog.o

然後我們建立庫檔案:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ gcc -shared -Wl,-soname,libctest.so.1 -o libctest.so.1.0 *.o

ctest1.c ctest1.o ctest2.c ctest2.o libctest.so.1.0 prog.c prog.o

-shared選項指明生成共享目标檔案,-W1(注意是小寫L而不是一)指明傳傳入連結接器的參數,在此我們設定了該庫的soname為libctest.so.1,-o則指明了生成的目标庫檔案為libctest.so.1.0(這個就是real name)。

最後建立所需的符号連結:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ sudo mv libctest.so.1.0 /usr/local/lib/libctest.so.1.0

gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ sudo ln -sf /usr/local/lib/libctest.so.1.0 /usr/local/lib/libctest.so.1

gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ sudo ln -sf /usr/local/lib/libctest.so.1.0 /usr/local/lib/libctest.so

建立的libctest.so就是上面所謂linker name,用于編譯時-lctest選項。

建立的libctest.so.1就是soname,我們在上邊說過程式在運作時需要這個名字的符号連結。

此時我們的共享庫就建好了,接着我們編譯程式:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ gcc -Wall -L/usr/local/lib prog.c -lctest -o prog

ctest1.c ctest1.o ctest2.c ctest2.o prog prog.c prog.o

我們編譯完畢,該庫并不會包含在可執行檔案中,隻有在執行時來會動态加載進來。我們可以通過ldd列出一個可執行程式所有的依賴,在我的系統中還找不到/usr/local/bin的路徑:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ ldd prog

linux-gate.so.1 => (0x00a5c000)

libctest.so.1 => not found

libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00a6f000)

/lib/ld-linux.so.2 (0x00451000)

此時,運作會報找不到庫的錯誤:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ ./prog

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

我們可以将所需庫的路徑加入到系統路徑中,有三種方法可以完成:

A.在/etc/ld.so.conf中加入所在路徑,然後執行ldconfig配置連結器運作時綁定配置。你也可以建立一個檔案,将路徑寫入,然後使用ldconfig –f filename将配置寫入。

B.修改LD_LIBRARY_PATH環境變量(Linux下,AIX下為LIBPATH),在其中添加路徑。若你直接在.bashrc檔案中配置則重新開機後不失效,否則在shell中設定重新開機後失效。

我們使用A方法中的-f選項:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ vi libctest.conf

gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ sudo ldconfig -f libctest.conf

linux-gate.so.1 => (0x00f6f000)

libctest.so.1 => /usr/local/lib/libctest.so.1 (0x005d9000)

libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00718000)

/lib/ld-linux.so.2 (0x001e6000)

其中libctest.conf中寫入路徑:/usr/local/lib。程式運作正常。

4.動态加載庫的使用

動态加載庫是在非程式啟動時動态加載進入程式的庫,這對于實作插件或動态子產品有很大的幫助。在Linux中,動态加載庫的形式并不特殊,它使用上述兩種程式庫,使用提供的API在程式運作時動态加載。注意,在不同平台上動态加載庫的API并不相同,是以可能會有移植問題出現。

我們可以通過nm指令先檢視一下我們建立的庫裡面有哪些symbol(可以了解為函數方法)供我們使用:

gnuhpc@gnuhpc-desktop:~/MyCode/lib$ nm /usr/local/lib/libctest.so

00001f18 a _DYNAMIC

00001ff4 a _GLOBAL_OFFSET_TABLE_

w _Jv_RegisterClasses

00001f08 d __CTOR_END__

00001f04 d __CTOR_LIST__

00001f10 d __DTOR_END__

00001f0c d __DTOR_LIST__

000005a0 r __FRAME_END__

00001f14 d __JCR_END__

00001f14 d __JCR_LIST__

00002014 A __bss_start

w __cxa_finalize@@GLIBC_2.1.3

00000540 t __do_global_ctors_aux

00000420 t __do_global_dtors_aux

00002010 d __dso_handle

w __gmon_start__

000004d7 t __i686.get_pc_thunk.bx

00002014 A _edata

0000201c A _end

00000578 T _fini

000003a0 T _init

00002014 b completed.7021

000004dc T ctest1

000004ec T ctest2

00002018 b dtor_idx.7023

000004a0 t frame_dummy

000004fc T main

U printf@@GLIBC_2.0

這個指令對靜态庫和共享庫都支援,第二列為symbol類型,小寫字母表示符号是本地的,大寫字母表示符号是全局(外部)的,幾個常見的字母含義如下:T為代碼段普通定義,D為已初始化資料段,B為未初始化資料段,U為未定義(用到該符号但是沒有在該庫中定義)。

我們建立ctest.h:

1: #ifndef CTEST_H

2: #define CTEST_H

3:

4: #ifdef __cplusplus

5: extern "C" {

6: #endif

7:

8: void ctest1(int *);

9: void ctest2(int *);

11: #ifdef __cplusplus

13: #endif

14:

15: #endif這裡使用extern C是為了使得該庫既可以用于C語言又可以用于C++。

我們動态加載庫進來:progdl.c

2: #include

3: #include "ctest.h"

5: int main(int argc, char **argv)

7: void *lib_handle;

8: double (*fn)(int *);

9: int x;

10: char *error;

11:

12: lib_handle = dlopen("/usr/local/lib/libctest.so", RTLD_LAZY);

13: if (!lib_handle)

14: {

15: fprintf(stderr, "%sn", dlerror());

16: exit(1);

17: }

18:

19: fn = dlsym(lib_handle, "ctest1");

20: if ((error = dlerror()) != NULL)

21: {

22: fprintf(stderr, "%sn", error);

23: exit(1);

24: }

25:

26: (*fn)(&x);

27: printf("Valx=%dn",x);

28:

29: dlclose(lib_handle);

30: return 0;

31: }裡面的方法解釋如下:

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

若filename為絕對路徑,那麼dlopen就會試圖打開它而不搜尋相關路徑,否則就現在環境變量LD_LIBRARY_PATH處搜尋,然後在/etc/ld.so.cache以及/lib和/usr/lib搜尋。flag我們隻解釋兩個常用的選項:若為RTLD_LAZY則表示在動态庫執行時解決未定義符号問題,而RTLD_NOW則表示在dlopen傳回前解決未定義符号問題。當你調試時你應該用RTLD_NOW,這個時候若存在未解決的引用程式還可以繼續進行。另外,RTLD_NOW選項可能會使打開庫的這個操作稍微慢一點,但是以後尋找函數時就會快一點。注意,若程式庫互相依賴則應該按依賴順序依次載入,比如X依賴Y,那麼要先載入Y然後再載入X。傳回的是一個句柄,若失敗則傳回null.

char *dlerror(void);

報告任何上一次對加載庫操作的錯誤。兩次調用期間若有操作錯誤則第二次會報告, 否則第二次則傳回null——它報告完錯誤就等待下一個錯誤的發生,上一次錯誤的情況一旦報告就不再提及。

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

尋找對應symbol的函數方法,handle就是dlopen傳回的句柄。一般如下使用:

1: dlerror(); /* clear error code */

2: s = (actual_type) dlsym(handle, symbol_being_searched_for);

3: if ((err = dlerror()) != NULL) {

4: /* handle error, the symbol wasn't found */

5: } else {

6: /* symbol found, its value is in s */

7: }int dlclose(void *handle);

關閉一個動态加載庫。當一個動态庫被加載多次時,你需要用同樣次數dlclose該動态庫才可以deallocated.

我們編譯該代碼gcc -g -rdynamic -o progdl progdl.c -ldl,即可得到可執行檔案(其中-g選項是為了gdb調試所用),其中的庫為動态加載後又關閉的。我們使用gdb看一下代碼:

(gdb) b main

Breakpoint 1 at 0x804878d: file progdl.c, line 12.

(gdb) r

Starting program: /home/gnuhpc/MyCode/lib/dynamic/progdl

Breakpoint 1, main (argc=1, argv=0xbffff4a4) at progdl.c:12

12 lib_handle = dlopen("/usr/local/lib/libctest.so", RTLD_LAZY);

(gdb) f

#0 main (argc=1, argv=0xbffff4a4) at progdl.c:12

(gdb) s

13 if (!lib_handle)

(gdb) n

19 fn = dlsym(lib_handle, "ctest1");

(gdb)

20 if ((error = dlerror()) != NULL)

26 (*fn)(&x);

27 printf("Valx=%dn",x);

(gdb) p x

$1 = 5

(gdb) p fn

$2 = (double (*)(int *)) 0x28c4dc

可以看到fn獲得了ctest1的位址。

參考文獻:

http://www.yolinux.com/TUTORIALS/LibraryArchives-StaticAndDynamic.html

http://www.linuxjournal.com/article/3687

http://www.dwheeler.com/program-library/Program-Library-HOWTO/

繼續閱讀