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