反匯編的目的
缺乏某些必要的說明資料的情況下, 想獲得某些軟件系統的源代碼、設計思想及理念, 以便復制, 改造、移植和發展;
從源碼上對軟件的可靠性和安全性進行驗證,對那些直接與CPU 相關的目標代碼進行安全性分析;
涉及的主要內容
分析ARM處理器指令的特點,以及編譯以后可執行的二進制文件代碼的特征;
將二進制機器代碼經過指令和數據分開模塊的加工處理;
分解標識出指令代碼和數據代碼;
然后將指令代碼反匯編并加工成易于閱讀的匯編指令形式的文件;
下面給出個示例,匯編源代碼,對應的二進制代碼,以及對應的反匯編后的結果
源代碼:
二進制代碼:
反匯編后的結果:
反匯編軟件要完成的工作就是在指令和數據混淆的二進制BIN文件中,分解并標識出指令和數據,然后反匯編指令部分,得到易于閱讀的匯編文件,如下圖:
ARM體系結構及指令編碼規則分析
略,請參考相關資料,如ARM Limited. ARM Architecture Reference Manual [EB/OL]。 http://infocenter.arm.com/等;
主要可參考下圖,ARM指令集的編碼:
ARM可執行二進制BIN文件分析
目前主要的ARM可執行文件種類:
ELF文件格式:Linux系統下的一種常用、可移植目標文件格式;
BIN文件:直接的二進制文件,內部沒有地址標記,里面包括了純粹的二進制數據;一般用編程器燒寫時,從0開始,而如果下載運行,則下載到編譯時的地址即可;
HEX格式:Intel HEX文件是記錄文本行的ASCII文本文件;
本文主要研究BIN文件的反匯編;
BIN映像文件的結構
ARM程序運行時包含RO,RW,ZI三部分內容,RO(READONLY),是代碼部分,即一條條指令,RW(READWRITE),是數據部分,ZI,是未初始化變量。其中RO和RW會包含在映像文件中,因為一個程序的運行是需要指令和數據的,而ZI是不會包含在映像文件的,因為其中數據都為零,程序運行前會將這部分數據初始化為零。
ARM映像文件是一個層次性結構的文件,包括了域(region),輸出段(output section)和輸入段(input section)。一個映像文件由一個或者多個域組成,每個域最多由三個輸出段(RO,RW,IZ)組成,每個輸出段又包含一個或者多個輸入段,各個輸入段包含了目標文件中的代碼和數據。
域(region):一個映像文件由一個或多個域組成。是組成映象文件的最大結構。所謂域指的就是整個bin映像文件所在的區域,又分為加載域和運行域,一般簡單的程序只有一個加載域。
輸出段(output section):有兩個輸出段,RO和RW。
輸入段(input section):兩個輸入段,CODE和DATA部分,CODE部分是代碼部分,只讀的屬于RO輸出段,DATA部分,可讀可寫,屬于RW輸出段。
ARM的BIN映像文件的結構圖
舉一個例子,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,即代碼部分和數據部分。其中代碼部分,READONLY,屬于RO輸出段;數據部分,READWRITE,屬于RO輸出段。
接下來再看看上述代碼經過編譯生成的BIN映像文件的二進制形式,及該映像文件反匯編后的匯編文件,如下圖:
從圖中我們很容易發現,BIN文件分成了兩部分,指令部分和數據部分。先看一下左圖,從中我們發現,BIN文件的第一條指令編碼是0xe59f0020,即右圖中的00000000h到00000003h,由于存儲方式的原因,小端模式,指令的低字節存放在低地址部分,不過這不影響我們的分析。在BIN文件中從00000000h開始一直到00000027h都是指令部分,即RO輸出段,最后一條指令0xef123456存儲在在BIN文件的00000024h到00000027h。剩下的為數據部分,即RW輸出段,有興趣的讀者可以對照源代碼一一查找之間的對應關系。
ARM反匯編軟件設計要解決的主要問題
一、指令與數據的分離
馮·諾依曼機器中指令和數據是不加區別共同存儲的,以 0、1 二進制編碼形式存在的目標代碼對于分析人員來說,很難讀懂其含義。二進制程序中指令和數據混合存放,按地址尋址訪問,反匯編如果采取線性掃描策略,將無法判斷讀取的二進制編碼是指令還是數據,從而無法實現指令和數據的分離。
那么,怎樣才能實現指令和數據的分離?
眾所周知,凡是指令,控制流是必經之處,凡是數據,數據流是必到之處,存取指令一定會訪問,對于一般指令,控制流是按地址順序遞增而走向的,只有在出現各種轉移指令時,控制流才出現偏離。因此,抓住控制流這一線索,即跟蹤程序的控制流[9]走向而遍歷整個程序的每一條指令,從而達到指令與數據分開的目的。
怎樣才能跟蹤程序的控制流呢?
一般來說控制流與控制轉移指令有關,控制轉移指令一般可分為兩大類:
單分支指令,即直接跳轉,如B;BL;MOV PC,**;LDR PC,**等等;
雙分支指令,有條件的跳轉,如BNE;MOVNE PC,**等等
當該指令為雙分支指令時,會有兩個轉向地址,我們把條件滿足時的轉向地址稱為顯示地址,條件不滿足時的地址稱為隱式地址。
在跟蹤控制流的過程中,還要設置三個表:
(1)段表,將所有轉移指令除條件轉移的轉移地址填入此表包括本指令地址和轉向地址。其實可以不要這個表,但是加進去,會得到若干段的代碼段,比較清晰明了。
(2)返回表,用于記錄程序調用時的返回地址。
?。?)顯示表,碰到雙分支指令時,將其顯示地址和現場(程序各寄存器的值)填入該表中。
以上都準備好之后,就可以開始跟蹤程序控制流了具體步驟如下:
?。?)將程序的起始地址填入段表,且作為當前地址。
?。?)按當前地址逐條分析指令類型,若非無條件轉移指令及二分支指令,則直至終結語句并轉(7), 否則轉(3)。
?。?)若為無條件轉移指令(B指令,MOV PC,0x16等),則將此指令所在地址填段表,其顯式地址填段表,且將顯式地址作為當前地址,然后轉(2),否則轉(4)。
?。?)若為無條件轉移指令子程序調用指令(BL指令),則將此指令所在地址填段表,返回地址和現場信息填入返回表,顯式地址填段表,且將顯式地址作為當前地址,然后轉 (2), 否則轉(5)。
?。?)若為無條件轉移指令中的返回指令(MOV PC,LR),則在返回地址表中按“后進先出”原則找到返回地址,將此指令所在地址填段表,其返回地址填段表,且將返回地址作為當前地址,然后轉(2),否則轉(6)。
?。?)若為二叉點指令(BEQ,MOVEQ PC,0x16等等), 則將顯式地址和現場信息填入顯式表,然后將隱式地址作為當前地址轉(2)。
?。?)判顯式表空否,若空則算法終止,否則從顯式表中按“先進先出”順序取出一個顯式地址作為當前地址,且恢復當時的現場信息轉(2)。
經過以上處理,可以遍歷到所有的指令,且當訪問到該條指令后,要把改地址處的指令標記為指令。接下來可以采用線性掃描策略,當遇到標記為指令的二進制代碼時,把它反匯編成匯編指令即可。
不過,在實現跟蹤程序控制流過程中還有一個比較難處理的問題,就是間接轉移類指令的處理,因為這類指令的轉移地址隱含在寄存器或內存單元中,無法由指令碼本身判斷其值,而且這些隱含在寄存器內或內存單元中的值往往在程序執行時,被動態地進行設置或修改,因此很難判斷這類指令的轉移地址,從而難以完整的確定程序的指令區。
本軟件處理這個問題的方法是設置多個寄存器全局變量,在緊跟程序控制流的過程中,實時更新各寄存器的值,因此可以確定這類指令的轉移地址。
二、代碼部分的反匯編
ARM指令集中的每天指令都有一個對應的二進制代碼,如何從二進制代碼中翻譯出對應的指令,即代碼部分反匯編所要完成的工作。
ARM 指令的一般格式可以表示為如下形式:
《opcode》{condition}{S}《operand0》{!},《operand1》{, 《operand2》}
指令格式中《·》符號內的項是必須的,{·}符號內的項是可選的,/ 符號表示選其中之一,其中opcode 表示指令操作符部分,后綴 conditon、S 及!構成了指令的條件詞,operand0、operand1、operand2 為操作數部分。指令反匯編就是將指令的各組成部分解析出來。
為了使指令反匯編更加清晰、高效,可以采用分類的思想方法來解決該問題,把那些具有相同編碼特征的指令分成同一種類型的指令,然后為每一類指令設計一個與之對應的處理函數。
指令的反匯編的步驟,首先是判斷哪條指令,可以由操作符及那些固定位來確定,然后是指令條件域的翻譯,這個與二進制編碼也有唯一的對應關系,然后操作數的翻譯,在操作數的翻譯過程中,可能涉及到各種操作數的計算方法,移位操作等情況。
ARM反匯編軟件的設計
ARM反匯編軟件的總體設計方案如下圖:
其中,各模塊主要完成以下功能:
輸入模塊:要解決如何從外部文件中讀取二進制格式的文件,另外對讀進來的目標代碼要合理組織和存儲,便于接下來的后續處理。
指令和數據分離模塊:在內存中對讀進來的二進制源代碼進行分析,指令流可以到達的部分標識為代碼,最后剩下的指令流未經過的部分為數據,這樣就分離出代碼和數據。
指令反匯編模塊:對分離出來的代碼段部分,按照各條指令編碼的對應關系進行反匯編,生成目標代碼對應的匯編文件,包括ARM指令地址,ARM指令,可以用于閱讀。
輸出模塊:即要將反匯編后的匯編文件顯示在窗口,且可以生成文件在磁盤上。
ARM處理器反匯編軟件流程
ARM反匯編軟件的整體工作流程如下圖所示,首先是讀取目標二進制文件至內存,然后采用兩遍掃描的策略,第一遍掃描的目的是區分指令和數據,第二遍掃描是將指令部分反匯編成匯編指令形式,將數據部分直接翻譯成數據,最后將結果輸出顯示。
模塊設計-輸入模塊
輸入模塊的流程圖如下圖所示,首先從目標二進制代碼中讀取四個字節存放到Content對象里,再將Content對象存放到Vector容器里,然后按上述操作繼續讀取文件,直到文件結尾。這里有一點要說明的是,由于時間等原因,本軟件只考慮32位ARM指令的反匯編,Thumb指令反匯編不會涉及,所以每次都讀取4個字節。
模塊設計-指令和數據分離模塊
指令和數據分離模塊的設計如下圖所示,由于指令和數據分離模塊設計的關鍵是跟蹤程序的控制流,標識出每一條指令,所以此模塊的關鍵就是要遍歷每一條指令,而遍歷每一條指令的關鍵是要緊跟PC值。
關于段表,顯示表,返回表的概念前面已經說明過了。另外,在程序流程圖中的“根據具體轉移指令,更新PC值,顯示表,返回表”,這里的具體情況如下:
?。?)若為無條件轉移指令(B指令等,MOV PC,0x16),則將此指令所在地址填段表,其顯式地址填段表,且將顯式地址作為當前PC地址。
?。?)若為無條件轉移指令子程序調用指令(BL指令),則將此指令所在地址填段表,返回地址填入返回地址表,顯式地址填段表,且將顯式地址作為當前PC地址
?。?)若為無條件轉移指令中的返回指令(MOV PC,LR),則在返回地址表中按“后進先出”原則找到返回地址,將此指令所在地址填段表,其返回地址填段表,且將返回地址作為當前PC地址。
?。?)若為二叉點指令(BEQ,MOVEQ PC,0x16等等), 則將顯式地址填入顯式地址表(還要保存當時的寄存器值),然后將隱式地址作為當前PC地址。
模塊設計-反匯編模塊
在反匯編模塊中除了要反匯編指令,還要翻譯數據。總的設計思想是依次從裝滿對象的contentVector容器中依次取出對象,判斷該對象是指令還是數據,指令的話,就反匯編成匯編指令形式,數據的話,直接翻譯成數據的值,如下圖所示。
上述流程圖是總的反匯編模塊,在圖中的指令反匯編部分,是該模塊的重點,也是整個反匯編軟件設計的重點,其流程圖如下圖所示。
模塊設計-輸出模塊
關于顯示模塊,比較簡單,直接把結果顯示出來即可,該模塊跟第三個模塊反匯編模塊聯系最緊密,在反匯編模塊中,其實已經包含了輸出模塊。不過輸出模塊也有自己的特殊任務。比如說如何以十六進制形式顯示一個32位數(顯示地址的時候)以及如何顯示一個8位數(顯示讀進來數據的編碼,因為是一個字節一個字節讀進來的,存放的時候也是一個字節一個字節存放在Content對象中),如下圖就是一個顯示32位數的流程圖。
ARM反匯編軟件的具體實現
ARM反匯編軟件主要有四個大模塊組成,二進制可執行代碼讀取模塊、指令和數據分離模塊、指令反匯編模塊、輸出模塊。本章節將結合程序中的源代碼主要介紹ARM反匯編軟件各模塊具體實現。由于時間等因素影響,本軟件設計只考慮到了ARM指令,THUMB指令不在考慮范圍,不過其實都是同樣道理的事情,ARM指令能夠實現的話,THUMB指令添加進去只是時間的問題。
一、數據組織方式
讀進來的內容的組織如下所示:
class Content
{
public:
unsigned int addr; //地址
bool isInstruction; //指令標記
bool isVisited; //訪問標記
bool isHasLable; //是否有標簽標記
unsigned int firstB; //第1個字節
unsigned int secondB;
unsigned int thirdB;
unsigned int fourthB;
};
在該類中,addr表示該內容的地址,是邏輯字節地址;isInstruction判斷該內容是否是指令代碼;isVisited判斷是否被訪問過;isHasLable,判斷該指令前面要不要加一個標簽,firstB表示內容從左到右表示的第一個字節,secondB、thirdB、fourthB以此類推。
讀源文件類
class MyRead
{
public:
vector 《Content》 contentVector; //內容存儲容器
void readSourceFile(char * addr); //讀取源文件函數
};
內容容器contentVector,用于存儲從源文件讀進來的內容,4個字節為一個元素;readSourceFile方法,讀取源文件二進制代碼,并按個字節一個存儲在容器里。
指令編碼內容的組織
指令編碼的組織其實還是很關鍵的,好的組織方式可以大大節約后續工作的時間,后續開發或者維護都會變得更簡單。由于篇幅關系,這里將介紹一些比較常用到的指令內容的組織。這里介紹的指令內容的組織都將采用結構體的形式,其實也可以用類來實現。
typedef unsigned int QByte; //32位無符號數
?。?)SWI指令
typedef struct swi
{
QByte comment : 24;
QByte mustbe1111 : 4;
QByte condition : 4;
} SWI;
SWI指令的編碼格式:
由指令的編碼格式我們可以看到,comment的內容表示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;
分支指令的編碼格式:
從指令的編碼格式中,我們發現,Offset其實是確定了轉向地址的絕對值;sign表明正負數;L用于區分是否要把返回地址寫入R14中;mustbe101表明這是一條分支指令,固定的,cond是條件域。
?。?)加載/存儲指令
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;
加載/存儲指令編碼格式:
其中offset, I, P, U, W, Rn共同決定計算出內存中的地址,Rd是目的寄存器,
L位用于區分是LDR還是STR指令;B位用于區分unsigned byte (B==1) 和 a word (B==0)。
(4)數據處理指令
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;
對應的編碼格式如下:
其中operand2是第二操作數,其計算方式也有多種形式,可參考ARM手冊上的“Addressing Mode 1 - Data-processing operands on page A5-2”;Rd為目的寄存;Rn為第一操作數;S位用于區分是否影響CPSR;opcode用于明確是那種數據操作,如MOV,ADD等;I用于區分第二操作數是寄存器形式還是立即數形式;mustbe00是固定的;Cond為條件域。
段表、顯示表、返回表的組織
為簡單起見,本程序中這些表統統用容器Vector來實現
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結構體用于存儲顯示表里面的元素。segmentTable為段表,returnAddrTable為返回表,showAddrTable為顯示表。
所有的填表操作都是用push_back 函數來實現的,還有就是操作顯示表和返回表時要記住返回表是后進先出的。
指令共用體
View Code
這個指令共用體的設置應該說是十分巧妙的,涵蓋了所有類型的指令。在反匯編過程中,首先是從目標二進制代碼中讀取一個32位數到內存,然后將這個32位數從內存拷貝到共用體變量中,由于共用體的存儲空間是共享的,定義一個上面的共用體,其實也就只是4個字節大小的空間,但是他可以指代任何一條指令,因此用這個共用體變量來判斷讀進來的32位數是那條指令非常方便和直觀,具體判斷的將在接下來的指令反匯編模塊介紹。
其他數據結構
協處理器寄存器:
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” }
};
數據處理指令的操作碼編碼,從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;
數據處理指令字符:
static const char *opcodes[16] =
{
“AND”, “EOR”, “SUB”, “RSB”,
“ADD”, “ADC”, “SBC”, “RSC”,
“TST”, “TEQ”, “CMP”, “CMN”,
“ORR”, “MOV”, “BIC”, “MVN”
};
注意里面的順序,要跟其編碼規則對應。
二、二進制可執行代碼讀取模塊
二進制可執行代碼的讀取模塊主要完成從外部文件中讀取二進制代碼到內存中,并要合理組織數據。
讀取函數如下所示:
View Code
讀取模塊首先要做的是打開目標文件,然后一個字節一個字節的讀取進來,這里所謂的第一個字節FirstB指的是一個32位數從左往右數算起第一個的,總共四個字節。然后要設置一個Content 類型的temp中間值,將讀進來的字節依次往temp賦值,賦完四個字節后,把地址,是否訪問標志等等也都初始化一下;最后將這個temp中間值壓入contentVector容器中,這個容器里面存放的就是目標代碼讀進來的二進制代碼,四個字節為一個元素存放。
三、指令和數據分離并標識模塊
前面已經講了指令和數據分離的思想,主要是跟蹤程序的控制流,這里講結合程序的實現繼續說明一下。首先看一下指令和數據分離的函數。
View Code
上述separateI_D函數的目的是為了跟蹤程序的控制流,遍歷每一條指令(有點類似有向圖的遍歷),從而標識出指令。首先是segmentTable.push_back(0),將起始地址填入段表;然后按當前地址逐條分析指令類型disPart(read);在disPart函數中,當訪問這條指令時,首先要做的是把該指令的isInstruction和isVisited標志設置為true;然后disARM,disARM是反匯編函數,這個函數可以反匯編出該條指令是哪條指令,在下面的反匯編模塊將會有更加詳細的說明,在這里只要知道他可以識別出是哪條指令即可。
當反匯編出來的指令是無條件轉移指令時,以B指令為例,要執行以下代碼:
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)再然后顯示地址作為當前地址,由于已經設置PC自動加了,所以這里先減一下,(4)同時轉向地址處的指令前面是一個標簽,把isHasLable設置為true;
若為無條件轉移指令子程序調用指令,以BL為例,要執行以下代碼:
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;
各條語句的意思依次是
(1)保存返回地址到R14
?。?)此指令所在地址填入段表
?。?)返回地址填入返回地址表
?。?)顯示地址填入段表
(5)顯示地址作為當前地址,由于已經設置PC自動加了,所以這里先減一下
(6)該地址處指令前面有個標簽。
若為無條件轉移指令中的返回指令,以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;
依次做了以下事情,
?。?)返回地址表中按“后進先出”原則找到返回地址
?。?)刪除返回表最后一個元素
?。?)指令所在地址填段表
?。?)返回地址填段表
?。?)設置當前的PC值
?。?)設置標簽標志
若為二分支指令,以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;
依次做了以下事情,
?。?)保存“現場”
?。?)顯式地址填入顯式表(包括當時的寄存器值)
?。?)設置標簽標志。
若不是轉移指令,則繼續按PC值自增下去,直到終結語句,然后還要判斷顯示表是否空,這里用了個K變量來實現,實際上并沒有真的從容器中刪除取出的顯示表里的元素。if(k == showAddrTable.size()) break;用來判斷是否顯示表里的地址都訪問過了。如果沒有,則繼續按這條指令的地址disPart下去。直到把顯示表里的地址都訪問一遍。
執行完以上的代碼后,就實現了按控制流遍歷每條指令,是指令的基本上也都做上了標識。
四、指令反匯編模塊
指令反匯編模塊在宏觀上主要體現在disArm這個函數上,代碼如下:
View Code
在前面,我們已經介紹了ARM指令的編碼規則,從圖2.2中,我們可以看到圖中每一行都是一種指令的集和,或者說都是同一類型的。當得到一個32位數時,怎樣進行反匯編得到其匯編形式的匯編指令呢?
?。?)首先將這個32位數從內存中拷貝到instruction指令共用體變量中
memcpy(&instruction, &uint,sizeof(uint));之所以拷貝到這個共用體里面是因為它已經定義了所有類別的指令,這樣比較直觀清晰。
?。?)判斷該指令是哪種指令,這步是有一定規則的,要從下往上以此判斷,具體可看附錄里的代碼,目的是為了考慮到所有的指令。區分是哪條指令的依據是圖2.2中的那些固定位,只要讀進來的這32位數中的某幾位跟表中的那幾位符合,就可以確定該指令屬于哪一類。比如說MOV r0, #0,該指令編碼為0xe3a00000,由于[27:25]==001,所以為Data processing immediate [2]這種類型,之所以不是Undefined instruction [3](它的[27:25]==001),是因為在判斷是不是Undefined instruction [3]前面已經判斷過不是了,在(1)里已經說過要從下往上判斷,就是這個原因。
(3)知道哪種類型的指令后,要繼續區分是這種類型指令里的哪條指令,然后再區分條件域,是否影響CPRS[13],操作數等等。以(2)里的MOV r0, #0為例,由于其[24:21]==1101,所以可以判斷其為MOV指令,條件域[31:28]==1110,表示總是執行,不需添加條件。20位S為0,表示不影響CPRS,[15:12]==0000,表示目的寄存器為R0,第二操作數也可計算得出為0.最后反匯編的結果為MOV r0, #0。
下面將結合程序代碼重點探討一下數據處理指令中MOV指令的反匯編,因為這種指令是最常見的,明白了這種指令的反匯編,同他指令也可同理得出。
上述已經講過,經過附錄里disArm函數的判斷之后,就可以知道是哪種指令,所在知道了是數據處理指令后,要做以下工作。
首先判斷是不是MOV指令,if (dataproc.opcode == MOV) 即可完成判斷,在確定是MOV指令后,還要判斷第二操作數的形式,是立即數形式還是寄存器形式,這直接影響到后面的反匯編。if (dataproc.I)即可完成上述判斷,真的話說明是立即數,假的話,說明是寄存器形式。
如果是立即數形式,執行以下代碼:
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],是執行條件,(sFlag[dataproc.S])是S標志位, registers[dataproc.Rd]為目的寄存器,dataoperandIm(dataproc)這個函數執行后將返回第二操作數的值。函數源代碼如下,其目的很簡單,就是講一個8位數擴展成32位,再循環右移偶數位,得到一個32位數。updateReg(dataproc,-1,read);是更新各寄存器的值,跟蹤程序控制流的時候需要。
static char *dataoperandIm (DataProc dataproc)
{
QByte data, rotate;
rotate = (dataproc.operand2 》》 8) * 2;
data = dataproc.operand2 & 0xFF;
sprintf(workstr, “ 0x%lX”,
(data 》》 rotate)|(data《《(32-rotate)));
return (workstr);
}
如果是寄存器形式,將會比較復雜,因為第二操作數會有多種情況,移位啊什么的,反匯編過程如下。
如果目的寄存器大于15,表示無效的指令。
if (dataproc.Rd 》 15 ) sprintf(disasmstr, “Invalid opcode”);
?。?)寄存器的值就是操作數
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)寄存器循環右移
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]
?。?
}
除此之外,還有以下情況,由于處理方式類似,這里就不一一列出代碼了。
?。?)《Rm》, LSL #《shift_imm》 立即數邏輯左移
?。?)《Rm》, LSL 《Rs》 寄存器邏輯左移
?。?)《Rm》, LSR #《shift_imm》 立即數邏輯右移
?。?)《Rm》, LSR 《Rs》 寄存器邏輯右移
?。?) 《Rm》, ASR #《shift_imm》 立即數算數右移
?。?) 《Rm》, ASR 《Rs》 寄存器算數右移
?。?)《Rm》, ROR #《shift_imm》 立即數循環右移
經過上述處理后,基本上把MOV指令的所有情況都考慮進去了,MOV指令的反匯編工作也基本完成,其他種類型的指令基本上也是這樣一個流程。
其實,二進制機器代碼與匯編指令都是唯一對應的,指令的反匯編最重要的就是要找到這種對應關系,知道每一位所表示的意思,唯一確定出操作符,操作數等。
五、輸出顯示模塊
本軟件的輸出顯示模塊是用MFC完成的,在vs2008環境下新建一個MFC工程,選擇對話框框,然后添加菜單,編輯框,還有消息處理函數。主要處理三個消息,點擊Read File菜單項、Sequence Disassembling菜單項、ControlFlowDisassembling菜單項,分別用三個函數完成,OnFileSelectfile(),OnOperateSequence(),OnOperateControlflowdisassembling()。
順序反匯編策略的界面如下圖,其中左邊第一列是指令或數據的地址,從0開始;第二列是指令的二進制編碼,是存放在二進制可執行BIN文件里的直接內容,第三列是反匯編后的匯編指令。由圖可見,采用順序掃描策略反匯編后的結果沒有區分出指令和數據,把許多數據也反匯編成指令,可讀性比較差,整體比較混亂。
基于控制流的反匯編界面如下圖,其中從左邊開始第一列是指令或數據的地址(有些地方有標簽),第二列如果是指令的話顯示指令的二進制編碼,如果是數據的話直接顯示數據的值,第三列顯示的是指令的匯編形式,數據不顯示。從圖中我們可以發現,基于控制流的反匯編技術比較好的實現了指令和數據的分離,整體感覺相對清楚,可讀性較好。
至此,本軟件的輸出模塊就結束了,其實,關于輸出模塊的設計還有很多可以改進的地方。比如可以實現當點擊某個菜單項時,反匯編出一條指令,同時在右邊的Other顯示界面顯示出其內部各個寄存器的值,即單步反匯編,類似調試過程;同時也可以一步到底反匯編。由于時間關系,本軟件在設計時,寄存器值的更新并沒有涉及所有的指令,只有當執行一些關鍵或簡單指令時才更新寄存器的值,所以右邊的界面也就沒有實現了。
不足
由于時間、水平和經驗有限,許多方面仍有不足之處,有改進的余地,比如
在跟蹤程序的控制流的過程中,按理說當執行完每一條指令后,只要碰到會改變寄存器值的指令,都應該更新寄存器的值,但由于時間等因素,只實現了部分指令;
而且在基于控制流的反匯編策略時,總共掃描了兩次目標代碼,效率比較低,理想的方法是只掃描一遍;
另外本軟件沒有實現對THUMB指令的支持。
評論
查看更多