【轉】C/C++符號隱藏與依賴管理:庫的符號隱藏


當程序規模變大之后,人們會對軟件進行模塊划分,以便分而治之。有了模塊之后,就可以將其構建成庫(靜態庫或者動態庫)發布給別人使用。

前文所述的符號隱藏手段對於模塊內代碼的信息隱藏是夠的,但是對於庫來說是不夠的。

當程序規模變大后,我們不可能把所有代碼都寫到同一個C文件或者CPP文件中。當代碼被拆分到多個實現文件中,它們之間需要互相訪問就必須通過頭文件暴露自己的可訪問API給別人。但是當所有文件都被打包在一起編譯成庫再提供給第三方的時候,這些內部開放的接口卻未必都需要被作為庫接口暴露出去。

常見的一種做法是將庫的內部頭文件和外部的頭文件分開,對外不發布內部頭文件。這是C/C++常用的一種庫級別的頭文件管理手段,后面我們會專門介紹。遺憾的是,僅通過不發布私有頭文件,並沒有解決所有問題。

即便不發布內部頭文件,內部跨編譯單元可被訪問的符號默認情況下仍舊會被庫全部導出。這樣不僅浪費了二進制的空間,增加了庫之間符號沖突的概率,而且還讓軟件包承擔了不必要的安全風險。導出的內部符號仍舊可以被外部強制extern,或者是被拿來做一些hack的事情。

現代編程語言會引入module機制來管理軟件模塊或者庫的外部可見性問題,讓開發者在發布軟件的時候顯示的指定需要導出給外部的API,其它的符號都只能被內部訪問。但是C和C++語言由於歷史包袱重(新的特性需要盡量兼容已經編譯過的既有代碼),C++語言直到20版本才將module特性標准化,而C語言的module特性至今仍不見蹤影。(事實上Java的module特性從2011年提出直到2017年才通過Java9發布,也歷時七年之久)。

由於C++20標准剛剛出來不久,編譯器對module機制的支持還很不完善,所以該特性離進入實用還有不少距離。感興趣的同學可以看看我的朋友張超寫的這篇文章《C++ Modules 初窺》

回到現實中,在沒有語言直接支持的情況下,我們如何隱藏庫的內部符號,顯示的指定需要導出的API呢?

方法是有的,那就是借助編譯器擴展。

GCC4之后支持使用-fvisibility=hidden編譯選項,將庫的所有符號默認設置為對外不可見。這樣編譯出的二進制就不會導出可供外部鏈接的符號。然后再結合GCC的__attribute__ ((visibility ("default")))屬性,在代碼中明確指定可以暴露給外部的API,於是我們就可以顯示的控制庫的對外API的可見性。

如下代碼示例:

// entry.h void function1(); __attribute__ ((visibility ("default"))) void entry_point(); 
// entry.cpp #include "entry.h" void function1() { // ... } void entry_point() { function1(); } 

當我們采用-fvisibility=hidden將entry.cpp編譯成靜態庫或者動態庫后,無論用戶是靜態鏈接還是使用dlopen動態庫的方式,都只能訪問到void entry_point()函數,而不能訪問到void funcion1()

通過該方法,我們不僅能顯示控制庫的導出API,還可以幫助編譯器和鏈接器優化出更好的二進制,並且縮短動態庫的加載時間。

Windows下也有類似的機制__declspec(dllexport),它和gcc下的__attribute__ ((visibility ("default")))作用類似。稍微不同的是Windows下還存在__declspec(dllimport)用於API的使用方顯示導入外部API,以便編譯器對代碼進行優化,但gcc下沒有對應的擴展。

為了讓使用上述編譯器擴展的代碼能夠跨平台,使用該特性的時候可以封裝一個宏,根據代碼所在的平台和編譯器版本,自動轉化成不同的實現。

