1. 程式人生 > >Linux C 靜態庫和動態庫

Linux C 靜態庫和動態庫

這次分享的宗旨是——讓大家學會建立與使用靜態庫、動態庫,知道靜態庫與動態庫的區別,知道使用的時候如何選擇。這裡不深入介紹靜態庫、動態庫的底層格式,記憶體佈局等,有興趣的同學,推薦一本書《程式設計師的自我修養——連結、裝載與庫》。

什麼是庫

庫是寫好的現有的,成熟的,可以複用的程式碼。現實中每個程式都要依賴很多基礎的底層庫,不可能每個人的程式碼都從零開始,因此庫的存在意義非同尋常

本質上來說庫是一種可執行程式碼的二進位制形式,可以被作業系統載入記憶體執行。庫有兩種:靜態庫(.a、.lib)和動態庫(.so、.dll)。

所謂靜態、動態是指連結。回顧一下,將一個程式編譯成可執行程式的步驟:

clip_image002[4]

圖:編譯過程

靜態庫

之所以成為【靜態庫】,是因為在連結階段,會將彙編生成的目標檔案.o與引用到的庫一起連結打包到可執行檔案中。因此對應的連結方式稱為靜態連結。

試想一下,靜態庫與彙編生成的目標檔案一起連結為可執行檔案,那麼靜態庫必定跟.o檔案格式相似。其實一個靜態庫可以簡單看成是一組目標檔案(.o/.obj檔案)的集合,即很多目標檔案經過壓縮打包後形成的一個檔案。

靜態庫特點總結:

l  靜態庫對函式庫的連結是放在編譯時期完成的。

l  程式在執行時與函式庫再無瓜葛,移植方便。

l  浪費空間和資源,因為所有相關的目標檔案與牽涉到的函式庫被連結合成一個可執行檔案。

Linux下使用ar

工具、Windows下vs使用lib.exe,將目標檔案壓縮到一起,並且對其進行編號和索引,以便於查詢和檢索。一般建立靜態庫的步驟如圖所示:

clip_image004[4]

圖:建立靜態庫過程

Linux下建立與使用靜態庫

Linux靜態庫命名規則

Linux靜態庫命名規範,必須是"lib[your_library_name].a":lib為字首,中間是靜態庫名,副檔名為.a。

建立靜態庫(.a)

通過上面的流程可以知道,Linux建立靜態庫過程如下:

// add.c
int add(int a, int b)
{
   return (a+b);
}

      l  首先,將程式碼檔案編譯成目標檔案.o(add.o)

gcc -c add.c

注意帶引數-c,否則直接編譯為可執行檔案

l  然後,通過ar工具將目標檔案打包成.a靜態庫檔案

ar -crv libadd.a add.o

生成靜態庫libadd.a

大一點的專案會編寫makefile檔案(CMake等等工程管理工具)來生成靜態庫,輸入多個命令太麻煩了。

使用靜態庫

編寫使用上面建立的靜態庫的測試程式碼:

// main.c
#include<stdio.h>

extern int add(int a, int b);
int main(void)
{
   int n = add(3,4);
   printf("add(3,4) = %d \n",n);

   return 0;
}


測試:
1.  gcc main.c -ladd -o add
這種如果在當前目錄下能找到靜態庫,否則要設定路徑,一般用 -L 選項後面跟路徑
    gcc main.c -L. -ladd -o add 

2. 如果不用 -L 選項去設定路徑,可以設定 export LIBARY_PATH=xxx 也是可以找到

3. 如果同時有相同名字的靜態和動態庫,如果想使用靜態庫 要加上-static:可強制編譯時使用靜態庫,
   如果不使用這個引數,而靜態庫與動態庫同名的話,會優先使用動態庫

l  -L:表示要連線的庫所在目錄

l  -l:指定連結時需要的動態庫,編譯器查詢動態連線庫時有隱含的命名規則,即在給出的名字前面加上lib,後面加上.a或.so來確定庫的名稱。

動態庫

通過上面的介紹發現靜態庫,容易使用和理解,也達到了程式碼複用的目的,那為什麼還需要動態庫呢?

為什麼還需要動態庫?

為什麼需要動態庫,其實也是靜態庫的特點導致。

clip_image021[4]

l  空間浪費是靜態庫的一個問題。

