2013年10月25日 星期五

以 disasm 為例讓我們真槍實彈挑戰軟體的移植與編譯

今天本文的標題很簡單,讓我們來練習自己移植與編譯軟體的原始碼,講專業的一點就是
學習軟體環境的建構技術,也就是說本文的目標就是要示範給各位讀者知道如何在一支自己
完全陌生的軟體源碼中,自己能夠排除錯誤,對於不存在的函式,能夠移植或撰寫相容函式
將原始碼中缺的部分給補齊,最後下達編譯與連結指令製造出該軟體的執行檔。

許多程式設計人員最缺乏的就是這塊的能力,因為這是大家最不想面對也是最無趣的一環
最好有現成已經寫好的 Makefile 下個 make 或 nmake 就搞定,或是像 Visual Studio 下簡單的
按下 F7 ,完全不用知道 Visual Studio 幕後做了哪些事情就得到了編譯與連結後的檔案,然而
學好軟體環境的建構技術才是深耕自我編程能力的本源,怎麼講呢?因為一支不能編譯與
執行的原始碼,不論裡面內容寫得如何精彩,就是一堆垃圾,所以,學習怎麼自己拿到一支
程式碼會建構編譯環境,把有殘缺的部分補足,使得程式可以執行,這種能力比會寫程式更
重要,因為但能夠編譯過關與執行,讀者就會對該程式須要有哪些要素才能夠編譯會有一定
程度的掌握,當讀者要開始從原始碼裡頭抽出要的部分移入自己的專案時,才會知道還有哪
些其餘的要素也需要跟著遷移。

那要怎麼學習這種能力呢?這還真沒甚麼書在教,這是一種高強度依賴經驗的能力,通常
都是見招拆招,有時候對付同樣的情況,也許不同人的作法完全不同,所以,在下選了一支
很經典的程式帶領讀者入門如何在拿到一支陌生的專案原始碼懂著自己將程式編譯出來。

這支專案程式為 disasm,這是一支相當有名的反組譯程式,它也能對單行組合語言作
組譯,這支程式非常有名,它就是 OllyDbg 除錯器作者從 1.04 分來出來的小型反組譯專案
有在研究 x86 指令集模擬程式的人,大部分都從這支原始碼搭配 Intel 的程式設計手冊去深入
學習 x86 CPU 是怎麼去解釋 Opcode,有於 OllyDbg 1.04 的年代最熱門環境的就是 Windows 程
式設計,已經是 保護模式 32位元 環境,所以 disasm 是一支只實作 保護模式 32位元 環境下
的指令解碼,它會假設目前 CPU 處於保護模式,也就是說,預設的 size operator 為 32-bit,那
麼對於指令的 size operator 為 16-bit,則需要 prefix 做額外的描述,讓我們看看一個實際的範例

這邊我用 FASM 產生 mov eax,eaxmov ax,ax 的機械碼給各位看


讀者們可以看到,在保護模式下產生的機械碼:

mov eax,eax 為 89 C0
mov ax,ax   為 66 89 C0

第二條為 16-bit 指令,讀者可以發現多出一個 66,這個就是 prefix,有興趣可以去 FASM
的論壇抓,這是 FASM 發展上的重要里程碑,作者已經模組化組譯器核心成為 DLL,這個
軟體是附在這個模組化組譯器的範例內,示範怎麼呼叫該 DLL 即時產生機械碼,這個很像
現在 GPU 內的 Shading Language,由於 GPU 設計商不想要開放原始的 GPU 內部的機械碼
所以 GPU 的程式 Shading Language 會以原始碼文字串流形式直接輸入給驅動程式內部的
GPU 組譯器即時組譯出機械碼串流,而 FASM 的組譯器核心產生的則是 x86 機械碼串流,上
圖可以注意到,要記得使用 format binary,這樣組譯器核心只會把指令翻譯為機械碼,不會
帶有特性平台的特殊檔頭。

那本文要討論的 disasm 呢?它的任務正好跟 FASM 相反,讀者讓它吃 x86 機械碼串流,它的
能力是要能吐出組合語言串流,也就是原始碼串流,當然它還有可以單獨組譯一行為機械碼
的能力,不過這種功能在 FASM 前面是小巫見大巫,因為組語的除錯常常需要 OllyDbg 裡面
即時修改一段組合語言,所以 disasm 才會有這種功能,但是不可能像專業的組譯器會支援
標籤、化名,運算元是位址的話,得要全部都是輸入魔術般的數字,因為這種工具本質還是
在於除錯,修改組語的功能只是附加功能,以下是 disasm 的基本特色:

1. 認識所有標準 80x86 指令
2. 認識 FPU、MMX、3DNow!
3. 也可以正確反組譯 16-bit 指令於保護模式下的機械碼型式,像前面那種範例
4. 注意:它不認識 SSE 或 SSE2 指令

第 4 點要特別注意,那個時候,作者還沒有支援 SSE 或 SSE2 指令,所以讀者玩這支程式的
時候,不要讓它吃有 SSE 或 SSE2 指令機械碼的程式。

接下來難纏的部分就出來了,就是怎麼編譯這支程式呢?

