当前位置:   article > 正文

深度探讨Linux内核源码ARM处理器设计(图例解析)_armlinux内核源码剖析

armlinux内核源码剖析

反汇编的目的

缺乏某些必要的说明资料的情况下, 想获得某些软件系统的源代码、设计思想及理念, 以便复制, 改造、移植和发展;

从源码上对软件的可靠性和安全性进行验证,对那些直接与CPU 相关的目标代码进行安全性分析;

涉及的主要内容

  1. 分析ARM处理器指令的特点,以及编译以后可执行的二进制文件代码的特征;
  2. 将二进制机器代码经过指令和数据分开模块的加工处理;
  3. 分解标识出指令代码和数据代码;
  4. 然后将指令代码反汇编并加工成易于阅读的汇编指令形式的文件;

下面给出个示例,汇编源代码,对应的二进制代码,以及对应的反汇编后的结果

源代码:

二进制代码:

反汇编后的结果:

反汇编软件要完成的工作就是在指令和数据混淆的二进制BIN文件中,分解并标识出指令和数据,然后反汇编指令部分,得到易于阅读的汇编文件,如下图:

ARM体系结构及指令编码规则分析

主要可参考下图,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里的程序

  1. AREA Word, CODE, READONLY ; name this block of code
  2. num EQU 20 ; Set number of words to be copied
  3. ENTRY ; mark the first instruction to call
  4. start
  5. LDR r0, =src ; r0 = pointer to source block
  6. LDR r1, =dst ; r1 = pointer to destination block
  7. MOV r2, #num ; r2 = number of words to copy
  8. wordcopy
  9. LDR r3, [r0], #4 ; a word from the source
  10. STR r3, [r1], #4 ; store a word to the destination
  11. SUBS r2, r2, #1 ; decrement the counter
  12. BNE wordcopy ; ... copy more
  13. stop
  14. MOV r0, #0x18 ; angel_SWIreason_ReportException
  15. LDR r1, =0x20026 ; ADP_Stopped_ApplicationExit
  16. SWI 0x123456 ; ARM semihosting SWI
  17. AREA BlockData, DATA, READWRITE
  18. src DCD 1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,1,2,3,4
  19. dst DCD 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
  20. END

可以看出,该程序由两部分组成,CODE和DATA,即代码部分和数据部分。其中代码部分,READONLY,属于RO输出段;数据部分,READWRITE,属于RO输出段。

接下来再看看上述代码经过编译生成的BIN映像文件的二进制形式,及该映像文件反汇编后的汇编文件,如下图:

  从图中我们很容易发现,BIN文件分成了两部分,指令部分和数据部分。先看一下左图,从中我们发现,BIN文件的第一条指令编码是0xe59f0020,即右图中的00000000h到00000003h,由于存储方式的原因,小端模式,指令的低字节存放在低地址部分,不过这不影响我们的分析。在BIN文件中从00000000h开始一直到00000027h都是指令部分,即RO输出段,最后一条指令0xef123456存储在在BIN文件的00000024h到00000027h。剩下的为数据部分,即RW输出段,有兴趣的读者可以对照源代码一一查找之间的对应关系。

【文章福利】小编在群文件上传了一些个人觉得比较好得学习书籍、视频资料,有需要的可以进群【977878001】领取!!!额外赠送一份价值699的内核资料包(含视频教程、电子书、实战项目及代码)

内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

 

ARM反汇编软件设计要解决的主要问题

一、指令与数据的分离

  冯·诺依曼机器中指令和数据是不加区别共同存储的,以 0、1 二进制编码形式存在的目标代码对于分析人员来说,很难读懂其含义。二进制程序中指令和数据混合存放,按地址寻址访问,反汇编如果采取线性扫描策略,将无法判断读取的二进制编码是指令还是数据,从而无法实现指令和数据的分离。
  那么,怎样才能实现指令和数据的分离?
  众所周知,凡是指令,控制流是必经之处,凡是数据,数据流是必到之处,存取指令一定会访问,对于一般指令,控制流是按地址顺序递增而走向的,只有在出现各种转移指令时,控制流才出现偏离。因此,抓住控制流这一线索,即跟踪程序的控制流[9]走向而遍历整个程序的每一条指令,从而达到指令与数据分开的目的。
怎样才能跟踪程序的控制流呢?
  一般来说控制流与控制转移指令有关,控制转移指令一般可分为两大类:

  1. 单分支指令,即直接跳转,如B;BL;MOV PC,**;LDR PC,**等等;
  2. 双分支指令,有条件的跳转,如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指令添加进去只是时间的问题。

