1. 程式人生 > 其它 >【轉】C/C++符號隱藏與依賴管理:庫的符號隱藏

【轉】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
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。