GCC定位庫

2020-07-16 10:05:22
之前我們學習的是製作庫檔案,不管是靜態庫或者是共用庫,根據具體的編譯流程就可以製作,但是無論是什麼庫,使用的時候都需要連結。為了使庫檔案能夠正確的連結,連結的時候需要能夠定位庫。對於靜態庫連結程式來說,所有的目標檔案都集中在一起儲存成一個獨立的可執行檔案,這個可執行檔案完全可以移植到相互相容的系統中並能正確執行,甚至目標檔案都不存在也沒有關係。但是共用庫連結的時候必須存在,每次執行程式的時候需要使用。

連結時定位庫

無論在什麼時候,只要連結程式就需要查詢庫,編譯器會查詢指定的目錄列表。這些目錄是否被包含進查詢路徑,依賴於使用哪種競爭模式 ld(ld是連結器),編譯的時候如何設定 ld,以及命令列指定的目錄。大多數的系統庫放置的位置是目錄/lib/usr/lib中,這是系統設定好的,因此會自動查詢這兩個目錄。通過使用一個或者多個-L選項,指定其他的查詢目錄。命令在執行的時候就會直接到我們指定的目錄下尋找連結檔案。

範例:/home/lib目錄下存放著我們的庫檔案libtest.a(靜態庫),編譯的時候命令如下:

 gcc -L/home/lib mian.o -o main -ltest

執行命令結束,我們就可以得到最終的目標檔案。

執行時載入庫

只要程式被連結想要使用共用庫,就必須在執行的時候能夠找到共用庫的位置。因為是通過名字確定庫,而不是通過目錄定位庫,因此可以在連結程式的時候使用一個庫,而在執行的時候使用另一個庫。如過我們改變庫檔案的版本號,而沒有改變程式的版本,在執行的時候會出現問題,所以很多庫會把版本號作為名字的一部分,例如:libm.so或libutil-2.2.4.so。

無論何時載入程式並打算執行的時候,共用庫都應該位於以下的位置:
  1. 環境變數LD_LIBRARY_PATH列出的所有用分號分割的目錄;
  2. 檔案/etc/ld.so.cache中找到的庫的列表,由工具ldconfig維護
  3. 目錄/usr
  4. 目錄/usr/lib

如果想要知道載入了那個庫,以及確切的了解應用程式使用的那個庫,可以使用ldd。命令格式如下:

ldd option filename

option可以表示的選項如下:
-d:進程資料重定址
-r:進程資料和函數重定址
-u:列印出未使用的直接依賴關係
-v:列印出所有的資訊
範例:我們當前目錄下存在一個可執行檔案main。在命令列輸入命令:

ldd -v main

顯示的資訊如下:

linux-vdso.so.1 (0x00007ffe6d730000)
       libadd.so => /usr/lib/libadd.so (0x00007fca6e1c5000)
       libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fca6ddd4000)
       /lib64/ld-linux-x86-64.so.2 (0x00007fca6e5c9000)
 
Version information:
./main:
       libc.so.6 (GLIBC_2.2.5) => /lib/x86_64-linux-gnu/libc.so.6
/lib/x86_64-linux-gnu/libc.so.6:
       ld-linux-x86-64.so.2 (GLIBC_2.3) => /lib64/ld-linux-x86-64.so.2
       ld-linux-x86-64.so.2 (GLIBC_PRIVATE) => /lib64/ld-linux-x86-64.so.2

上面顯示的資訊,檔案連結的庫檔案,以及庫檔案的版本號和庫的位置。

動態載入動態庫函數

共用庫中的函數可被載入並執行而不需要連結到程式裡。載入動態連結庫,首先要為共用庫分配實體記憶體,然後在進程對應的頁表項中建立虛擬頁和物理頁面之間的對映。動態載入函數需要使用四個基本函數分別是:dlopen()、dlsym()、dlerror()、dlclose(),使用這些函數需要包含標頭檔案dlfcn.h。

分別介紹一下各個函數的作用和使用方式(我們也可以通過man手冊檢視)。

1. 開啟動態連結庫檔案函數為 dlopen,函數原型如下:

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

dlopen用於開啟指定名字(filename)的動態連結庫(最好檔案絕對路徑),並返回操作控制代碼。

flag的選項可以是以下兩種:

RTLD_NOW:立即決定,返回前解除所有未決定的符號。
RTLD_LAZY:暫緩決定,等有需要時再解出符號

返回值:開啟錯誤返回NULL,成功返回庫參照。編譯時要加入“-ldl”選項(指定dl庫),範例:

gcc test.c -o test -ldl

2. 取函數執行地址的的函數為 dlsym,函數原型如下:

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

dlsym 根據動態連結庫操作控制代碼 (handle) 與符號 (symbol) ,返回符號對應的函數的執行程式碼地址。
 
3. 關閉動態連結庫函數為dlclose,函數原型如下:

int dlclose (void *handle);

dlclose 用於關閉指定控制代碼的動態連結庫,只有當此動態連結庫的使用計數為 0 時,才會真正被系統解除安裝。返回值為 0 表示成功,非零表示失敗。
 
4. 動態庫錯誤函數為 dlerror,函數原型如下:

const char *dlerror(void);

當動態連結庫操作函數執行失敗時,dlerror 可以返回出錯資訊,返回值為 NULL 時表示操作函數執行成功。
 
範例:只要兩個函數向標準輸出中顯示字元,說明他們被呼叫了。
/*sayhello.c*/
#include <stdio.h>
 
void sayhello()
{
    printf("hellon");
}
 
/*saysomething.c*/
#include <stdio.h>
 
void saysomething(char *str)
{
    printf("%sn",str);
}
把上面這兩個函數製作成靜態庫檔案。使用命令:

gcc -shared -fpic sayhello.c saysomrthing.c -o libsayfn.so

 呼叫函數如下:
/*main.c*/
#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>
 
int main(int argc,char *argv[])
{
    void *handler;
    char *error;
    void (*sayhello)(void);
    void (*saysomething)(char *);
    handler = dlopen("libsayfn.so",RTLD_LAZY);
    if(error = dlerror())
    {
        printf("%sn",error);
        exit(1);
    }
    sayhello = dlsym(handler,"sayhello");
    if(error = dlerror())
    {
        printf("%sn",error);
        exit(1);
    }
    saysomething = dlsym(handler,"saysomething");
    if(error = dlerror())
    {
        printf("%sn",error);
        exit(1);
    }
    sayhello();
    saysomething("this is somethng");
 
    dlclose(handler);
    return 0;
}
最後編譯main.c檔案,使用下面的命令:

gcc main.c -ldl -o main

編譯的時候需要注意,需要新增連結選項-ldl,還有就是在執行程式的時候需要把動態庫檔案放到指定的位置,否則就會出現動態庫找不到的錯誤資訊。
執行程式就可以看到如下的資訊:

hello
This is something

這就說明我們這裡例子沒有問題。