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

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