1. 程式人生 > 其它 >linux下動態連結庫(.so)的顯式呼叫和隱式呼叫

linux下動態連結庫(.so)的顯式呼叫和隱式呼叫

linux下動態連結庫(.so)的顯式呼叫和隱式呼叫

2021-12-21

進入主題前,先看看兩點預備知識。

一、顯式呼叫和隱式呼叫的區別

        我們知道,動態庫相比靜態庫的區別是:靜態庫是編譯時就載入到可執行檔案中的,而動態庫是在程式執行時完成載入的,所以使用動態庫的程式的體積要比使用靜態庫程式的體積小,並且使用動態庫的程式在執行時必須依賴所使用的動態庫檔案(.so檔案),而使用靜態庫的程式一旦編譯好,就不再需要依賴的靜態庫檔案了(.a檔案)。

        動態庫的呼叫又分為顯示和隱式兩種方式,區別如下:

        1、 隱式呼叫需要呼叫者寫的程式碼量少,呼叫起來和使用當前專案下的函式一樣直接

;而顯式呼叫則要求程式設計師在呼叫時,指明要載入的動態庫的名稱和要呼叫的函式名稱。

        2、隱式呼叫由系統載入完成,對程式設計師透明;顯式呼叫由程式設計師在需要使用時自己載入,不再使用時,自己負責解除安裝。

        3、由於顯式呼叫由程式設計師負責載入和解除安裝,好比動態申請記憶體空間,需要時就申請,不用時立即釋放,因此顯式呼叫對記憶體的使用更加合理, 大型專案中應使用顯示呼叫。

        4、當動態連結庫中只提供函式介面,而該函式沒有封裝到類裡面時,如果使用顯式呼叫的方式,呼叫方甚至不許要包含動態連結庫的標頭檔案(需要呼叫的函式名是通過dlsym函式的引數指明的),而使用隱式呼叫時,則呼叫方必須要加上動態庫中的標頭檔案

,g++編譯時還需要要用引數-I指明包含的標頭檔案的位置。需要注意的是,當動態連結庫中的介面函式是作為成員函式封裝在類裡面時,即使使用顯式呼叫的方式,呼叫方也必須包含動態庫中的相應標頭檔案(詳見五、顯示呼叫動態連結中的類成員函式)。

        5、顯式呼叫更加靈活,可以模擬多型效果(具體見後文)。

        6、顯式呼叫的方式,必須加入標頭檔案dlfcn.h,makefile中的連結命令中要加入引數-ldl,需要用dlopen載入庫,dlsym取函式符號(函式名應用新定義的),dlclose解除安裝庫。

        7、隱式呼叫的方式,makefile中的連結命令中要加入引數-l加庫名,直接用庫裡的函式名就可以

二、extern "C"的作用

        C++程式(或庫、目標檔案)中,所有非靜態(non-static)函式在二進位制檔案中都是以“符號(symbol)”形式出現的。這些符號都是唯一的字串,從而把各個函式在程式、庫、目標檔案中區分開來。在C中,符號名正是函式名,兩者完全一樣。而C++允許過載(不同的函式有相同的名字但不同的引數,甚至const過載),並且有很多C所沒有的特性──比如類、成員函式、異常說明──幾乎不可能直接用函式名作符號名。為了解決這個問題,C++採用了所謂的name mangling。它把函式名和一些資訊(如引數數量和大小)雜糅在一起,改造成奇形怪狀,只有編譯器才懂的符號名。例如,被mangle後的foo可能看起來像foo@4%6^,或者,符號名裡頭甚至不包括“foo”。

        其中一個問題是,C++標準並沒有定義名字必須如何被mangle,所以每個編譯器都按自己的方式來進行name mangling。有些編譯器甚至在不同版本間更換mangling演算法(尤其是g++ 2.x和3.x)。前文說過,在顯示呼叫動態庫中的函式時,需要指明呼叫的函式名,即使您搞清楚了您的編譯器到底怎麼進行mangling的,從而知道呼叫的函式名被C++編譯器轉換為了什麼形式,,但可能僅僅限於您手頭的這個編譯器而已,而無法在下一版編譯器下工作。

extern "C"即可以解決這個問題。用 extern "C"宣告的函式將使用函式名作符號名,就像C函式一樣。因此,只有非成員函式才能被宣告為extern "C",並且不能被過載。儘管限制多多,extern "C"函式還是非常有用,因為它們可以象C函式一樣被dlopen動態載入。冠以extern "C"限定符後,並不意味著函式中無法使用C++程式碼了,相反,它仍然是一個完全的C++函式,可以使用任何C++特性和各種型別的引數。所以extern "C" 只是告訴編譯器編和連結的時候都用c的方式的函式名字,函式裡的內容可以為c的程式碼也可以為c++的。


       有了上面兩個預備知識後,下面以實際例子來演示兩種不同的動態庫呼叫方式。例子的結構組織為如下:

    so1.h和so1.cc是第一個動態庫中的檔案,會編譯連結為libso1.so;so2.h和so2.cc是第一個動態庫中的檔案,會編譯連結為libso2.so;test.cc是呼叫兩個動態庫的程式。

三、顯式呼叫

