Make Library for Linux

使用函式庫(API)讓我們開發程式速度增加,而且小細節的部份不用去特別注意,因為函式庫已經幫你解決掉了,自己辛苦寫了那麼多的程式,一定也會想有自己的函式庫,方便自己以後開發程式使用,所以這邊講解在Linux上所用到的函式庫有那些,以及如何使用。

※ 預先了解觀念

* 內外部連結(external linkage, internal linkage)
* 多檔案程式製成
* 基本gcc的使用
* Linux目錄樹

※ 將學會的觀念

* static library (.a library)
* shared library (.so library)
* dynamic loader (DL library)

※ 練習檔

Example:

test1.c for share and static library

#include
#include
#include "adio.h"
#include "adstring.h"
int main()
{
char input[21];
char buffer[11];
char buf[11];
const char* str = "hello world!!";
adstring_strcpy(buffer, str, 11);
puts(buffer);
adio_fgets(input, 21, stdin);
puts(input);
adstring_strcpy(buffer, input, 5);
puts(buffer);
int a = 12345;
puts(adstring_itoa(buf, a, 11));
puts(buf);
return 0;
}

test2.c for dynamic loader

#include
#include
#include
#include
int main()
{
void* handle;
char* (*so_strcpy)(char*, const char*, int);
char* (*so_gets)(char*, int);
char* (*so_itoa)(char*, int, int);
char* error;
handle = dlopen("./libadlib.so", RTLD_LAZY);
if (!handle)
{
puts (dlerror());
exit(1);
}
so_strcpy = dlsym(handle, "adstring_strcpy");
so_gets = dlsym(handle, "adio_fgets");
so_itoa = dlsym(handle, "adstring_itoa");
char input[21];
char buffer[11];
char buf[11];
const char* str = "hello world!!";
so_strcpy(buffer, str, 11);
puts(buffer);
so_gets(input, 21);
puts(input);
so_strcpy(buffer, input, 5);
puts(buffer);
int a = 12345;
so_itoa(buf, a, 11);
puts(buf);
dlclose(handle);
return 0;
}

adio.h for static, shared include

#ifndef ADVENCE_IO_H
#define ADVENCE_IO_H
#include
char* adio_fgets(char* buf, int num, FILE* fp);
void adio_stdinclean(void);
#endif

adstring.h for static, shared include

#ifndef ADVENCE_STRING_H
#define ADVENCE_STRING_H
char* adstring_strcpy(char* to, const char* from, int num);
char* adstring_itoa(char* to, int from, int num);
#endif

adio.c for make lib

#include
#include
char* adio_gets(char* buf, int num, FILE* fp)
{
char* find = 0;
fgets(buf, num, stdin);
if ((find = strrchr(buf, '\n')))
{
*find = '\0';
}
else
{
while (fgetc(fp) != '\n');
}
return buf;
}
void adio_stdinclean()
{
while (getchar() != '\n');
}

adstring.c for make lib

#include
#include
char* adstring_strcpy(char* to, const char* from, int num)
{
int size = num-1;
strncpy(to, from, size);
if (strlen(from) >= size)
{
to[size] = '\0';
}
return to;
}
char* adstring_itoa(char* to, int from, int num)
{
char tmp[11];
sprintf(tmp, "%d", from);
adstring_strcpy(to, tmp, num);
return to;
}

※ 靜態函式庫(static library)

所謂的靜態函式庫,簡單講是把一堆object檔用ar(archiver)包裝集合起來,檔名以「.a」結尾。優點是執行效能通常會比後兩者快,而且因為是靜態連結,所以不易發生執行時找不到library或版本錯置而無法執行的問題。缺點則是檔案較大,維護度較低;例如library如果發現bug需要更新,那麼就必須重新連結執行檔。

* 連結時期在程式還沒有開始執行就已連結
* 執行檔因為包含了函式庫,所以檔案較大
* 執行檔因為不用去尋找函式庫,所以執行較快
* 函式庫如果更新的話,使用到此函式庫所有的檔案要全部重新連結

Example:
Make static library

gcc adio.c adstring.c -Wall -c (編出adio.o adstring.o)
ar rcs libadlib.a adio.o adstring.o (包成libadlib.a檔)

如此我們可以建立由adio.o adstring.o所產生的static library(.a)其中檔名libadlib.a前面的「lib」和後面的「.a」是不能少的,因此你的靜態函式庫命名要以lib(xxxxx).a 才行,再使用ar指令將.o檔包在一個函式庫裡面,接著我們來看使用方法。

Example:
Use static library

gcc test.c -I. -L. -ladlib -o test1