一、数据组织方式

读进来的内容的组织如下所示:

  1. class Content
  2. {
  3. public:
  4. unsigned int addr; //地址
  5. bool isInstruction; //指令标记
  6. bool isVisited; //访问标记
  7. bool isHasLable; //是否有标签标记
  8. unsigned int firstB; //第1个字节
  9. unsigned int secondB;
  10. unsigned int thirdB;
  11. unsigned int fourthB;
  12. };

在该类中,addr表示该内容的地址,是逻辑字节地址;isInstruction判断该内容是否是指令代码;isVisited判断是否被访问过;isHasLable,判断该指令前面要不要加一个标签,firstB表示内容从左到右表示的第一个字节,secondB、thirdB、fourthB以此类推。

读源文件类

  1. class MyRead
  2. {
  3. public:
  4. vector <Content> contentVector; //内容存储容器
  5. void readSourceFile(char * addr); //读取源文件函数
  6. };

内容容器contentVector,用于存储从源文件读进来的内容,4个字节为一个元素;readSourceFile方法,读取源文件二进制代码,并按个字节一个存储在容器里。

指令编码内容的组织

指令编码的组织其实还是很关键的,好的组织方式可以大大节约后续工作的时间,后续开发或者维护都会变得更简单。由于篇幅关系,这里将介绍一些比较常用到的指令内容的组织。这里介绍的指令内容的组织都将采用结构体的形式,其实也可以用类来实现。

typedef unsigned  int QByte;                    //32位无符号数

(1)SWI指令

  1. typedef struct swi
  2. {
  3. QByte comment : 24;
  4. QByte mustbe1111 : 4;
  5. QByte condition : 4;
  6. } SWI;

SWI指令的编码格式:

由指令的编码格式我们可以看到,comment的内容表示immed_24,正好24位;mustbe1111是固定的,所有SWI指令的[27:24]都是1111,condition表示条件域,总共有16种情况,用4位表示即可。

(2) 分支指令

  1. typedef struct branch {
  2. QByte offset : 23;
  3. QByte sign : 1;
  4. QByte L : 1;
  5. QByte mustbe101 : 3;
  6. QByte condition : 4;
  7. } Branch;

分支指令的编码格式:

从指令的编码格式中,我们发现,Offset其实是确定了转向地址的绝对值;sign表明正负数;L用于区分是否要把返回地址写入R14中;mustbe101表明这是一条分支指令,固定的,cond是条件域。

(3)加载/存储指令

  1. typedef struct singledatatrans {
  2. QByte offset : 12;
  3. QByte Rd : 4;
  4. QByte Rn : 4;
  5. QByte L : 1;
  6. QByte W : 1;
  7. QByte B : 1;
  8. QByte U : 1;
  9. QByte P : 1;
  10. QByte I : 1;
  11. QByte mustbe01 : 2;
  12. QByte condition : 4;
  13. } SingleDataTrans;

加载/存储指令编码格式:

其中offset, I, P, U, W, Rn共同决定计算出内存中的地址,Rd是目的寄存器,

L位用于区分是LDR还是STR指令;B位用于区分unsigned byte (B==1) 和 a word (B==0)。

(4)数据处理指令

  1. typedef struct dataproc {
  2. QByte operand2 : 12;
  3. QByte Rd : 4;
  4. QByte Rn : 4;
  5. QByte S : 1;
  6. QByte opcode : 4;
  7. QByte I : 1;
  8. QByte mustbe00 : 2;
  9. QByte condition : 4;
  10. } DataProc;

对应的编码格式如下:

其中operand2是第二操作数,其计算方式也有多种形式,可参考ARM手册上的“Addressing Mode 1 - Data-processing operands on page A5-2”;Rd为目的寄存;Rn为第一操作数;S位用于区分是否影响CPSR;opcode用于明确是那种数据操作,如MOV,ADD等;I用于区分第二操作数是寄存器形式还是立即数形式;mustbe00是固定的;Cond为条件域。

段表、显示表、返回表的组织

为简单起见,本程序中这些表统统用容器Vector来实现

  1. typedef struct Elem{
  2. int re[16];
  3. unsigned int addr;
  4. }Elem;
  5. static vector <unsigned int> segmentTable; //段表
  6. static vector < unsigned int > returnAddrTable; //返回表
  7. static vector <Elem> showAddrTable; //显示表

如上所述,其中Elem结构体用于存储显示表里面的元素。segmentTable为段表,returnAddrTable为返回表,showAddrTable为显示表。

