Unix/Linux環境下創建和使用靜/動態庫

庫的作用

  大體上庫的存在,有兩方面的原因,一是代碼的複用,二是聲明和實現的分離。將功能相近的使用模塊封裝成庫,使代碼的複用、管理和分發變得簡單了 許多,例如著名的開源圖形庫ncurses,你可以自行編譯,更可以直接使用已經編譯好的現成的庫文件。另外,由於庫是二進制文件,某種意義上講,將功能 的實現部分隱藏了起來,這就爲商業代碼的保護提供了一種方式。
  庫文件按照鏈接方式和時機,可以分爲動態庫和靜態庫,下面分別介紹它們在Linux環境中的創建和使用方法。

靜態鏈接庫

  靜態庫是指在程序的鏈接階段,其中被用到的代碼會被直接鏈接到可執行文件中的庫。靜態鏈接的可執行程序包含了其所需的全部庫函數:所有庫函數都 連接到程序中。 這類程序是完整的,其運行不需要外部庫的支持。 靜態鏈接程序的優點之一是其安裝之前不需要做環境準備工作 。
  Linux中,靜態庫的擴展名通常爲.a,它僅僅是一些目標文件(.o)的歸檔(archive)或者說打包,另外,爲了鏈接時能夠快速地定位其中的符號(函數、變量名等),靜態庫還會包含一個對其中符號的索引。
  創建靜態庫的過程十分簡單,除編譯所必須的工具之外,要用到的命令只有兩個ar和ranlib。ar可以將各個目標文件進行歸檔,ranlib對ar 生成的歸檔文件(即靜態庫文件)進行索引。假設現在有這樣幾個源文件:plus.c, sub.c及相應的頭文件,另外還有一個用來調用庫文件中函數的主文件main.c,它們的內容分別是:
plus.c,

sub.c,

main.c,

 

    下面將plus.c和sub.c編譯,製作成靜態庫libmath.a,依次執行:

$ cc
 -c
 plus.c sub.c
$ ar rc libmath.a plus.o sub.o
$ ranlib libmath.a

    現在我們的libmath.a靜態庫已經制作完成,其中使用了ar命令的兩個選項,c表示若庫文件不存在則創建之,
r表示庫文件中若已經存在某個目標文件,且較舊,則執行替換。使用這個靜態庫更加簡單:

$ cc main.c -L. -lmath -o main
$ ./ main
3


動態鏈接庫
  動態庫和靜態庫相似,也是各個目標文件的集合。但相比靜態鏈接的程序,動態鏈接可執行程序要小得多:這類程序運行時需要外部共享 函數庫的支持,因此好像並不完整。除了程序體小之外,動態鏈接允許程序包指定必須的庫,而不必將庫裝入程序包內。動態鏈接技術還允許多個運行中的程序共享 一個庫,這樣就不會出現同一代碼的多份拷貝共佔內存的情況了。由於這些原因,當前多數程序採用動態鏈接技術。

  在Linux中的擴展名通常爲.so。但在鏈接時,並不會被鏈接到可執行文件中,而是在執行時(需要時)由操作系統的動態加載模塊動態地加載到內存,並鏈接到可執行文件地址空間的相應位置。
  動態鏈接庫的創建也分爲編譯和”歸檔”兩個階段,但不同的是在這兩個階段需要使用一些不同的命令選項。首先,需要將源文件編譯成一種成爲位置無關碼 (PIC: Position Independent Code)的目標文件,這種代碼可以被加入到內存的任何位置卻不需要加載器對其進行重定位,關於這種格式可以參考《鏈接器與加載器》和《程序員的自我修養 –鏈接裝載與庫》中較爲詳盡的描述。接下來需要將這些位置無關碼“歸檔”爲.so文件。整個過程只需一個工具即可,即gcc。還是上面的源文件,執行以下 命令:

 

$ cc -c   -fpic plus.c sub.c

$ cc -shared -o libmath.so * .o

   這樣,包含plus.o和sub.o的動態庫文件libmath.so就創建完成了。其中cc(gcc的符號鏈接)命令的-fpic或-fPIC選項使之創建位置
無關的目標文件,使用-shared選項可以創建最終的動態庫文件。使用動態庫文件有兩種方法,一種是讓操作系統的動態加載模塊
(如ld-linux.so.2)在執行時加載動態庫。另一種是在代碼中使用dl庫動態加載庫文件。

  先介紹下第一種方法。使用這種方法需要在編譯可執行文件時指明庫名及其路徑(對於自己的編寫的動態庫而言):

