這裡介紹如何使用 gcc 編譯器自己製作 C/C++ 靜態、共享與動態載入函式庫,讓程式碼可以更方便的重複使用。

在開發不同的程式時,如果有一些程式碼是重複會用到的話,就可以將其製作成獨立的函式庫,不僅維護上更方便,也可以方便其他專案或是開發者使用,以下介紹以 gcc 還有 ar 等工具製作靜態、共享與動態載入函式庫的步驟。

範例程式

這裡我們用一個很簡單的 C 語言函數來做示範,假設我們有一個計算總和的 sum 函數,不同的專案或開發者都會需要用到它,sum.c 的內容如下:

#include "sum.h"
double sum(double a, double b) {
  return a + b;
}

其對應的標頭檔(header file)sum.h 內容如下:

#ifndef __SUM_H__
#define __SUM_H__
double sum(double a, double b);
#endif

這是一個簡單的 main.c 使用範例,其內容如下:

#include <stdio.h>
#include "sum.h"
int main(int argc, char *argv[]) {
  double a = 2.6, b = 4.2, c;
  c = sum(a, b);
  printf("%.1f + %.1f = %.1f\n", a, b, c);
  return 0;
}

若就一般正常的編譯程序,我們會使用 gcc 直接編譯所有的 C 檔案,連結後產生執行檔:

gcc -o main main.c sum.c

執行之:

./main
2.6 + 4.2 = 6.8

如果 sum.c 的內容只有在這一個專案中會用到,那麼這樣直接編譯的做法是最簡單又省事的。

如果有其他的專案也會用到 sum.c 裡面函數的話,最直接的作法就是把 sum.csum.h 兩個原始碼檔案複製一份到新的專案中,然後也用同樣的編譯方式如法泡製,但是這樣的缺點有兩個,一個就是每次都要重新編譯 sum.c 的內容,比較量費時間,另外就是未來若要持續維護 sum.c 的內容時,就需要同時修改好多個副本,既容易出錯也很麻煩。

另外如果 sum.c 的程式碼有機密性的問題,也就是說只想讓別人用,但是不想給原始碼的時候,也沒辦法直接用這種方式處理。

靜態函式庫

靜態函式庫(static library)就是由一些物件檔案(object files)所構成的封裝檔,通常其檔案名稱都會以 lib 開頭,而副檔名則為 .a

使用靜態連結函式庫的好處就是所有的程式都包裝在執行檔中,不會因為缺少函式庫的檔案而不能執行,不過缺點就是這樣的執行檔大小會比較大,而如果函式庫有更新的話,整個執行檔也要跟著重新編譯。

以下我們示範將 sum.c 的內容製作成一個靜態函式庫的步驟。

Step 1
編譯 sum.c 的程式碼:

gcc -c -o sum.o sum.c

Step 2
使用 ar 指令將 sum.o 打包成 libsum.a 這個靜態連結函式庫:

ar -rcs libsum.a sum.o

Step 3
使用 libsum.a 這個靜態連結函式庫:

gcc main.c -L. -lsum -o main_static

也可以直接把 libsum.a 這個靜態連結函式庫檔案放進去編譯:

gcc main.c libsum.a -o main_static

這樣就可以將 main.c 這個程式編譯成 main_static 這個執行檔,而在編譯的過程中就不需要 sum.c 這個原始碼的檔案。

使用靜態連結函式庫所編譯出來的執行檔可以獨立執行,不需要原本的 libsum.a 檔:

ldd main_static
	linux-vdso.so.1 =>  (0x00007fffb0d1a000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f0411e46000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f0412220000)

共享函式庫

共享函式庫(shared library)是在程式實際開始執行時,才會被載入的函式庫,執行檔本身與共享函式庫是分離的,這樣可以讓執行檔的大小比較小,而且未來共享函式庫在更新之後,執行檔也不需要重新編譯,而缺點則是執行檔在執行時就會需要共享函式庫的檔案,如果缺少了共享函式庫的檔案,就會無法執行。

共享函式庫的檔案通常也是以 lib 開頭,但是其副檔名則為 .so,以下是建立共享函式庫的步驟。

Step 1
編譯時加入 -fPIC 參數,產生共享函式庫所需要的 position independant code:

