高級C/C++編譯技術之讀書筆記(三)之動態庫設計

                                                                                    

  最近有幸閱讀了《高級C/C++編譯技術》深受啓發,該書深刻淺出地講解了構建過程(編譯、連接)中的各類細節,從多個角度展現了程序與庫文件或代碼的集成方法,提出了面向代碼複用和系統集成的軟件架構設計方法,以及系統開發過程當中疑難問題的解決方案。
  如下將回頭記錄下其中的關鍵要點,以便後面查閱。

本節思惟導圖

1. 關於-fPIC編譯器選項

1.1 -fPIC表明什麼

  「PIC」是位置無關代碼(Position-independent Code)的縮寫,說到位置無關代碼,咱們會立馬想到加載重定位,加載重定位將動態庫加載到進程內存空間中,可是隻有第一次加載這個動態庫的進程可使用它,當其它進程須要加載同一動態庫的時候,除了將動態庫的完整副本加載到自身內存空間之外,別無他法,當更多的進程須要加載某一特定的動態庫時,內存中會存在更多的副本。這種限制的根本緣由在於加載過程設計的缺陷,在將動態庫加載到進程中以前,裝載器須要修改動態庫的代碼段,使得在加載該庫的進程中,動態庫的全部符號是有意義的,即使這種方法能夠知足基本的運行需求,但其致使的最終結果是因爲動態庫代碼的修改是不可逆的,所以其它進程難以直接複用這個已加載的動態庫。linux

  爲了解決加載重定位的缺陷,從新設計加載機制,避免將加載的動態庫代碼段綁定到第一個加載該動態庫的進程中,提出了PIC機制,使得多個進程能夠無縫映射到已加載動態庫的內存映射中windows

1.2 必定要使用-fPIC編譯器選項來建立嗎?

  在32位體系架構中,咱們不須要使用-fPIC編譯器選項,若是沒有指定該選項,編譯出來的動態庫就會遵循舊式的裝載時重定位機制架構

  在64位體系結構中,簡單地忽略-fPIC編譯器選項就會致使連接錯誤,要修正連接錯誤,一種方法是向編譯器傳遞-fPIC選項,另外一種方法是向編譯器傳遞-mcmodel=large選項函數

1.3 只有在編譯動態庫時纔會使用-fPIC編譯選項嗎?可否在靜態連接庫的狀況下使用?

  在32位體系結構中,編譯靜態庫時是否使用-fPIC選項是無所謂的,這樣會對編譯生成的代碼結構產生必定的影響,可是對於靜態庫的連接和運行時行爲的影響是微乎其微的spa

  在64爲體系結構中,狀況會變得更加有意思:架構設計

  (1)若是靜態庫是連接到可執行文件中,那麼編譯時能夠指定也能夠不指定,設計

  (2)若是靜態庫連接到的是動態庫,那麼必須使用-fPIC選項編譯調試

2. C++引發的連接問題

2.1 C++使用了更加複雜的符號命名規則

  爲了惟一地標識函數,鏈接器在爲函數入口點創建符號的時候,必須使用某種方法來包含函數的從屬信息和輸入參數信息,連接器的設計爲了知足這種更加複雜的需求,最終產生了「名稱修飾」這種技術。名稱修飾是將函數名、函數的從屬信息、函數的參數列表進行組合,最後生成符號名稱的過程。code

  爲了統一,當咱們但願避免名稱修飾時,必須使用一個特殊的關鍵字來告知鏈接器不要修飾符號名稱對象

#ifdef __cplusplus
extern "C"
{
#endif // __cplusplus
void fun1(void);
void fun2(void);
void fun3(void);
void printMessage(void);

#ifdef __cplusplus
}
#endif // __cplusplus

2.2 靜態初始化順序問題

  C語言中的一項遺留特性:連接器能夠處理很簡單的初始化變量,不管是簡單數據類型仍是結構體,鏈接器只須要在數據段中保留存儲空間,並將初始值寫入該位置便可,在C語言領域,變量初始化的順序一般不是很重要,關鍵在於變量的初始化其實在程序啓動時候就完成了。

  可是在C++中,數據類型每每是一個對象,對象的初始化是在運行時經過對象構造函數來完成的,爲了初始化C++對象,鏈接器須要作更多的工做,爲了幫助鏈接器完成其任務,編譯器將特定文件須要使用的全部構造器的列表嵌入到目標文件中,並將相關信息存放在特定的目標文件段中,在鏈接時,鏈接器會檢查全部的目標文件,並將其中的構造函數列表合併成完整的列表,以備運行時執行,說了這麼多,總的一句話是C++對象的初始化,加劇了編譯器和鏈接器的負擔,因爲鏈接器依然不夠智能,在大多數狀況下,程序在加載時會引發很是嚴重的崩潰,並且是在任何調試器可以捕捉到以前

  發生這種狀況是由於初始化的對象依賴於另一些須要在器以前初始化的對象,而且沒有任何規則能夠指定靜態對象的初始化順序,咱們將這類問題一般稱爲靜態初始化順序問題。

解決方案:

(1)爲_init()函數提供自定義實現,這是一個在動態庫加載時會被當即調用的標準函數,在該函數中能夠經過靜態成員函數初始化對象,以經過構造函數強制初始化,所以,也能夠爲_fini()函數提供自定義實現

(2)調用一個自定義函數去訪問特定對象,而不是直接訪問該函數會包含C++類的一個靜態實例,並返回其引用

2.3 模版

這涉及到編譯器的設計問題:

(1)編譯器能夠保證生成全部的模版特殊化代碼,併爲每一個特殊化版本建立一個弱符號

(2)鏈接器在連接結束以前都不包含模版特殊化的機器碼是想愛你,但其他全部的連接任務都完成後,鏈接器會檢查代碼,肯定到底須要哪些特殊化版本,並調用C++編譯器建立所需的模版特殊化,最後,將機器碼插入可執行文件中

3. 控制動態庫符號的可見性

  在Linux中全部動態庫鏈接器符號默認都是外部可見的,任未嘗試連接這些動態庫的用戶均可以訪問這些符號

  在windows中,DLL連接符號默認都是外部不可見的

3.1 導出linux動態庫符號

(1)方法一

  經過向編譯器傳遞編譯選項-fvisibility=hidden就能夠將全部的動態庫符號置爲對外不可見,默承認見

(2)方法二

  __attibute__((visibility("<default | hidden>")))

  經過在函數前面使用編譯器屬性修飾,能夠指示連接器容許或禁止對外部提供該符號

(3)方法三

  #pragma visibility push(hidden)

  #pragma visibility pop

3.2 導出windows動態連接庫符號

  __descspec(dllexport)

4. 動態庫連接模式

  (1)加載時動態連接

  (2)運行時動態連接

目的 Linux版本 Windows版本
加載庫 dlopen() LoadLibrary()
查找符號 dlsym() GetProcAddress()
卸載庫 dlclose() FreeLibrary()
錯誤報告 dlerror() GetLastError()

示例僞代碼:

handle = do_load_library("<library path>", optional_flags);
if(NULL == handle)
{
    report_error();  
}

pRunction = (function_type)do_find_library_symbol(handle);
if(NULL == pFunction)
{
    report_error();
    unload_libray();
    handle = NULL;
    return;
}

pFunction(function arguments);

do_unload_library(handle);
handle = NULL;
相關文章
相關標籤/搜索