so1.h:

extern "C" void fcn();
so1.cc:
#include <iostream>
#include "so1.h"

void fcn() {
std::cout << "this is fcn in so1" << std::endl;
}
so1的makefile:

libso1.so:so1.o
g++ so1.o -shared -o libso1.so
so1.o:so1.cc so1.h
g++ -c so1.cc -fPIC -o so1.o

.PHONY:clean
clean:
rm so1.o libso1.so
make之後,將生成的libso1.so拷貝到test.cc所在目錄下。


so2.h:

extern "C" void fcn();
so2.cc:
#include <iostream>
#include "so2.h"

void fcn() {
std::cout << "this is fcn in so2" << std::endl;
}
so2的makefile:
libso2.so:so2.o
g++ so2.o -shared -o libso2.so
so2.o:so2.cc so2.h
g++ -c so2.cc -fPIC -o so2.o

.PHONY:clean
clean:
rm so2.o libso2.so
make之後,將生成的libso2.so拷貝到test.cc所在目錄下。


test.cc:

#include <iostream>
#include <cstdlib>
#include <dlfcn.h>

using namespace std;

int main(int argc, char **argv) {
if(argc != 2) {
cout << "argument error!" << endl;
exit(1);
}

//pointer to function
typedef void (*pf_t)();

char *err = NULL;
//open the lib
void *handle = dlopen(argv[1], RTLD_NOW);

if(!handle) {
cout << "load " << argv[1] << "failed! " << dlerror() << endl;
exit(1);
}

//clear error info
dlerror();

pf_t pf = (pf_t)dlsym(handle, "fcn");
err = dlerror();
if(err) {
cout << "can't find symbol fcn! " << err << endl;
exit(1);
}

//call function by pointer
pf();

dlclose(handle);

return 0;
}
test的makefile:
test:test.o
g++ test.o -lso1 -L. -lso2 -L. -ldl -Wl,-rpath=. -o test
test.o:test.cc
g++ -c test.cc -o test.o
make之後,終端執行結果如下:

可以看到這裡,通過輸入不同的引數,呼叫了不同的共享庫中的fcn函式,是一種多型的表現,許多軟體的不同外掛就是這樣實現的。

需要注意的是,要使用顯式呼叫的方式,必須加入標頭檔案dlfcn.h,makefile中的連結命令中要加入引數-ldl,否則報錯。

dlfcn.h中提供的API說明如下:

1)        dlopen

函式原型:void *dlopen(const char *libname,int flag);

功能描述:dlopen必須在dlerror,dlsym和dlclose之前呼叫,表示要將庫裝載到記憶體,準備使用。如果要裝載的庫依賴於其它庫,必須首先裝載依賴庫。如果dlopen操作失敗,返回NULL值;如果庫已經被裝載過,則dlopen會返回同樣的控制代碼。

引數中的libname一般是庫的全路徑,這樣dlopen會直接裝載該檔案;如果只是指定了庫名稱,在dlopen會按照下面的機制去搜尋:

a.根據環境變數LD_LIBRARY_PATH查詢

b.根據/etc/ld.so.cache查詢

c.查詢依次在/lib和/usr/lib目錄查詢。

flag引數表示處理未定義函式的方式,可以使用RTLD_LAZY或RTLD_NOW。RTLD_LAZY表示暫時不去處理未定義函式,先把庫裝載到記憶體,等用到沒定義的函式再說;RTLD_NOW表示馬上檢查是否存在未定義的函式,若存在,則dlopen以失敗告終。

2)        dlerror

函式原型:char *dlerror(void);

功能描述:dlerror可以獲得最近一次dlopen,dlsym或dlclose操作的錯誤資訊,返回NULL表示無錯誤。dlerror在返回錯誤資訊的同時,也會清除錯誤資訊。

3)        dlsym

函式原型:void *dlsym(void *handle,const char *symbol);

功能描述:在dlopen之後,庫被裝載到記憶體。dlsym可以獲得指定函式(symbol)在記憶體中的位置(指標)。如果找不到指定函式,則dlsym會返回NULL值。但判斷函式是否存在最好的方法是使用dlerror函式,

4)        dlclose

函式原型:int dlclose(void *);

功能描述:將已經裝載的庫控制代碼減一,如果控制代碼減至零,則該庫會被解除安裝。如果存在解構函式,則在dlclose之後,解構函式會被呼叫。

四、隱式呼叫

隱式呼叫不需要包含標頭檔案dlfcn.h,只需要包含動態連結庫中的標頭檔案,使用動態庫中的函式也不需要像顯示呼叫那麼複雜。

五、顯式呼叫動態連結中的類成員函式

顯示呼叫動態連結庫的類成員函式,有單獨的寫法,但比較少用。推薦的寫法是為每個要被外部呼叫的類成員函式設計一個普通的藉口函式,在介面函式內部使用類的成員函式。當然這就需要將類設計為單例模式,因為不可能在每個介面函式中都構造一個類的物件。


---------------------
作者:hujingLiu
來源:CSDN
原文:https://blog.csdn.net/lc_910927/article/details/42393121
版權宣告:本文為博主原創文章,轉載請附上博文連結!