其中-I是代表目前的標頭檔尋找位子,-L是代表目前的函式庫尋找位子,-ladlib是使用函式庫libadlib.a所以剛剛說的lib(xxxx).a現在看的出來只取出xxxx前後都不見了,如此可以成功使用static library了。

一般來說static library使用的目的是不想給別人你的source所以你都包好成為.a檔,之後只需要傳給別人你的標頭檔,和你的靜態函式庫.a別人就可以直接使用了。

※ 共享函式庫(shared library)

Shared library會在程式執行起始時才被自動載入。因為程式庫與執行檔是分離的,所以維護彈性較好。有兩點要注意,shared library是在程式起始時就要被載入,而不是執行中用到才載入,而且在連結階段需要有該程式庫才能進行連結。首先有一些名詞要弄懂,soname、 realname與linkername。

soname用來表示是一個特定library 的名稱,像是libmylib.so.1 。前面以「lib」開頭,接著是該library 的名稱,然後是「.so」接著是「版號」,用來表名他的介面;如果介面改變時,就會增加版號來維護相容度。

realname 是實際放有library程式的檔案名稱,後面會再加上minor 版號與release 版號,像是libmylib.so.1.0.0 。一般來說,最尾碼的release版號用於程式內容的修正,介面完全沒有改變。中間的minor用於有新增加介面,但相舊介面沒改變,所以與舊版本相容。最前面的version版號用於原介面有移除或改變,與舊版不相容時。

linkername是用於連結時的名稱,是不含版號的soname ,如: libmylib.so。通常linker name與real name是用ln 指到對應的real name ,用來提供彈性與維護性。

* 連結時期在程式開始時連結載入
* 執行檔因為不含有函式庫所以比較小
* 執行檔需要尋找所使用的.so檔所以比較慢
* 函式庫若有更改,重新建立函式庫即可,程式方面不用重編譯
* 可具有函式庫版本管理

Example:
Make shared library

gcc adio.c adstring.c -c -Wall -fPIC (編出adio.o adstring.o)
gcc adio.o adstring.o -shared -Wl,-soname,libadlib.so.1 -o libadlib.so.1.0.1 (編出libadlib.so.1.0.1共享函式庫的檔)
ln -s libadlib.so.1 libadlib.so (連結時的linker name)
ldconfig -n . (產生libadlib.so.1 -> libadlib.so.1.0.1的s-link)

編譯時要加上-fPIC用來產生position-independent code。也可以用-fpic參數。(不太清楚差異,只知道-fPIC 較通用於不同平台,但產生的code較大,而且編譯速度較慢)。而-shared 表示要編譯成shared library,-Wl 用於參遞參數給linker,因此-soname與libmylib.so.1會被傳給linker處理,-soname用來指名soname 為libadlib.so.1,library會被輸出成libadlib.so.1.0.1 (也就是real name),若不指定soname 的話,在編譯結連後的執行檔會以連時的library檔名為soname,並載入他。否則是載入soname指定的library檔案,所以我們產生以下各檔。

* soname: libadlib.so.1 (由ldconfig -n . 產生)
* realname: libadlib.so.1.0.1 (由gcc -shared -Wl .....產生)
* linkername: libadlib.so (自己用ln -s所建立的)

其中我們要將上面三個soname, realname, linkername用符號連結串起來libadlib.so -> libadlib.so.1 -> libadlib.so.1.0.1,為什麼呢?因為當我們連結使用的時候會連結libadlib.so而在程式執行的時候,會因為soname的關系 ld(連結器)會去尋找libadlib.so.1,而透過符號連結,可以由libadlib.so.1找到libadlib.so.1.0.1我們真正的共享函式庫檔,這樣做有一些好處。

* 連結的時候連結libadlib.so沒有版次的問題,因為會自動連到最新的版次,如果那天有更新的soname的版次出來的話只需要更改libadlib.so -> libadlib.so.2新的版次即可
* 如果共享函式庫做一點點小修正,而後只要增加共享函式庫的編號即可,如libadlib.so.1.0.2表示修正過,因此目錄下會有 libadlib.so.1.0.1(舊版本)libadlib.s0.1.0.2(新版本),但是透過「ldconfig -n .」會自動產生libadlib.so.1 -> libadlib.so.1.0.2的符號連結,不過先決條件是你此版的soname還是libadlib.so.1,而使用了ldconfig才會自動產生這樣的符號連結
* ldconfig是專門用在自動產生soname對映到最近期產生,或是編號最新,共享函式庫的符號連結檔

產生出來了libadlib.so(s-link), libadlib.so.1(s-link), libadlib.so.1.0.1(share library)我們來看看應該如何使用這些檔案。

Example:
Use shared library

gcc test.c -I. -L. -ladlib -o test2 (和static library沒有兩樣)