l  另一個問題是靜態庫對程式的更新、部署和釋出頁會帶來麻煩。如果靜態庫liba.lib更新了,所以使用它的應用程式都需要重新編譯、釋出給使用者(對於玩家來說,可能是一個很小的改動,卻導致整個程式重新下載全量更新)。

動態庫在程式編譯時並不會被連線到目的碼中,而是在程式執行是才被載入。不同的應用程式如果呼叫相同的庫,那麼在記憶體裡只需要有一份該共享庫的例項,規避了空間浪費問題。動態庫在程式執行是才被載入,也解決了靜態庫對程式的更新、部署和釋出頁會帶來麻煩。使用者只需要更新動態庫即可,增量更新

clip_image023[4]

動態庫特點總結:

l  動態庫把對一些庫函式的連結載入推遲到程式執行的時期。

l  可以實現程序之間的資源共享。(因此動態庫也稱為共享庫)

l  將一些程序升級變得簡單。

l  甚至可以真正做到連結載入完全由程式設計師在程式程式碼中控制(顯示呼叫)。

Window與Linux執行檔案格式不同,在建立動態庫的時候有一些差異。

l  在Windows系統下的執行檔案格式是PE格式,動態庫需要一個DllMain函式做出初始化的入口,通常在匯出函式的宣告時需要有_declspec(dllexport)關鍵字

l  Linux下gcc編譯的執行檔案預設是ELF格式,不需要初始化入口,亦不需要函式做特別的宣告,編寫比較方便。

與建立靜態庫不同的是,不需要打包工具(ar、lib.exe),直接使用編譯器即可建立動態庫。

Linux下建立與使用動態庫

linux動態庫的命名規則

動態連結庫的名字形式為 libxxx.so,字首是lib,字尾名為“.so”。

l  針對於實際庫檔案,每個共享庫都有個特殊的名字“soname”。在程式啟動後,程式通過這個名字來告訴動態載入器該載入哪個共享庫。

l  在檔案系統中,soname僅是一個連結到實際動態庫的連結。對於動態庫而言,每個庫實際上都有另一個名字給編譯器來用。它是一個指向實際庫映象檔案的連結檔案(lib+soname+.so)。

建立動態庫(.so)

l  首先,生成目標檔案,此時要加編譯器選項-fpic

gcc -fPIC -c add.c

-fPIC 建立與地址無關的編譯程式(pic,position independent code),是為了能夠在多個應用程式間共享。

l  然後,生成動態庫,此時要加連結器選項-shared

gcc -shared -o libadd.so add.o

-shared指定生成動態連結庫。

     上面可以合成一句:

gcc -shared -fPIC add.c -o libadd.so

使用動態庫

編寫使用動態庫的測試程式碼:

程式碼和上面的 靜態庫一樣,這裡不再重複。來看下編譯情況:

gcc main.c  -ladd -o add 
這裡再當前目錄下能找到,如果不在當前目錄下,編譯會出錯
/usr/bin/ld: cannot find -ladd
collect2: error: ld returned 1 exit status

所以這裡也有幾種解決方法:
1. 和靜態庫一樣設定 LIBRARY_PATH 路徑。
2. 通過 -L 選項去設定路徑也和靜態一樣。
gcc main.c  -Lxxx -ladd -o add


但是此時執行會出現錯誤
./add: error while loading shared libraries: libadd.so: cannot open shared object file: No such file or directory

我們可以通過 ldd add 檢視:

ldd add 

        linux-vdso.so.1 =>  (0x00007ffec790d000)
        libadd.so => not found
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007eff52f32000)
        /lib64/ld-linux-x86-64.so.2 (0x000055a96247c000)

可以看到 libadd.so 雖然編譯的時候找到了,但是執行裝載時候找不到 not found

這是時候我們可以通過設定 LD_LIBRARY_PATH 如 export LD_LIBRARY_PATH =. 
這個只對動態庫有用,靜態不需要設定。
這時候再來看下

ldd add
        linux-vdso.so.1 =>  (0x00007ffd6cf2f000)
        libadd.so => ./libadd.so (0x00007f58ca33b000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f58c9f5f000)
        /lib64/ld-linux-x86-64.so.2 (0x00005561b2dbf000)

可以看到找到了。

當然如果我們用的時候嫌麻煩,可以直接放到系統PATH 路徑下面,就不需要設定上面的各種路徑。