所有的填表操作都是用push_back 函数来实现的,还有就是操作显示表和返回表时要记住返回表是后进先出的。

指令共用体

 View Code

这个指令共用体的设置应该说是十分巧妙的,涵盖了所有类型的指令。在反汇编过程中,首先是从目标二进制代码中读取一个32位数到内存,然后将这个32位数从内存拷贝到共用体变量中,由于共用体的存储空间是共享的,定义一个上面的共用体,其实也就只是4个字节大小的空间,但是他可以指代任何一条指令,因此用这个共用体变量来判断读进来的32位数是那条指令非常方便和直观,具体判断的将在接下来的指令反汇编模块介绍。

其他数据结构

协处理器寄存器:

  1. static const char *cregister[16] = {
  2. "cr0", "cr1", "cr2", "cr3",
  3. "cr4", "cr5", "cr6", "cr7",
  4. "cr8", "cr9", "cr10", "cr11",
  5. "cr12", "cr13", "cr14", "cr15"
  6. };

寄存器:

  1. static const char *registers[16] = {
  2. "R0", "R1", "R2", "R3",
  3. "R4", "R5", "R6", "R7",
  4. "R8", "R9", "R10","R11",
  5. "R12", "R13", "R14", "PC"
  6. };

条件域:

  1. static const char *condtext[16] = {
  2. "EQ", //"Equal (Z)"
  3. "NE", //"Not equal (!Z)"
  4. "CS", //"Unsigned higher or same (C)" },
  5. "CC", //"Unsigned lower (!C)" },
  6. "MI", //"Negative (N)" },
  7. "PL", //"Positive or zero (!N)" },
  8. "VS", //"Overflow (V)" },
  9. "VC", // "No overflow (!V)" },
  10. "HI", // "Unsigned higher (C&!Z)" },
  11. "LS", //"Unsigned lower or same (!C|Z)" },
  12. "GE", // "Greater or equal ((N&V)|(!N&!V))" },
  13. "LT", //"Less than ((N&!V)|(!N&V)" },
  14. "GT", //"Greater than(!Z&((N&V)|(!N&!V)))" },
  15. "LE", //"Less than or equal (Z|(N&!V)|(!N&V))" },
  16. "", //"Always" }, //AL,可以省略掉
  17. "NV" //, "Never - Use MOV R0,R0 for nop" }
  18. };

数据处理指令的操作码编码,从0到15,之所以弄成以下形式是为了增加程序的可读性。

  1. typedef enum operatecode
  2. {
  3. AND,EOR ,SUB ,RSB //AND=0; EOR=1.
  4. ,ADD,ADC ,SBC ,RSC
  5. ,TST, TEQ,CMP,CMN
  6. ,OPR ,MOV ,BIC,MVN
  7. }OperCode;

数据处理指令字符:

  1. static const char *opcodes[16] =
  2. {
  3. "AND", "EOR", "SUB", "RSB",
  4. "ADD", "ADC", "SBC", "RSC",
  5. "TST", "TEQ", "CMP", "CMN",
  6. "ORR", "MOV", "BIC", "MVN"
  7. };

注意里面的顺序,要跟其编码规则对应。

二、二进制可执行代码读取模块

二进制可执行代码的读取模块主要完成从外部文件中读取二进制代码到内存中,并要合理组织数据。

读取函数如下所示:

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为例,要执行以下代码:

  1. segmentTable.push_back(r[15]);
  2. segmentTable.push_back(r[15] + addr);
  3. r[15]=(r[15]+addr-4);
  4. read.contentVector[(r[15]/4)].isHasLable = true;

各条语句的意思依次是

(1)保存返回地址到R14

(2)此指令所在地址填入段表

(3)返回地址填入返回地址表

(4)显示地址填入段表

(5)显示地址作为当前地址,由于已经设置PC自动加了,所以这里先减一下

(6)该地址处指令前面有个标签。

若为无条件转移指令中的返回指令,以MOV PC,LR为例,如下:

  1. unsigned int raddr;
  2. if(returnAddrTable.size()>0) {
  3. raddr = returnAddrTable[returnAddrTable.size()-1]; return AddrTable.pop_back();
  4. }
  5. segmentTable.push_back(r[15]);
  6. segmentTable.push_back(raddr);
  7. r[15] = raddr-4;
  8. read.contentVector[(raddr/4)].isHasLable = true;

依次做了以下事情,

(1)返回地址表中按“后进先出”原则找到返回地址

(2)删除返回表最后一个元素

(3)指令所在地址填段表

(4)返回地址填段表

(5)设置当前的PC值

