寫 Linux 動態庫的最佳實踐

在定義全局變量和函數是,若是咱們使用 static 關鍵字修飾他們,就只可以在同一個文件內引用他們;若是咱們不使用 static 關鍵字,就能夠在其餘文件中引用他們。html

然而,當實現動態庫時,問題就變得有些複雜。api

動態庫的接口函數能夠被動態庫內的其餘文件引用,也能夠被其餘動態庫引用。而動態庫的內部函數只能被同一個動態庫內的其餘文件引用,不能被其餘動態庫引用。oracle

對於「如何讓函數能夠被動態庫內的其餘文件引用,而不能被其餘動態庫引用」的需求,static 關鍵字是無能爲力的。函數

這時,咱們就須要修改符號的可見性(visibility)this

符號

對於 ELF 文件來講,程序中出現的全部變量和函數都是符號(symbol)spa

變量所在的內存單元和函數的函數體被稱做符號的定義(definition)命令行

當咱們使用 static 關鍵字修飾變量或者函數時,咱們是在修改符號的 binding(綁定關係)。在 C 語言中,咱們一般稱之爲做用域。code

符號的 Binding

符號一共有三種 binding,分別是:cdn

binding 含義
LOCAL 本地符號,只能在文件內被引用
GLOBAL 強全局符號,能夠被其餘文件引用,並且只能在一個文件中被定義
WEAK 弱全局符號,能夠被其餘文件引用,可是能夠在多個文件中被定義

Local Symbol

使用 static 關鍵字修飾的全局變量和函數是 local symbol。htm

這類符號只能在同一個文件中被引用,而不能被其餘文件引用。多個文件能夠定義同名的 local 符號,可是這些符號不會互相影響。

一個動態庫中的 local symbol 和另外一個動態庫的同名 local symbol 之間不會互相影響。

Global Symbol

不使用 static 關鍵字修飾的全局變量和函數是 global symbol 。

這類符號能在其餘文件中被引用,也能夠其餘動態庫引用。也就是說,這樣的符號在整個進程空間內有惟一的定義。

在連接時,若是多個文件中定義了重名的 global 符號,就會引起連接錯誤。

在動態加載時,若是多個動態庫定義了重名的 global 符號,那麼就只會保留其中的一個定義。這就意味着,在訪問同一個動態庫內定義的 global 符號時,有可能訪問到的是其餘動態庫中的定義。

在 ELF 文件層面,在動態庫中訪問 global symbol 都須要藉助 PLT 和 GOT,而不能直接訪問,所以速度也比訪問 local symbol 慢。

Weak 符號

在 C 和 C++ 程序中,有如下方法能夠定義 weak symbol:

  1. 使用 __attribute__((weak)) 修飾的全局變量和函數是 weak symbol;
  2. C++ 庫中的 operator newoperator delete 是 weak symbol; 3.若是定義了內聯函數,可是該內聯函數生成了一個獨立的函數體,那麼該符號爲 weak symbol;
  3. 在 C++ 中,在類定義裏直接定義的成員函數都自帶 inline 效果,所以也是 weak symbol;
  4. 函數模版實例化後的代碼是 weak symbol。

Weak symbol 能夠在多個文件中被定義,可是連接時只有一個定義會被保留。保留的規則是:

  1. 若是有多個同名的 weak symbol,那麼符號長度最長的會被保留。
    1. 對於變量,就是大小最大的定義會被保留。
    2. 對於函數,就是函數體最長的定義會被保留。
  2. 若是有多個同名的 weak symbol 和一個 global symbol,那麼那個 global symbol 的定義會被保留。

所以,若是用戶定義了 operator new 函數,那麼連接器就會使用用戶定義的實現,而不是標準庫中的實現。

符號的 Visiblity

爲了解決全局符號可能在動態庫之間互相干擾的問題,ELF 引入了符號的可見性(visibility)。

在連接成動態庫或者可執行文件時,連接器根據符號的 visibility 修改它的 binding。

Visibility 一共有 7 種,可是經常使用的只有 default 和 hidden 兩種。它們的修飾符分別是:

  • __attribute__((visibility ("default")))
  • __attribute__((visibility ("hidden")))

默認的 visibility 是 default,可是能夠在編譯時傳入命令行參數 -fvisibility=hidden 將默認 visibility 設置爲 hidden。

Default Visibility

在連接時,符號的 binding 保持不變。

Visibility 爲 default 的 global 符號可能被其餘動態庫的同名符號覆蓋,致使在運行時訪問的是其餘動態庫中的定義,而非該動態庫內的定義。

一般,須要導出的符號的 visibility 爲 default。

Hidden Visibility

這類符號在連接成動態庫或者可執行文件後,binding 會從 global 變成 local,同時 visibility 變成 default。

所以,這類符號只能在動態庫內部被訪問,而不能被其餘動態庫訪問。

對於動態庫或者可執行程序來講,全部不須要導出的符號的 visibility 都應該是 hidden。

最佳實踐

在實現 C 和 C++ 的動態庫時,使用 -fvisibility=hidden 來編譯動態庫

在定義 API 時,建議使用 DLL_PUBLICDLL_LOCAL 宏來控制符號的可見性,它在 Windows、Cygwin、Linux 和 macOS 上均可以正常工做:

#if defined _WIN32 || defined __CYGWIN__
  #ifdef BUILDING_DLL
    #ifdef __GNUC__
      #define DLL_PUBLIC __attribute__ ((dllexport))
    #else
      // Note: actually gcc seems to also supports this syntax.
      #define DLL_PUBLIC __declspec(dllexport)
    #endif
  #else
    #ifdef __GNUC__
      #define DLL_PUBLIC __attribute__ ((dllimport))
    #else
      // Note: actually gcc seems to also supports this syntax.
      #define DLL_PUBLIC __declspec(dllimport)
    #endif
    #define DLL_LOCAL
  #endif
#else
  #if __GNUC__ >= 4
    #define DLL_PUBLIC __attribute__ ((visibility ("default")))
    #define DLL_LOCAL __attribute__ ((visibility ("hidden")))
  #else
    #define DLL_PUBLIC
    #define DLL_LOCAL
  #endif
#endif
複製代碼

在 C 中,可使用這個宏導出函數和變量:

// 使用 DLL_PUBLIC 修飾須要導出的符號
DLL_PUBLIC int my_exported_api_func();
DLL_PUBLIC int my_exported_api_val;

// 不使用 DLL_PUBLIC 修飾動態庫內部的符號,
// 由於默承認見性被修改成 hidden
int my_internal_global_func();
複製代碼

在 C++ 中,可使用這個宏來導出一個類:

// 使用 DLL_PUBLIC 修飾須要導出的類
class DLL_PUBLIC MyExportedClass {
 public:
  // 類裏面的全部方法默認都是 DLL_PUBLIC 的
  MyExportedClass();
  ~MyExportedClass();
  int my_exported_method();
  
 private:
  int c;

  // 使用 DLL_LOCAL 修飾動態庫的內部符號
  DLL_LOCAL int my_internal_method();
};
複製代碼

參考閱讀

Symbol Table Section ELF 文件中符號表的定義,詳細描述了 binding 與 visibility。

Visibility GCC wiki 中關於 visibility 的最佳實踐。

相關文章
相關標籤/搜索