當(dāng)程序規(guī)模變大之后境蔼,人們會(huì)對(duì)軟件進(jìn)行模塊劃分延塑,以便分而治之。有了模塊之后惕蹄,就可以將其構(gòu)建成庫(kù)(靜態(tài)庫(kù)或者動(dòng)態(tài)庫(kù))發(fā)布給別人使用。
前文所述的符號(hào)隱藏手段對(duì)于模塊內(nèi)代碼的信息隱藏是夠的,但是對(duì)于庫(kù)來(lái)說(shuō)是不夠的卖陵。
當(dāng)程序規(guī)模變大后遭顶,我們不可能把所有代碼都寫(xiě)到同一個(gè)C文件或者CPP文件中。當(dāng)代碼被拆分到多個(gè)實(shí)現(xiàn)文件中泪蔫,它們之間需要互相訪問(wèn)就必須通過(guò)頭文件暴露自己的可訪問(wèn)API給別人棒旗。但是當(dāng)所有文件都被打包在一起編譯成庫(kù)再提供給第三方的時(shí)候,這些內(nèi)部開(kāi)放的接口卻未必都需要被作為庫(kù)接口暴露出去鸥滨。
常見(jiàn)的一種做法是將庫(kù)的內(nèi)部頭文件和外部的頭文件分開(kāi)嗦哆,對(duì)外不發(fā)布內(nèi)部頭文件。這是C/C++常用的一種庫(kù)級(jí)別的頭文件管理手段婿滓,后面我們會(huì)專門(mén)介紹老速。遺憾的是,僅通過(guò)不發(fā)布私有頭文件凸主,并沒(méi)有解決所有問(wèn)題橘券。
即便不發(fā)布內(nèi)部頭文件,內(nèi)部跨編譯單元可被訪問(wèn)的符號(hào)默認(rèn)情況下仍舊會(huì)被庫(kù)全部導(dǎo)出卿吐。這樣不僅浪費(fèi)了二進(jìn)制的空間旁舰,增加了庫(kù)之間符號(hào)沖突的概率,而且還讓軟件包承擔(dān)了不必要的安全風(fēng)險(xiǎn)嗡官。導(dǎo)出的內(nèi)部符號(hào)仍舊可以被外部強(qiáng)制extern箭窜,或者是被拿來(lái)做一些hack的事情。
現(xiàn)代編程語(yǔ)言會(huì)引入module機(jī)制來(lái)管理軟件模塊或者庫(kù)的外部可見(jiàn)性問(wèn)題衍腥,讓開(kāi)發(fā)者在發(fā)布軟件的時(shí)候顯示的指定需要導(dǎo)出給外部的API磺樱,其它的符號(hào)都只能被內(nèi)部訪問(wèn)。但是C和C++語(yǔ)言由于歷史包袱重(新的特性需要盡量兼容已經(jīng)編譯過(guò)的既有代碼)婆咸,C++語(yǔ)言直到20版本才將module特性標(biāo)準(zhǔn)化竹捉,而C語(yǔ)言的module特性至今仍不見(jiàn)蹤影。(事實(shí)上Java的module特性從2011年提出直到2017年才通過(guò)Java9發(fā)布尚骄,也歷時(shí)七年之久)块差。
由于C++20標(biāo)準(zhǔn)剛剛出來(lái)不久,編譯器對(duì)module機(jī)制的支持還很不完善倔丈,所以該特性離進(jìn)入實(shí)用還有不少距離憨闰。感興趣的同學(xué)可以看看我的朋友張超寫(xiě)的這篇文章《C++ Modules 初窺》。
回到現(xiàn)實(shí)中需五,在沒(méi)有語(yǔ)言直接支持的情況下鹉动,我們?nèi)绾坞[藏庫(kù)的內(nèi)部符號(hào),顯示的指定需要導(dǎo)出的API呢警儒?
方法是有的训裆,那就是借助編譯器擴(kuò)展眶根。
GCC4之后支持使用-fvisibility=hidden
編譯選項(xiàng),將庫(kù)的所有符號(hào)默認(rèn)設(shè)置為對(duì)外不可見(jiàn)边琉。這樣編譯出的二進(jìn)制就不會(huì)導(dǎo)出可供外部鏈接的符號(hào)属百。然后再結(jié)合GCC的__attribute__ ((visibility ("default")))
屬性,在代碼中明確指定可以暴露給外部的API变姨,于是我們就可以顯示的控制庫(kù)的對(duì)外API的可見(jiàn)性族扰。
如下代碼示例:
// entry.h
void function1();
__attribute__ ((visibility ("default"))) void entry_point();
// entry.cpp
#include "entry.h"
void function1() {
// ...
}
void entry_point() {
function1();
}
當(dāng)我們采用-fvisibility=hidden
將entry.cpp編譯成靜態(tài)庫(kù)或者動(dòng)態(tài)庫(kù)后,無(wú)論用戶是靜態(tài)鏈接還是使用dlopen
動(dòng)態(tài)庫(kù)的方式定欧,都只能訪問(wèn)到void entry_point()
函數(shù)渔呵,而不能訪問(wèn)到void funcion1()
。
通過(guò)該方法砍鸠,我們不僅能顯示控制庫(kù)的導(dǎo)出API扩氢,還可以幫助編譯器和鏈接器優(yōu)化出更好的二進(jìn)制,并且縮短動(dòng)態(tài)庫(kù)的加載時(shí)間爷辱。
Windows下也有類似的機(jī)制__declspec(dllexport)
录豺,它和gcc下的__attribute__ ((visibility ("default")))
作用類似。稍微不同的是Windows下還存在__declspec(dllimport)
用于API的使用方顯示導(dǎo)入外部API饭弓,以便編譯器對(duì)代碼進(jìn)行優(yōu)化双饥,但gcc下沒(méi)有對(duì)應(yīng)的擴(kuò)展。
為了讓使用上述編譯器擴(kuò)展的代碼能夠跨平臺(tái)弟断,使用該特性的時(shí)候可以封裝一個(gè)宏咏花,根據(jù)代碼所在的平臺(tái)和編譯器版本,自動(dòng)轉(zhuǎn)化成不同的實(shí)現(xiàn)阀趴。
// 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"中給出的宏定義昏翰。它根據(jù)不同的平臺(tái)和編譯器版本,定義了MOD_PUBLIC
和MOD_LOCAL
的不同實(shí)現(xiàn)舍咖。
#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
在庫(kù)的內(nèi)部和外部都可訪問(wèn)锉桑,但是類的void privateMethod()
接口只能在庫(kù)的內(nèi)部使用排霉,外部是無(wú)法使用的。
至此民轴,我們給出當(dāng)前現(xiàn)狀下C/C++庫(kù)級(jí)別API的管理建議:可以使用編譯選項(xiàng)默認(rèn)隱藏庫(kù)的符號(hào)攻柠,然后使用編譯器屬性顯示指定庫(kù)需要導(dǎo)出的API。
最后我們補(bǔ)充一點(diǎn)對(duì)動(dòng)態(tài)庫(kù)的要求后裸。
不同平臺(tái)對(duì)于靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)的使用大部分時(shí)候是相似的瑰钮,但在某些細(xì)節(jié)上仍然會(huì)有區(qū)別。
所有平臺(tái)下的靜態(tài)庫(kù)(.a或者.lib)都是可以缺符號(hào)的微驶,即在生成時(shí)可以存在待鏈接的外部符號(hào)浪谴。然而對(duì)于動(dòng)態(tài)庫(kù)开睡,OSX下要求不能缺符號(hào)(OSX下動(dòng)態(tài)庫(kù)是dylib格式,生成時(shí)是需要鏈接成功的苟耻,如果缺符號(hào)鏈接器會(huì)報(bào)錯(cuò))篇恒。而在Linux系統(tǒng)下動(dòng)態(tài)庫(kù)(.so)生成的時(shí)候卻是可以缺符號(hào)的。
在Linux下凶杖,如果是在鏈接期使用缺符號(hào)的so胁艰,需要構(gòu)建目標(biāo)通過(guò)指定其它的動(dòng)態(tài)庫(kù)或者靜態(tài)庫(kù)為缺失符號(hào)的so把符號(hào)補(bǔ)全,否則就會(huì)鏈接失敗智蝠。而如果是采用dlopen
的方式打開(kāi)so的話腾么,那么該so必須自身符號(hào)是完備的,否則在動(dòng)態(tài)加載的時(shí)候會(huì)出錯(cuò)杈湾。
因此解虱,這里我們給出另一個(gè)C/C++庫(kù)符號(hào)管理的建議:保證動(dòng)態(tài)庫(kù)不要缺符號(hào),是自滿足的漆撞。如果違反了這條原則饭寺,那么這個(gè)動(dòng)態(tài)庫(kù)就無(wú)法用于動(dòng)態(tài)加載;即使只是鏈接期使用叫挟,因?yàn)榘逊?hào)缺失的細(xì)節(jié)泄露給了使用者艰匙,造成使用方的麻煩,所以也是不推薦的抹恳。
動(dòng)態(tài)庫(kù)可以和靜態(tài)庫(kù)進(jìn)行鏈接员凝,以獲取自己需要的符號(hào)。但是有些時(shí)候我們只想要和靜態(tài)庫(kù)進(jìn)行鏈接奋献,卻不想在動(dòng)態(tài)庫(kù)中將靜態(tài)庫(kù)中的符號(hào)間接暴露出去健霹。這時(shí)可以采用-fvisibility=hidden
選項(xiàng)重新編譯該靜態(tài)庫(kù)。但遺憾的是我們不總是能夠控制第三方靜態(tài)庫(kù)的編譯過(guò)程瓶蚂,這時(shí)可以借助鏈接器提供的顯示指定符號(hào)表的方法糖埋。該方法需要按照鏈接器的規(guī)范寫(xiě)一個(gè)導(dǎo)出符號(hào)表,在鏈接期通過(guò)參數(shù)傳遞給鏈接器窃这,這樣就可以精細(xì)的控制動(dòng)態(tài)庫(kù)需要暴露的符號(hào)了瞳别。該方法并不常用,因此我們不多做介紹杭攻,具體用法可以參考https://www.gnu.org/software/gnulib/manual/html_node/LD-Version-Scripts.html祟敛。
而動(dòng)態(tài)庫(kù)和動(dòng)態(tài)庫(kù)的鏈接,其實(shí)并不需要把對(duì)方的二進(jìn)制真實(shí)鏈接進(jìn)來(lái)兆解。目標(biāo)的動(dòng)態(tài)庫(kù)會(huì)記住它所依賴的動(dòng)態(tài)庫(kù)(通過(guò)目標(biāo)動(dòng)態(tài)庫(kù)中的rpath)馆铁。這種情況下也算該動(dòng)態(tài)庫(kù)是自滿足的,因?yàn)橛脩粼谑褂迷搫?dòng)態(tài)庫(kù)的時(shí)候锅睛,并不需要再為其尋找依賴埠巨。
最后我們總結(jié)一下對(duì)于庫(kù)符號(hào)管理的一些建議:
1)推薦使用編譯選項(xiàng)默認(rèn)隱藏庫(kù)的所有符號(hào)历谍,然后使用編譯器屬性顯示指定庫(kù)需要導(dǎo)出的API;
(建議對(duì)該方法進(jìn)行封裝辣垒,以保證代碼兼容各種平臺(tái)和編譯器版本)
2)保證動(dòng)態(tài)庫(kù)不要缺符號(hào)扮饶,是自滿足的;