為什麼要在 AutoIt 裡使用機械碼呢?詳情我在「調用機械碼的技巧與 MemoryDLL 的故事」裡已經說過了。不清楚什麼是機械碼的人,建議先看過那篇文章。
我們設想一個情況,你需在 AutoIt 裡加入一段演算法,而這個演算法早已有開源的 C 源碼可參考,比如說 MD5 或 SHA1 好了。你要怎麼應用到 AutoIt 上呢?用 AutoIt 語法重寫是練功的好辦法,但可不實用,畢竟速度差太多了。把它編譯成 DLL 讓 AutoIt 調用如何?是個可行的辦法,但需要額外的檔案,你又不想用 FileInstall 等方法臨時寫入硬碟。
我提出過一種方式:把編譯好的 DLL 轉成 Hex 字串並以本文儲存,再配合我開發的 MemoryDLL 函式庫做內嵌 DLL 的調用。這是一個很好的解決辦法,但如果你是絕對的完美主義者,嫌 DLL 太過臃腫,或是速度要求真的很高,連 MemoryDLL 都嫌慢了,那本篇的技巧就派上用場了。(DLL 函式和機械碼函式的執行速度差異不大,但調用函式的速度,或者說從 AutoIt 跳轉到函式入口的速度,MemoryDLL 會比 DllCallAddress 慢上一些。當執行函式本身所花的時間小於跳轉經過的時間,這種差異就會很明顯。)
一直以來,要在 AutoIt 裡調用機械碼,或者說是調用內存位址,只能靠 CallWindowProc API 或是我開發的 MemoryFuncCall 函式。但自從 AutoIt 3.3.8.0 版開始,新增了 DllCallAddress 函式,其實也就是 MemoryFuncCall 的內建版,讓內存位址的調用更為容易,也不再需要額外的函式庫。只是沒有一定的步驟,往往做不出適合調用的機械碼。現在就開始討論要如何生成 AutoIt 可調用的機械碼,以及要如何調用等問題吧。話說在前頭,我雖然已盡力簡化流程,但還是需要一定的技術成份。如果真有不懂的地方,Google 或百度是你的好朋友,當然回文討論也是很 ok 的。
首先,我需要個源碼做例子,常用的 MD5 什麼的都太沒新鮮感了,而且需要一些進階的技巧來處理產生的機械碼,這次只想先說說比較基本的東西。跟各位介紹一個我最近發現的有趣玩意:xxHash。簡單說,它一個是 32 位元的 Hash 算法,可取代 CRC32 做檔案內容驗證。但它的可靠度比 CRC32 強,達到與 32 位元的 MD5 相當,且運算速度是 MD5 的 22 倍,CRC32 的 17 倍,厲害吧。此演算法由 Yann Collet 開發,在 http://code.google.com/p/xxhash/ 可下載演算法的源碼。
先下載並研究一下這份源碼吧,很簡單只有兩個檔案,xxHash.c 和 xxHash.h。裡面有兩個可供外部調用的函式,XXH_fast32 和 XXH_strong32。一個是快速版本,一個是強化版本。我們的目標就是寫出兩個功能與其相同的 AutoIt 函式,當然,是機械碼函式。
除了此份源碼以外,你還會用到三個工具。其一當然是 C 源碼的編譯器,推薦的版本是 GCC4.x/MinGW 與 MinGW-w64(如果你需要相容 64 位元的 AutoIt)。另外你還需要 ObjConv,這是神人 Agner Fog. 所開發的強悍 OBJ 檔案工具,可在 http://www.agner.org/optimize/objconv.zip 下載。最後一個工具是 FASM 組譯器,請至 FASM 官網下載。
工具都準備好了嗎?那就進入命令列模式,跟著我一步一步來做吧。首先把 C 源碼編譯起來,鍵入指令:
一下就通過了且沒有任何錯誤訊息,痛快啊。如果你準備的其它源碼編譯過不了,那可能就要自己想辦法囉!編譯通過之後我們得到的檔案是 xxHash.o,它是一個標準的 COFF32 OBJ 檔。下一個指令是:
這指令幹嘛呢?雖然 ObjConv 的功能很好很強大,但我們需要的只是「反組譯」的功能而已。ObjConv 可以很完美的把 OBJ 檔反組譯成指定的 ASM 格式,至於其它的功能就有興趣再自己研究吧。為什麼要把已生成的 OBJ 檔又反組譯回 ASM 呢?OBJ 裡面不就是機械碼了嗎?這就是關鍵了。OBJ 檔裡的機械碼一來提取不易,二來格式常常不符。但從頭到尾自己寫 ASM 又沒那功夫。所以我的做法是,反組譯 C 編譯器的結果成 ASM 後再來修改!當然也可以直接用 gcc -S 生成 ASM,但之後要提取機械碼會比較困難一點。
好了,下一步,用文字編輯器看看上一個指令的成果 xxHash.asm 並做修改。因為我慣用的組譯器是 FASM,所以我提出的修改是為了通過 FASM 的組譯。如果你慣用 NASM 或其它組譯器,可以依我說的要領自行應變。
- 開頭所有 global 宣告都刪掉,如 global _XXH_small 什麼的。
- 所有 SECTION 都刪掉,如 SECTION .text、SECTION .data 等。
- 檔頭新增一行: use32,指明產生 32 位元機械碼。
好了,完工!就是這麼簡單,趕快來組譯看看:
一切順利的話,xxHash.bin 這個我們期待已久的二進位檔就跑出來了。它裡面沒有多餘的格式,就只有我們要的機械碼,也完全符合 AutoIt 調用所需。當然在一些複雜的情況下,ASM 可能要做更多的修改才能符合調用所需。但今天講的是基礎,所以我也特意挑了個不用額外修改的 C 源碼,其它修改技巧留待以後有機會再說吧。
機械碼已經有了,下一個問題就是怎麼在 AutoIt 裡面調用呢?在討論之前,我先請各位把下面幾行加到 xxHash.asm 的開頭(use32 的下一行),再重新組譯一次。 db 0xFF, 0x01
jmp _XXH_fast32
db 0xFF, 0x02
jmp _XXH_strong32
為什麼要這樣做呢?因為這段機械碼裡有三個函式,其中兩個是我們要透過 DllCallAddress 調用的,偏偏這兩個函式的進入點都不在開頭(當然也不可能同時在開頭)。加上這一小段標記和跳轉指令,可方便我們在 AutoIt 裡找到函式的進入點。重新組譯之後,把生成的 xxHasn.bin 轉成 Hex 字串格式(別說你不會啊),再寫一段 AutoIt 程式把這串機械碼包裝成 AutoIt 函式。實際的範例如下:; -----------------------------------------------------------------------------
; xxHash Machine Code UDF
; Purpose: Provide The Machine Code Version of xxHash Algorithm In AutoIt
; Required: AutoIt v3.3.8.0
; Author: Ward
; xxHash Copyright (C) 2012, Yann Collet.
; -----------------------------------------------------------------------------
#Include-once
#Include <Memory.au3>
Global $_xxHash_CodeBuffer, $_xxHash_CodeBufferPtr
Global $_xxHash_FastPtr, $_xxHash_StrongPtr
Func _xxHash_Shutdown()
$_xxHash_CodeBuffer = 0
_MemVirtualFree($_xxHash_CodeBufferPtr, 0, $MEM_RELEASE)
EndFunc
Func _xxHash_Startup()
If Not IsDllStruct($_xxHash_CodeBuffer) Then
Local $Code
If @AutoItX64 Then
$Code = '0xFF01E997000000FF02E9DA0100004863C24883EC084181E84F86C861488D040141B9B16756164C8D50FCEB27478D0C0844030941FFC04883C1044589CB41C1CB0F4569DB2FEBD427478D0C0B4569C9B179379E4C39D172D4EB18440FB611478D0C0848FFC141FFC04501D14569C9B179379E4839C172E3418D141189D0C1E80F31D069D077CAEB8589D0C1E80D31D069D03DAEB2C289D0C1E81031D05AC383FA0F537F065BE964FFFFFF4181E84F86C8614C63DA4169C077CAEB854E8D1C19498D5BF001D04469D03DAEB2C24469C833FC435AEB2141C1C813C1C81544030103410441C1CA0F41C1C90D440351084403490C4883C1104839D972DA4489C1C1C90F468D040189C1C1C90D8D04014489D1C1C913468D14114489C94569C0B179379E450343F0C1C9154569D2B179379E450353F8468D0C0969C0B179379E410343F44569C077CAEB854569C9B179379E44034B0C4489C14569D277CAEB85C1C915468D04015B69C077CAEB854489D14569C977CAEB85C1C90D468D14114189C341C1CB0F4101C34489C8C1C8134569DB3DAEB2C2468D0C084169CA3DAEB2C241C1CB1DC1C91A4569C03DAEB2C2418D0C0B4169C13DAEB2C24401C1C1C81701C169D2B179379E89C8C1E80B31C88D94107FD278B689D0C1E80F31D069D077CAEB8589D0C1E80D31D0C356534883EC0883FA0F7F085B5B5EE913FEFFFF4181E84F86C8614C63DA4169C077CAEB854E8D1C19498D5BF001D04469D03DAEB2C24469C833FC435AEB534489C6C1CE13468D040689C6C1CE158D04064489D6C1CE0F468D14164489CEC1CE0D468D0C0E4569C0B179379E44030169C0B179379E0341044569D2B179379E440351084569C9B179379E4403490C4883C1104839D972A84489C1C1C90F468D040189C1C1C90D8D04014489D1C1C913468D14114489C94569C0B179379E450343F0C1C9154569D2B179379E450353F8468D0C0969C0B179379E410343F44569C077CAEB854569C9B179379E44034B0C4489C14569D277CAEB85C1C915468D040169C077CAEB854489D14569C977CAEB85C1C90D468D14114189C341C1CB0F4101C34489C8C1C8134569DB3DAEB2C2468D0C084169CA3DAEB2C241C1CB1DC1C91A4569C03DAEB2C2418D0C0B4169C13DAEB2C24401C1C1C81701C189C869D2B179379EC1E80B31C8598D94107FD278B65B89D0C1E80F31D069D077CAEB855E89D0C1E80D31D0C3'
Else
$Code = '0xFF01E989000000FF02E9C301000055B9B167561689E5578B4508568B5510538B5D0C81EA4F86C8618D1C188D73FCEB1D8D0C0A42030883C00489CFC1C71169FF2FEBD4278D0C0F69C9B179379E39F072DFEB100FB6308D0C0A404201F169C9B179379E39D872EC034D0C5B5E89C8C1E80F31C869D077CAEB855F5D89D0C1E80D31D069D03DAEB2C289D0C1E81031D0C35589E557565383EC088B450C8B55088B4D1083F80F8945F07F0B5B5E5B5E5F5DE959FFFFFF81E94F86C8618B7DF069C177CAEB850345F001D7897DEC69F03DAEB2C283EF1069D833FC435AEB1AC1C10DC1C00B030A034204C1C611C1C313037208035A0C83C21039FA72E289CAC1CA0F8D0C0A89C2C1CA0D01C289F0C1C8138D343089D8C1C8158D1C1869C2B179379E8B55EC69C9B179379E03470469F6B179379E034AF003770869C077CAEB8569C977CAEB8569DBB179379E035F0C69F677CAEB8589CAC1C20B69DB77CAEB858D0C0A89C2C1C2118D040289F2C1C2138D343289DAC1C20D69C03DAEB2C269F63DAEB2C28D1C1A69C93DAEB2C269DB3DAEB2C2C1C606C1C00301F001C8C1C3096955F0B179379E8D1C1889D8C1E80B31D88D94107FD278B689D0C1E80F31D069D077CAEB8589D0C1E80D31D05A595B5E5F5DC35589E557565383EC108B450C8B4D088B551083F80F8945EC7F0C83C4105B5E5F5DE917FEFFFF8B7DEC81EA4F86C86169C277CAEB850345EC01CF897DE883EF1069F03DAEB2C269D833FC435A897DE4EB4289D7C1C70D01FA89C7C1C70B01F889F7C1C71101FE89DFC1C71301FB69D2B179379E69C0B179379E69F6B179379E69DBB179379E031103410403710803590C83C1103B4DE472B989D18B7DE4C1C90F01D189C2C1CA0D894DF08D0C0289F0C1C8138D343089D8C1C8158D1C1869C1B179379E8B4DE86955F0B179379E03470469F6B179379E0351F003770869C077CAEB8569D277CAEB8569DBB179379E035F0C69F677CAEB8589D1C1C10B69DB77CAEB858D141189C1C1C1118D040189F1C1C1138D343189D9C1C10D69C03DAEB2C269F63DAEB2C28D1C1969D23DAEB2C269DB3DAEB2C2C1C606C1C00301F001D0C1C3096955ECB179379E8D1C1889D8C1E80B31D88D94107FD278B689D0C1E80F83C41031D069D077CAEB855B5E5F89D0C1E80D31D05DC3'
EndIf
Local $Offset_Fast = (StringInStr($Code, "FF01") + 1) / 2
Local $Offset_Strong = (StringInStr($Code, "FF02") + 1) / 2
Local $Opcode = Binary($Code)
$_xxHash_CodeBufferPtr = _MemVirtualAlloc(0, BinaryLen($Opcode), $MEM_COMMIT, $PAGE_EXECUTE_READWRITE)
$_xxHash_CodeBuffer = DllStructCreate("byte[" & BinaryLen($Opcode) & "]", $_xxHash_CodeBufferPtr)
DllStructSetData($_xxHash_CodeBuffer, 1, $Opcode)
$_xxHash_FastPtr = $_xxHash_CodeBufferPtr + $Offset_Fast
$_xxHash_StrongPtr = $_xxHash_CodeBufferPtr + $Offset_Strong
OnAutoItExitRegister("_xxHash_Shutdown")
EndIf
EndFunc
Func _xxHash_Fast($Data, $Seed = 0)
If Not IsDllStruct($_xxHash_CodeBuffer) Then _xxHash_Startup()
$Data = Binary($Data)
Local $Len = BinaryLen($Data)
Local $Buffer = DllStructCreate("byte[" & $Len & "]")
DllStructSetData($Buffer, 1, $Data)
Local $Ret = DllCallAddress("uint:cdecl", $_xxHash_FastPtr, "ptr", DllStructGetPtr($Buffer), "uint", $Len, "uint", $Seed)
Return $Ret[0]
EndFunc
Func _xxHash_Strong($Data, $Seed = 0)
If Not IsDllStruct($_xxHash_CodeBuffer) Then _xxHash_Startup()
$Data = Binary($Data)
Local $Len = BinaryLen($Data)
Local $Buffer = DllStructCreate("byte[" & $Len & "]")
DllStructSetData($Buffer, 1, $Data)
Local $Ret = DllCallAddress("uint:cdecl", $_xxHash_StrongPtr, "ptr", DllStructGetPtr($Buffer), "uint", $Len, "uint", $Seed)
Return $Ret[0]
EndFunc
沒幾行,相信對熟悉 AutoIt 的各位來說不困難,對不對?不過還是做些簡單的說明好了。我們對 Hex 字串做的第一件事是用 StringInStr 查看 FF01 和 FF02 在哪裡,這個 FF01 和 FF02 就是上面要求各位加上的標記。經過簡單的運算,就可算出指定函式入口的跳轉位址了。當然也可以不要用標記和跳轉,直接計算函式入口的偏移位址並呼叫,但標記的方式可讓我們以後不管怎麼修改 ASM 源碼,都不用重新計算入口偏移,相對的方便許多。而且有的時候,同一段機械碼可能有七八個以上的函式入口,要一一計算入口偏移也是相當累人啊,不如用標記來的簡單。
接著看下去,算出跳轉位址後,把 Hex 字串轉成 Binary 格式,再跟系統要一塊內存來存放它。可不是隨便的內存區塊都可以,要有 $PAGE_EXECUTE_READWRITE 的權限才行哪,不然在某些系統會產生 DEP 錯誤(防止資料執行)。所以申請內存的工作就交給 _MemVirtualAlloc 函式來做吧,可以順便指定權限。這裡為了簡單起見,沒有對 _MemVirtualAlloc 的失敗做處理,但實際上這麼一丁點的內存需求,幾乎是不太可能會失敗的。
為了達到執行效率最佳化,在初始化時就先把所有函式的入口位址(就是內存位址加偏移位址)算好,放在 Global 變數裡。而 _xxHash_Fast 和 _xxHash_Strong 兩個函式的工作,就只剩下把指定要計算 Hash 的資料用 DllStructCreate 和 DllStructSetData 存到內存,再用 DllCallAddress 呼叫我們算好的入口位址而已。要注意的是我們直接編譯 C 源碼而沒有指定 stdcall,所以呼叫函式時要記得指定 C 語言用的 cdecl 規範。另外就是參數的順序和格式,直接參考 xxHash.h 的宣告即可。如果你對 DllCallAddress 或 DllStruct 相關函式的用法還不熟,趕快去參考說明書吧。對機械碼函式來說,它們可是很重要的喔!
附帶一提,為了使用方便,我習慣在函式開頭加上自動初始化的步驟。但如果速度真的對你很重要,把這行是否要進行初始化的判斷拿掉,並確實在首次使用前呼叫初始化函式,可以加快函式的執行速度。還有 $Data = Binary($Data) 這行也是個可以修改的小地方。為什麼要加這行呢?因為後面有兩個地方使用 $Data 變數,且都要求是 Binary 格式。如果我們輸入的 $Data 資料是字串格式,又沒有先強制把 $Data 轉換為 Binary,那這兩個地方就會分別各自轉換,反而耗費更多時間。但如果你很確定呼叫這個函式時,$Data 就一定是 Binary 格式的資料,那這行轉換反而就是多餘的了。所以要如何最佳化你的函式,就看你如何使用它囉。(加個 IsBinary 判斷再決定要不要轉換如何?如果 $Data 是字串反而更慢,如果 $Data 是 Binary 則相差不多,結論是不如不加。)
好了,以上就是 32 位元機械碼函式製作的全部流程。那 64 位元的部份怎樣辦?別擔心,跟上面完全一樣的要領,只要改用 MinGW-w64 來編譯源碼,還有 ASM 檔頭的 use32 記得改成 use64 ,最後在 Hex 字串的地方,加個 @AutoItX64 的判斷就行了,其它地方都完全不用改啦。
用 AutoIt 包裝機械碼的方式差不多都講完了,再回過頭來總結一下機械碼的生成:我們把 C 源碼編譯成 OBJ 檔,反組譯出 ASM 本文,修改後再組譯回純二進位碼。其中最後的修改步驟,往往是最花心思的。因為這篇只是基礎介紹,所以我選的範例實做起來意外簡單。但其實大多數的情況,我們要修改定址存取指令,也要加入缺乏的外部函式,才能得到所需的機械碼。也因為必須做這些改變,我才會建議用這樣迂迴的方式。不然以本篇的例子來說,直接從 OBJ 或 DLL 提取機械碼也是完全可行的。
這篇教程就先到這邊告個段落,有機會再跟各位討論上述的「修改」所會遇到的問題。拋磚引玉,我介紹我熟悉的工具和我的做法,當然你可以照我的路走,但或許你也可以用你熟悉的東西,走出自己的路後,再分享給大家一起進步,共勉之! |