$ cc main.c -L. -lmath -o main

  這時可執行文件main就被創建了,上面的命令並沒有將libmath.so中相應的目標代碼鏈接到main中(你可以對比一下這裏的main 和靜態鏈接的main的大小),只是在庫中查找和確認main.c中用到的符號而已。但通常這個main現在還無法正常執行,這涉及到系統查找動態庫的路 徑問題,系統通常只在某些指定的目錄(標準路徑)下查找所需的庫文件,若在標準路徑中無法找到所需的庫,則會到環境變量 LD_LIBRARY_PATH(如果存在的話)指定的目錄下查找,若仍無法找到就會報錯。因爲libmath.so在main的當前目錄中,而當前目錄 通常並不在標準路徑之列。爲了使libmath.so能夠被找到和加載,你可以把它放到標準路徑中,但更好的方法是將其所在目錄加入到 LD_LIBRARY_PATH變量中。執行下面的命令:

 

 

$ LD_LIBRARY_PATH
=$LD_LIBRARY_PATH
 #添加當前目錄

$ export LD_LIBRARY_PATH # 將環境變量導出,使其在子shell中可用
$ ./ main
3

下面介紹使用dl庫加載動態庫,dl庫中函數很少很簡練,看main.c代碼:








  main.c中,首先使用dlopen打開需要的動態庫,其中參數RTLD_LAZY指明僅當需要調用該庫時才進行加載。dlopen返回一個
句柄,dlsym使用該句柄和符號來取得相應函數的地址,這裏使用int (*)(int,
int)函數指針來接收plus函數的地址。接下來使用得到的函數指針調用相應的函數,最後通過dlclose函數來關閉句柄。編譯這個程序需要使用dl
動態鏈接庫,因此需要使用gcc的-ldl選項:

$ cc main.c -ldl -o main
$ ./ main
3

.so與.a文件的對比

  最後附上libmath.a和libmath.so文件的內容,使用objdump的-d選項查看的:
libmath.a,

In archive libmath.a:

plus.o: file format elf32-i386

Disassembly of section .text:



00000000 <plus>:

0: 55 push %ebp

1: 89 e5 mov %esp,%ebp

3: 8b 45 0c mov 0xc(%ebp),%eax

6: 8b 55 08 mov 0x8(%ebp),%edx

9: 8d 04 02 lea (%edx,%eax,1),%eax

c: 5d pop %ebp

d: c3 ret



sub.o: file format elf32-i386

Disassembly of section .text:



00000000 <sub>:

0: 55 push %ebp

1: 89 e5 mov %esp,%ebp

3: 8b 45 0c mov 0xc(%ebp),%eax

6: 8b 55 08 mov 0x8(%ebp),%edx

9: 89 d1 mov %edx,%ecx

b: 29 c1 sub %eax,%ecx

d: 89 c8 mov %ecx,%eax

f: 5d pop %ebp

10: c3 ret



libmath.so:

libmath.so:     file format elf32-i386

Disassembly of section .init:

00000324 <_init>:

324: 55 push %ebp

325: 89 e5 mov %esp,%ebp

327: 53 push %ebx

328: 83 ec 04 sub $0x4,%esp

......

0000044c <plus>:

44c: 55 push %ebp

44d: 89 e5 mov %esp,%ebp

44f: 8b 45 0c mov 0xc(%ebp),%eax

452: 8b 55 08 mov 0x8(%ebp),%edx

455: 8d 04 02 lea (%edx,%eax,1),%eax

458: 5d pop %ebp

459: c3 ret

45a: 90 nop

45b: 90 nop



0000045c <sub>:

45c: 55 push %ebp

45d: 89 e5 mov %esp,%ebp

45f: 8b 45 0c mov 0xc(%ebp),%eax

462: 8b 55 08 mov 0x8(%ebp),%edx

465: 89 d1 mov %edx,%ecx

467: 29 c1 sub %eax,%ecx

469: 89 c8 mov %ecx,%eax

46b: 5d pop %ebp

46c: c3 ret

46d: 90 nop

46e: 90 nop

46f: 90 nop

......



Disassembly of section .fini:



000004a8 <_fini>:

4a8: 55 push %ebp

4a9: 89 e5 mov %esp,%ebp

4ab: 53 push %ebx

4ac: 83 ec 04 sub $0x4,%esp

4af: e8 00 00 00 00 call 4b4 <_fini+0xc>

4b4: 5b pop %ebx

4b5: 81 c3 40 1b 00 00 add $0x1b40,%ebx

4bb: e8 d0 fe ff ff call 390 <__do_global_dtors_aux>

4c0: 59 pop %ecx

4c1: 5b pop %ebx

4c2: c9 leave

4c3: c3 ret

  可見,相對.a,.so有很多額外的代碼段,其中比較重要的是<_init>段和<_fini>段。它們分別進行一些 前期和後期處理工作,例如<_init>通常在dlopen返回之前執行一些.so庫中的一些初始化工作(C++中就可能是全局構造或者靜態 對象的構造函數)。

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