那麼,在執行的時候是如何定位共享庫檔案的呢?

1)   當系統載入可執行程式碼時候,能夠知道其所依賴的庫的名字,但是還需要知道絕對路徑。此時就需要系統動態載入器(dynamic linker/loader)。

2)   對於elf格式的可執行程式,是由ld-linux.so*來完成的,它先後搜尋elf檔案的 DT_RPATH段—環境變數LD_LIBRARY_PATH—/etc/ld.so.cache檔案列表—/lib/,/usr/lib 目錄找到庫檔案後將其載入記憶體。

如何讓系統能夠找到它:

l  如果安裝在/lib或者/usr/lib下,那麼ld預設能夠找到,無需其他操作。

l  如果安裝在其他目錄,需要將其新增到/etc/ld.so.cache檔案中,步驟如下:

n  編輯/etc/ld.so.conf檔案,加入庫檔案所在目錄的路徑

n  執行ldconfig ,該命令會重建/etc/ld.so.cache檔案

動態庫的顯式呼叫

上面介紹的動態庫使用方法和靜態庫類似屬於隱式呼叫,編譯的時候指定相應的庫和查詢路徑。其實,動態庫還可以顯式呼叫。【在C語言中】,顯示呼叫一個動態庫輕而易舉!

在Linux下顯式呼叫動態庫

#include <dlfcn.h>,提供了下面幾個介面:

l  void * dlopen( const char * pathname, int mode ):函式以指定模式開啟指定的動態連線庫檔案,並返回一個控制代碼給呼叫程序。

l  void* dlsym(void* handle,const char* symbol):dlsym根據動態連結庫操作控制代碼(pHandle)與符號(symbol),返回符號對應的地址。使用這個函式不但可以獲取函式地址,也可以獲取變數地址。

l  int dlclose (void *handle):dlclose用於關閉指定控制代碼的動態連結庫,只有當此動態連結庫的使用計數為0時,才會真正被系統解除安裝。

l  const char *dlerror(void):當動態連結庫操作函式執行失敗時,dlerror可以返回出錯資訊,返回值為NULL時表示操作函式執行成功。

顯式呼叫須要包括標頭檔案#include <dlfcn.h>。

涉及到以下幾個函式:dlopen()、dlsym()、dlerror()、dlclose()。

dlopen()函式以指定模式開啟指定的動態連結庫檔案,並返回一個控制代碼給dlsym()的呼叫程序。

使用dlclose()來解除安裝開啟的庫。當動態連結庫操作函式執行失敗時,dlerror能夠返回出錯資訊,返回值為
NULL時表示操作函式執行成功。

編譯時候要增加 -ldl (指定dl庫)
詳細的函式原型例如以下:

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

char *dlerror(void);

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

int dlclose(void *handle);

dlopen以指定模式開啟指定的動態連線庫檔案。並返回一個控制代碼給呼叫程序,dlerror返回出現的錯誤,dlsym
通過控制代碼和連線符名稱獲取函式名或者變數名,dlclose來解除安裝開啟的庫。
如果已經生成libcaculate.so庫,裡面定義了add(),sub(),mul(),div()等函式。這裡給出呼叫演示樣例:

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

//動態連結庫路徑
#define LIB_CACULATE_PATH "./libcaculate.so"

//函式指標
typedef int (*CAC_FUNC)(int, int);

int main()
{
    void *handle;
    char *error;
    CAC_FUNC cac_func = NULL;

    //開啟動態連結庫
    handle = dlopen(LIB_CACULATE_PATH, RTLD_LAZY);
    if (!handle) {
    fprintf(stderr, "%s\n", dlerror());
    exit(EXIT_FAILURE);
    }

    //清除之前存在的錯誤
    dlerror();

    //獲取一個函式
    *(void **) (&cac_func) = dlsym(handle, "add");
    if ((error = dlerror()) != NULL)  {
    fprintf(stderr, "%s\n", error);
    exit(EXIT_FAILURE);
    }
    printf("add: %d\n", (*cac_func)(2,7));

    cac_func = (CAC_FUNC)dlsym(handle, "sub");
    printf("sub: %d\n", cac_func(9,2));

    cac_func = (CAC_FUNC)dlsym(handle, "mul");
    printf("mul: %d\n", cac_func(3,2));

    cac_func = (CAC_FUNC)dlsym(handle, "div");
    printf("div: %d\n", cac_func(8,2));

    //關閉動態連結庫
    dlclose(handle);
    exit(EXIT_SUCCESS);
}