雖然建立好像和static library差別不大,如果目錄下同時有static與shared library的話,會以shared為主,使用-static參數可以避免使用shared連結,但是由上例編完後開始執行的時候應該會告訴你,找不到你的共享函式庫,嘿嘿!!因為路徑的問題,預設下只會找尋/usr/lib或是/usr/local/lib所以當然啦。你都放在自己的目錄當然找不到,我們可以用ldd來觀查看看。

Example:
查看test2中使用那些shared library

ldd ./test2 (會告訴你libadlib.so.1找不到啦)
(libadlib.so.1 => not found)

因此我們有幾個解決的辦法,來解決找不到你的shared library的方法。

1. 把libadlib.so(linkername), libadlib.so.1(soname), libadlib.so.1.0.1(realname)全都複製一份到/usr/lib中
2. /etc/ld.so.conf,加入一個新的library搜尋目錄,並執行ldconfig更新快取
3. 設定LD_LIBRARY_PATH環境變數來搜尋library,如:export LD_LIBRARY_PATH=.

以上的三種方式都可以讓你的shared library被正確的找到,但是我們通常使用第一種方法,因為這樣你的函式庫就可以被使用此系統的所有人共享了,而第三種方法通常都是用在暫時搬到某台機器上測式時暫時讓目前的目錄會被搜尋到。

※ 動態載入函式庫 (dynamic loading library)

其實動態載入函式庫不是一個檔案或是程式,而是一個技術,只有當你的函式庫為共享函式庫的時候,才可以使用dynamic loading這樣的技術,而此技術是不需要在連結的時候告訴ld說我們要用那個函式庫,而是在程式執行期間,執行某一行,真的需要用到那個函式庫裡面的某個函式的時候,才去和此函式庫連結,而且只單取用其中的某個函式,如此對系統需求更是少,但是同樣的因為載入的關係,會造成程式執行速度比較慢,此技術類似windows上的DLL動態連結檔。

這個技術比較有一點難度的就是,它透過一些函式來達到這樣的效果,動態載入是透過dl function的一套函式來實作,以下列出這些常使用的dl函式,而它的標頭檔為dlfcn.h。

void* dlopen(const char* filename, int flag);
(開啟載入filename指定的library載入執行程式中)

* filename 要載入的.so檔名(含路徑)
* flag 載入的方式(RTLD_LAZY)
* 回傳函式庫的記憶體位址

void* dlsym(void* handle, const char* symbol);
(取得symbol指定的函式名稱,所指向的函式記憶體位址)

* handle 由dlopen所取得的函式庫記憶體位址
* symbol 要使用此函式庫的某個函式名稱
* 回傳函式的記憶體位址

int dlclose(void *handle);
(關閉dlopen開啟的handle)

char *dlerror(void);
(傳回最近所發生的錯誤訊息)

以上的test2.c是由test1.c改變而來的,結果都是完全一樣的,但是test2.c是使用dynamic loading的技術完成的,此範例可以看到如果使用上面說的四個函式,程式會寫了之後,還需要怎麼連結才行,因此來我們連結看看吧!假設上面的 shared library所建立的三個檔libadlib.so, libadlib.so.1, libadlib.so.1.0.1都在目前的目錄下面。

Example:
dynamic loading shared library

gcc test2.c -ldl -o test3 (dl是dlfcn.h的函式庫)

對於動態載入沒有路徑上的問題,只要你在dlopen裡面的filename路徑設定是正確的,那就可以開啟使用你的shared library達到動態載入的能力,它和之前使用函式庫的方法有一些不同。

* 在test2.c中並不用含入adio.h adstring.h標頭檔
* 使用函都是透過dlsym回傳的函式指標來使用函式庫中的函式
* 在編譯的時候非常簡單,只要-ldl即可
* 對於函式的介面要非常清楚(不然怎麼寫函式指標)

※ 總結

使用static library, shared library, dynamic loading library都各有好處,通常如果小程式的話製成static library就很好用了,編譯的時候-L. -ladlib就連好了不用考慮會找不到函式庫的問題,但是如果你的函式庫常常要改動的話,使用shared library比較好,因為透過ldconfig可以為你的soname自動建立符號連結,指向你的realname的函式庫檔,所以只要編好新的 shared library就可以換掉舊的,而且程式不用重編譯,但是麻煩在於如果要移到別台機器上,移動過去配置就要花上不少時間,有點麻煩。最後使用 dynamic loading這是個技術,只能用在shared library,讓你單獨用到某個函式庫中的幾個函式,但是麻煩的地方是,載入你全都要自已來控制,自己撰寫程式碼,透過dlopen, dlsym, dlclose等等來控制。

1 意見:

    很棒的一篇文章
    幫助我很多!!