DLL Hell
DLL Hell是因?yàn)?span style="color:#ff0000">DLL當(dāng)初是作為函數(shù)級共享庫設(shè)計(jì)的,并不能真正提供一個類所必需的信息(導(dǎo)類的時候出現(xiàn)問題)
DLL動態(tài)鏈接庫是程序復(fù)用的重要方式,DLL可以導(dǎo)出函數(shù),使函數(shù)被多個程序復(fù)用,DLL中的函數(shù)實(shí)現(xiàn)可以被修改而無需重新編譯和連接使用該DLL的應(yīng)用程序。作為一名面向?qū)ο蟮某绦騿T,希望DLL可以導(dǎo)出類,以便在類的層次上實(shí)現(xiàn)復(fù)用。所幸的是,DLL確實(shí)也可以導(dǎo)出類。
?
然而事實(shí)卻沒這么簡單,導(dǎo)出類的DLL在維護(hù)和修改時有很多地方必需很小心,增加成員變量、修改導(dǎo)出類的基類等操作都可能導(dǎo)致意想不到的后果,也許用戶更新了最新版本的DLL庫后,應(yīng)用程序就再也不能工作了。這就是著名的DLL Hell(DLL地獄)問題。
?
DLL地獄問題是怎么產(chǎn)生的呢?看下面的例子,假設(shè)DLL有一個導(dǎo)出類ClassD1:
class ClassD
{
???????public:?????????int GetInt();
???????private:??????int m_i;
};
int ClassD::GetInt()
{
???????return m_i;
}
?
應(yīng)用程序使用現(xiàn)在的代碼來使用這個類:
ClassD d;
printf(“%d”, d.GetInt());
?
程序進(jìn)行正正常,沒有什么問題。后來DLL需要升級,對ClassD進(jìn)行了修改,增加了一個成員變量,如下:
class ClassD //?修改后
{
???????public:?????????int GetInt();
???????private:??????int m_i2;??int m_i;
};
?
把新的DLL編譯連接完成后,復(fù)制到應(yīng)用程序目錄,這個倒楣的應(yīng)用程序調(diào)用GetInt方法恐怕再也無法得正確的值了。事實(shí)上它還算幸運(yùn)的,如果GetInt的實(shí)現(xiàn)改成如下這樣,那么它馬上就要出錯退出了。
int ClassD::GetInt() //?修改后
{
???????return m_i++;
}
?
這樣的事情,稱它是個地獄(Hell)一點(diǎn)也不夸張。為什么會出錯呢?我們要先從類實(shí)例的創(chuàng)建開始,看看使用一個類的工作過程。
?
首先,程序語句“ClassD d;”為這個類申請一塊內(nèi)存。這塊內(nèi)存保存該類的所有成員變量,以及虛函數(shù)表。內(nèi)存的大小由類的聲明決定,在應(yīng)用程序編譯時就已經(jīng)確定。
?
然后,當(dāng)調(diào)用“d.GetInt()”時,把申請的這一塊內(nèi)存做為this指針傳給GetInt函數(shù),GetInt函數(shù)從this指向的位置開始,加上m_i應(yīng)有的偏移量,計(jì)算m_i所在的內(nèi)存位置,并從該位置取數(shù)據(jù)返回。m_i相對this的偏移量是由m_i在類中定義的位置決定的,定義在前的成員變量在內(nèi)存中也更靠前。這個偏移量在DLL編譯時確定。
?
當(dāng)ClassD的定義改為修改后的狀態(tài)時,有些東西變了。
?
第一個變的是內(nèi)存的大小。因?yàn)樾薷暮蟮腃lassD多了一個成員變量,所以內(nèi)存也變大了。然而這一點(diǎn)應(yīng)用程序并不知道。
?
第二個變的是m_i的偏移地址。因?yàn)樵趍_i之前定義了一個m_i2,m_i的實(shí)現(xiàn)偏移地址實(shí)際已經(jīng)靠后了。所以d.GetInt()訪問的將是原來m_i后面的那個位置,而這個位置已經(jīng)超出原來那塊內(nèi)存的后部范圍了。
?
很顯然,在更換了DLL后,應(yīng)用程序還按原來的大小申請了一塊內(nèi)存,而它調(diào)用的方法卻訪問了比這塊內(nèi)存更大的區(qū)域,出錯再在所難免。
?
同樣的情形還會發(fā)生在以下這些種情況中:
?
1)?應(yīng)用程序直接訪問類的公有變量,而該公有變量在新DLL中定義的位置發(fā)生了變化;
2)?應(yīng)用程序調(diào)用類的一個虛函數(shù),而新的類中,該虛函數(shù)的前面又增加了一個虛函數(shù);
3)?新類的后面增加了成員變量,并且新類的成員函數(shù)將訪問、修改這些變量;
4)?修改了新類的基類,基類的大小發(fā)生了變化;
?
等等,總言而之,一不小心,你的程序就會掉進(jìn)地獄。
通過對這些引起出錯的情況進(jìn)行分析,會發(fā)現(xiàn)其實(shí)只有三點(diǎn)變化會引起出錯,因?yàn)檫@三點(diǎn)是使用這個DLL的應(yīng)用程序在編譯時就需要確定的內(nèi)容,它們分別是:
1)?類的大小;
2)?類成員的偏移地址;
3)?虛函數(shù)的順序。
?
要想做一個可升級的DLL,必需避免以上三個問題。所以以下三點(diǎn)用來使DLL遠(yuǎn)離地獄。
?
1,不直接生成類的實(shí)例。對于類的大小,當(dāng)我們定義一個類的實(shí)例,或使用new語句生成一個實(shí)例時,內(nèi)存的大小是在編譯時決定的。要使應(yīng)用程序不依賴于類的大小,只有一個辦法:應(yīng)用程序不生成類的實(shí)例,使用DLL中的函數(shù)來生成。把導(dǎo)出類的構(gòu)造函數(shù)定義為私有的(privated),在導(dǎo)出類中提供靜態(tài)(static)成員函數(shù)(如NewInstance())用來生成類的實(shí)例。因?yàn)镹ewInstance()函數(shù)在新的DLL中會被重新編譯,所以總能返回大小正確的實(shí)例內(nèi)存。
?
2,不直接訪問成員變量。應(yīng)用程序直接訪問類的成員變量時會用到該變量的偏移地址。所以避免偏移地址依賴的辦法就是不要直接訪問成員變量。把所有的成員變量的訪問控制都定義為保護(hù)型(protected)以上的級別,并為需要訪問的成員變量定義Get或Set方法。Get或Set方法在編譯新DLL時會被重新編譯,所以總能訪問到正確的變量位置。
?
3,忘了虛函數(shù)吧,就算有也不要讓應(yīng)用程序直接訪問它。因?yàn)轭惖臉?gòu)造函數(shù)已經(jīng)是私有(privated)的了,所以應(yīng)用程序也不會去繼承這個類,也不會實(shí)現(xiàn)自己的多態(tài)。如果導(dǎo)出類的父類中有虛函數(shù),或設(shè)計(jì)需要(如類工場之類的框架),一定要把這些函數(shù)聲明為保護(hù)的(protected)以上的級別,并為應(yīng)用程序重新設(shè)計(jì)調(diào)用該慮函數(shù)的成員函數(shù)。這一點(diǎn)也類似于對成員變量的處理。
?
如果導(dǎo)出的類能遵循以上三點(diǎn),那么以后對DLL的升級將可以認(rèn)為是安全的。
?
如果對一個已經(jīng)存在的導(dǎo)出類的DLL進(jìn)行維護(hù),同樣也要注意:不要改動所有的成員變量,包括導(dǎo)出類的父類,無論定義的順序還是數(shù)量;不要動所有的虛函數(shù),無論順序還是數(shù)量。
?
總結(jié)起來,其實(shí)是一句話:只導(dǎo)出類的函數(shù) ??導(dǎo)出類的DLL不要導(dǎo)出除了函數(shù)以外的任何內(nèi)容。聽起來是不是有點(diǎn)可笑呢!
?
事實(shí)上,建議你在發(fā)布導(dǎo)出類的DLL的時候,重新定義一個類的聲明,這個聲明可以不管原來的類里的成員變量之類的,只把接口函數(shù)列在類的聲明里,如下面的例子:
class ClassInterface
{
???????privated:
??????????????ClassInterface();
???????public:
??????????????static ClassInterface * NewInstance();
??????????????int GetXXX();
??????????????void SetXXX();
??????????????void Function();
};
?
使用該DLL的應(yīng)用程序用上面的定義作為ClassInterface的頭文件,便不會有任何可能導(dǎo)致的安全問題。
?
DLL Hell問是歸根結(jié)底是因?yàn)?span style="color:#ff0000">DLL當(dāng)初是作為函數(shù)級共享庫設(shè)計(jì)的,并不能真正提供一個類所必需的信息。類層上的程序復(fù)用只有Java和C#生成的類文件才能做到。
總結(jié)
- 上一篇: Android Java和JavaScr
- 下一篇: Linux中ELF格式 可执行文件+动