Ads 468x60px

##EasyReadMore##

30 8月, 2017

[C/C++] Linux 的動態連結與載入

動態函式庫的優點是可任意抽換,不需刻意更新舊程式,系統隨時保持最新狀態。
一但有了『動態連結技術』,就能很容易的實作出『動態載入技術』。所位的動態載入技術,是在程式中再決定要載入哪些函數的方法。舉例而言,我們可以讓使用者在程式中輸入某個參數,然後立刻用『動態載入技術』載入該函式庫執行。這會使得程式具有較大的彈性,因為,我們可以在必要的時候呼叫動態載入器,讓使用者決定要載入哪些函式庫。
dlopen
基本定義
功能:打開一個動態鏈接庫
包含頭文件:
#include <dlfcn.h>
函數定義:
void * dlopen( const char * pathname, int mode );
函數描述:
在dlopen的()函數以指定模式打開指定的動態連接庫文件,並返回一個句柄給調用進程。使用dlclose()來卸載打開的庫。
mode:分為這兩種
RTLD_LAZY 暫緩決定,等有需要時再解出符號 
RTLD_NOW 立即決定,返回前解除所有未決定的符號。
RTLD_LOCAL
RTLD_GLOBAL 允許導出符號
RTLD_GROUP
RTLD_WORLD
返回值:
打開錯誤返回NULL
成功,返回庫引用
編譯時候要加入 -ldl (指定dl庫)
例如
gcc test.c -o test -ldl
編輯本段
使用 dlopen
dlopen()是一個強大的庫函數。該函數將打開一個新庫,並把它裝入內存。該函數主要用來加載庫中的符號,這些符號在編譯的時候是不知道的。比如 Apache Web 服務器利用這個函數在運行過程中加載模塊,這為它提供了額外的能力。一個配置文件控制了加載模塊的過程。這種機制使得在系統中添加或者刪除一個模塊時,都不需要重新編譯了。
可以在自己的程序中使用 dlopen()。dlopen() 在 dlfcn.h 中定義,並在 dl 庫中實現。它需要兩個參數:一個文件名和一個標誌。文件名可以是我們學習過的庫中的 soname。標誌指明是否立刻計算庫的依賴性。如果設置為 RTLD_NOW 的話,則立刻計算;如果設置的是 RTLD_LAZY,則在需要的時候才計算。另外,可以指定 RTLD_GLOBAL,它使得那些在以後才加載的庫可以獲得其中的符號。
當庫被裝入後,可以把 dlopen() 返回的句柄作為給 dlsym() 的第一個參數,以獲得符號在庫中的地址。使用這個地址,就可以獲得庫中特定函數的指針,並且調用裝載庫中的相應函數。
--------------------------------------------------------------------------------------------------------------------------
dlsym
dlsym()的函數原型是
void* dlsym(void* handle,const char* symbol)
該函數在<dlfcn.h>文件中。
handle是由dlopen打開動態鏈接庫後返回的指針,symbol就是要求獲取的函數的名稱,函數返回值是void*,指向函數的地址,供調用使用
取動態對像地址:
#include <dlfcn.h>
void *dlsym(void *pHandle, char *symbol);
dlsym根據動態鏈接庫操作句柄(pHandle)與符號(symbol),返回符號對應的地址。
使用這個函數不但可以獲取函數地址,也可以獲取變量地址。比如,假設在so中
定義了一個void mytest()函數,那在使用so時先聲明一個函數指針:
void (*pMytest)(),然後使用dlsym函數將函數指針pMytest指向mytest函數,
pMytest = (void (*)())dlsym(pHandle, "mytest");
--------------------------------------------------------------------------------------------------------------------------
dlclose
dlclose()
包含頭文件:
#include <dlfcn.h>
函數原型為:
int dlclose (void *handle);
函數描述:
dlclose用於關閉指定句柄的動態鏈接庫,只有當此動態鏈接庫的使用計數為0時,才會真正被系統卸載。
--------------------------------------------------------------------------------------------------------------------------
dlerror
dlerror()
包含頭文件:
#include <dlfcn.h>
函數原型:
const char *dlerror(void);
函數描述:
當動態鏈接庫操作函數執行失敗時,dlerror可以返回出錯信息,返回值為NULL時表示操作函數執行成功。
LINUX創建與使用動態鏈接庫並不是一件難事。
編譯函數源程序時選用-shared選項即可創建動態鏈接庫,注意應以.so後綴命名,最好放到公用庫目錄(如/lib,/usr/lib等)下面,並要寫好用戶接口文件,以便其它用戶共享。
使用動態鏈接庫,源程序中要包含dlfcn.h頭文件,寫程序時注意dlopen等函數的正確調用,編譯時要採用-rdynamic選項與-ldl選項 ,以產生可調用動態鏈接庫的執行代碼

