寫 Linux 動態庫的最佳實踐

番茄吐司君發表於2018-01-12

寫 Linux 動態庫的最佳實踐

在定義全域性變數和函式是,如果我們使用 static 關鍵字修飾他們,就只能夠在同一個檔案內引用他們;如果我們不使用 static 關鍵字,就可以在其他檔案中引用他們。

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

動態庫的介面函式可以被動態庫內的其他檔案引用,也可以被其他動態庫引用。而動態庫的內部函式只能被同一個動態庫內的其他檔案引用,不能被其他動態庫引用。

對於“如何讓函式可以被動態庫內的其他檔案引用,而不能被其他動態庫引用”的需求,static 關鍵字是無能為力的。

這時,我們就需要修改符號的可見性(visibility)

符號

對於 ELF 檔案來說,程式中出現的所有變數和函式都是符號(symbol)

變數所在的記憶體單元和函式的函式體被稱作符號的定義(definition)

當我們使用 static 關鍵字修飾變數或者函式時,我們是在修改符號的 binding(繫結關係)。在 C 語言中,我們通常稱之為作用域。

符號的 Binding

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

binding 含義
LOCAL 本地符號,只能在檔案內被引用
GLOBAL 強全域性符號,可以被其他檔案引用,而且只能在一個檔案中被定義
WEAK 弱全域性符號,可以被其他檔案引用,但是可以在多個檔案中被定義

Local Symbol

使用 static 關鍵字修飾的全域性變數和函式是 local symbol。

這類符號只能在同一個檔案中被引用,而不能被其他檔案引用。多個檔案可以定義同名的 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 的最佳實踐。

相關文章