gcc -c -fPIC -o sum.o sum.c

Step 2
使用 gcc 建立共享函式庫:

gcc -shared -Wl,-soname,libsum.so.1 -o libsum.so.1.0.0 sum.o

這裡的 -Wl 是用來將一些參數設定傳給連結器(linker),所以之後的 -soname 等參數就是傳給連結器的參數。

-soname 指定為 libsum.so.1 是代表函式庫的名稱,以 lib 開頭,接著是自己取的名稱,最後加上 .so 與 version 版本號碼,這一個 version 版本號碼所代表的是函示庫的介面版本,如果介面有改變時就會增加 version 版本號碼,以維護相容性的問題。

而最後產生的實際檔案名稱也跟 soname 類似,不過後面多了 minor 與 release 版本號碼,中間的 minor 號碼是用於標示新增加的介面,而最後面的 release 號碼則是用於程式內容的修正(介面不變的情況)。

如果程式使用到這個共享函式庫,則在執行時就會依據 soname 所指定的名稱來尋找函式庫的檔案,如果想要看共享函式庫的 soname 屬性,可以使用 objdump 指令:

objdump -p libsum.so.1.0.0 | grep SONAME
  SONAME               libsum.so.1

建立好共享函式庫之後,要建立一個不含版本號碼的 .so 連結檔,gcc 在連結時所需要的函式庫檔案是這一個:

ln -s libsum.so.1.0.0 libsum.so

另外再建立一個執行時要用的連結檔:

ln -s libsum.so.1.0.0 libsum.so.1

使用共享函式庫來編譯執行檔:

gcc main.c -L. -lsum -o main_dynamic

或是這樣編譯也可以:

gcc main.c libsum.so -o main_dynamic

編譯完成後,要執行時需要指定 LD_LIBRARY_PATH

LD_LIBRARY_PATH=. ./main_dynamic
2.6 + 4.2 = 6.8

這一個 main_dynamic 執行檔在執行時,會需要 libsum.so.1 這一個共享函式庫檔案:

LD_LIBRARY_PATH=. ldd main_dynamic
	linux-vdso.so.1 =>  (0x00007ffc5f1f7000)
	libsum.so.1 => ./libsum.so.1 (0x00007efd0148d000)
	libc.so.6 => /lib64/libc.so.6 (0x00007efd010b3000)
	/lib64/ld-linux-x86-64.so.2 (0x00007efd01690000)

動態載入函式庫

動態載入函式庫(dynamically loaded library)就類似 Windows 的 dll 檔,是等到程式真正要用到時才會載入函式庫,其實作方式是透過 DL 函式庫配合一般的共享函式庫來處理的,以下是將之前製作的 libsum.so.1 共享函式庫改為動態載入的範例。

#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>
int main(int argc, char **argv) {
  void *handle;
  double (*sum)(double, double);
  char *error;

  // 動態開啟共享函式庫
  handle = dlopen ("libsum.so.1", RTLD_LAZY);
  if (!handle) {
    fputs (dlerror(), stderr);
    exit(1);
  }

  // 取得 sum 函數的位址
  sum = dlsym(handle, "sum");
  if ((error = dlerror()) != NULL)  {
    fputs(error, stderr);
    exit(1);
  }

  // 使用 sum 函數
  double a = 2.6, b = 4.2, c;
  c = sum(a, b);
  printf("%.1f + %.1f = %.1f\n", a, b, c);

  // 關閉共享函式庫
  dlclose(handle);
  return 0;
}

將這段程式碼儲存為 main_dl.c 之後,按照一般的方式進行編譯:

gcc main_dl.c -ldl -o main_dl

在使用上跟共享函式庫差不多,一樣要指定 LD_LIBRARY_PATH

LD_LIBRARY_PATH=. ./main_dl
2.6 + 4.2 = 6.8

表面上看起來跟一般的共享函式庫類似,不過這種動態載入函式庫方式與共享函式庫有很大的不同,共享函式庫是在程式一開始執行時就要載入(不管實際上有沒有使用到),而動態載入函式庫的做法則是可以在真正需要用到時才載入(如果沒用到就可以不需要載入)。

參考資料:René NyffeneggerTLDPMURMURING