C 語言範例:

(注意… 只能寫在 c file , 寫在 cpp file 編譯時會有  error: invalid conversion from ‘void*’ to ‘void (*)()’ 問題)

sum.h:

#ifndef __SUM_H__
#define __SUM_H__
double sum(double a, double b);
#endif

sum.c:

#include "sum.h"
double sum(double a, double b) {
  return a + b;
}

test.c:

#include 
#include 
#include 
int main(int argc, char **argv) {
  void *handle;
  double (*sum)(double, double);
  char *error;

  // 動態開啟共享函式庫
  handle = dlopen ("libsum.so", 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;
}

編譯完成後,要執行時需要指定 LD_LIBRARY_PATH

$ LD_LIBRARY_PATH=. ./test libsum.so
2.6 + 4.2 = 6.8

C++ 範例:

sum.cpp:

#include   
#include "sum.h"  
  
void fcn(int a) {  
    std::cout << "your value is : " << a<<="" }="">

sum.h:

extern "C" void fcn(int a);  

test.cpp:

#include 
#include 
#include 

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_LAZY);
    
    if(!handle) {
        cout << "load " << argv[1] << " failed! " << dlerror() << endl;
        exit(1);
    }

    //clear error info
    dlerror();

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

    //call function by pointer
    pf();

    dlclose(handle);

    return 0;
}

一、顯式調用和隱式調用的區別

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

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

        1、 隱式調用需要調用者寫的代碼量少,調用起來和使用當前項目下的函數一樣直接;而顯式調用則要求程序員在調用時,指明要加載的動態庫的名稱和要調用的函數名稱。

        2、隱式調用由系統加載完成,對程序員透明;顯式調用由程序員在需要使用時自己加載,不再使用時,自己負責卸載。

        3、由於顯式調用由程序員負責加載和卸載,好比動態申請內存空間,需要時就申請,不用時立即釋放,因此顯式調用對內存的使用更加合理, 大型項目中應使用顯示調用。

        4、當動態鏈接庫中只提供函數接口,而該函數沒有封裝到類裡面時,如果使用顯式調用的方式,調用方甚至不許要包含動態鏈接庫的頭文件(需要調用的函數名是通過dlsym函數的參數指明的),而使用隱式調用時,則調用方不可避免要加上動態庫中的頭文件,g++編譯時還需要要用參數-I指明包含的頭文件的位置。需要注意的是,當動態鏈接庫中的接口函數是作為成員函數封裝在類裡面時,即使使用顯式調用的方式,調用方也必須包含動態庫中的相應頭文件(詳見五、顯示調用動態鏈接中的類成員函數)。

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

二、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++的。


If this program were in a file named "foo.c", you would build the program with the following command: gcc -rdynamic -o foo foo.c -ldl

錯誤處理:

使用dlsym時出現invalid conversion from void* to...

void (*FUNC)(int a);  
FUNC = dlsym(handle, "FUNC");  

這個錯誤是出現在C++下面的,因為dlsym返回的是void*,但是C++又不允許隱形轉換通用指針,所以就報這個錯。

解決辦法就是寫一個typedef,然後做一次顯示類型轉換

typedef void (*fptr_FUNC)(int a);  
fptr_FUNC FUNC = NULL;  
FUNC = (fptr_FUNC)dlsym(handle, "FUNC");  

Reference:


Linux 的動態連結與載入 (Dynamic Linking) - 陳鍾誠的網站 - https://goo.gl/NwIdqZ
LINUX下动态链接库的使用-dlopen dlsym dlclose dlerror【zt】 - jernymy的专栏 - CSDN博客 - https://goo.gl/uWKdYr

使用 gcc 自製 C/C++ 靜態、共享與動態載入函式庫教學 - G. T. Wang - https://goo.gl/W4EYYF

Keep Learning Everyday: [轉] 使用dlsym時出現invalid conversion from void* to... - https://goo.gl/gzmrnK

linux下动态链接库(.so)的显式调用和隐式调用 - hujing_Liu - CSDN博客 - https://goo.gl/mkEE98

</std::endl;>

0 意見:

張貼留言

 
Blogger Templates