版本
從客戶(hù)的角度,最常見(jiàn)的版本問(wèn)題就是我們所說(shuō)的 DLL Hell 問(wèn)題。簡(jiǎn)單地講, DLL Hell 是指當(dāng)多個(gè)應(yīng)用程序試圖共享一個(gè)公用組件(如某個(gè)動(dòng)態(tài)連接庫(kù)(DLL)或某個(gè)組件對(duì)象模型(COM)類(lèi))時(shí)所引發(fā)的一系列問(wèn)題。最典型的情況是,某個(gè)應(yīng)用程序?qū)⒁惭b一個(gè)新版本的共享組件,而該組件與機(jī)器上的現(xiàn)有版本不向后兼容。雖然剛安裝的應(yīng)用程序運(yùn)行正常,但原來(lái)依賴(lài)前一版本共享組件的應(yīng)用程序也許已無(wú)法再工作。在某些情況下,問(wèn)題的起因更加難以預(yù)料。比如,當(dāng)用戶(hù)瀏覽某些 Web 站點(diǎn)時(shí)會(huì)同時(shí)下載某個(gè) Microsoft ActiveX® 控件。如果下載該控件,它將替換機(jī)器上原有的任何版本的控件。如果機(jī)器上的某個(gè)應(yīng)用程序恰好使用該控件,則很可能也會(huì)停止工作。
在許多情況下,用戶(hù)需要很長(zhǎng)時(shí)間才會(huì)發(fā)現(xiàn)應(yīng)用程序已停止工作。結(jié)果往往很難記起是何時(shí)的機(jī)器變化影響到了該應(yīng)用程序。用戶(hù)可能會(huì)回憶起一周前安裝了一些東西,但安裝與目前看到的狀態(tài)并沒(méi)有任何明顯的關(guān)聯(lián)。 更糟的是,現(xiàn)在很少有診斷工具幫助用戶(hù)(或幫助他們的技術(shù)支持人員)確定有什么問(wèn)題。
這些問(wèn)題的原因是應(yīng)用程序不同組件的版本信息沒(méi)有由系統(tǒng)記錄或加強(qiáng)。而且系統(tǒng)為某個(gè)應(yīng)用程序所做的改變會(huì)影響機(jī)器上的所有應(yīng)用程序—現(xiàn)在建立完全從變化中隔離出來(lái)的應(yīng)用程序并不容易。
我想大家一定碰到過(guò)由于新裝完應(yīng)用程序后,DLL的更新導(dǎo)致原來(lái)的一些應(yīng)用程序加載失敗的情況。這種情況有一個(gè)讓人望而生畏的名字-DLL地獄。下面的文章從DLL的原理及code的角度詳細(xì)分析了當(dāng)DLL導(dǎo)出類(lèi)時(shí)所導(dǎo)致的錯(cuò)誤。 DLL動(dòng)態(tài)鏈接庫(kù)是程序復(fù)用的重要方式,DLL可以導(dǎo)出函數(shù),使函數(shù)被多個(gè)程序復(fù)用,DLL中的函數(shù)實(shí)現(xiàn)可以被修改而無(wú)需重新編譯和連接使用該DLL的應(yīng)用程序。作為一名面向?qū)ο蟮某绦騿T,希望DLL可以導(dǎo)出類(lèi),以便在類(lèi)的層次上實(shí)現(xiàn)復(fù)用。所幸的是,DLL確實(shí)也可以導(dǎo)出類(lèi)。 然而事實(shí)卻沒(méi)這么簡(jiǎn)單,導(dǎo)出類(lèi)的DLL在維護(hù)和修改時(shí)有很多地方必需很小心,增加成員變量、修改導(dǎo)出類(lèi)的基類(lèi)等操作都可能導(dǎo)致意想不到的后果,也許用戶(hù)更新了最新版本的DLL庫(kù)后,應(yīng)用程序就再也不能工作了。這就是著名的DLL Hell(DLL地獄)問(wèn)題。 DLL地獄問(wèn)題是怎么產(chǎn)生的呢?看下面的例子,假設(shè)DLL有一個(gè)導(dǎo)出類(lèi)ClassD1:
class ClassD
{
public:
int GetInt();
private:
int m_i;
};
int ClassD::GetInt()
{
return m_i;
} 應(yīng)用程序使用現(xiàn)在的代碼來(lái)使用這個(gè)類(lèi):
ClassD d;
printf(“%d”, d.GetInt());
程序進(jìn)行正正常,沒(méi)有什么問(wèn)題。后來(lái)DLL需要升級(jí),對(duì)ClassD進(jìn)行了修改,增加了一個(gè)成員變量,如下:
class ClassD // 修改后
{
public:
int GetInt();
private:
int m_i2;
int m_i;
}; 把新的DLL編譯連接完成后,復(fù)制到應(yīng)用程序目錄,這個(gè)倒楣的應(yīng)用程序調(diào)用GetInt方法恐怕再也無(wú)法得正確的值了。事實(shí)上它還算幸運(yùn)的,如果GetInt的實(shí)現(xiàn)改成如下這樣,那么它馬上就要出錯(cuò)退出了。
int ClassD::GetInt() // 修改后
{
return m_i++;
} 這樣的事情,稱(chēng)它是個(gè)地獄(Hell)一點(diǎn)也不夸張。為什么會(huì)出錯(cuò)呢?我們要先從類(lèi)實(shí)例的創(chuàng)建開(kāi)始,看看使用一個(gè)類(lèi)的工作過(guò)程。 首先,程序語(yǔ)句“ClassD d;”為這個(gè)類(lèi)申請(qǐng)一塊內(nèi)存。這塊內(nèi)存保存該類(lèi)的所有成員變量,以及虛函數(shù)表。內(nèi)存的大小由類(lèi)的聲明決定,在應(yīng)用程序編譯時(shí)就已經(jīng)確定。 然后,當(dāng)調(diào)用“d.GetInt()”時(shí),把申請(qǐng)的這一塊內(nèi)存做為this指針傳給GetInt函數(shù),GetInt函數(shù)從this指向的位置開(kāi)始,加上m_i應(yīng)有的偏移量,計(jì)算m_i所在的內(nèi)存位置,并從該位置取數(shù)據(jù)返回。m_i相對(duì)this的偏移量是由m_i在類(lèi)中定義的位置決定的,定義在前的成員變量在內(nèi)存中也更靠前。這個(gè)偏移量在DLL編譯時(shí)確定。 當(dāng)ClassD的定義改為修改后的狀態(tài)時(shí),有些東西變了。 第一個(gè)變的是內(nèi)存的大小。因?yàn)樾薷暮蟮腃lassD多了一個(gè)成員變量,所以?xún)?nèi)存也變大了。然而這一點(diǎn)應(yīng)用程序并不知道。 第二個(gè)變的是m_i的偏移地址。因?yàn)樵趍_i之前定義了一個(gè)m_i2,m_i的實(shí)現(xiàn)偏移地址實(shí)際已經(jīng)靠后了。所以d.GetInt()訪(fǎng)問(wèn)的將是原來(lái)m_i后面的那個(gè)位置,而這個(gè)位置已經(jīng)超出原來(lái)那塊內(nèi)存的后部范圍了。 很顯然,在更換了DLL后,應(yīng)用程序還按原來(lái)的大小申請(qǐng)了一塊內(nèi)存,而它調(diào)用的方法卻訪(fǎng)問(wèn)了比這塊內(nèi)存更大的區(qū)域,出錯(cuò)再在所難免。 同樣的情形還會(huì)發(fā)生在以下這些種情況中: 1) 應(yīng)用程序直接訪(fǎng)問(wèn)類(lèi)的公有變量,而該公有變量在新DLL中定義的位置發(fā)生了變化;
2) 應(yīng)用程序調(diào)用類(lèi)的一個(gè)虛函數(shù),而新的類(lèi)中,該虛函數(shù)的前面又增加了一個(gè)虛函數(shù);
3) 新類(lèi)的后面增加了成員變量,并且新類(lèi)的成員函數(shù)將訪(fǎng)問(wèn)、修改這些變量;
4) 修改了新類(lèi)的基類(lèi),基類(lèi)的大小發(fā)生了變化; 等等,總言而之,一不小心,你的程序就會(huì)掉進(jìn)地獄。通過(guò)對(duì)這些引起出錯(cuò)的情況進(jìn)行分析,會(huì)發(fā)現(xiàn)其實(shí)只有三點(diǎn)變化會(huì)引起出錯(cuò),因?yàn)檫@三點(diǎn)是使用這個(gè)DLL的應(yīng)用程序在編譯時(shí)就需要確定的內(nèi)容,它們分別是:
1) 類(lèi)的大小;
2) 類(lèi)成員的偏移地址;
3) 虛函數(shù)的順序。 要想做一個(gè)可升級(jí)的DLL,必需避免以上三個(gè)問(wèn)題。所以以下三點(diǎn)用來(lái)使DLL遠(yuǎn)離地獄。 1,不直接生成類(lèi)的實(shí)例。對(duì)于類(lèi)的大小,當(dāng)我們定義一個(gè)類(lèi)的實(shí)例,或使用new語(yǔ)句生成一個(gè)實(shí)例時(shí),內(nèi)存的大小是在編譯時(shí)決定的。要使應(yīng)用程序不依賴(lài)于類(lèi)的大小,只有一個(gè)辦法:應(yīng)用程序不生成類(lèi)的實(shí)例,使用DLL中的函數(shù)來(lái)生成。把導(dǎo)出類(lèi)的構(gòu)造函數(shù)定義為私有的(privated),在導(dǎo)出類(lèi)中提供靜態(tài)(static)成員函數(shù)(如NewInstance())用來(lái)生成類(lèi)的實(shí)例。因?yàn)镹ewInstance()函數(shù)在新的DLL中會(huì)被重新編譯,所以總能返回大小正確的實(shí)例內(nèi)存。 2,不直接訪(fǎng)問(wèn)成員變量。應(yīng)用程序直接訪(fǎng)問(wèn)類(lèi)的成員變量時(shí)會(huì)用到該變量的偏移地址。所以避免偏移地址依賴(lài)的辦法就是不要直接訪(fǎng)問(wèn)成員變量。把所有的成員變量的訪(fǎng)問(wèn)控制都定義為保護(hù)型(protected)以上的級(jí)別,并為需要訪(fǎng)問(wèn)的成員變量定義Get或Set方法。Get或Set方法在編譯新DLL時(shí)會(huì)被重新編譯,所以總能訪(fǎng)問(wèn)到正確的變量位置。 3,忘了虛函數(shù)吧,就算有也不要讓?xiě)?yīng)用程序直接訪(fǎng)問(wèn)它。因?yàn)轭?lèi)的構(gòu)造函數(shù)已經(jīng)是私有(privated)的了,所以應(yīng)用程序也不會(huì)去繼承這個(gè)類(lèi),也不會(huì)實(shí)現(xiàn)自己的多態(tài)。如果導(dǎo)出類(lèi)的父類(lèi)中有虛函數(shù),或設(shè)計(jì)需要(如類(lèi)工場(chǎng)之類(lèi)的框架),一定要把這些函數(shù)聲明為保護(hù)的(protected)以上的級(jí)別,并為應(yīng)用程序重新設(shè)計(jì)調(diào)用該慮函數(shù)的成員函數(shù)。這一點(diǎn)也類(lèi)似于對(duì)成員變量的處理。 如果導(dǎo)出的類(lèi)能遵循以上三點(diǎn),那么以后對(duì)DLL的升級(jí)將可以認(rèn)為是安全的。 如果對(duì)一個(gè)已經(jīng)存在的導(dǎo)出類(lèi)的DLL進(jìn)行維護(hù),同樣也要注意:不要改動(dòng)所有的成員變量,包括導(dǎo)出類(lèi)的父類(lèi),無(wú)論定義的順序還是數(shù)量;不要?jiǎng)铀械奶摵瘮?shù),無(wú)論順序還是數(shù)量。 總結(jié)起來(lái),其實(shí)是一句話(huà):導(dǎo)出類(lèi)的DLL不要導(dǎo)出除了函數(shù)以外的任何內(nèi)容。聽(tīng)起來(lái)是不是有點(diǎn)可笑呢! 事實(shí)上,建議你在發(fā)布導(dǎo)出類(lèi)的DLL的時(shí)候,重新定義一個(gè)類(lèi)的聲明,這個(gè)聲明可以不管原來(lái)的類(lèi)里的成員變量之類(lèi)的,只把接口函數(shù)列在類(lèi)的聲明里,如下面的例子:
class ClassInterface
{
privated:
ClassInterface();
public:
static ClassInterface * NewInstance();
int GetXXX();
void SetXXX();
void Function();
}; 使用該DLL的應(yīng)用程序用上面的定義作為ClassInterface的頭文件,便不會(huì)有任何可能導(dǎo)致的安全問(wèn)題。 DLL地獄問(wèn)是歸根結(jié)底是因?yàn)镈LL當(dāng)初是作為函數(shù)級(jí)共享庫(kù)設(shè)計(jì)的,并不能真正提供一個(gè)類(lèi)所必需的信息。類(lèi)層上的程序復(fù)用只有Java和C#生成的類(lèi)文件才能做到。
|