(6)设置标签标志

若为二分支指令,以BNE为例,如下:

  1. Elem temp;temp.addr = r[15] + addr; for(int ii=0;ii<16;ii++) temp.re[ii] = r[ii];
  2. showAddrTable.push_back(temp);
  3. 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)即可完成上述判断,真的话说明是立即数,假的话,说明是寄存器形式。

如果是立即数形式,执行以下代码:

  1. sprintf(disasmstr, " %s%s%s %s, %s",opcodes[dataproc.opcode],
  2. condtext[dataproc.condition],(sFlag[dataproc.S]),registers[dataproc.Rd],
  3. dataoperandIm(dataproc) );
  4. 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);是更新各寄存器的值,跟踪程序控制流的时候需要。

  1. static char *dataoperandIm (DataProc dataproc)
  2. {
  3. QByte data, rotate;
  4. rotate = (dataproc.operand2 >> 8) * 2;
  5. data = dataproc.operand2 & 0xFF;
  6. sprintf(workstr, " 0x%lX",
  7. (data >> rotate)|(data<<(32-rotate)));
  8. return (workstr);
  9. }

如果是寄存器形式,将会比较复杂,因为第二操作数会有多种情况,移位啊什么的,反汇编过程如下。

如果目的寄存器大于15,表示无效的指令。

  1. if (dataproc.Rd > 15 )
  2.   sprintf(disasmstr, "Invalid opcode");

(1)寄存器的值就是操作数

  1. if ( 0==(dataproc.operand2&0xFF0) )
  2. {
  3. updateReg(dataproc,r[dataproc.operand2&0x0F],read);
  4. sprintf(disasmstr, " %s%s%s %s, %s",
  5. opcodes[dataproc.opcode],
  6. condtext[dataproc.condition],
  7. (sFlag[dataproc.S]),
  8. registers[dataproc.Rd],
  9. registers[dataproc.operand2&0x0F]);
  10. }

(2)寄存器循环右移

  1. else if ( 0x70==(dataproc.operand2 &0x0F0) ) {
  2. sprintf(disasmstr, " %s%s%s %s, %s, ror %s",
  3. opcodes[dataproc.opcode],
  4. condtext[dataproc.condition],
  5. (sFlag[dataproc.S]),
  6. registers[dataproc.Rd],
  7. registers[dataproc.operand2&0x0F],
  8. registers[(dataproc.operand2&0xF00)>>8]
  9. );
  10. }

除此之外,还有以下情况,由于处理方式类似,这里就不一一列出代码了。

(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指令的支持。

本文链接地址

参考列表:

[1] 殷文建.面向ARM体系结构的代码逆向分析关键技术研究[D]. 郑州:中国人民解放军信息工程大学,2010.

[2] 董仁飞 .反汇编技术在嵌入式软件安全性分析中的应用[J]. 质量与可靠 性,2011,02:57-59.

[3] 方海义,陈利峰,涂时亮. 基于ARM嵌入式处理器的反汇编方法[J].计算机工程,2003,29(z1):159-161.

[4] 张不同,王虹,王开铸.反汇编程序设计原理与实现方法[J].哈尔滨工业大学学报,1989,6:21-25.

[5] 李驹光. ARM应用系统开发详解——基于S3C4510B的系统设计[M].清华大学出版社,2004:1-18.

[6] ARM Limited. ARM Architecture Reference Manual [EB/OL]. http://infocenter.arm.com/,2005.

[7] 许敏,陈前斌.静态反汇编算法研究[J].计算机与数字工程,2007,35(5):13-16.

[8] 吴金波, 蒋烈辉, 赵鹏.基于控制流的静态反汇编算法研究 [J]. 计算机工程与应用, 2005, 41(30):89-90.

[9] 吴金波,蒋烈辉.反静态反汇编技术研究 [J]. 计算机应用, 2005, 25(3):623-625.

[10] 欧阳清华.反汇编原理及其实现技术[M].武汉大学出版社, 1992:13-48.

[11] 王长青.一种微处理器汇编和反汇编系统设计与研究[D]. 西安:西北工业大学,2001.

[12] B.Schwarz, S.Debray, G.Andrews. Disassembly of Executable Code Revisited[C]. Proceedings of the Ninth Working Conference on Reverse Engineering,2002:45-54.

[13] Andrew N.Sloss, Dominic Symes, Chris Wright. ARM System Developer’s Guide Designing and Optimizing System Software [M]. Morgan Kaufmann publications, 2004:47-53.

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/577867
推荐阅读
相关标签
  

闽ICP备14008679号