在Windows下顯式呼叫動態庫

應用程式必須進行函式呼叫以在執行時顯式載入 DLL。為顯式連結到 DLL,應用程式必須:

l  呼叫 LoadLibrary(或相似的函式)以載入 DLL 和獲取模組控制代碼。

l  呼叫 GetProcAddress,以獲取指向應用程式要呼叫的每個匯出函式的函式指標。由於應用程式是通過指標呼叫 DLL 的函式,編譯器不生成外部引用,故無需與匯入庫連結。

l  使用完 DLL 後呼叫 FreeLibrary

顯式呼叫C++動態庫注意點

對C++來說,情況稍微複雜。顯式載入一個C++動態庫的困難一部分是因為C++的name mangling另一部分是因為沒有提供一個合適的API來裝載類,在C++中,您可能要用到庫中的一個類,而這需要建立該類的一個例項,這不容易做到。

name mangling可以通過extern "C"解決。C++有個特定的關鍵字用來宣告採用C binding的函式:extern "C" 。用 extern "C"宣告的函式將使用函式名作符號名,就像C函式一樣。因此,只有非成員函式才能被宣告為extern "C",並且不能被過載。儘管限制多多,extern "C"函式還是非常有用,因為它們可以象C函式一樣被dlopen動態載入。冠以extern "C"限定符後,並不意味著函式中無法使用C++程式碼了,相反,它仍然是一個完全的C++函式,可以使用任何C++特性和各種型別的引數。

另外如何從C++動態庫中獲取類,附上幾篇相關文章,但我並不建議這麼做:

“顯式”使用C++動態庫中的Class是非常繁瑣和危險的事情,因此能用“隱式”就不要用“顯式”,能靜態就不要用動態。

附件:Linux下庫相關命令

g++(gcc)編譯選項

l  -shared :指定生成動態連結庫。

l  -static :指定生成靜態連結庫。

l  -fPIC :表示編譯為位置獨立的程式碼,用於編譯共享庫。目標檔案需要建立成位置無關碼, 念上就是在可執行程式裝載它們的時候,它們可以放在可執行程式的記憶體裡的任何地方。

l  -L. :表示要連線的庫所在的目錄。

l  -l:指定連結時需要的動態庫。編譯器查詢動態連線庫時有隱含的命名規則,即在給出的名字前面加上lib,後面加上.a/.so來確定庫的名稱。

l  -Wall :生成所有警告資訊。

l  -ggdb :此選項將盡可能的生成gdb 的可以使用的除錯資訊。

l  -g :編譯器在編譯的時候產生除錯資訊。

l  -c :只啟用預處理、編譯和彙編,也就是把程式做成目標檔案(.o檔案) 。

l  -Wl,options :把引數(options)傳遞給連結器ld 。如果options 中間有逗號,就將options分成多個選項,然後傳遞給連結程式。

nm命令

有時候可能需要檢視一個庫中到底有哪些函式,nm命令可以打印出庫中的涉及到的所有符號。庫既可以是靜態的也可以是動態的。nm列出的符號有很多,常見的有三種:

l  一種是在庫中被呼叫,但並沒有在庫中定義(表明需要其他庫支援),用U表示;

l  一種是庫中定義的函式,用T表示,這是最常見的;

l  一種是所謂的弱態”符號,它們雖然在庫中被定義,但是可能被其他庫中的同名符號覆蓋,用W表示。

$nm libhello.h

ldd命令

ldd命令可以檢視一個可執行程式依賴的共享庫。

總結

二者的不同點在於程式碼被載入的時刻不同

l  靜態庫在程式編譯時會被連線到目的碼中,程式執行時將不再需要該靜態庫,因此體積較大

l  動態庫在程式編譯時並不會被連線到目的碼中,而是在程式執行是才被載入,因此在程式執行時還需要動態庫存在,因此程式碼體積較小

動態庫的好處是,不同的應用程式如果呼叫相同的庫,那麼在記憶體裡只需要有一份該共享庫的例項。帶來好處的同時,也會有問題!如經典的DLL Hell問題,關於如何規避動態庫管理問題,可以自行查詢相關資料。