現(xiàn)如今,提供穩(wěn)定可靠且能滿足人民群眾日益增長(zhǎng)的物質(zhì)文化需要的服務(wù)是互聯(lián)網(wǎng)服務(wù)商的基本責(zé)任,所以服務(wù)端軟件一定要夠壯夠強(qiáng)夠靈活。服務(wù)程序一旦跑起來(lái)那就最好7X24小時(shí)地永遠(yuǎn)別掛,而且多變的、不停增長(zhǎng)的用戶需求也得盡快滿足。可問(wèn)題是,永遠(yuǎn)也別指望程序員寫出沒(méi)有bug的程序,任何架構(gòu)師也沒(méi)有水晶球可以預(yù)測(cè)將來(lái)的花花世界,無(wú)論
當(dāng)時(shí)看起來(lái)多么完美的代碼,將來(lái)也會(huì)因?yàn)榉N種原因要被修改(或者被丟棄?)。既然如此,那么我們或許應(yīng)該想辦法給程序加上點(diǎn)進(jìn)化能力,讓它能永不停歇地任勞任怨地工作,而同時(shí)還不斷地反省自己、糾正自己并茁壯成長(zhǎng)。
??? 本文是寫給C++程序員的,如果你的工具是Lisp、Erlang、Ruby這樣的動(dòng)態(tài)語(yǔ)言,那么因?yàn)樗鼈兣的高級(jí)動(dòng)態(tài)特性,你壓根就用不著象我們這樣,在二進(jìn)制層干刀口舔血的勾當(dāng)。
??? 簡(jiǎn)單來(lái)說(shuō),熱更新就是程序邊運(yùn)行邊更新。有人一定覺(jué)得我在故意(象專家那樣)裝B,把簡(jiǎn)單的問(wèn)題搞得異常復(fù)雜,因?yàn)閯?dòng)態(tài)鏈接庫(kù)本身就是可以動(dòng)態(tài)加載和卸載的,只要在新的動(dòng)態(tài)庫(kù)build和部署好后通知程序重新加載一下不就搞定了?
??? 在這里,我要語(yǔ)重心長(zhǎng)地告訴你們:第一,我沒(méi)有裝B,因?yàn)椴幌朐饫着坏诙@種簡(jiǎn)單的方案在少數(shù)情況下是行得通的,但在大多數(shù)情況下卻不行,因?yàn)閷?shí)際的程序是代碼與數(shù)據(jù)結(jié)構(gòu)的正確結(jié)合,代碼幾乎總是要操作相應(yīng)的數(shù)據(jù)結(jié)構(gòu)。舉個(gè)例子,A庫(kù)的create函數(shù)創(chuàng)建了數(shù)據(jù)對(duì)象data,foo函數(shù)能正確地操作data,然后我們用B庫(kù)熱更新了A庫(kù),這樣現(xiàn)在的create和foo函數(shù)都是B庫(kù)實(shí)現(xiàn)的,而且新的create產(chǎn)生的數(shù)據(jù)對(duì)象與data(二進(jìn)制布局)格式不同,新的foo也只能正確地操作新的數(shù)據(jù)對(duì)象;假設(shè)此后應(yīng)用程序又需要用(B庫(kù)的)foo操作由A庫(kù)創(chuàng)建的data(我們無(wú)法避免,因?yàn)閿?shù)據(jù)的生存周期是和應(yīng)用邏輯息息相關(guān)的),這個(gè)時(shí)候嚴(yán)重的錯(cuò)誤是不是就極可能發(fā)生?所以一個(gè)模塊被熱替換掉后,由它創(chuàng)建的所有數(shù)據(jù)對(duì)象也要跟著進(jìn)行格式轉(zhuǎn)換,轉(zhuǎn)換為與新版本兼容的(二進(jìn)制布局)格式。可是這又帶來(lái)了新問(wèn)題:如何才能找到所有由舊模塊創(chuàng)建的數(shù)據(jù)對(duì)象?我們就象蹩腳數(shù)學(xué)家一樣,把一個(gè)骯臟的問(wèn)題轉(zhuǎn)化成了另外一個(gè)骯臟的問(wèn)題。
??? 換一種思路,如果在編程時(shí)愿意遵循一定的規(guī)范(規(guī)范是一種約束,但合理的約束卻常常能提高總體的自由),而這規(guī)范使我們能避開(kāi)找到所有舊版本數(shù)據(jù)對(duì)象這樣的棘手問(wèn)題,那么就能實(shí)現(xiàn)安全的熱更新。
??? 本文建議的規(guī)范是采用類似COM、XPCOM這樣的組件對(duì)象模型:程序由一個(gè)個(gè)的組件對(duì)象組成,每種對(duì)象提供了若干功能,外部只能通過(guò)對(duì)象的接口來(lái)使用相應(yīng)功能。接口通常都用C++抽象基類來(lái)構(gòu)建,從ABI(Application Binary Interface)的角度來(lái)看,C++抽象基類最關(guān)鍵的是規(guī)定了子類的虛函數(shù)表的布局。也可以用其它方法來(lái)構(gòu)建接口機(jī)制,但是要保證與C++的虛函數(shù)表模型(g++, vc++等主流編譯器在這方面的實(shí)現(xiàn)都是一樣的)在ABI層上兼容。模塊是物理上的對(duì)象容器,可以包含一個(gè)或多個(gè)組件對(duì)象。模塊最常見(jiàn)的形式就是動(dòng)態(tài)鏈接庫(kù)(so或dll),本文探索的動(dòng)態(tài)更新機(jī)制便是以模塊為最小單位。
??? 從ABI層來(lái)看,通過(guò)組件對(duì)象接口來(lái)調(diào)用相關(guān)功能實(shí)際就是調(diào)用該對(duì)象虛表中對(duì)應(yīng)項(xiàng)所指向的虛函數(shù)實(shí)現(xiàn),正因?yàn)檎{(diào)用虛函數(shù)需要一個(gè)查表才能找到真正函數(shù)地址的中間操作,所以才使得我們能夠hook住組件對(duì)象的調(diào)用,從而有機(jī)會(huì)把老版本的對(duì)象轉(zhuǎn)換為新版本。那么如何才能hook對(duì)象的虛函數(shù)調(diào)用呢?方法很簡(jiǎn)單,修改虛表,讓虛表的每一項(xiàng)都指向我們
的hook代碼,這樣修改之后,無(wú)論何時(shí)何地外部模塊調(diào)用老版本對(duì)象都會(huì)首先執(zhí)行hook代碼。有朋友一定覺(jué)得這太hack、太不安全了:你咋就能確定虛表的位置,虛表的項(xiàng)數(shù)呢?是的,你的質(zhì)疑一點(diǎn)都沒(méi)錯(cuò),如果不遵循任何規(guī)范,那么對(duì)林林總總的詭異的C++編譯器抖這么點(diǎn)小機(jī)靈的確是一種非常危險(xiǎn)的動(dòng)作。但幸運(yùn)的是,如果你采用XPCOM這樣的組件對(duì)象模型,那么各大C++編譯器在抽象類的虛表實(shí)現(xiàn)上難得的共識(shí)就可以保證我們找到虛表的正確位置,而且模型額外提供的(標(biāo)準(zhǔn)C++不具有的)運(yùn)行期接口類型信息又可以保證我們安全地修改適當(dāng)個(gè)數(shù)的虛表項(xiàng)。
??? 現(xiàn)在可以用個(gè)簡(jiǎn)單的例子來(lái)試驗(yàn)一下上述思路是否行得通。這里提供了代碼的zip包下載,目前僅支持跑在Intel IA32架構(gòu)CPU上的windows和freebsd平臺(tái)(當(dāng)然,其它unix平臺(tái)也應(yīng)該沒(méi)問(wèn)題,只是頭文件的包含路徑有可能需要調(diào)整)的實(shí)現(xiàn),windows平臺(tái)需要安裝mingw。解壓后在相應(yīng)目錄下運(yùn)行
??? gmake PLAT=windows
或
??? gmake PLAT=freebsd
就會(huì)生成test程序。在freebsd平臺(tái)下記得先用
??? export LD_LIBRARY_PATH=./
將當(dāng)前目錄加入到動(dòng)態(tài)庫(kù)搜索路徑中后再運(yùn)行。
??? 例子包括如下幾個(gè)源程序文件:nsIBase.h nsImp1.h nsImp1.cpp nsImp2.h nsImp2.cpp dynahook.cpp test.cpp 。
??? nsIBase.h定義了一個(gè)抽象基類nsIBase,它代表著一個(gè)接口,其中包含有2個(gè)接口方法,分別是Hello和Foo。
??? nsImp1.h和nsImp1.cpp共同組成了nsIBase接口的第一個(gè)版本的實(shí)現(xiàn),它們會(huì)被build成一個(gè)名為libimp1.dll(unix下是libimp1.so,下同)的動(dòng)態(tài)鏈接庫(kù),這就是一個(gè)模塊;頭文件中定義了nsImp1具體類,它繼承自nsIBase抽象類。cpp文件中除了包括接口方法的具體實(shí)現(xiàn),還有三個(gè)約定的導(dǎo)出函數(shù):<1>create,相當(dāng)于工廠方法,因?yàn)橥獠磕K不知道組件對(duì)象的具體實(shí)現(xiàn),所以只能用它來(lái)創(chuàng)建對(duì)象實(shí)例; <2>on_swapping,當(dāng)該模塊被新版本模塊熱替換時(shí),該函數(shù)會(huì)被調(diào)用,并且傳遞給它新版本模塊的格式轉(zhuǎn)換函數(shù)指針作參數(shù),該函數(shù)應(yīng)當(dāng)更改當(dāng)前版本對(duì)象之虛表中的各項(xiàng)函數(shù)指針,指向特殊的hook代碼,使得此后外部模塊對(duì)這些對(duì)象的任何調(diào)用都會(huì)首先被hook截獲,然后新模塊的格式轉(zhuǎn)換函數(shù)被執(zhí)行,最后才進(jìn)行通常的接口函數(shù)調(diào)用;<3>converter,格式轉(zhuǎn)換函數(shù),當(dāng)該模塊熱替換別的模塊時(shí),它用于把老版本的對(duì)象轉(zhuǎn)換為自己版本的對(duì)象,這就包括給對(duì)象設(shè)置新的虛表指針、轉(zhuǎn)換數(shù)據(jù)塊等等。因?yàn)閚sImp1是第一個(gè)版本的實(shí)現(xiàn),沒(méi)有比它更老的實(shí)現(xiàn)需要它替換,所以它的converter是個(gè)空函數(shù)。
??? nsImp2.h和nsImp2.cpp組成了nsIBase接口的第二個(gè)版本的實(shí)現(xiàn),它們被build成libimp2.dll。nsImp2的組織結(jié)構(gòu)和nsImp1差不多,只不過(guò)它的converter是真正要做實(shí)際的轉(zhuǎn)換工作的。
??? dynahook.cpp是最有趣的部分,它提供了一個(gè)函數(shù):
void dynahook(void **p_old_vtbl, int method_count, converter_t cf),其中p_old_vtbl指向老版本對(duì)象的虛函數(shù)表,method_count指明表中有多少需要被監(jiān)控的接口方法, cf是(新模塊提供的)用于轉(zhuǎn)換老版本對(duì)象格式的轉(zhuǎn)換函數(shù)指針,作用就是產(chǎn)生特殊的hook代碼來(lái)監(jiān)控相應(yīng)的接口方法調(diào)用。而這所謂特殊的hook代碼其實(shí)也很簡(jiǎn)單,它的實(shí)現(xiàn)如下(80X86匯編,gnu assembler格式):
??? pushl %ebp????? ; 保存caller的stack frame base
??? movl %esp, %ebp ; 設(shè)置自己的stack frame base
??? pushl 8(%ebp)?? ; this指針進(jìn)棧
??? call *converter ; 調(diào)用converter轉(zhuǎn)換函數(shù),它應(yīng)該為對(duì)象設(shè)置新的虛函數(shù)表指針
??? subl $4, %esp?? ; 調(diào)整棧頂
??? movl 8(%ebp), %eax ; this指針讀入eax
??? movl (%eax), %eax? ; (新版本的)虛函數(shù)表首地址讀入eax
??? addl $method_offset, %eax" ; 加上正確的接口函數(shù)偏移量
??? movl (%eax), %eax? ; (新版本的)虛函數(shù)入口地址讀入eax
??? leave????????????? ; 恢復(fù)ebp和esp,注意其后跟的并非通常的ret指令
??? jmp *%eax????????? ; 直接跳轉(zhuǎn)到(新版本的)接口函數(shù)的實(shí)現(xiàn)中
??? 上述代碼只是個(gè)模板,dynahook對(duì)每個(gè)需要hook的接口方法都會(huì)依照該模板產(chǎn)生一段幾乎一模一樣的機(jī)器碼,只是其中converter的地址和method_offset都會(huì)被重新設(shè)置,因?yàn)樗鼈円谶\(yùn)行期才能確定,然后讓虛函數(shù)表中的函數(shù)指針指向這些動(dòng)態(tài)生成的hook機(jī)器碼,這不就實(shí)現(xiàn)了動(dòng)態(tài)監(jiān)控嗎?特別要注意的是,hook的實(shí)現(xiàn)依賴于caller通過(guò)棧來(lái)傳遞接口方法的第一個(gè)(隱含)參數(shù)――this指針。g++能滿足這一點(diǎn),可是Visual C++系列卻不這樣:即使沒(méi)有使用fastcall這樣的調(diào)用規(guī)范,它也會(huì)把this指針?lè)旁趀cx寄存器中傳遞(以上結(jié)論來(lái)自對(duì)編譯器生成的匯編碼的觀察,如有錯(cuò)誤請(qǐng)指正)。有興趣的讀者可以自己改一改這段hook代碼,使它也能適合Visual C++編譯器。
??? test.cpp演示了如何使用組件對(duì)象和熱替換,部分代碼如下:
??? nsIBase *o1 = (*create_v1)(); // 創(chuàng)建版本為1的組件對(duì)象
??? nsIBase *o2 = (*create_v1)(); // 創(chuàng)建版本為1的組件對(duì)象
??? printf("create objects of version 1: %p, %p\n",
?????????? o1,o2);
??? o1->Hello();? // 直接調(diào)用版本1的實(shí)現(xiàn)
??? o2->Foo();??? // 直接調(diào)用版本1的實(shí)現(xiàn)
??? // 熱替換
??? ......
??? o1->Hello(); // 將會(huì)先觸發(fā)轉(zhuǎn)換函數(shù),再調(diào)用版本2的實(shí)現(xiàn)
??? o2->Foo();?? // 將會(huì)先觸發(fā)轉(zhuǎn)換函數(shù),再調(diào)用版本2的實(shí)現(xiàn)
??? o1->Hello(); // 直接調(diào)用版本2的實(shí)現(xiàn)
??? o2->Hello(); // 直接調(diào)用版本2的實(shí)現(xiàn)
??? o1->Foo();?? // 直接調(diào)用版本2的實(shí)現(xiàn)
??? o2->Foo();?? // 直接調(diào)用版本2的實(shí)現(xiàn)
程序的輸出則是:
create objects of version 1: 00032BC8, 00032CE0
obj(00032BC8), version1, Hello(), m_data->a = 2008
obj(00032CE0), version1, Foo(), m_data->a = 2008
convert object(00032BC8) from version 1 to 2
obj(00032BC8) version 2, Hello(), m_data->a = 2008, m_data->b:77
convert object(00032CE0) from version 1 to 2
obj(00032CE0) version 2, Foo(), m_data->a:2008, m_data->b:77
obj(00032BC8) version 2, Hello(), m_data->a = 2008, m_data->b:77
obj(00032CE0) version 2, Hello(), m_data->a = 2008, m_data->b:77
obj(00032BC8) version 2, Foo(), m_data->a:2008, m_data->b:77
obj(00032CE0) version 2, Foo(), m_data->a:2008, m_data->b:77
??? 現(xiàn)在來(lái)談?wù)勥@種方案的缺點(diǎn)和局限性。
??? 首先,方案要求新版本模塊能知道舊版本對(duì)象的二進(jìn)制布局,這就意味著要有舊的組件對(duì)象的定義文件等。如果舊版本對(duì)象聚合了第三方的(非組件型)對(duì)象,而且對(duì)象的內(nèi)部狀態(tài)又不提供接口復(fù)制,那么新版本的實(shí)現(xiàn)就沒(méi)有辦法替換掉第三方對(duì)象。這是一個(gè)極大的限制。
??? 其次,即使擁有舊的組件對(duì)象的定義,也需要保證每個(gè)模塊的關(guān)鍵編譯參數(shù)(比如結(jié)構(gòu)體的字節(jié)對(duì)齊數(shù))相同,否則轉(zhuǎn)換函數(shù)訪問(wèn)舊版本對(duì)象的數(shù)據(jù)塊就是一件非常危險(xiǎn)的事情。
??? 再者,為了能比較方便地做數(shù)據(jù)塊的格式轉(zhuǎn)換,要求每一個(gè)版本的實(shí)現(xiàn)不能直接內(nèi)嵌數(shù)據(jù)成員,只能用一個(gè)指針指向一塊結(jié)構(gòu)體,就象nsImp1這樣:
class nsImp1 : public nsIBase
{
public:
??? nsImp1();
??? virtual void Hello();
??? virtual void Foo();
protected:
??? struct data_t
??? {
??????? int a;
??? };
??? data_t *m_data;
};
這無(wú)疑使得代碼的編寫更繁瑣。
???
??? 最后,正確地更改虛表需要知道一些諸如接口包含多少方法之類的類型信息,而C++沒(méi)有標(biāo)準(zhǔn)的方法去取得,因此組件
框架提供的動(dòng)態(tài)類型信息至關(guān)重要。
本文來(lái)自CSDN博客,轉(zhuǎn)載請(qǐng)標(biāo)明出處:http://blog.csdn.net/soloist/archive/2008/06/23/2580396.aspx