寫在前面 2012年寫的,僅供參考 反彙編的目的 缺乏某些必要的說明資料的情況下, 想獲得某些軟體系統的源代碼、設計思想及理念, 以便複製, 改造、移植和發展; 從源碼上對軟體的可靠性和安全性進行驗證,對那些直接與CPU 相關的目標代碼進行安全性分析; 涉及的主要內容 分析ARM處理器指令的特點,以
寫在前面
2012年寫的,僅供參考
反彙編的目的
缺乏某些必要的說明資料的情況下, 想獲得某些軟體系統的源代碼、設計思想及理念, 以便複製, 改造、移植和發展;
從源碼上對軟體的可靠性和安全性進行驗證,對那些直接與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)返回表,用於記錄程式調用時的返回地址。
(3)顯示表,碰到雙分支指令時,將其顯示地址和現場(程式各寄存器的值)填入該表中。
以上都準備好之後,就可以開始跟蹤程式控制流了具體步驟如下:
(1)將程式的起始地址填入段表,且作為當前地址。
(2)按當前地址逐條分析指令類型,若非無條件轉移指令及二分支指令,則直至終結語句並轉(7), 否則轉(3)。
(3)若為無條件轉移指令(B指令,MOV PC,0x16等),則將此指令所在地址填段表,其顯式地址填段表,且將顯式地址作為當前地址,然後轉(2),否則轉(4)。
(4)若為無條件轉移指令子程式調用指令(BL指令),則將此指令所在地址填段表,返回地址和現場信息填入返回表,顯式地址填段表,且將顯式地址作為當前地址,然後轉 (2), 否則轉(5)。
(5)若為無條件轉移指令中的返回指令(MOV PC,LR),則在返回地址表中按“後進先出”原則找到返回地址,將此指令所在地址填段表,其返回地址填段表,且將返回地址作為當前地址,然後轉(2),否則轉(6)。
(6)若為二叉點指令(BEQ,MOVEQ PC,0x16等等), 則將顯式地址和現場信息填入顯式表,然後將隱式地址作為當前地址轉(2)。
(7)判顯式表空否,若空則演算法終止,否則從顯式表中按“先進先出”順序取出一個顯式地址作為當前地址,且恢復當時的現場信息轉(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值,顯示表,返回表”,這裡的具體情況如下:
(1)若為無條件轉移指令(B指令等,MOV PC,0x16),則將此指令所在地址填段表,其顯式地址填段表,且將顯式地址作為當前PC地址。
(2)若為無條件轉移指令子程式調用指令(BL指令),則將此指令所在地址填段表,返回地址填入返回地址表,顯式地址填段表,且將顯式地址作為當前PC地址
(3)若為無條件轉移指令中的返回指令(MOV PC,LR),則在返回地址表中按“後進先出”原則找到返回地址,將此指令所在地址填段表,其返回地址填段表,且將返回地址作為當前PC地址。
(4)若為二叉點指令(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位無符號數
(1)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是條件域。
(3)載入/存儲指令
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 函數來實現的,還有就是操作顯示表和返回表時要記住返回表是後進先出的。
指令共用體
typedef union access32 { QByte qbyte; SWI swi; //SWI instruction CPRegTrans cpregtrans; // Coprocessor registesr transfers CPDataOp cpdataop; // Coprocessor data processing // Coprocessor load/store and double register transfers[6] CPDataTrans cpdatatrans; //Branch and branch with link and change to Thumb Branch branch; Undefined4 undefined4; //Undefined Ins[4] LoadStoreMultiple loadstoremultiple; //load/store multiple Undefined47 undefined47; //Undefined instruction [4,7] Undefined undefined; //Undefined instruction //Load,store immediate offset,Load,store register offset SingleDataTrans singledatatrans; MSRImeToSReg msrimetosreg; //Move immediate to status register Undefined3 undefined3; //Undefined instruction [3] DataProc dataproc; DataProcImmediate dataprocimme; MultiplyExtraLoadStore multiextraloadstore; //Multiplies, extra load/stores: MiscellanesInstructOne miscelinstrone; //Miscellaneous instructions DataProcRegShift dataprocregshift; MiscellanesInstructTwo miscelinstrtwo; //Miscellaneous instructions: DataProcImmeShift dataprocimmeshit; } Access32;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" };
註意裡面的順序,要跟其編碼規則對應。
二、二進位可執行代碼讀取模塊
二進位可執行代碼的讀取模塊主要完成從外部文件中讀取二進位代碼到記憶體中,並要合理組織數據。
讀取函數如下所示:
void MyRead::readSourceFile(char *addr) { ifstream f1(addr,ios::binary); if(!f1) { cerr<<addr<<"不能打開"<<endl; return; } unsigned char x; int k=0,i=0; while(f1.read(( char *)&x,1)) { Content temp; // 定義一個Content臨時對象 if(i==0) { temp.fourthB=x; //一個字里的第4個位元組 } else if(i==1) { temp.thirdB=x; //一個字里的第3個位元組 } else if(i==2) { temp.secondB=x; //一個字里的第2個位元組 } else if(i==3) { temp.firstB=x; //一個字里的第1個位元組 } i++; if(i==4) { i=0; temp.addr=4*k; //給地址賦值 temp.isVisited = false; temp.isHasLable = false; k++; this->contentVector.push_back(temp); //將temp存放到容器里 Content temp; } } f1.close(); }View Code
讀取模塊首先要做的是打開目標文件,然後一個位元組一個位元組的讀取進來,這裡所謂的第一個位元組FirstB指的是一個32位數從左往右數算起第一個的,總共四個位元組。然後要設置一個Content 類型的temp中間值,將讀進來的位元組依次往temp賦值,賦完四個位元組後,把地址,是否訪問標誌等等也都初始化一下;最後將這個temp中間值壓入contentVector容器中,這個容器裡面存放的就是目標代碼讀進來的二進位代碼,四個位元組為一個元素存放。
三、指令和數據分離並標識模塊
前面已經講了指令和數據分離的思想,主要是跟蹤程式的控制流,這裡講結合程式的實現繼續說明一下。首先看一下指令和數據分離的函數。
void MyDisassemble::separateI_D(MyRead &read) { int k = 0; segmentTable.push_back(0); r[15] = 0; //PC設為0 disPart(read); while(true) { if(k == showAddrTable.size()) break; //顯示表都訪問結束 for(int i = 0;i<16;i++) r[i] = showAddrTable[k].re[i]; //恢復現場 if(read.contentVector[((showAddrTable[k].addr)/4)].isVisited == false) //是否訪問 disPart(read); k++; } } void MyDisassemble::disPart(MyRead &read) { for(int i =r[15]/4;;i = r[15]/4) { unsigned int foo=((read.contentVector[i].firstB)<<24)+ ((read.contentVector[i].secondB)<<16)+ ((read.contentVector[i].thirdB)<<8)+read.contentVector[i].fourthB; read.contentVector[i].isInstruction = true; //設置是指令 read.contentVector[i].isVisited = true; //已訪問過 disArm(foo,read); //反彙編指令 if ((0x0F&read.contentVector[i].firstB)==0x0f) //程式結束 { segmentTable.push_back(r[15]); break; } r[15]=r[15]+4; } }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
(2)此指令所在地址填入段表
(3)返回地址填入返回地址表
(4)顯示地址填入段表
(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;
依次做了以下事情,
(1)返回地址表中按“後進先出”原則找到返回地址
(2)刪除返回表最後一個元素
(3)指令所在地址填段表
(4)返回地址填段表
(5)設置當前的PC值
(6)設置標簽標誌
若為二分支指令,以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;
依次做了以下事情,
(1)保存“現場”
(2)顯式地址填入顯式表(包括當時的寄存器值)
(3)設置標簽標誌。
若不是轉移指令,則繼續按PC值自增下去,直到終結語句,然後還要判斷顯示表是否空,這裡用了個K變數來實現,實際上並沒有真的從容器中刪除取出的顯示表裡的元素。if(k == showAddrTable.size()) break;用來判斷是否顯示表裡的地址都訪問過了。如果沒有,則繼續按這條指令的地址disPart下去。直到把顯示表裡的地址都訪問一遍。
執行完以上的代碼後,就實現了按控制流遍歷每條指令,是指令的基本上也都做上了標識。
四、指令反彙編模塊
指令反彙編模塊在巨集觀上主要體現在disArm這個函數上,代碼如下: