反匯編的目的
缺乏某些必要的說(shuō)明資料的情況下, 想獲得某些軟件系統(tǒng)的源代碼、設(shè)計(jì)思想及理念, 以便復(fù)制, 改造、移植和發(fā)展;
從源碼上對(duì)軟件的可靠性和安全性進(jìn)行驗(yàn)證,對(duì)那些直接與CPU 相關(guān)的目標(biāo)代碼進(jìn)行安全性分析;
涉及的主要內(nèi)容
分析ARM處理器指令的特點(diǎn),以及編譯以后可執(zhí)行的二進(jìn)制文件代碼的特征;
將二進(jìn)制機(jī)器代碼經(jīng)過(guò)指令和數(shù)據(jù)分開(kāi)模塊的加工處理;
分解標(biāo)識(shí)出指令代碼和數(shù)據(jù)代碼;
然后將指令代碼反匯編并加工成易于閱讀的匯編指令形式的文件;
下面給出個(gè)示例,匯編源代碼,對(duì)應(yīng)的二進(jìn)制代碼,以及對(duì)應(yīng)的反匯編后的結(jié)果
源代碼:
二進(jìn)制代碼:
反匯編后的結(jié)果:
反匯編軟件要完成的工作就是在指令和數(shù)據(jù)混淆的二進(jìn)制BIN文件中,分解并標(biāo)識(shí)出指令和數(shù)據(jù),然后反匯編指令部分,得到易于閱讀的匯編文件,如下圖:
ARM體系結(jié)構(gòu)及指令編碼規(guī)則分析
略,請(qǐng)參考相關(guān)資料,如ARM Limited. ARM Architecture Reference Manual [EB/OL]。 http://infocenter.arm.com/等;
主要可參考下圖,ARM指令集的編碼:
ARM可執(zhí)行二進(jìn)制BIN文件分析
目前主要的ARM可執(zhí)行文件種類:
ELF文件格式:Linux系統(tǒng)下的一種常用、可移植目標(biāo)文件格式;
BIN文件:直接的二進(jìn)制文件,內(nèi)部沒(méi)有地址標(biāo)記,里面包括了純粹的二進(jìn)制數(shù)據(jù);一般用編程器燒寫(xiě)時(shí),從0開(kāi)始,而如果下載運(yùn)行,則下載到編譯時(shí)的地址即可;
HEX格式:Intel HEX文件是記錄文本行的ASCII文本文件;
本文主要研究BIN文件的反匯編;
BIN映像文件的結(jié)構(gòu)
ARM程序運(yùn)行時(shí)包含RO,RW,ZI三部分內(nèi)容,RO(READONLY),是代碼部分,即一條條指令,RW(READWRITE),是數(shù)據(jù)部分,ZI,是未初始化變量。其中RO和RW會(huì)包含在映像文件中,因?yàn)橐粋€(gè)程序的運(yùn)行是需要指令和數(shù)據(jù)的,而ZI是不會(huì)包含在映像文件的,因?yàn)槠渲袛?shù)據(jù)都為零,程序運(yùn)行前會(huì)將這部分?jǐn)?shù)據(jù)初始化為零。
ARM映像文件是一個(gè)層次性結(jié)構(gòu)的文件,包括了域(region),輸出段(output section)和輸入段(input section)。一個(gè)映像文件由一個(gè)或者多個(gè)域組成,每個(gè)域最多由三個(gè)輸出段(RO,RW,IZ)組成,每個(gè)輸出段又包含一個(gè)或者多個(gè)輸入段,各個(gè)輸入段包含了目標(biāo)文件中的代碼和數(shù)據(jù)。
域(region):一個(gè)映像文件由一個(gè)或多個(gè)域組成。是組成映象文件的最大結(jié)構(gòu)。所謂域指的就是整個(gè)bin映像文件所在的區(qū)域,又分為加載域和運(yùn)行域,一般簡(jiǎn)單的程序只有一個(gè)加載域。
輸出段(output section):有兩個(gè)輸出段,RO和RW。
輸入段(input section):兩個(gè)輸入段,CODE和DATA部分,CODE部分是代碼部分,只讀的屬于RO輸出段,DATA部分,可讀可寫(xiě),屬于RW輸出段。
ARM的BIN映像文件的結(jié)構(gòu)圖
舉一個(gè)例子,ADS1.2自帶的examples里的程序
AREA Word, CODE, READONLY ; name this block of code
num EQU 20 ; Set number of words to be copied
ENTRY ; mark the first instruction to call
start
LDR r0, =src ; r0 = pointer to source block
LDR r1, =dst ; r1 = pointer to destination block
MOV r2, #num ; r2 = number of words to copy
wordcopy
LDR r3, [r0], #4 ; a word from the source
STR r3, [r1], #4 ; store a word to the destination
SUBS r2, r2, #1 ; decrement the counter
BNE wordcopy ; 。。。 copy more
stop
MOV r0, #0x18 ; angel_SWIreason_ReportException
LDR r1, =0x20026 ; ADP_Stopped_ApplicationExit
SWI 0x123456 ; ARM semihosting SWI
AREA BlockData, DATA, READWRITE
src DCD 1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,1,2,3,4
dst DCD 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
END
可以看出,該程序由兩部分組成,CODE和DATA,即代碼部分和數(shù)據(jù)部分。其中代碼部分,READONLY,屬于RO輸出段;數(shù)據(jù)部分,READWRITE,屬于RO輸出段。
接下來(lái)再看看上述代碼經(jīng)過(guò)編譯生成的BIN映像文件的二進(jìn)制形式,及該映像文件反匯編后的匯編文件,如下圖:
從圖中我們很容易發(fā)現(xiàn),BIN文件分成了兩部分,指令部分和數(shù)據(jù)部分。先看一下左圖,從中我們發(fā)現(xiàn),BIN文件的第一條指令編碼是0xe59f0020,即右圖中的00000000h到00000003h,由于存儲(chǔ)方式的原因,小端模式,指令的低字節(jié)存放在低地址部分,不過(guò)這不影響我們的分析。在BIN文件中從00000000h開(kāi)始一直到00000027h都是指令部分,即RO輸出段,最后一條指令0xef123456存儲(chǔ)在在BIN文件的00000024h到00000027h。剩下的為數(shù)據(jù)部分,即RW輸出段,有興趣的讀者可以對(duì)照源代碼一一查找之間的對(duì)應(yīng)關(guān)系。
ARM反匯編軟件設(shè)計(jì)要解決的主要問(wèn)題
一、指令與數(shù)據(jù)的分離
馮·諾依曼機(jī)器中指令和數(shù)據(jù)是不加區(qū)別共同存儲(chǔ)的,以 0、1 二進(jìn)制編碼形式存在的目標(biāo)代碼對(duì)于分析人員來(lái)說(shuō),很難讀懂其含義。二進(jìn)制程序中指令和數(shù)據(jù)混合存放,按地址尋址訪問(wèn),反匯編如果采取線性掃描策略,將無(wú)法判斷讀取的二進(jìn)制編碼是指令還是數(shù)據(jù),從而無(wú)法實(shí)現(xiàn)指令和數(shù)據(jù)的分離。
那么,怎樣才能實(shí)現(xiàn)指令和數(shù)據(jù)的分離?
眾所周知,凡是指令,控制流是必經(jīng)之處,凡是數(shù)據(jù),數(shù)據(jù)流是必到之處,存取指令一定會(huì)訪問(wèn),對(duì)于一般指令,控制流是按地址順序遞增而走向的,只有在出現(xiàn)各種轉(zhuǎn)移指令時(shí),控制流才出現(xiàn)偏離。因此,抓住控制流這一線索,即跟蹤程序的控制流[9]走向而遍歷整個(gè)程序的每一條指令,從而達(dá)到指令與數(shù)據(jù)分開(kāi)的目的。
怎樣才能跟蹤程序的控制流呢?
一般來(lái)說(shuō)控制流與控制轉(zhuǎn)移指令有關(guān),控制轉(zhuǎn)移指令一般可分為兩大類:
單分支指令,即直接跳轉(zhuǎn),如B;BL;MOV PC,**;LDR PC,**等等;
雙分支指令,有條件的跳轉(zhuǎn),如BNE;MOVNE PC,**等等
當(dāng)該指令為雙分支指令時(shí),會(huì)有兩個(gè)轉(zhuǎn)向地址,我們把條件滿足時(shí)的轉(zhuǎn)向地址稱為顯示地址,條件不滿足時(shí)的地址稱為隱式地址。
在跟蹤控制流的過(guò)程中,還要設(shè)置三個(gè)表:
(1)段表,將所有轉(zhuǎn)移指令除條件轉(zhuǎn)移的轉(zhuǎn)移地址填入此表包括本指令地址和轉(zhuǎn)向地址。其實(shí)可以不要這個(gè)表,但是加進(jìn)去,會(huì)得到若干段的代碼段,比較清晰明了。
(2)返回表,用于記錄程序調(diào)用時(shí)的返回地址。
?。?)顯示表,碰到雙分支指令時(shí),將其顯示地址和現(xiàn)場(chǎng)(程序各寄存器的值)填入該表中。
以上都準(zhǔn)備好之后,就可以開(kāi)始跟蹤程序控制流了具體步驟如下:
?。?)將程序的起始地址填入段表,且作為當(dāng)前地址。
?。?)按當(dāng)前地址逐條分析指令類型,若非無(wú)條件轉(zhuǎn)移指令及二分支指令,則直至終結(jié)語(yǔ)句并轉(zhuǎn)(7), 否則轉(zhuǎn)(3)。
?。?)若為無(wú)條件轉(zhuǎn)移指令(B指令,MOV PC,0x16等),則將此指令所在地址填段表,其顯式地址填段表,且將顯式地址作為當(dāng)前地址,然后轉(zhuǎn)(2),否則轉(zhuǎn)(4)。
?。?)若為無(wú)條件轉(zhuǎn)移指令子程序調(diào)用指令(BL指令),則將此指令所在地址填段表,返回地址和現(xiàn)場(chǎng)信息填入返回表,顯式地址填段表,且將顯式地址作為當(dāng)前地址,然后轉(zhuǎn) (2), 否則轉(zhuǎn)(5)。
(5)若為無(wú)條件轉(zhuǎn)移指令中的返回指令(MOV PC,LR),則在返回地址表中按“后進(jìn)先出”原則找到返回地址,將此指令所在地址填段表,其返回地址填段表,且將返回地址作為當(dāng)前地址,然后轉(zhuǎn)(2),否則轉(zhuǎn)(6)。
?。?)若為二叉點(diǎn)指令(BEQ,MOVEQ PC,0x16等等), 則將顯式地址和現(xiàn)場(chǎng)信息填入顯式表,然后將隱式地址作為當(dāng)前地址轉(zhuǎn)(2)。
?。?)判顯式表空否,若空則算法終止,否則從顯式表中按“先進(jìn)先出”順序取出一個(gè)顯式地址作為當(dāng)前地址,且恢復(fù)當(dāng)時(shí)的現(xiàn)場(chǎng)信息轉(zhuǎn)(2)。
經(jīng)過(guò)以上處理,可以遍歷到所有的指令,且當(dāng)訪問(wèn)到該條指令后,要把改地址處的指令標(biāo)記為指令。接下來(lái)可以采用線性掃描策略,當(dāng)遇到標(biāo)記為指令的二進(jìn)制代碼時(shí),把它反匯編成匯編指令即可。
不過(guò),在實(shí)現(xiàn)跟蹤程序控制流過(guò)程中還有一個(gè)比較難處理的問(wèn)題,就是間接轉(zhuǎn)移類指令的處理,因?yàn)檫@類指令的轉(zhuǎn)移地址隱含在寄存器或內(nèi)存單元中,無(wú)法由指令碼本身判斷其值,而且這些隱含在寄存器內(nèi)或內(nèi)存單元中的值往往在程序執(zhí)行時(shí),被動(dòng)態(tài)地進(jìn)行設(shè)置或修改,因此很難判斷這類指令的轉(zhuǎn)移地址,從而難以完整的確定程序的指令區(qū)。
本軟件處理這個(gè)問(wèn)題的方法是設(shè)置多個(gè)寄存器全局變量,在緊跟程序控制流的過(guò)程中,實(shí)時(shí)更新各寄存器的值,因此可以確定這類指令的轉(zhuǎn)移地址。
二、代碼部分的反匯編
ARM指令集中的每天指令都有一個(gè)對(duì)應(yīng)的二進(jìn)制代碼,如何從二進(jìn)制代碼中翻譯出對(duì)應(yīng)的指令,即代碼部分反匯編所要完成的工作。
ARM 指令的一般格式可以表示為如下形式:
《opcode》{condition}{S}《operand0》{!},《operand1》{, 《operand2》}
指令格式中《·》符號(hào)內(nèi)的項(xiàng)是必須的,{·}符號(hào)內(nèi)的項(xiàng)是可選的,/ 符號(hào)表示選其中之一,其中opcode 表示指令操作符部分,后綴 conditon、S 及!構(gòu)成了指令的條件詞,operand0、operand1、operand2 為操作數(shù)部分。指令反匯編就是將指令的各組成部分解析出來(lái)。
為了使指令反匯編更加清晰、高效,可以采用分類的思想方法來(lái)解決該問(wèn)題,把那些具有相同編碼特征的指令分成同一種類型的指令,然后為每一類指令設(shè)計(jì)一個(gè)與之對(duì)應(yīng)的處理函數(shù)。
指令的反匯編的步驟,首先是判斷哪條指令,可以由操作符及那些固定位來(lái)確定,然后是指令條件域的翻譯,這個(gè)與二進(jìn)制編碼也有唯一的對(duì)應(yīng)關(guān)系,然后操作數(shù)的翻譯,在操作數(shù)的翻譯過(guò)程中,可能涉及到各種操作數(shù)的計(jì)算方法,移位操作等情況。
ARM反匯編軟件的設(shè)計(jì)
ARM反匯編軟件的總體設(shè)計(jì)方案如下圖:
其中,各模塊主要完成以下功能:
輸入模塊:要解決如何從外部文件中讀取二進(jìn)制格式的文件,另外對(duì)讀進(jìn)來(lái)的目標(biāo)代碼要合理組織和存儲(chǔ),便于接下來(lái)的后續(xù)處理。
指令和數(shù)據(jù)分離模塊:在內(nèi)存中對(duì)讀進(jìn)來(lái)的二進(jìn)制源代碼進(jìn)行分析,指令流可以到達(dá)的部分標(biāo)識(shí)為代碼,最后剩下的指令流未經(jīng)過(guò)的部分為數(shù)據(jù),這樣就分離出代碼和數(shù)據(jù)。
指令反匯編模塊:對(duì)分離出來(lái)的代碼段部分,按照各條指令編碼的對(duì)應(yīng)關(guān)系進(jìn)行反匯編,生成目標(biāo)代碼對(duì)應(yīng)的匯編文件,包括ARM指令地址,ARM指令,可以用于閱讀。
輸出模塊:即要將反匯編后的匯編文件顯示在窗口,且可以生成文件在磁盤上。
ARM處理器反匯編軟件流程
ARM反匯編軟件的整體工作流程如下圖所示,首先是讀取目標(biāo)二進(jìn)制文件至內(nèi)存,然后采用兩遍掃描的策略,第一遍掃描的目的是區(qū)分指令和數(shù)據(jù),第二遍掃描是將指令部分反匯編成匯編指令形式,將數(shù)據(jù)部分直接翻譯成數(shù)據(jù),最后將結(jié)果輸出顯示。
模塊設(shè)計(jì)-輸入模塊
輸入模塊的流程圖如下圖所示,首先從目標(biāo)二進(jìn)制代碼中讀取四個(gè)字節(jié)存放到Content對(duì)象里,再將Content對(duì)象存放到Vector容器里,然后按上述操作繼續(xù)讀取文件,直到文件結(jié)尾。這里有一點(diǎn)要說(shuō)明的是,由于時(shí)間等原因,本軟件只考慮32位ARM指令的反匯編,Thumb指令反匯編不會(huì)涉及,所以每次都讀取4個(gè)字節(jié)。
模塊設(shè)計(jì)-指令和數(shù)據(jù)分離模塊
指令和數(shù)據(jù)分離模塊的設(shè)計(jì)如下圖所示,由于指令和數(shù)據(jù)分離模塊設(shè)計(jì)的關(guān)鍵是跟蹤程序的控制流,標(biāo)識(shí)出每一條指令,所以此模塊的關(guān)鍵就是要遍歷每一條指令,而遍歷每一條指令的關(guān)鍵是要緊跟PC值。
關(guān)于段表,顯示表,返回表的概念前面已經(jīng)說(shuō)明過(guò)了。另外,在程序流程圖中的“根據(jù)具體轉(zhuǎn)移指令,更新PC值,顯示表,返回表”,這里的具體情況如下:
?。?)若為無(wú)條件轉(zhuǎn)移指令(B指令等,MOV PC,0x16),則將此指令所在地址填段表,其顯式地址填段表,且將顯式地址作為當(dāng)前PC地址。
?。?)若為無(wú)條件轉(zhuǎn)移指令子程序調(diào)用指令(BL指令),則將此指令所在地址填段表,返回地址填入返回地址表,顯式地址填段表,且將顯式地址作為當(dāng)前PC地址
(3)若為無(wú)條件轉(zhuǎn)移指令中的返回指令(MOV PC,LR),則在返回地址表中按“后進(jìn)先出”原則找到返回地址,將此指令所在地址填段表,其返回地址填段表,且將返回地址作為當(dāng)前PC地址。
(4)若為二叉點(diǎn)指令(BEQ,MOVEQ PC,0x16等等), 則將顯式地址填入顯式地址表(還要保存當(dāng)時(shí)的寄存器值),然后將隱式地址作為當(dāng)前PC地址。
模塊設(shè)計(jì)-反匯編模塊
在反匯編模塊中除了要反匯編指令,還要翻譯數(shù)據(jù)??偟脑O(shè)計(jì)思想是依次從裝滿對(duì)象的contentVector容器中依次取出對(duì)象,判斷該對(duì)象是指令還是數(shù)據(jù),指令的話,就反匯編成匯編指令形式,數(shù)據(jù)的話,直接翻譯成數(shù)據(jù)的值,如下圖所示。
上述流程圖是總的反匯編模塊,在圖中的指令反匯編部分,是該模塊的重點(diǎn),也是整個(gè)反匯編軟件設(shè)計(jì)的重點(diǎn),其流程圖如下圖所示。
模塊設(shè)計(jì)-輸出模塊
關(guān)于顯示模塊,比較簡(jiǎn)單,直接把結(jié)果顯示出來(lái)即可,該模塊跟第三個(gè)模塊反匯編模塊聯(lián)系最緊密,在反匯編模塊中,其實(shí)已經(jīng)包含了輸出模塊。不過(guò)輸出模塊也有自己的特殊任務(wù)。比如說(shuō)如何以十六進(jìn)制形式顯示一個(gè)32位數(shù)(顯示地址的時(shí)候)以及如何顯示一個(gè)8位數(shù)(顯示讀進(jìn)來(lái)數(shù)據(jù)的編碼,因?yàn)槭且粋€(gè)字節(jié)一個(gè)字節(jié)讀進(jìn)來(lái)的,存放的時(shí)候也是一個(gè)字節(jié)一個(gè)字節(jié)存放在Content對(duì)象中),如下圖就是一個(gè)顯示32位數(shù)的流程圖。
ARM反匯編軟件的具體實(shí)現(xiàn)
ARM反匯編軟件主要有四個(gè)大模塊組成,二進(jìn)制可執(zhí)行代碼讀取模塊、指令和數(shù)據(jù)分離模塊、指令反匯編模塊、輸出模塊。本章節(jié)將結(jié)合程序中的源代碼主要介紹ARM反匯編軟件各模塊具體實(shí)現(xiàn)。由于時(shí)間等因素影響,本軟件設(shè)計(jì)只考慮到了ARM指令,THUMB指令不在考慮范圍,不過(guò)其實(shí)都是同樣道理的事情,ARM指令能夠?qū)崿F(xiàn)的話,THUMB指令添加進(jìn)去只是時(shí)間的問(wèn)題。
一、數(shù)據(jù)組織方式
讀進(jìn)來(lái)的內(nèi)容的組織如下所示:
class Content
{
public:
unsigned int addr; //地址
bool isInstruction; //指令標(biāo)記
bool isVisited; //訪問(wèn)標(biāo)記
bool isHasLable; //是否有標(biāo)簽標(biāo)記
unsigned int firstB; //第1個(gè)字節(jié)
unsigned int secondB;
unsigned int thirdB;
unsigned int fourthB;
};
在該類中,addr表示該內(nèi)容的地址,是邏輯字節(jié)地址;isInstruction判斷該內(nèi)容是否是指令代碼;isVisited判斷是否被訪問(wèn)過(guò);isHasLable,判斷該指令前面要不要加一個(gè)標(biāo)簽,firstB表示內(nèi)容從左到右表示的第一個(gè)字節(jié),secondB、thirdB、fourthB以此類推。
讀源文件類
class MyRead
{
public:
vector 《Content》 contentVector; //內(nèi)容存儲(chǔ)容器
void readSourceFile(char * addr); //讀取源文件函數(shù)
};
內(nèi)容容器contentVector,用于存儲(chǔ)從源文件讀進(jìn)來(lái)的內(nèi)容,4個(gè)字節(jié)為一個(gè)元素;readSourceFile方法,讀取源文件二進(jìn)制代碼,并按個(gè)字節(jié)一個(gè)存儲(chǔ)在容器里。
指令編碼內(nèi)容的組織
指令編碼的組織其實(shí)還是很關(guān)鍵的,好的組織方式可以大大節(jié)約后續(xù)工作的時(shí)間,后續(xù)開(kāi)發(fā)或者維護(hù)都會(huì)變得更簡(jiǎn)單。由于篇幅關(guān)系,這里將介紹一些比較常用到的指令內(nèi)容的組織。這里介紹的指令內(nèi)容的組織都將采用結(jié)構(gòu)體的形式,其實(shí)也可以用類來(lái)實(shí)現(xiàn)。
typedef unsigned int QByte; //32位無(wú)符號(hào)數(shù)
?。?)SWI指令
typedef struct swi
{
QByte comment : 24;
QByte mustbe1111 : 4;
QByte condition : 4;
} SWI;
SWI指令的編碼格式:
由指令的編碼格式我們可以看到,comment的內(nèi)容表示immed_24,正好24位;mustbe1111是固定的,所有SWI指令的[27:24]都是1111,condition表示條件域,總共有16種情況,用4位表示即可。
(2) 分支指令
typedef struct branch {
QByte offset : 23;
QByte sign : 1;
QByte L : 1;
QByte mustbe101 : 3;
QByte condition : 4;
} Branch;
分支指令的編碼格式:
從指令的編碼格式中,我們發(fā)現(xiàn),Offset其實(shí)是確定了轉(zhuǎn)向地址的絕對(duì)值;sign表明正負(fù)數(shù);L用于區(qū)分是否要把返回地址寫(xiě)入R14中;mustbe101表明這是一條分支指令,固定的,cond是條件域。
?。?)加載/存儲(chǔ)指令
typedef struct singledatatrans {
QByte offset : 12;
QByte Rd : 4;
QByte Rn : 4;
QByte L : 1;
QByte W : 1;
QByte B : 1;
QByte U : 1;
QByte P : 1;
QByte I : 1;
QByte mustbe01 : 2;
QByte condition : 4;
} SingleDataTrans;
加載/存儲(chǔ)指令編碼格式:
其中offset, I, P, U, W, Rn共同決定計(jì)算出內(nèi)存中的地址,Rd是目的寄存器,
L位用于區(qū)分是LDR還是STR指令;B位用于區(qū)分unsigned byte (B==1) 和 a word (B==0)。
?。?)數(shù)據(jù)處理指令
typedef struct dataproc {
QByte operand2 : 12;
QByte Rd : 4;
QByte Rn : 4;
QByte S : 1;
QByte opcode : 4;
QByte I : 1;
QByte mustbe00 : 2;
QByte condition : 4;
} DataProc;
對(duì)應(yīng)的編碼格式如下:
其中operand2是第二操作數(shù),其計(jì)算方式也有多種形式,可參考ARM手冊(cè)上的“Addressing Mode 1 - Data-processing operands on page A5-2”;Rd為目的寄存;Rn為第一操作數(shù);S位用于區(qū)分是否影響CPSR;opcode用于明確是那種數(shù)據(jù)操作,如MOV,ADD等;I用于區(qū)分第二操作數(shù)是寄存器形式還是立即數(shù)形式;mustbe00是固定的;Cond為條件域。
段表、顯示表、返回表的組織
為簡(jiǎn)單起見(jiàn),本程序中這些表統(tǒng)統(tǒng)用容器Vector來(lái)實(shí)現(xiàn)
typedef struct Elem{
int re[16];
unsigned int addr;
}Elem;
static vector 《unsigned int》 segmentTable; //段表
static vector 《 unsigned int 》 returnAddrTable; //返回表
static vector 《Elem》 showAddrTable; //顯示表
如上所述,其中Elem結(jié)構(gòu)體用于存儲(chǔ)顯示表里面的元素。segmentTable為段表,returnAddrTable為返回表,showAddrTable為顯示表。
所有的填表操作都是用push_back 函數(shù)來(lái)實(shí)現(xiàn)的,還有就是操作顯示表和返回表時(shí)要記住返回表是后進(jìn)先出的。
指令共用體
View Code
這個(gè)指令共用體的設(shè)置應(yīng)該說(shuō)是十分巧妙的,涵蓋了所有類型的指令。在反匯編過(guò)程中,首先是從目標(biāo)二進(jìn)制代碼中讀取一個(gè)32位數(shù)到內(nèi)存,然后將這個(gè)32位數(shù)從內(nèi)存拷貝到共用體變量中,由于共用體的存儲(chǔ)空間是共享的,定義一個(gè)上面的共用體,其實(shí)也就只是4個(gè)字節(jié)大小的空間,但是他可以指代任何一條指令,因此用這個(gè)共用體變量來(lái)判斷讀進(jìn)來(lái)的32位數(shù)是那條指令非常方便和直觀,具體判斷的將在接下來(lái)的指令反匯編模塊介紹。
其他數(shù)據(jù)結(jié)構(gòu)
協(xié)處理器寄存器:
static const char *cregister[16] = {
“cr0”, “cr1”, “cr2”, “cr3”,
“cr4”, “cr5”, “cr6”, “cr7”,
“cr8”, “cr9”, “cr10”, “cr11”,
“cr12”, “cr13”, “cr14”, “cr15”
};
寄存器:
static const char *registers[16] = {
“R0”, “R1”, “R2”, “R3”,
“R4”, “R5”, “R6”, “R7”,
“R8”, “R9”, “R10”,“R11”,
“R12”, “R13”, “R14”, “PC”
};
條件域:
static const char *condtext[16] = {
“EQ”, //“Equal (Z)”
“NE”, //“Not equal (!Z)”
“CS”, //“Unsigned higher or same (C)” },
“CC”, //“Unsigned lower (!C)” },
“MI”, //“Negative (N)” },
“PL”, //“Positive or zero (!N)” },
“VS”, //“Overflow (V)” },
“VC”, // “No overflow (!V)” },
“HI”, // “Unsigned higher (C&!Z)” },
“LS”, //“Unsigned lower or same (!C|Z)” },
“GE”, // “Greater or equal ((N&V)|(!N&!V))” },
“LT”, //“Less than ((N&!V)|(!N&V)” },
“GT”, //“Greater than(!Z&((N&V)|(!N&!V)))” },
“LE”, //“Less than or equal (Z|(N&!V)|(!N&V))” },
“”, //“Always” }, //AL,可以省略掉
“NV” //, “Never - Use MOV R0,R0 for nop” }
};
數(shù)據(jù)處理指令的操作碼編碼,從0到15,之所以弄成以下形式是為了增加程序的可讀性。
typedef enum operatecode
{
AND,EOR ,SUB ,RSB //AND=0; EOR=1.
,ADD,ADC ,SBC ,RSC
,TST, TEQ,CMP,CMN
,OPR ,MOV ,BIC,MVN
}OperCode;
數(shù)據(jù)處理指令字符:
static const char *opcodes[16] =
{
“AND”, “EOR”, “SUB”, “RSB”,
“ADD”, “ADC”, “SBC”, “RSC”,
“TST”, “TEQ”, “CMP”, “CMN”,
“ORR”, “MOV”, “BIC”, “MVN”
};
注意里面的順序,要跟其編碼規(guī)則對(duì)應(yīng)。
二、二進(jìn)制可執(zhí)行代碼讀取模塊
二進(jìn)制可執(zhí)行代碼的讀取模塊主要完成從外部文件中讀取二進(jìn)制代碼到內(nèi)存中,并要合理組織數(shù)據(jù)。
讀取函數(shù)如下所示:
View Code
讀取模塊首先要做的是打開(kāi)目標(biāo)文件,然后一個(gè)字節(jié)一個(gè)字節(jié)的讀取進(jìn)來(lái),這里所謂的第一個(gè)字節(jié)FirstB指的是一個(gè)32位數(shù)從左往右數(shù)算起第一個(gè)的,總共四個(gè)字節(jié)。然后要設(shè)置一個(gè)Content 類型的temp中間值,將讀進(jìn)來(lái)的字節(jié)依次往temp賦值,賦完四個(gè)字節(jié)后,把地址,是否訪問(wèn)標(biāo)志等等也都初始化一下;最后將這個(gè)temp中間值壓入contentVector容器中,這個(gè)容器里面存放的就是目標(biāo)代碼讀進(jìn)來(lái)的二進(jìn)制代碼,四個(gè)字節(jié)為一個(gè)元素存放。
三、指令和數(shù)據(jù)分離并標(biāo)識(shí)模塊
前面已經(jīng)講了指令和數(shù)據(jù)分離的思想,主要是跟蹤程序的控制流,這里講結(jié)合程序的實(shí)現(xiàn)繼續(xù)說(shuō)明一下。首先看一下指令和數(shù)據(jù)分離的函數(shù)。
View Code
上述separateI_D函數(shù)的目的是為了跟蹤程序的控制流,遍歷每一條指令(有點(diǎn)類似有向圖的遍歷),從而標(biāo)識(shí)出指令。首先是segmentTable.push_back(0),將起始地址填入段表;然后按當(dāng)前地址逐條分析指令類型disPart(read);在disPart函數(shù)中,當(dāng)訪問(wèn)這條指令時(shí),首先要做的是把該指令的isInstruction和isVisited標(biāo)志設(shè)置為true;然后disARM,disARM是反匯編函數(shù),這個(gè)函數(shù)可以反匯編出該條指令是哪條指令,在下面的反匯編模塊將會(huì)有更加詳細(xì)的說(shuō)明,在這里只要知道他可以識(shí)別出是哪條指令即可。
當(dāng)反匯編出來(lái)的指令是無(wú)條件轉(zhuǎn)移指令時(shí),以B指令為例,要執(zhí)行以下代碼:
segmentTable.push_back(r[15]);
segmentTable.push_back(r[15] + addr);
r[15]=(r[15]+addr-4);
read.contentVector[(r[15]/4)].isHasLable = true;
r[15]指pc,r[15] + addr為顯示地址。(1)首先將此指令所在地址填入段表,(2)然后顯示地址填入段表,(3)再然后顯示地址作為當(dāng)前地址,由于已經(jīng)設(shè)置PC自動(dòng)加了,所以這里先減一下,(4)同時(shí)轉(zhuǎn)向地址處的指令前面是一個(gè)標(biāo)簽,把isHasLable設(shè)置為true;
若為無(wú)條件轉(zhuǎn)移指令子程序調(diào)用指令,以BL為例,要執(zhí)行以下代碼:
r[14] = r[15] + 4;
segmentTable.push_back(r[15]);
returnAddrTable.push_back(r[14]);
segmentTable.push_back(r[15] + addr);
r[15]=(r[15]+addr-4);
read.contentVector[((r[15]+4)/4)].isHasLable = true;
各條語(yǔ)句的意思依次是
?。?)保存返回地址到R14
(2)此指令所在地址填入段表
?。?)返回地址填入返回地址表
?。?)顯示地址填入段表
?。?)顯示地址作為當(dāng)前地址,由于已經(jīng)設(shè)置PC自動(dòng)加了,所以這里先減一下
?。?)該地址處指令前面有個(gè)標(biāo)簽。
若為無(wú)條件轉(zhuǎn)移指令中的返回指令,以MOV PC,LR為例,如下:
unsigned int raddr;
if(returnAddrTable.size()》0) {
raddr = returnAddrTable[returnAddrTable.size()-1]; return AddrTable.pop_back();
}
segmentTable.push_back(r[15]);
segmentTable.push_back(raddr);
r[15] = raddr-4;
read.contentVector[(raddr/4)].isHasLable = true;
依次做了以下事情,
?。?)返回地址表中按“后進(jìn)先出”原則找到返回地址
?。?)刪除返回表最后一個(gè)元素
(3)指令所在地址填段表
?。?)返回地址填段表
?。?)設(shè)置當(dāng)前的PC值
?。?)設(shè)置標(biāo)簽標(biāo)志
若為二分支指令,以BNE為例,如下:
Elem temp;temp.addr = r[15] + addr; for(int ii=0;ii《16;ii++) temp.re[ii] = r[ii];
showAddrTable.push_back(temp);
read.contentVector[(( r[15] + addr)/4)].isHasLable = true;
依次做了以下事情,
?。?)保存“現(xiàn)場(chǎng)”
(2)顯式地址填入顯式表(包括當(dāng)時(shí)的寄存器值)
?。?)設(shè)置標(biāo)簽標(biāo)志。
若不是轉(zhuǎn)移指令,則繼續(xù)按PC值自增下去,直到終結(jié)語(yǔ)句,然后還要判斷顯示表是否空,這里用了個(gè)K變量來(lái)實(shí)現(xiàn),實(shí)際上并沒(méi)有真的從容器中刪除取出的顯示表里的元素。if(k == showAddrTable.size()) break;用來(lái)判斷是否顯示表里的地址都訪問(wèn)過(guò)了。如果沒(méi)有,則繼續(xù)按這條指令的地址disPart下去。直到把顯示表里的地址都訪問(wèn)一遍。
執(zhí)行完以上的代碼后,就實(shí)現(xiàn)了按控制流遍歷每條指令,是指令的基本上也都做上了標(biāo)識(shí)。
四、指令反匯編模塊
指令反匯編模塊在宏觀上主要體現(xiàn)在disArm這個(gè)函數(shù)上,代碼如下:
View Code
在前面,我們已經(jīng)介紹了ARM指令的編碼規(guī)則,從圖2.2中,我們可以看到圖中每一行都是一種指令的集和,或者說(shuō)都是同一類型的。當(dāng)?shù)玫揭粋€(gè)32位數(shù)時(shí),怎樣進(jìn)行反匯編得到其匯編形式的匯編指令呢?
?。?)首先將這個(gè)32位數(shù)從內(nèi)存中拷貝到instruction指令共用體變量中
memcpy(&instruction, &uint,sizeof(uint));之所以拷貝到這個(gè)共用體里面是因?yàn)樗呀?jīng)定義了所有類別的指令,這樣比較直觀清晰。
?。?)判斷該指令是哪種指令,這步是有一定規(guī)則的,要從下往上以此判斷,具體可看附錄里的代碼,目的是為了考慮到所有的指令。區(qū)分是哪條指令的依據(jù)是圖2.2中的那些固定位,只要讀進(jìn)來(lái)的這32位數(shù)中的某幾位跟表中的那幾位符合,就可以確定該指令屬于哪一類。比如說(shuō)MOV r0, #0,該指令編碼為0xe3a00000,由于[27:25]==001,所以為Data processing immediate [2]這種類型,之所以不是Undefined instruction [3](它的[27:25]==001),是因?yàn)樵谂袛嗍遣皇荱ndefined instruction [3]前面已經(jīng)判斷過(guò)不是了,在(1)里已經(jīng)說(shuō)過(guò)要從下往上判斷,就是這個(gè)原因。
(3)知道哪種類型的指令后,要繼續(xù)區(qū)分是這種類型指令里的哪條指令,然后再區(qū)分條件域,是否影響CPRS[13],操作數(shù)等等。以(2)里的MOV r0, #0為例,由于其[24:21]==1101,所以可以判斷其為MOV指令,條件域[31:28]==1110,表示總是執(zhí)行,不需添加條件。20位S為0,表示不影響CPRS,[15:12]==0000,表示目的寄存器為R0,第二操作數(shù)也可計(jì)算得出為0.最后反匯編的結(jié)果為MOV r0, #0。
下面將結(jié)合程序代碼重點(diǎn)探討一下數(shù)據(jù)處理指令中MOV指令的反匯編,因?yàn)檫@種指令是最常見(jiàn)的,明白了這種指令的反匯編,同他指令也可同理得出。
上述已經(jīng)講過(guò),經(jīng)過(guò)附錄里disArm函數(shù)的判斷之后,就可以知道是哪種指令,所在知道了是數(shù)據(jù)處理指令后,要做以下工作。
首先判斷是不是MOV指令,if (dataproc.opcode == MOV) 即可完成判斷,在確定是MOV指令后,還要判斷第二操作數(shù)的形式,是立即數(shù)形式還是寄存器形式,這直接影響到后面的反匯編。if (dataproc.I)即可完成上述判斷,真的話說(shuō)明是立即數(shù),假的話,說(shuō)明是寄存器形式。
如果是立即數(shù)形式,執(zhí)行以下代碼:
sprintf(disasmstr, “ %s%s%s %s, %s”,opcodes[dataproc.opcode],
condtext[dataproc.condition],(sFlag[dataproc.S]),registers[dataproc.Rd],
dataoperandIm(dataproc) );
updateReg(dataproc,-1,read);
其中disasmstr 保存的是反匯編后的字符,opcodes[dataproc.opcode]是操作符,condtext[dataproc.condition],是執(zhí)行條件,(sFlag[dataproc.S])是S標(biāo)志位, registers[dataproc.Rd]為目的寄存器,dataoperandIm(dataproc)這個(gè)函數(shù)執(zhí)行后將返回第二操作數(shù)的值。函數(shù)源代碼如下,其目的很簡(jiǎn)單,就是講一個(gè)8位數(shù)擴(kuò)展成32位,再循環(huán)右移偶數(shù)位,得到一個(gè)32位數(shù)。updateReg(dataproc,-1,read);是更新各寄存器的值,跟蹤程序控制流的時(shí)候需要。
static char *dataoperandIm (DataProc dataproc)
{
QByte data, rotate;
rotate = (dataproc.operand2 》》 8) * 2;
data = dataproc.operand2 & 0xFF;
sprintf(workstr, “ 0x%lX”,
?。╠ata 》》 rotate)|(data《《(32-rotate)));
return (workstr);
}
如果是寄存器形式,將會(huì)比較復(fù)雜,因?yàn)榈诙僮鲾?shù)會(huì)有多種情況,移位啊什么的,反匯編過(guò)程如下。
如果目的寄存器大于15,表示無(wú)效的指令。
if (dataproc.Rd 》 15 ) sprintf(disasmstr, “Invalid opcode”);
?。?)寄存器的值就是操作數(shù)
if ( 0==(dataproc.operand2&0xFF0) )
{
updateReg(dataproc,r[dataproc.operand2&0x0F],read);
sprintf(disasmstr, “ %s%s%s %s, %s”,
opcodes[dataproc.opcode],
condtext[dataproc.condition],
?。╯Flag[dataproc.S]),
registers[dataproc.Rd],
registers[dataproc.operand2&0x0F]);
}
(2)寄存器循環(huán)右移
else if ( 0x70==(dataproc.operand2 &0x0F0) ) {
sprintf(disasmstr, “ %s%s%s %s, %s, ror %s”,
opcodes[dataproc.opcode],
condtext[dataproc.condition],
?。╯Flag[dataproc.S]),
registers[dataproc.Rd],
registers[dataproc.operand2&0x0F],
registers[(dataproc.operand2&0xF00)》》8]
?。?
}
除此之外,還有以下情況,由于處理方式類似,這里就不一一列出代碼了。
(3)《Rm》, LSL #《shift_imm》 立即數(shù)邏輯左移
?。?)《Rm》, LSL 《Rs》 寄存器邏輯左移
?。?)《Rm》, LSR #《shift_imm》 立即數(shù)邏輯右移
(6)《Rm》, LSR 《Rs》 寄存器邏輯右移
?。?) 《Rm》, ASR #《shift_imm》 立即數(shù)算數(shù)右移
?。?) 《Rm》, ASR 《Rs》 寄存器算數(shù)右移
?。?)《Rm》, ROR #《shift_imm》 立即數(shù)循環(huán)右移
經(jīng)過(guò)上述處理后,基本上把MOV指令的所有情況都考慮進(jìn)去了,MOV指令的反匯編工作也基本完成,其他種類型的指令基本上也是這樣一個(gè)流程。
其實(shí),二進(jìn)制機(jī)器代碼與匯編指令都是唯一對(duì)應(yīng)的,指令的反匯編最重要的就是要找到這種對(duì)應(yīng)關(guān)系,知道每一位所表示的意思,唯一確定出操作符,操作數(shù)等。
五、輸出顯示模塊
本軟件的輸出顯示模塊是用MFC完成的,在vs2008環(huán)境下新建一個(gè)MFC工程,選擇對(duì)話框框,然后添加菜單,編輯框,還有消息處理函數(shù)。主要處理三個(gè)消息,點(diǎn)擊Read File菜單項(xiàng)、Sequence Disassembling菜單項(xiàng)、ControlFlowDisassembling菜單項(xiàng),分別用三個(gè)函數(shù)完成,OnFileSelectfile(),OnOperateSequence(),OnOperateControlflowdisassembling()。
順序反匯編策略的界面如下圖,其中左邊第一列是指令或數(shù)據(jù)的地址,從0開(kāi)始;第二列是指令的二進(jìn)制編碼,是存放在二進(jìn)制可執(zhí)行BIN文件里的直接內(nèi)容,第三列是反匯編后的匯編指令。由圖可見(jiàn),采用順序掃描策略反匯編后的結(jié)果沒(méi)有區(qū)分出指令和數(shù)據(jù),把許多數(shù)據(jù)也反匯編成指令,可讀性比較差,整體比較混亂。
基于控制流的反匯編界面如下圖,其中從左邊開(kāi)始第一列是指令或數(shù)據(jù)的地址(有些地方有標(biāo)簽),第二列如果是指令的話顯示指令的二進(jìn)制編碼,如果是數(shù)據(jù)的話直接顯示數(shù)據(jù)的值,第三列顯示的是指令的匯編形式,數(shù)據(jù)不顯示。從圖中我們可以發(fā)現(xiàn),基于控制流的反匯編技術(shù)比較好的實(shí)現(xiàn)了指令和數(shù)據(jù)的分離,整體感覺(jué)相對(duì)清楚,可讀性較好。
至此,本軟件的輸出模塊就結(jié)束了,其實(shí),關(guān)于輸出模塊的設(shè)計(jì)還有很多可以改進(jìn)的地方。比如可以實(shí)現(xiàn)當(dāng)點(diǎn)擊某個(gè)菜單項(xiàng)時(shí),反匯編出一條指令,同時(shí)在右邊的Other顯示界面顯示出其內(nèi)部各個(gè)寄存器的值,即單步反匯編,類似調(diào)試過(guò)程;同時(shí)也可以一步到底反匯編。由于時(shí)間關(guān)系,本軟件在設(shè)計(jì)時(shí),寄存器值的更新并沒(méi)有涉及所有的指令,只有當(dāng)執(zhí)行一些關(guān)鍵或簡(jiǎn)單指令時(shí)才更新寄存器的值,所以右邊的界面也就沒(méi)有實(shí)現(xiàn)了。
不足
由于時(shí)間、水平和經(jīng)驗(yàn)有限,許多方面仍有不足之處,有改進(jìn)的余地,比如
在跟蹤程序的控制流的過(guò)程中,按理說(shuō)當(dāng)執(zhí)行完每一條指令后,只要碰到會(huì)改變寄存器值的指令,都應(yīng)該更新寄存器的值,但由于時(shí)間等因素,只實(shí)現(xiàn)了部分指令;
而且在基于控制流的反匯編策略時(shí),總共掃描了兩次目標(biāo)代碼,效率比較低,理想的方法是只掃描一遍;
另外本軟件沒(méi)有實(shí)現(xiàn)對(duì)THUMB指令的支持。
評(píng)論