汇编语言是一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号(Symbol)或标号(Label)代替指令或操作数的地址。
Smali语言最早是由JesusFreke发布在Google Code上的一个开源项目,并不是拥有官方标准的语言。Smali是Dalvik 虚拟机字节码的反向翻译,Dalvik虚拟机(Dalvik VM)是Google专门为Android平台设计的一套虚拟机,在Dalvik虚拟机上运行的文件是dex文件,dex文件反编译之后就是Smali代码。因此也将Smali语言称作Android虚拟机的反汇编语言。
Smali基本数据类型中包含两种类型,原始类型和引用类型。对象类型和数组类型是引用类型,其它都是原始类型。具体数据类型如下表所示。
Smali | Java | 说明 |
---|---|---|
v | void | 只能用于返回值类型 |
Z | boolean | 布尔类型 |
B | byte | 字节类型 |
S | short | 短整型 |
C | char | 字符型 |
I | int | 整数类型 |
J | long | 长整型 |
F | float | 浮点型数据类型 |
D | double | 双精度浮点型 |
Lpackage/name; | 对象类型 | L接完整的包名,使用“;”表示对象名称的结束 |
[数据类型 | 数组 | [Ljava/lang/String,表示一个String类型的数组 |
如果熟悉java的数据类型,就会发现Smali的原始数据类型除boolean类型外都是java基本数据类型首字母的大写,很容易理解。这里重点介绍对象类型和数组类型:
对象类型,在java代码中使用完整的包名的方式表示对象类型,比如:java.lang.String。而在Smali中则是以LpackageName/objectName的形式表示对象类型。L即上面定义的java类类型,表示后面跟着的是类的全限定名。比如java中的java.lang.String对象类型在smali中对应的描述是Ljava/lang/String;。
数组类型,Smali中的数组类型使用“[”进行标记,“[”后跟着基本数据类型的描述符。比如java中的int[]数组在Smali中表示是[I,二维数组int[][]为[[I,三维数组则用[[[I表示。对于对象数组来说,“[”后跟着对应类的全限定符即可。比如java当中的String[]数组在Smali中对应描述是[java/lang/String;。
Smali中寄存器的数量和Dalvik虚拟机有关最多支持65536个寄存器,具体程序中使用的寄存数量由具体函数中的参数和变量决定。每个寄存器可存储的数据长度为32位,可以存储任何类型的数据。比如int类型使用一个寄存器,Long类型的数据使用两个寄存器即可存储。
寄存器命名方式有两种:
将通过一段代码实例详细介绍这两种寄存器命名方式的使用方法,具体smali代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
第一个方法为静态方法传入2个参数,方法内部定义了两个变量。由v0和v1寄存器命名可知这是函数内部定义的变量,而使用p命名法命名的p0和p2寄存器表示的是函add传入的参数。在非静态方法中需要使用一个寄存器保存this指针,一般使用p0寄存器存储,因此在第二个非静态方法中函数传入的第一个参数为由p1开始。在函数内部使用.locals N标记内部可以使用的本地寄存器数量。例如第一个函数中.locals 2的含义即该函数中有2个本地寄存器。
要想快速学习任何一门编程语言首先需要需要学习了解该语言最基本的指令。本节将介绍smali汇编语言中经常用到的一些基础指令,以便我们能快速学习和分析smali代码。
数据定义指令,数据定义指令用于定义代码中使用的常量,类等数据,基础指令是const。
1 2 |
|
数据操作指令,Smali语言中使用move指令和return指令进行数据操。move指令用于进行赋值操作,return指令用于返回数据。
命令 | 指令含义 |
---|---|
move vA,vB | 将 vB 寄存器的值赋值给 vA寄存器 |
move-wide vA,vB | 将 vB 中存储的 long 或 double 类型数据赋值给 vA |
move/from16 vA,vBB | 将vBB中的值赋给vA,源寄存器16位,目的寄存器8位 |
move-object vA,vB | 将 vB 存储的对象引用赋值给 vA |
move-result vA | 将上一个方法调用的结果值移到vA中 |
move-result-wide vA | 将上个方法调用的结果 ( 类型为 double 或 long ) 移到vA中 |
move-result-object vA | 将上一个方法调用后得到的对象的引用赋值给 vA |
move-exception vA | 将程序执行过程中抛出的异常赋值给 vA |
return-void | 表示函数从一个 void方法返回 |
return vA | 函数返回寄存器中存储的值 |
return-object vA | 表示函数返回一个对象类型的值 |
示例代码
1 2 3 |
|
对象操作指令,对象实例相关的操作,比如对象类型转换等。
示例代码
1 2 |
|
数组操作指令,Smali语言中有专门用于操作数组的指令。
命令 | 指令含义 |
---|---|
new-array vA,vB,[email protected] | 创建指定类型和指定大小的数组,并将其赋值给vA寄存器。(寄存器vB表示数组大小) |
fill-array-data vA,+BBBB | 使用指定的数据填充数组,vA代表数组的引用(即数组中第一个元素的地址) |
示例代码
1 2 |
|
比较指令,Smali中有cmp,cmpl,cmpg三种比较指令用于比较两个寄存器中值的大小。cmpl表示寄存器vB小于vC中的值的条件是否成立,成立则返回1,否则返回-1,相等返回0;cmp和cmpg含义一致,表示寄存器vB大于vC中的值的条件是否成立,成立则返回1,否则返回-1,相等返回0。 比较指令一般用于配合跳转指令使用。
命令 | 指令含义 |
---|---|
cmpl-float vA,vB,vC | 比较单精度的浮点数。如果vB寄存器中的值大于vC寄存器的值返回-1到寄存器vA中,相等则返回0,小于返回1。 |
cmpg-float vA,vB,vC | 比较单精度的浮点数,如果vB寄存器中的值大于vC的值返回1到寄存器vA中,相等返回0,小于返回-1。 |
cmpl-double vA,vB,vC | 比较双精度浮点数,如果vBB寄存器中的值大于vCC的值,则返回-1,相等返回0,小于则返回1。 |
cmpg-double vA,vB,vC | 比较双精度浮点数,和cmpl-float的语意一致。 |
cmp-double vA,vB,vC | 等价与cmpg-double vA,vB,vC指令。 |
跳转指令,跳转指令用于从当前地址条状到指定的偏移地址,在if和switch分支中使用的居多。
命令 | 指令含义 |
---|---|
if-eq vA,vB,target | 判断寄存器vA,vB中的值是否相等,等价于java中的if(a==b) |
if-ne vA,vB,target | 判断寄存器vA,vB中的值是否不相等,等价与java中的if(a!=b) |
if-lt vA,vB,target | 判断寄存器vA中的值小于vB中的值,等价于java中的if(a |
if-gt vA,vB,target | 判断寄存器vA中的值大于vB中的值,等价于java中的if(a>b) |
if-ge vA,vB,target | 判断寄存器vA中的值大于等于vB中的值,等价于java中的if(a>=b) |
if-le vA,vB,target | 判断寄存器vA中的值小等于于vB中的值,等价于java中的if(a<=b) |
packed-switch vA,+BB | 分支跳转指令,vA寄存器中的值是switch分支中需要判断的,+BB则是偏移表(packed-switch-payload)中的索引值, |
spare-switch vA,+BB | 分支跳转指令,和packed-switch类似,只不过BB偏移表(spare-switch-payload)中的索引值 |
goto +AA | 无条件跳转到指定偏移处(AA即偏移量) |
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Smali语言提供了很多修饰符,有的编程语言中也将其叫做关键字。修饰符是一个重复关键词的修饰词,然后添加与该词有关的信息或描述性细节 。修饰符可以用来标记类、方法或者变量,通常放在语句的最前端。下面的列表展示了smali中经常用到的语法修饰符来:
修饰符 | 具体含义 |
---|---|
.class | 定义java类名 |
.super | 定义父类名 |
.source | 定义Java源文件名 |
.filed | 定义字段 |
.method | 定义方法开始 |
.end method | 定义方法结束 |
.annotation | 定义注解开始 |
.end annotation | 定义注解结束 |
.implements | 定义接口指令 |
.local | 指定了方法内局部变量的个数 |
.registers | 指定方法内使用寄存器的总数 |
.prologue | 表示方法中代码的开始处 |
.line | 表示java源文件中指定行 |
.paramter | 指定了方法的参数 |
.param | 和.paramter含义一致,但是表达格式不同 |
通过以下这段smali代码说明这些修饰符的具体用处,smali文件的前三行描述了当前类的信息
1 2 3 |
|
备注:<>中的内容表示必不可缺的,[]表示的是可选择的。
访问权限修饰符即所谓的public,protected,private即default。而非权限修饰符则指的是final,abstract。
示例代码
1 2 3 |
|
在文件头之后便是文件的正文,即类的主体部分,包括类实现的接口描述,注解描述,字段描述和方法描述四部分.下面我们就分别看看字段和方法的结构.(别忘了我们在Davilk中说过的方法和字段的表示)
Nage接口描述,如果该类实现了某个接口,则会通过.implements定义,其格式如下:
示例代码
1 |
|
注解描述,如果一个类中使用注解,会用.annotation定义:其格式如下:
1 2 3 4 |
|
示例代码
1 2 |
|
字段描述,Smali中使用.field描述字段,我们知道java中分为静态字段(类属性)和普通字段(实例属性),它们在smali中的表示如下:
普通字段:
1 2 |
|
访问权限修饰符相比各位已经非常熟了,而此处非权限修饰符则可是final,volidate,transient.
举例说明:
1 |
|
静态字段,静态字段知识在普通字段的的定义中添加了static,其格式如下:
1 2 |
|
需要注意:smali文件还为静态字段,普通字段分别添加#static field和#instan filed注释,举例说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
函数是整个程序的基石,开始讲Smali函数调用之前,首先介绍Smali汇编语言中的函数构成,Smali中函数定义格式为:
1 2 3 4 5 6 7 |
|
使用".method"标识函数由此开始,".end method"标识函数到底结束。方法名前使用修饰符对方法进行标识。例如直接方法用private修饰的,虚方法用public或protected。".locals"标识方法内使用的局部变量的个数。".parameter"标识了该方法中的参数。".prologue"标识方法中代码的开始处。
函数示例代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Smali汇编中的函数和java语言中的函数一样存在访问控制,根据访问级别不同可分为direct和virtual两类。修饰符direct声明的函数是等同于java代码中的private类型函数,修饰符virtual的函数等同于java代码中protected和public类型函数。按方法类型可以分为三类static、interface和super,即静态方法、接口方法和父方法。
根据函数的访问权限和方法类型进行区分,调用函数时有invoke-direct,invoke-virtual,invoke-static、invoke-super以及invoke-interface等几种不同的指令,具体调用格式如下:
1 |
|
直接方法调用,即private方法调用。参考示例代码如下:
1 |
|
上述代码中start方法是定义在Test类中的一个private函数,可以通过invoke-direct调用。
虚方法调用,即用于调用protected或public函数。参考示例代码如下:
1 |
|
上述代码中start()就是定义在Test中的一个public函数,可以通过invoke-virtual调用。
静态方法调用。参考示例代码如下:
1 |
|
调用父类方法用的指令invoke-super,一般用于调用onCreate、onDestroy等方法。参考示例代码如下:
1 |
|
通过invoke-interface指令调用接口类方法,参考示例代码如下:
1 |
|
函数返回值
在Smali代码中根据函数返回类型可以分为三类:空值、基本数据类型、对象类型。因为返回值类型不同用到的返回指令也各不相同。具体如下表:
指令 | 指令含义 |
---|---|
return-void | 表示函数从一个void方法返回 |
return vA | 表示函数返回一个32位非对象类型的值 |
return-wide vA | 表示函数返回一个64位非对象类型的值. |
return-object vA | 表示函数返回一个对象类型的值.返回值为8位的寄存器vA |
在Java代码中调用函数并返回函数执行结果只需一条语句便可完成,而在Smali代码中调用函数和返回函数结果需要分开实现。如果调用的函数返回结果为基本数据类型,需要使用move-result或move-result-wide指令将结果移动到指定的寄存器;如果调用的函数返回结果为对象则需要使用move-result-object指令将结果对象移动到指定的寄存器。
返回空值
1 2 3 4 5 |
|
返回基本数据类型
1 2 3 4 5 6 7 8 |
|
返回对象数据类型
1 2 3 4 5 6 7 |
|
ARM 是 Advanced RISC Machine 的缩写,可以理解为一种处理器的架构,还可以将它作为一套完整的处理器指令集。RISC(Reduced Instruction Set Computing) 精简指令集计算机:一种执行较少类型计算机指令的微处理器。
ARM 处理器是典型的 RISC 处理器,因为它们执行的是加载/存储体系结构。只有加载和存储指令才能访问内存。数据处理指令只操作寄存器的内容。目前市面上绝大多数的手机CPU都是基于ARM架构的,但是也有少数采用了英特尔X86架构的CPU。
ARM处理器共有37个32位的寄存器,其中31个为通用寄存器,6个为状态寄存器。
●31个通用寄存器,包括1个程序计数器 (PC)和30 个通用寄存器。
●6个状态寄存器,包括1个CPSR寄存器和5个SPSR寄存器。虽然这些寄存器都是32位的,但实际只使用了其中的12位。
虽然ARM处理器可用寄存器有37个,但是这些寄存器是无法同时被访问的,具体的哪些寄存器可以访问是由处理器的工作状态和运行模式决定。不同的处理器模式中使用不同的寄存器组。如图所示,在任何处理器模式下通用寄存器(R0~R15)、1或2个状态寄存器都是可以访问的。图中每列展示寄存器的即是该用户模式下所有可见的寄存器。
通用寄存器包括R0~R15,可以分为三类:
不分组寄存器R0-R7,不分组寄存器在所有的处理器模式中均可以访问,是真正的通用寄存器。但有一点需要注意在中断或异常处理进行模式转换时可能会造成寄存器中数据的损坏。
分组寄存器R8~R14,分组寄存器的访问与当前处理器模式相关,如果想不依赖处理器模式访问特定的寄存器则需要使用规定的寄存器名字。
寄存器R13通常用做堆栈指针,因此有时也被称作SP。程序进行异常运行模式如函数调用R13寄存器会在指向异常模式分配的堆栈,异常处理程序可将当前运行环境中其它寄存器地的值存储到堆栈中。当函数执行结束将堆栈中的值重新恢复到原来的寄存器中,这样异常处理前的运行环境恢复继续执行后续流程。
寄存器R14通常用做子程序连接寄存器,就是通常说的LR寄存器。当执行程序跳转指令BL或BLX时,程序计数器R15中的子程序返回地址将被拷贝到R14。待子程序执行结束返回时,R14中存储的返回地址会恢复到R15中。
●执行指令如下
子程序入口,执行如下指令把寄存器R14存到堆栈
1 |
|
使用如下指令把寄存器将堆栈中数据进行恢复
1 |
|
程序计数器R15,R15寄存器在分类上属于通用寄存器,但默认其做为程序计数器使用,不能做为通用寄存器使用,因此寄存器R15也被叫做PC寄存器。如果强行将其做为通用寄存器使用则可能会导致程序出现不可预知的行为。
状态寄存器,程序状态寄存器有包含1个当前程序状态寄存器CPSR和5个备份的程序状态寄存器SPSR。当前程序状态器CPSR在任何处理器模式下均可被访问,CPSR用于标记当前程序的运算结果、处理器状态、当前运行模式等。
SPSR寄存器用来备份当前的程序状态寄存器,当程序触发异常中断时,可将CPSR的值存放到SPSR。异常处理程序执行结束返回时,再将SPSR中存放地的当前程序状态值恢复至CPSR。CPSR和SPSR格式是相同,具体如下图所示.
条件标志位,N(Negative)、Z(Zero)、C(Carry)和V(oVerflow)被通称为条件标志位。条件标志位的值会根据程序中的算数或逻辑指令的执行结果值决定,程序可以根据条件标志位中的值决定程序的执行流程,常见的判断逻辑如if条件语句和switch语句。具体用处如下图所示:
控制位,CSPR的低8位统称为控制位,当异常发生这些值将会发生变化,也可在特权模式下对这些标志位进行修改。
中断禁止位I、F和T标志位具体功能如下图所示:
模式控制位M[4:0]用于标识了处理器的工作模式,同时也标记 了当前处理器模式下可访问的寄存器。具体含义工作模式下可访问的寄存器对照关系如表所示:
本节将简要介绍ARM的指令集和它的基本用法。作为汇编语言的基本单位,了解指令的用法,指令间的如何关联以及将指令进行组合能实现什么功能对于学习汇编语言是至关重要的。
ARM汇编由ARM指令组成。ARM指令通常跟一到两个操作数,具体的语法格式如下所示:
1 |
|
需要指出的是,并不是所有ARM指令用都会用到指令模板中的所有域,正常情况只会使用部分域。模板中各字段的具体含义如下所示:
助记符、S扩展位、目的寄存器和第一个操作数的作用很好理解,不多做解释,这里补充解释一下执行条件和第二个操作数。设置了执行条件的指令在执行指令前先校验CPSR寄存器中的标志位,只有标志位的组合匹配所设置的执行条件指令才会被执行。第二个操作数被称为可变操作数,因为它可以被设置为多种形式,包括立即书、寄存器、带移位操作的寄存器,如下图所示为ARM汇编中常用的一些指令和使用方法:
条件指令用举例:
1 2 3 4 5 6 |
|
相应的ARM指令代码如下:设R0为a,R1为b
1 2 3 |
|
1 2 3 4 |
|
对应的ARM指令为:
1 2 3 |
|
1 2 3 4 |
|
对应的ARM指令为:
1 2 3 |
|
4.分支(跳转)允许我们跳转到另一个代码段,比较两个初始值并返回最大值,C语代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
对应的ARM指令为:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
5.怎么使用条件分支实现循环,C语言伪代码:
1 2 3 4 5 6 7 8 9 |
|
对应的ARM指令为:
1 2 3 4 5 6 7 8 9 |
|
函数调用对于熟悉编程的读者来说都不陌会生,即调用者向被调用者传递一些参数,然后执行被调用者的代码,并获取执行结果的过程。任何语言的函数调用都是发生在栈上,如果调用者要在被调用函数返回后继续正常执行,那就需要在跳转到被调用的函数之前保存当前函数的堆栈信息,以便被调用函执行结束后,返回到调用函数时将其运行所需的堆栈信息恢复。
要理解ARM中的函数,必须要先了解ARM中函数的构成。此处为了方便大家理解函数暂且将ARM函数分为三部分函数头、函数体和函数尾。
函数头,该部分主要功能就是保存当前函数的执行环境,设置栈帧的起始位置,并在栈上为程序中使用的变量开辟存储空间。示例代码如下图所示:
1 2 3 |
|
函数体,即该函数内部真正的逻辑部分,示例代码如下图所示:
1 2 3 4 5 6 7 |
|
上面的代码的含义设置SUM参数具体的数值,并跳转到SUM函数的过程。同时还展示了通过栈为函数SUM递参数的过程。函数中接收的形参数量少于或等于4,参数通过R0,R1,R2,R3四个寄存器进行传递。若要传递的参数超过4个时,超出的部分参数需要通过堆栈进行传递。
函数尾,即函数的最后部分,用于将函数体的执行结果返回给的调用者,同时还原到函数初始的状态,这样就可以继续函数被调用的地方执行。这个过程需要在被调用函数中调整栈指针SP,通过加减帧指针寄存器FP来实现的。重新调整栈指针后,将之前保存的寄存器值从堆栈弹出到相应的寄存器来还原这些寄存器值。根据函数类型,一般LDMFD/POP指令是函数最后结束的指令 。示例代码如下图所示:
1 2 3 4 |
|
函数通过情况是使用寄存器R0返回结果,无论SUM函数执行结果结果是什么,都要在函数结束后从寄存器R0中取出返回值。如果函数返回结果的长度是 64 位的,结果需使用寄存器R0和R1组合返回。
完整ARM函数调用示例代码如下图所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
汇编代码对应的C代码
1 2 3 4 5 6 7 8 9 |
|
通过对ARM函数调用的分析可以知道ARM中函数调用过程主要可以总结为四部分:
至此,ARM函数的调用过程就讲完了。函数的调用其实不难,只要明白如何保存以及还原FP和LR寄存器,就能明白函数是如何通过栈帧进行调用和返回的了。
ARM64位采用ARMv8架构,64位操作长度,拥有31个64位的通用寄存器。对于ARM64汇编指令中以 X 开头的是64位的寄存器,以 W 开头的就是32位的寄存器, 其中32位寄存器就是64位寄存器的低32位部分。ARM64汇编中寄存器介绍如下图所示:
寄存器 | 含义 |
---|---|
x0-x7 | x0-x7: 常用于子程序调用时的参数传递,X0还用于存储返回值。如果返回结果大于64位可通过x1:x0的方式返回。 |
x8 | 通常用于保护子程序的返回地址,不要随意赋值。 |
x9-x15 | 通用寄存器,可以随意使用。 |
x16-x17 | 常用用子程序内部调用,不要随意使用。 |
x18 | 平台寄存器,在内核模式下,指向当前处理器的 KPCR;在用户模式下,指向TEB |
FP(x29) | 帧指针寄存器,保存栈帧地址 |
LR(x30) | 程序链接寄存器,保存子程序结束后需要执行的下一条指令 |
SP(x31) | 保存栈指针,可使用 SP/WSP来进行对SP寄存器的访问 |
PC | 程序计数器,总是指向即将要执行的下一条指令 |
SPRs | 状态寄存器,用于存储程序运行时的状态标识 |
ARM64相对ARM32的变动
ARM64的指令相对于ARM32的汇编指令没发生什么变化,此C程序反编译后可以验证。
1 2 3 4 5 6 7 8 |
|
对应的ARM64位汇编代码,由反编译得出的汇编代码可以发现,相对于ARM32为的汇编代码主要变动是使用的寄存器由32为的更改为ARM64中寄存器,其它操作指令变化不大。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
需要注意ARM64位参数调用规则遵循AAPCS64,规定堆栈为满递减堆栈。前8个参数是通过x0~x7传递,大于8个的参数通过栈来传递(第8个参数需要通过sp访问,第9个参数需要通过sp + 8 访问,第n个参数需要通过sp + 8*(n-8)访问)。