在寫這個自動化測試框架的時候,我一直在留意各方面的需求。畢竟,我本人并沒有做過真正的自動化測試。管理測試方面的領(lǐng)導(dǎo),提出一個需求,就是在用例運行失敗的時候,應(yīng)該將過程記錄下來,并形成報告,Email給相關(guān)人員。
個人認(rèn)為這個需求是非常合理的。事實上,任何系統(tǒng),如果沒有輸出,那么只能停留在程序員手里。有了報表,才叫真正解決了用戶的目標(biāo)需求。
在分析這個需求的過程,我提出了針對每一個操作接口的每一個方法,進(jìn)行Log。而完成這個工作的第一方法,就想到了AOP,也就是Hook技術(shù)的應(yīng)用。因為Delphi下面并沒有對AOP的直接支持,所以考慮這個實現(xiàn),變成了一個技術(shù)研究過程。
從技術(shù)上講,本篇博客只適合了解VCL的Delphi程序員閱讀。但其間的思想,相信大家都可以借鑒。下面我的描述過程,是以我的探索過程來進(jìn)行講述的。中間會帶出相關(guān)技術(shù)點,供大家參考。
第一、接口的方法,是由類來實現(xiàn)的。框架中,已經(jīng)對所有支持的類都進(jìn)行了登記。那么,只需要在這些類型中,找到所有實現(xiàn)的接口的所有方法的地址,那么Hook就變得有可能了。
TObject有一個方法:GetInterfaceTable,可以獲取所有接口列表。所有非常容易找到接口對應(yīng)的VTable。VTable在Delphi中并沒有明確的注釋,但是可以知道VTable是一個指針列表,每一項都記錄著一個方法的“實現(xiàn)地址”。
可惜的是,我發(fā)現(xiàn)VTable本身并沒有告訴你,這個接口有多少個方法!另外,你也不能得到每一個方法的名稱,以及參數(shù)等等描述。
第二、于是我考慮到接口的RTTI。接口的RTTI,我以前是沒有使用過的。通過VCL的代碼研究,發(fā)現(xiàn)接口中有一個非常特殊的接口定義:{$M+}IInvokable = interface;{$M-}。這個接口本身并沒有添加什么服務(wù),只是使用了編譯指令M,來使得接口擁有了RTTI。
實現(xiàn)的時候,可以通過從IInvokable派生,或者直接添加編譯指令,從而獲得RTTI服務(wù)。
下面的問題是,如何使用RTTI?我們知道,Delphi中有一個單元叫TypInfo.pas,后來我發(fā)現(xiàn),其實有另外一個單元叫:IntfInfo.pas。這里面有一個方法GetIntfMetaData可以幫助你獲得RTTI。另外,值得一提的是,獲取接口類型的PTypeInfo的方法是調(diào)用TypeInfo(IMyInterface);
第三、通過MetaData分析,我們可以知道接口的方法個數(shù)以及每一個方法的詳細(xì)定義。那么,現(xiàn)在就是如何Hook了。下面是一個Object的對象實例事例圖。
做左邊,有Self標(biāo)識的是對象的實例數(shù)據(jù)塊。某一個幾口指針I(yè)MyInterface指針,指向了一個VTable。而VTable中的每一個Method,都指向了一段代碼,這段代碼的前一部分,是為了計算EAX(保證將IMyInterface的地址,偏移到Self所在地址)。
分析上面的結(jié)構(gòu),再實際在CPU窗體中,調(diào)試以下接口方法的調(diào)用過程,發(fā)現(xiàn),必然和Method地址有關(guān)。因此,Hook的目標(biāo),就非常自然地變成修改VTable中的Method1的地址值。
第四、如何修改代碼?這里建議大家學(xué)習(xí)以下FastCode代碼。簡單一點,就是通過調(diào)用VirutalProtect方法,修改代碼段中內(nèi)存的訪問屬性,然后修改地址,最后再恢復(fù)回去。
顯然在Hook之前,必須聲明新的函數(shù)。
第五、新的函數(shù)并不是那么好聲明的。關(guān)注一下,接口函數(shù)的調(diào)用代碼,你會發(fā)現(xiàn)很多問題。下面舉一個簡單的例子。
IMyInterface = interface(IInvokable)
procedure AAA;
end;
假設(shè)TMyIntfImpl類實現(xiàn)了上面的接口。那么oIntfObj: IMyInterface聲明的對象,oIntfObj.AAA;的匯編代碼是如下的樣子:
上面是兩段代碼的圖片,其中[dex+$0c]指的就是$004661FD,也就是第二段代碼圖片的首地址。大家可以再聯(lián)系一下上面的示意圖理解一下地址的關(guān)鍵。
好,言歸正傳。這里注意一下,我們要修改的是[dex+$0c]里面的值。但是由于這個是call過去的。所以在call之前,會在堆棧中壓入函數(shù)返回地址。另外,在調(diào)用函數(shù)之前,還有函數(shù)參數(shù)的準(zhǔn)備。比如說Self指針的傳入到EAX中,如果本身方法還有參數(shù)的話,可能占用其他寄存器或者堆棧。
由于我們要求是Hook住所有的方法,并且所有方法的參數(shù)類型并不一定一樣。所以在call之前的代碼,是無法預(yù)計的。所以在新的函數(shù)中,必須考慮如何做到保存寄存器和做到ret時候的棧平衡。
通過我的實踐,我的做法是通過先彈出當(dāng)前的ret地址,保存到一個數(shù)據(jù)區(qū)中。等待調(diào)用完原先的代碼后,再壓棧。而調(diào)用Writelog的時候,先保存寄存器,調(diào)用完了之后,再恢復(fù)寄存器。這是因為寄存器也可能是返回值的地方。而且后續(xù)代碼有可能優(yōu)化使用。
第六、完成了匯編的編寫,還有一個問題,那就是由于每一個函數(shù)的原地址不一樣,所以必須為每一個函數(shù),定義一個代理函數(shù)。由于這些函數(shù)的地址和個數(shù)都是未定的,所以,這里就必須要用到動態(tài)創(chuàng)建代碼。
動態(tài)創(chuàng)建代碼的方法看上去簡單,申請一段空間,將那一段模板代碼地址復(fù)制過來。但是,實際情況并非如此。
首先,申請控件的時候,使用VirutalAlloc,并指定EXECUTE_READWRITE屬性。另外,要關(guān)注到原來的代碼是在代碼段執(zhí)行的,所以有些函數(shù)的地址可能只是一個偏移地址。而后申請的代碼,是在HEAP中運行的,所以,如果只是單純地復(fù)制,函數(shù)調(diào)用就會報錯了。
好了,上面講了六點關(guān)鍵因素。如果你足夠理解上面的過程,你也可以做到AOP了。這篇文章是一個純技術(shù)的,可能關(guān)心測試的會非常失望,只能說sorry了。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機(jī)微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