// keywords.h #if defined _WIN32 || defined __CYGWIN__ #ifdef BUILDING_MOD #ifdef __GNUC__ #define MOD_PUBLIC __attribute__ ((dllexport)) #else #define MOD_PUBLIC __declspec(dllexport) // Note: actually gcc seems to also supports this syntax. #endif #else #ifdef __GNUC__ #define MOD_PUBLIC __attribute__ ((dllimport)) #else #define MOD_PUBLIC __declspec(dllimport) // Note: actually gcc seems to also supports this syntax. #endif #endif #define MOD_LOCAL #else #if __GNUC__ >= 4 #define MOD_PUBLIC __attribute__ ((visibility ("default"))) #define MOD_LOCAL __attribute__ ((visibility ("hidden"))) #else #define MOD_PUBLIC #define MOD_LOCAL #endif #endif 

如上參考了"https://gcc.gnu.org/wiki/Visibility"中給出的宏定義。它根據不同的平台和編譯器版本,定義了MOD_PUBLICMOD_LOCAL的不同實現。

#include "keywords.h" MOD_PUBLIC void function(int a); class MOD_PUBLIC SomeClass { int c; // Only for use within this DSO(Dynamic Shared Object) MOD_LOCAL void privateMethod(); public: Person(int _c) : c(_c) { } static void foo(int a); }; 

如上的例子中,void function(int a)class SomeClass在庫的內部和外部都可訪問,但是類的void privateMethod()接口只能在庫的內部使用,外部是無法使用的。

至此,我們給出當前現狀下C/C++庫級別API的管理建議:可以使用編譯選項默認隱藏庫的符號,然后使用編譯器屬性顯示指定庫需要導出的API

最后我們補充一點對動態庫的要求。

不同平台對於靜態庫和動態庫的使用大部分時候是相似的,但在某些細節上仍然會有區別。

所有平台下的靜態庫(.a或者.lib)都是可以缺符號的,即在生成時可以存在待鏈接的外部符號。然而對於動態庫,OSX下要求不能缺符號(OSX下動態庫是dylib格式,生成時是需要鏈接成功的,如果缺符號鏈接器會報錯)。而在Linux系統下動態庫(.so)生成的時候卻是可以缺符號的。

在Linux下,如果是在鏈接期使用缺符號的so,需要構建目標通過指定其它的動態庫或者靜態庫為缺失符號的so把符號補全,否則就會鏈接失敗。而如果是采用dlopen的方式打開so的話,那么該so必須自身符號是完備的,否則在動態加載的時候會出錯。

因此,這里我們給出另一個C/C++庫符號管理的建議:保證動態庫不要缺符號,是自滿足的。如果違反了這條原則,那么這個動態庫就無法用於動態加載;即使只是鏈接期使用,因為把符號缺失的細節泄露給了使用者,造成使用方的麻煩,所以也是不推薦的。

動態庫可以和靜態庫進行鏈接,以獲取自己需要的符號。但是有些時候我們只想要和靜態庫進行鏈接,卻不想在動態庫中將靜態庫中的符號間接暴露出去。這時可以采用-fvisibility=hidden選項重新編譯該靜態庫。但遺憾的是我們不總是能夠控制第三方靜態庫的編譯過程,這時可以借助鏈接器提供的顯示指定符號表的方法。該方法需要按照鏈接器的規范寫一個導出符號表,在鏈接期通過參數傳遞給鏈接器,這樣就可以精細的控制動態庫需要暴露的符號了。該方法並不常用,因此我們不多做介紹,具體用法可以參考https://www.gnu.org/software/gnulib/manual/html_node/LD-Version-Scripts.html

而動態庫和動態庫的鏈接,其實並不需要把對方的二進制真實鏈接進來。目標的動態庫會記住它所依賴的動態庫(通過目標動態庫中的rpath)。這種情況下也算該動態庫是自滿足的,因為用戶在使用該動態庫的時候,並不需要再為其尋找依賴。

最后我們總結一下對於庫符號管理的一些建議:

1)推薦使用編譯選項默認隱藏庫的所有符號,然后使用編譯器屬性顯示指定庫需要導出的API;
(建議對該方法進行封裝,以保證代碼兼容各種平台和編譯器版本)

2)保證動態庫不要缺符號,是自滿足的;

C/C++符號隱藏與依賴管理(三):頭文件管理



作者:MagicBowen
鏈接:https://www.jianshu.com/p/97d28e4613a7
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM