在定義全域性變數和函式是,如果我們使用 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:
- 使用
__attribute__((weak))
修飾的全域性變數和函式是 weak symbol; - C++ 庫中的
operator new
和operator delete
是 weak symbol; 3.如果定義了行內函數,但是該行內函數生成了一個獨立的函式體,那麼該符號為 weak symbol; - 在 C++ 中,在類定義裡直接定義的成員函式都自帶 inline 效果,因此也是 weak symbol;
- 函式模版例項化後的程式碼是 weak symbol。
Weak symbol 可以在多個檔案中被定義,但是連結時只有一個定義會被保留。保留的規則是:
- 如果有多個同名的 weak symbol,那麼符號長度最長的會被保留。
- 對於變數,就是大小最大的定義會被保留。
- 對於函式,就是函式體最長的定義會被保留。
- 如果有多個同名的 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_PUBLIC
和 DLL_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 的最佳實踐。