探索一種C++二進位模組的熱更新機制

來源:互聯網
上載者:User
    現如今,提供穩定可靠且能滿足人民群眾日益增長的物質文化需要的服務是互連網服務商的基本責任,所以服務端軟體一定要夠壯夠強夠靈活。服務程式一旦跑起來那就最好7X24小時地永遠別掛,而且多變的、不停增長的使用者需求也得儘快滿足。可問題是,永遠也別指望程式員寫出沒有bug的程式,任何架構師也沒有水晶球可以預測將來的花花世界,無論
當時看起來多麼完美的代碼,將來也會因為種種原因要被修改(或者被丟棄?)。既然如此,那麼我們或許應該想辦法給程式加上點進化能力,讓它能永不停歇地任勞任怨地工作,而同時還不斷地反省自己、糾正自己並茁壯成長。

    本文是寫給C++程式員的,如果你的工具是Lisp、Erlang、Ruby這樣的動態語言,那麼因為它們牛B的進階動態特性,你壓根就用不著象我們這樣,在二進位層幹刀口舔血的勾當。

    簡單來說,熱更新就是程式邊運行邊更新。有人一定覺得我在故意(象專家那樣)裝B,把簡單的問題搞得異常複雜,因為動態連結程式庫本身就是可以動態載入和卸載的,只要在新的動態庫build和部署好後通知程式重新載入一下不就搞定了?

    在這裡,我要語重心長地告訴你們:第一,我沒有裝B,因為不想遭雷劈;第二,這種簡單的方案在少數情況下是行得通的,但在大多數情況下卻不行,因為實際的程式是代碼與資料結構的正確結合,代碼幾乎總是要操作相應的資料結構。舉個例子,A庫的create函數建立了資料對象data,foo函數能正確地操作data,然後我們用B庫熱更新了A庫,這樣現在的create和foo函數都是B庫實現的,而且新的create產生的資料對象與data(二進位布局)格式不同,新的foo也只能正確地操作新的資料對象;假設此後應用程式又需要用(B庫的)foo操作由A庫建立的data(我們無法避免,因為資料的生存周期是和應用邏輯息息相關的),這個時候嚴重的錯誤是不是就極可能發生?所以一個模組被熱替換掉後,由它建立的所有資料對象也要跟著進行格式轉換,轉換為與新版本相容的(二進位布局)格式。可是這又帶來了新問題:如何才能找到所有由舊模組建立的資料對象?我們就象蹩腳數學家一樣,把一個骯髒的問題轉化成了另外一個骯髒的問題。

    換一種思路,如果在編程時願意遵循一定的規範(規範是一種約束,但合理的約束卻常常能提高總體的自由),而這規範使我們能避開找到所有舊版本資料對象這樣的棘手問題,那麼就能實現安全的熱更新。

    本文建議的規範是採用類似COM、XPCOM這樣的元件物件模型:程式由一個個的組件對象組成,每種對象提供了若干功能,外部只能通過對象的介面來使用相應功能。介面通常都用C++抽象基類來構建,從ABI(Application Binary Interface)的角度來看,C++抽象基類最關鍵的是規定了子類的虛函數表的布局。也可以用其它方法來構建介面機制,但是要保證與C++的虛函數表模型(g++, vc++等主流編譯器在這方面的實現都是一樣的)在ABI層上相容。模組是物理上的對象容器,可以包含一個或多個組件對象。模組最常見的形式就是動態連結程式庫(so或dll),本文探索的動態更新機制便是以模組為最小單位。

    從ABI層來看,通過組件對象介面來調用相關功能實際就是調用該對象虛表中對應項所指向的虛函數實現,正因為調用虛函數需要一個查表才能找到真正函數地址的中間操作,所以才使得我們能夠hook住組件對象的調用,從而有機會把老版本的對象轉換為新版本。那麼如何才能hook對象的虛函數調用呢?方法很簡單,修改虛表,讓虛表的每一項都指向我們
的hook代碼,這樣修改之後,無論何時何地外部模組調用老版本對象都會首先執行hook代碼。有朋友一定覺得這太hack、太不安全了:你咋就能確定虛表的位置,虛表的項數呢?是的,你的質疑一點都沒錯,如果不遵循任何規範,那麼對林林總總的詭異的C++編譯器抖這麼點小機靈的確是一種非常危險的動作。但幸運的是,如果你採用XPCOM這樣的元件物件模型,那麼各大C++編譯器在抽象類別的虛表實現上難得的共識就可以保證我們找到虛表的正確位置,而且模型額外提供的(標準C++不具有的)運行期介面類型資訊又可以保證我們安全地修改適當個數的虛表項。

    現在可以用個簡單的例子來實驗一下上述思路是否行得通。這裡提供了代碼的zip包下載,目前僅支援跑在Intel IA32架構CPU上的windows和freebsd平台(當然,其它unix平台也應該沒問題,只是標頭檔的包含路徑有可能需要調整)的實現,windows平台需要安裝mingw。解壓後在相應目錄下運行
    gmake PLAT=windows

    gmake PLAT=freebsd
就會產生test程式。在freebsd平台下記得先用
    export LD_LIBRARY_PATH=./