很不幸的是作者原文說它是用 Borland C 寫,並不保證其它的編譯器會運作
嘿嘿,這就是在下挑這支程式來給大家做示範的原因,敝人就是偏偏要挑戰看看
到底用在下最愛的 Visual C++ 編譯器 CL 到底能不能過關,可以編譯過還要可以
順利執行。還有作者在解說文章裡面提到一段話

Please set the default character type to unsigned!

所以等一下我們也來看看 CL 有沒有這樣的功能,移植的基本步驟就是要盡可能的逼近
一支程式所使用的設定,而且要盡可能接近或找到等價於 Borland C 編譯器的功能。

我們先看看 disasm 專案的組成:

disasm.h  - common definitions  
disasm.c  - Disassembler  
assembl.c - Assembler  
asmserv.c - table of commands and service functions  
main.c    - demo program

對付不同使用不同編譯器的專案,第一步就是先用我們的 CL 編譯器看看會產生哪些錯誤

首先,先在該目錄下用編譯指令

cl /c *.c

上面的意思就是說只編譯不連結,而且編譯所有目錄下的 C 檔,所以多檔也不見得非得
寫 Makefile,其實編譯器老早就支援萬用字元啦,看看會出現甚麼錯誤:



囧,這種就是許多人自己編譯原始碼的噩夢,說沒有 dir.h,顯然這個與編譯器無關,這是
Borland C 環境才有的標頭檔,所以先解決少了該標頭檔的問題。

要解決 dir.h 就是先知道這裡面到底宣告的哪些函式,這邊在下就不仔細跟各位讀者說明,直
接告訴各位答案,Borland C 環境下的這個標頭檔,其實與 Linux 的 C 環境下 dirent.h 相容,所
以我常用的方法就是去找,那有沒有已經移植到 Win32 的版本,各位讀者可以找

dirent API for Microsoft Visual Studio

就可以找到 For Win32 版本的 dirent.h,然後把它的檔名改成 dir.h,放入 disasm 的專案內。
放入之後就在讓我們下達下面的編譯指令:

cl /I .\ /c *.c

為什麼編譯參數裡面多了 /I .\ ,因為原始檔內使用 <dir.h>,要能抓到用 < > 的標頭檔,在下
必須用 /I 參數指定當前目錄也是要收尋的路徑之一,.\ 表是當前目錄,接者看看發生啥事


根據編譯器給出的錯誤我們打開檔案看看:


錯誤的地方出現啦,Visual C++ 並不支援前置處理器有類似處理 C 語言型態轉換的能力
所以我們應該把原始碼改成

#if 0xFF!=255
#error Please set default char type to unsigned
#endif

或是整個註解掉解型,因為其實把 (char) 拿掉這個前置處理條件就變成一句廢話,拿掉只是為
了要讓編譯過關,但是看看裡面的 #error,作者提醒我們它的程式得要把 char 視為 unsigned
所以查閱 CL 的指令選像我們發現了 /J 參數

MSDN解釋 /J 變更預設 char 型別

所以我們應該還要在編譯的時候加上 /J,然後在讓我們編譯看看:


各位讀者可以看到,我們終於編譯過關啦,現在目錄底下有一堆目的檔等著被連結,所以
馬上在下達連結指令試試看,結果如圖:


囧,出現 Link Error 啦,竟然出現 _pow10l 函式無法被解析,怎麼會這樣呢?沒甚麼好擔心
這也是常常出現的錯誤,就是說顯然 Borland C 的 math 程式庫有這個數學函式,而我們心愛的
Visual C++ 沒有這個函式,怎麼辦,那就得自己模擬一下,先看看原本 Borland C 裡面這條
函式長甚麼模樣:

long double pow10l(int p)

所以我們就在專案的 common definitions 裡面也就是 disasm.h 實現這條函式
讓需要的 C 檔通通可以吃的到,這邊讀者就是可以順便學習另外一種函式庫的型式
直接以原始碼型式存在的函式庫,它們都被放在 H 檔裡頭,而且通通都是 static
這樣就可以確保呼叫的編譯單元一人一份不會打架,這通常是解決自己不熟悉的專案原始碼
有缺少特性函式的慣用手法,等到較為熟悉後在修改為其他形式。下面給出我改好
的 disasm.h




上面就是在下補進去的實作版本,很簡單的又呼叫了 pow 在實現一次以 10 為基底的
指數函式而已。再讓我們整個重新編譯與連結程式吧。



各位可以看到,disasm 程式到此移植完成,連結也過關了,最終的執行檔讓我們跑跑看
試試有沒有甚麼問題:



最終程式執行結果可以看到,我們的移植版本沒甚麼問題,程式完全跑出了 main.c 裡面
所寫的測試機械碼串流所反組譯的結果,最後一個測試是故意測試組譯的功能能不能發現
operand size 上的錯誤,前一個測試為語法正確下所產生的機械碼。

討論 x86 組譯與反組譯是相當複雜的課題,本文不會討論任何 disasm 內的原始碼,不
過有興趣的讀者,在下已經把本文內的可以經過 CL 編譯的 disasm 版本包含原始碼
分享於以下網址,想練習本篇移植與編譯技巧的讀者也可以下載試試。

disasm_vc.7z

http://sdrv.ms/1blbYrF

1 則留言: