反汇编的意图
缺少某些必要的阐明材料的状况下, 想取得某些软件体系的源代码、规划思维及理念, 以便仿制, 改造、移植和开展;
从源码上对软件的可靠性和安全性进行验证,对那些直接与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 函数来完结的,还有便是操作显现表和回来表时要记住回来表是后进先出的。
指令共用体
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
(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这个函数上,代码如下:
View Code
在前面,咱们现已介绍了ARM指令的编码规矩,从图2.2中,咱们能够看到图中每一行都是一种指令的集和,或许说都是同一类型的。当得到一个32位数时,怎样进行反汇编得到其汇编办法的汇编指令呢?
(1)首要将这个32位数从内存中拷贝到instruction指令共用体变量中
memcpy(&instruction, &uint,sizeof(uint));之所以拷贝到这个共用体里边是因为它现已界说了一切类别的指令,这样比较直观明晰。
(2)判别该指令是哪种指令,这步是有必定规矩的,要从下往上以此判别,详细可看附录里的代码,意图是为了考虑到一切的指令。区别是哪条指令的根据是图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”);
(1)寄存器的值便是操作数
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],
(sFlag[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],
(sFlag[dataproc.S]),
registers[dataproc.Rd],
registers[dataproc.operand2&0x0F],
registers[(dataproc.operand2&0xF00)》》8]
);
}
除此之外,还有以下状况,因为处理办法相似,这儿就不逐个列出代码了。
(3)《Rm》, LSL #《shift_imm》 当即数逻辑左移
(4)《Rm》, LSL 《Rs》 寄存器逻辑左移
(5)《Rm》, LSR #《shift_imm》 当即数逻辑右移
(6)《Rm》, LSR 《Rs》 寄存器逻辑右移
(7) 《Rm》, ASR #《shift_imm》 当即数管用右移
(8) 《Rm》, ASR 《Rs》 寄存器管用右移
(9)《Rm》, ROR #《shift_imm》 当即数循环右移
通过上述处理后,基本上把MOV指令的一切状况都考虑进去了,MOV指令的反汇编作业也基本完结,其他品种型的指令基本上也是这样一个流程。
其实,二进制机器代码与汇编指令都是仅有对应的,指令的反汇编最重要的便是要找到这种对应联络,知道每一位所标明的意思,仅有确认出操作符,操作数等。
五、输出显现模块
本软件的输出显现模块是用MFC完结的,在vs2008环境下新建一个MFC工程,挑选对话框框,然后增加菜单,编辑框,还有音讯处理函数。首要处理三个音讯,点击Read File菜单项、Sequence Disassembling菜单项、ControlFlowDisassembling菜单项,分别用三个函数完结,OnFileSelectfile(),OnOperateSequence(),OnOperateControlflowdisassembling()。
次序反汇编战略的界面如下图,其间左面榜首列是指令或数据的地址,从0开端;第二列是指令的二进制编码,是寄存在二进制可履行BIN文件里的直接内容,第三列是反汇编后的汇编指令。由图可见,选用次序扫描战略反汇编后的成果没有区别出指令和数据,把许多数据也反汇编成指令,可读性比较差,全体比较紊乱。
根据操控流的反汇编界面如下图,其间从左面开端榜首列是指令或数据的地址(有些当地有标签),第二列假如是指令的话显现指令的二进制编码,假如是数据的话直接显现数据的值,第三列显现的是指令的汇编办法,数据不显现。从图中咱们能够发现,根据操控流的反汇编技能比较好的完结了指令和数据的别离,全体感觉相对清楚,可读性较好。
至此,本软件的输出模块就完毕了,其实,关于输出模块的规划还有许多能够改善的当地。比方能够完结当点击某个菜单项时,反汇编出一条指令,一起在右边的Other显现界面显现出其内部各个寄存器的值,即单步反汇编,相似调试进程;一起也能够一步究竟反汇编。因为时刻联络,本软件在规划时,寄存器值的更新并没有触及一切的指令,只需当履行一些要害或简略指令时才更新寄存器的值,所以右边的界面也就没有完结了。
缺乏
因为时刻、水平缓经历有限,许多方面仍有缺乏之处,有改善的地步,比方
在盯梢程序的操控流的进程中,按理说当履行完每一条指令后,只需碰到会改动寄存器值的指令,都应该更新寄存器的值,但因为时刻等要素,只完结了部分指令;
并且在根据操控流的反汇编战略时,一共扫描了两次方针代码,功率比较低,抱负的办法是只扫描一遍;
别的本软件没有完结对THUMB指令的支撑。