在 C/C++ 開發中,符號 是連結器處理目標文件、共享庫及可執行文件時的基本單位。它對應著使用者定義的全域變數、函數名稱、命名空間、類別或結構等實體。當我們定義非靜態全域變數或非靜態函數時,編譯器會在目標文件中產生符號,連結器透過這些符號判斷不同模組是否共享相同的資料或程式碼。
雖然變數與函數皆可跨模組共享,但變數在共享庫與可執行檔之間共享的情況較少。通常僅讓函數對其他模組可見,這類函數常被稱為 API 或 導出符號。值得注意的是,符號可見性僅在動態連結階段生效,因為共享庫在程式執行時才被載入記憶體映像中。
不同平台上,編譯器對符號的預設導出行為各異:
導出所有符號雖然方便,但風險極高:
因此,只導出必要的符號 是庫開發的最佳實踐,兼顧安全與效能。
以下以簡單的 C++ 程式碼為例:
// a.C
int myintvar = 5;
int func0() { return ++myintvar; }
int func1(int i) { return func0() * i; }
假設我們只想導出 func1,隱藏 myintvar 和 func0。
static 關鍵字在檔案作用域加上 static,可禁止符號的外部連結,編譯器不會為連結器保留該符號資訊。
// b.C
static int myintvar = 5;
static int func0() { return ++myintvar; }
int func1(int i) { return func0() * i; }
缺點:static 限制符號僅在定義該檔案內可見,若同一模組的其他檔案需共享此變數/函數(如透過 extern),連結會失敗。因此它適合檔案級封裝,不適合庫級符號可見性控制。
visibility 屬性(僅限 GNU/ELF)ELF ABI 定義四種可見性,常用兩種:
STV_DEFAULT:導出,全域可見。STV_HIDDEN:不導出,其他物件不可用,但保留於符號表供靜態連結。int myintvar __attribute__((visibility("hidden")));
int __attribute__((visibility("hidden"))) func0() { return ++myintvar; }
int func1(int i) { return func0() * i; }
優點:隱藏符號不進入動態符號表,但仍可在模組內部共享,優於 static。缺點:XL C/C++ 目前不支援(即使在 PowerLinux 上)。
導出列表由開發者編寫或工具產生,傳遞給連結器,明確指定可導出的符號。
/* exportmap */
{
global: func1;
local: *;
};
編譯:gcc -shared -o libtest.so a.C -fPIC -Wl,--version-script=exportmap
僅列出需導出的符號(需為修飾後名稱):
func1__Fi export
func0__Fv hidden
myintvar hidden
使用 CreateExportList 工具可自動產生修飾名稱列表,再手動修改可見性關鍵字(export/hidden/protected/internal),最後以 -bE:exportfile 傳給連結器。
在 AIX 上啟用執行期連結(-brtl)時,導出符號可能被主程式同名符號搶佔(取代)。protected 可見性的符號會導出但禁止搶佔,保護關鍵符號不被外部覆蓋,增強安全性。
// 測試結果:func_DEFAULT 被搶佔,func_PROC (protected) 維持庫內版本
$ ./main
func_DEFAULT redefined in main program, Preempted ==> EXP
func_PROC in the shared library, Not preempted
| 方法 | 適用平台 | 粒度 | 是否需改原始碼 | 編譯器優化機會 | 備註 |
|---|---|---|---|---|---|
static | 所有 | 檔案級 | 是 | 佳 | 破壞模組內共享 |
GNU visibility | ELF/GNU | 符號級 | 是 | 佳 | XL C/C++ 不支援 |
| 導出列表 | 所有 | 符號級 | 否 | 較差 | 推薦用 local: * 白名單模式 |
下一篇將介紹 XL C/C++ 編譯器的新功能,進一步簡化導出列表的管理。