將目前的目錄加入到動態庫搜尋路徑中後再運行。

    例子包括如下幾個來源程式檔案:nsIBase.h nsImp1.h nsImp1.cpp nsImp2.h nsImp2.cpp dynahook.cpp test.cpp 。

    nsIBase.h定義了一個抽象基類nsIBase,它代表著一個介面,其中包含有2個介面方法,分別是Hello和Foo。

    nsImp1.h和nsImp1.cpp共同組成了nsIBase介面的第一個版本的實現,它們會被build成一個名為libimp1.dll(unix下是libimp1.so,下同)的動態連結程式庫,這就是一個模組;標頭檔中定義了nsImp1具體類,它繼承自nsIBase抽象類別。cpp檔案中除了包括介面方法的具體實現,還有三個約定的匯出函數:<1>create,相當於Factory 方法,因為外部模組不知道組件對象的具體實現,所以只能用它來建立對象執行個體; <2>on_swapping,當該模組被新版本模組熱替換時,該函數會被調用,並且傳遞給它新版本模組的格式轉換函式指標作參數,該函數應當更改目前的版本對象之虛表中的各項函數指標,指向特殊的hook代碼,使得此後外部模組對這些對象的任何調用都會首先被hook截獲,然後新模組的格式轉換函式被執行,最後才進行通常的介面函數調用;<3>converter,格式轉換函式,當該模組熱替換別的模組時,它用於把老版本的對象轉換為自己版本的對象,這就包括給對象設定新的虛表指標、轉換資料區塊等等。因為nsImp1是第一個版本的實現,沒有比它更老的實現需要它替換,所以它的converter是個空函數。

    nsImp2.h和nsImp2.cpp組成了nsIBase介面的第二個版本的實現,它們被build成libimp2.dll。nsImp2的組織圖和nsImp1差不多,只不過它的converter是真正要做實際的轉換工作的。

    dynahook.cpp是最有趣的部分,它提供了一個函數:
void dynahook(void **p_old_vtbl, int method_count, converter_t cf),其中p_old_vtbl指向老版本對象的虛函數表,method_count指明表中有多少需要被監控的介面方法, cf是(新模組提供的)用於轉換老版本對象格式的轉換函式指標,作用就是產生特殊的hook代碼來監控相應的介面方法調用。而這所謂特殊的hook代碼其實也很簡單,它的實現如下(80X86彙編,gnu assembler格式):

    pushl %ebp      ; 儲存caller的stack frame base
    movl %esp, %ebp ; 設定自己的stack frame base
    pushl 8(%ebp)   ; this指標進棧
    call *converter ; 調用converter轉換函式,它應該為對象設定新的虛函數表指標
    subl $4, %esp   ; 調整棧頂
    movl 8(%ebp), %eax ; this指標讀入eax
    movl (%eax), %eax  ; (新版本的)虛函數表首地址讀入eax
    addl $method_offset, %eax" ; 加上正確的介面函式位移
    movl (%eax), %eax  ; (新版本的)虛函數入口地址讀入eax
    leave              ; 恢複ebp和esp,注意其後跟的並非通常的ret指令
    jmp *%eax          ; 直接跳轉到(新版本的)介面函數的實現中

    上述代碼只是個模板,dynahook對每個需要hook的介面方法都會依照該模板產生一段幾乎一模一樣的機器碼,只是其中converter的地址和method_offset都會被重新設定,因為它們要在運行期才能確定,然後讓虛函數表中的函數指標指向這些動態產生的hook機器碼,這不就實現了動態監控嗎?特別要注意的是,hook的實現依賴於caller通過棧來傳遞介面方法的第一個(隱含)參數――this指標。g++能滿足這一點,可是Visual C++系列卻不這樣:即使沒有使用fastcall這樣的調用規範,它也會把this指標放在ecx寄存器中傳遞(以上結論來自對編譯器產生的彙編碼的觀察,如有錯誤請指正)。有興趣的讀者可以自己改一改這段hook代碼,使它也能適合Visual C++編譯器。

    test.cpp示範了如何使用組件對象和熱替換,部分代碼如下:

    nsIBase *o1 = (*create_v1)(); // 建立版本為1的組件對象
    nsIBase *o2 = (*create_v1)(); // 建立版本為1的組件對象

    printf("create objects of version 1: %p, %p/n",
           o1,o2);

    o1->Hello();  // 直接調用版本1的實現
    o2->Foo();    // 直接調用版本1的實現

    // 熱替換
    ......

    o1->Hello(); // 將會先觸發轉換函式,再調用版本2的實現
    o2->Foo();   // 將會先觸發轉換函式,再調用版本2的實現

    o1->Hello(); // 直接調用版本2的實現
    o2->Hello(); // 直接調用版本2的實現
    o1->Foo();   // 直接調用版本2的實現
    o2->Foo();   // 直接調用版本2的實現


程式的輸出則是:

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

    現在來談談這種方案的缺點和局限性。

    首先,方案要求新版本模組能知道舊版本對象的二進位布局,這就意味著要有舊的組件對象的定義檔案等。如果舊版本對象彙總了第三方的(非組件型)對象,而且對象的內部狀態又不提供介面複製,那麼新版本的實現就沒有辦法替換掉第三方對象。這是一個極大的限制。

    其次,即使擁有舊的組件對象的定義,也需要保證每個模組的關鍵編譯參數(比如結構體的位元組對齊數)相同,否則轉換函式訪問舊版本對象的資料區塊就是一件非常危險的事情。

    再者,為了能比較方便地做資料區塊的格式轉換,要求每一個版本的實現不能直接內嵌資料成員,只能用一個指標指向一塊結構體,就象nsImp1這樣:
class nsImp1 : public nsIBase
{
public:
    nsImp1();

    virtual void Hello();
    virtual void Foo();

protected:
    struct data_t
    {
        int a;
    };

    data_t *m_data;
};
這無疑使得代碼的編寫更繁瑣。
    
    最後,正確地更改虛表需要知道一些諸如介面包含多少方法之類的類型資訊,而C++沒有標準的方法去取得,因此組件
架構提供的動態類型資訊至關重要。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.