第三章 汇编基础
汇编语言是一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号(Symbol)或标号(Label)代替指令或操作数的地址。
3.1 Smali汇编基础
Smali语言最早是由JesusFreke发布在Google Code上的一个开源项目,并不是拥有官方标准的语言。Smali是Dalvik 虚拟机字节码的反向翻译,Dalvik虚拟机(Dalvik VM)是Google专门为Android平台设计的一套虚拟机,在Dalvik虚拟机上运行的文件是dex文件,dex文件反编译之后就是Smali代码。因此也将Smali语言称作Android虚拟机的反汇编语言。
3.1.1 基本类型
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;。
3.1.2 寄存器
Smali中寄存器的数量和Dalvik虚拟机有关最多支持65536个寄存器,具体程序中使用的寄存数量由具体函数中的参数和变量决定。每个寄存器可存储的数据长度为32位,可以存储任何类型的数据。比如int类型使用一个寄存器,Long类型的数据使用两个寄存器即可存储。
寄存器命名方式有两种:
- v命名法:使用v命名法命名的寄存器可以称做本地寄存器,此类寄存器常用于表示函数内部定义的变量。
- p命名法:使用p命名法命名的寄存器也被称作参数寄存器,此类寄存器常用于表示函数传入的参数。
将通过一段代码实例详细介绍这两种寄存器命名方式的使用方法,具体smali代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
.method static add(II)I . locals 2 const / 4 v0, 0x4 const / 4 v1, 0x5 add - int / 2addr p0, p1 return p0 .end method .method add(II)I . locals 0 add - int / 2addr p1, p2 return p1 .end method |
第一个方法为静态方法传入2个参数,方法内部定义了两个变量。由v0和v1寄存器命名可知这是函数内部定义的变量,而使用p命名法命名的p0和p2寄存器表示的是函add传入的参数。在非静态方法中需要使用一个寄存器保存this指针,一般使用p0寄存器存储,因此在第二个非静态方法中函数传入的第一个参数为由p1开始。在函数内部使用.locals N标记内部可以使用的本地寄存器数量。例如第一个函数中.locals 2的含义即该函数中有2个本地寄存器。
3.1.3 基础指令
要想快速学习任何一门编程语言首先需要需要学习了解该语言最基本的指令。本节将介绍smali汇编语言中经常用到的一些基础指令,以便我们能快速学习和分析smali代码。
数据定义指令,数据定义指令用于定义代码中使用的常量,类等数据,基础指令是const。
命令 | 指令含义 |
---|---|
const/4 vA, #+B | 将数值符号扩展为32后赋值给寄存器vA |
const-string vA, string@BB | 通过字符串索引高走字符串赋值给寄存器vA |
const-class vA,type@BB | 通过类型索引获取一个类的引用赋值给寄存器vA |
示例代码 |
1
2
|
const / 4 v0, 0x1 # 将值0x1存到寄存器v0 const - string v0, "HelloSmali" # 将字符串"HelloSmali"存到寄存器v0 |
数据操作指令,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
|
invoke - static {v0, v1}, Lcom / smali / test / Test; - >test(II)I move - result v0 # 将方法调用的结果值存储的v0 return v0 # 函数返回寄存器v0中存储的值 |
对象操作指令,对象实例相关的操作,比如对象类型转换等。
命令 | 指令含义 |
---|---|
new-instance vA,type@BB | 构造指定类型的对象,并将其引用赋值给vA寄存器。 |
instance-of vA,vB,type@BB | 判断vB寄存器中对象的引用是否是指定类型,如果是则将v1赋值为1,否则赋值为0。 |
check-cast vA,type@BB | 将vA寄存器中对象的引用转换成指定类型,成功则将结果赋值给vA,否则抛出ClassCastException异常。 |
示例代码
1
2
|
# 构造指定类型对象并将引用赋值给寄存器p1 new - instance p1, Lcom / smali / test / Test; |
数组操作指令,Smali语言中有专门用于操作数组的指令。
命令 | 指令含义 |
---|---|
new-array vA,vB,type@BB | 创建指定类型和指定大小的数组,并将其赋值给vA寄存器。(寄存器vB表示数组大小) |
fill-array-data vA,+BBBB | 使用指定的数据填充数组,vA代表数组的引用(即数组中第一个元素的地址) |
示例代码
1
2
|
const / 4 v0, 0x5 # 定义数组元素个数 new - array v1, v0, [Ljava / lang / String; # 创建字符串数组 |
比较指令,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
|
# if条件语句和goto跳转示例 .method static test(I)V if - eq p0, v0, :cond_1 ..... :cond_1 ..... goto :goto_0 ..... :goto_0 return - void .end method # packed-switch示例 .packed - switch v0 # packed-switch解析v0, 根据不同解析值则走相应的分支 :pswitch_0 :pswitch_1 :pswitch_2 .end packed - switch |
3.1.4 语法修饰
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
|
. class <访问权限修饰符> [非权限修饰符] <类名> . super <父类名> .source <源文件名称> |
备注:<>中的内容表示必不可缺的,[]表示的是可选择的。
访问权限修饰符即所谓的public,protected,private即default。而非权限修饰符则指的是final,abstract。
示例代码
1
2
3
|
. class public Lcom / smali / test / Test; . super Ljava / lang / Object ; .source "Test.java" |
在文件头之后便是文件的正文,即类的主体部分,包括类实现的接口描述,注解描述,字段描述和方法描述四部分.下面我们就分别看看字段和方法的结构.(别忘了我们在Davilk中说过的方法和字段的表示)
Nage接口描述,如果该类实现了某个接口,则会通过.implements定义,其格式如下:
1
2
|
#interfaces .implements <接口名称> |
示例代码
1
|
.implements Landroid / view / View$OnClickListener; |
注解描述,如果一个类中使用注解,会用.annotation定义:其格式如下:
1
2
3
4
|
.annotation [注解的属性] <注解类名> [注解字段 = 值] ... .end |
示例代码
1
2
|
.annotation runtime Ljava / lang / Deprecated; .end annotation |
字段描述,Smali中使用.field描述字段,我们知道java中分为静态字段(类属性)和普通字段(实例属性),它们在smali中的表示如下:
普通字段:
1
2
|
#instance fields .field <访问权限修饰符> [非权限修饰符] <字段名>:<字段类型> |
访问权限修饰符相比各位已经非常熟了,而此处非权限修饰符则可是final,volidate,transient.
举例说明:
1
|
.field private TAG:Ljava / lang / String; |
静态字段,静态字段知识在普通字段的的定义中添加了static,其格式如下:
1
2
|
#static fields .field <访问权限> static [修饰词] <字段名>:<字段类型> |
需要注意: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
|
# static fields .field private static final pi:F = 3.14f . class public Lcom / smali / test / Test; . super Ljava / lang / Object ; .source "Test.java" .method public constructor <init>()V . locals 2 .line 8 invoke - direct {p0}, Ljava / lang / Object ; - ><init>()V const - string v0, "Smali" const - string v1, "This is constructor method!" .line 9 invoke - static {v0, v1}, Landroid / util / Log; - >i(Ljava / lang / String;Ljava / lang / String;)I return - void .end method .method static add(II)I . locals 0 .annotation runtime Ljava / lang / Deprecated; .end annotation add - int / 2addr p0, p1 add - int / lit8 p0, p0, 0x1 return p0 .end method |
3.1.5 函数调用
函数是整个程序的基石,开始讲Smali函数调用之前,首先介绍Smali汇编语言中的函数构成,Smali中函数定义格式为:
1
2
3
4
5
6
7
|
.method public / private [static] method()<返回类型> <. locals > [.parameter] [.prologue] [.line] <代码逻辑> .end method |
使用”.method”标识函数由此开始,”.end method”标识函数到底结束。方法名前使用修饰符对方法进行标识。例如直接方法用private修饰的,虚方法用public或protected。”.locals”标识方法内使用的局部变量的个数。”.parameter”标识了该方法中的参数。”.prologue”标识方法中代码的开始处。
函数示例代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# direct methods .method public constructor ()V .registers 2 .prologue .line 8 invoke - direct {p0}, Landroid / app / Activity; - >()V .line 10 const - string v0, "MainActivity" iput - object v0, p0, Lcom / test / demo / MainActivity; - >TAG:Ljava / lang / String; .line 13 const / 4 v0, 0x0 iput - boolean v0, p0, Lcom / test / demo / MainActivity; - >running:Z return - void .end method |
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
|
invoke - 指令类型 {参数 1 , 参数 2 ,...}, L类名; - >方法名 |
直接方法调用,即private方法调用。参考示例代码如下:
1
|
invoke - direct {p0, v0, v1}, Lcom / smali / test / Test; - >start(II)I |
上述代码中start方法是定义在Test类中的一个private函数,可以通过invoke-direct调用。
虚方法调用,即用于调用protected或public函数。参考示例代码如下:
1
|
invoke - virtual {p1}, Lcom / smali / test / Test; - >start()V |
上述代码中start()就是定义在Test中的一个public函数,可以通过invoke-virtual调用。
静态方法调用。参考示例代码如下:
1
|
invoke - static {}, Lcom / smali / test / Test; - >test()V |
调用父类方法用的指令invoke-super,一般用于调用onCreate、onDestroy等方法。参考示例代码如下:
1
|
invoke - super {p0, p1}, Landroidx / appcompat / app / AppCompatActivity; - >onCreate(Landroid / os / Bundle;)V |
通过invoke-interface指令调用接口类方法,参考示例代码如下:
1
|
invoke - interface {v0}, Ljava / util / List ; - >size()I |
函数返回值
在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
|
.method public constructor <init>()V . locals 0 invoke - direct {p0}, Ljava / lang / Object ; - ><init>()V return - void .end method |
返回基本数据类型
1
2
3
4
5
6
7
8
|
.method public static start()I . locals 2 const / 4 v0, 0x4 const / 4 v1, 0x5 invoke - static {v0, v1}, Lcom / smali / test / Test; - >test(II)I move - result v0 return v0 .end method |
返回对象数据类型
1
2
3
4
5
6
7
|
.method public test()Ljava / lang / String; . locals 2 invoke - virtual {p0, v0}, Lcom / smali / test / Test; - >test() Ljava / lang / String; move - result - object v0 return - object v0 .end method |
3.2 ARM汇编基础
ARM 是 Advanced RISC Machine 的缩写,可以理解为一种处理器的架构,还可以将它作为一套完整的处理器指令集。RISC(Reduced Instruction Set Computing) 精简指令集计算机:一种执行较少类型计算机指令的微处理器。
ARM 处理器是典型的 RISC 处理器,因为它们执行的是加载/存储体系结构。只有加载和存储指令才能访问内存。数据处理指令只操作寄存器的内容。目前市面上绝大多数的手机CPU都是基于ARM架构的,但是也有少数采用了英特尔X86架构的CPU。
3.2.1 寄存器
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~R4
- 程序计数器R15(PC)
不分组寄存器R0-R7,不分组寄存器在所有的处理器模式中均可以访问,是真正的通用寄存器。但有一点需要注意在中断或异常处理进行模式转换时可能会造成寄存器中数据的损坏。
分组寄存器R8~R14,分组寄存器的访问与当前处理器模式相关,如果想不依赖处理器模式访问特定的寄存器则需要使用规定的寄存器名字。
寄存器R13通常用做堆栈指针,因此有时也被称作SP。程序进行异常运行模式如函数调用R13寄存器会在指向异常模式分配的堆栈,异常处理程序可将当前运行环境中其它寄存器地的值存储到堆栈中。当函数执行结束将堆栈中的值重新恢复到原来的寄存器中,这样异常处理前的运行环境恢复继续执行后续流程。
寄存器R14通常用做子程序连接寄存器,就是通常说的LR寄存器。当执行程序跳转指令BL或BLX时,程序计数器R15中的子程序返回地址将被拷贝到R14。待子程序执行结束返回时,R14中存储的返回地址会恢复到R15中。
●执行指令如下
1
2
|
MOV PC,LR BX LR |
子程序入口,执行如下指令把寄存器R14存到堆栈
1
|
STMFD SP!,{<rsgisters>,LR} |
使用如下指令把寄存器将堆栈中数据进行恢复
1
|
LDMFD SP!,{<registers>,PC} |
程序计数器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]用于标识了处理器的工作模式,同时也标记 了当前处理器模式下可访问的寄存器。具体含义工作模式下可访问的寄存器对照关系如表所示:
3.2.2 基础指令
本节将简要介绍ARM的指令集和它的基本用法。作为汇编语言的基本单位,了解指令的用法,指令间的如何关联以及将指令进行组合能实现什么功能对于学习汇编语言是至关重要的。
ARM汇编由ARM指令组成。ARM指令通常跟一到两个操作数,具体的语法格式如下所示:
1
|
MNEMONIC {S} {condition} {Rd}, Operand1, Operand2 |
需要指出的是,并不是所有ARM指令用都会用到指令模板中的所有域,正常情况只会使用部分域。模板中各字段的具体含义如下所示:
- MNEMONIC 表示指令的助记符,如MOV,ADD。
- {S} 表示可选的扩展位。如果指令后加了S,将依据计算结果更新CPSR寄存器中相应的FLAG
- {condition} 表示语句的执行条件,如果没有指定,默认为AL(无条件执行)
- {Rd} 目的寄存器,存储指令计算结果。
- Operand1 – 第一个操作数,可以是一个寄存器或一个立即数
- Operand2 – 第二个(可变)操作数,可以是一个立即数或寄存器甚至带移位操作的寄存器
助记符、S扩展位、目的寄存器和第一个操作数的作用很好理解,不多做解释,这里补充解释一下执行条件和第二个操作数。设置了执行条件的指令在执行指令前先校验CPSR寄存器中的标志位,只有标志位的组合匹配所设置的执行条件指令才会被执行。第二个操作数被称为可变操作数,因为它可以被设置为多种形式,包括立即书、寄存器、带移位操作的寄存器,如下图所示为ARM汇编中常用的一些指令和使用方法:
条件指令用举例:
- 比较两个值大小,C代码如下:
1
2
3
4
5
6
|
if (a > b) { a + + ; } else { b + + ; } |
相应的ARM指令代码如下:设R0为a,R1为b
1
2
3
|
CMP R0, R1 ;R0与R1比较 ADDHI R0,R0, #1 ;若R0>R1,则R0=R0+1 ADDLS R1,R1, #1 ;若R0<=R1,则R1=R1+1 |
- 若两个条件均成立,则将这两个数值相加C代码为:
1
2
3
4
|
if ((a ! = 10 ) && (b ! = 20 )) { a = a + b; } |
对应的ARM指令为:
1
2
3
|
CMP R0, #10 ;比较R0是否为10 CMPNE R1, #20 ;若R0不为10,则比较R1是否为20 ADDNE R0,R0,R1 ;若R0不为 10 且R1不为 20 ,则执行 R0 = R0 + R1 |
- 若两个条件有一个成立,则将这两个数值相加C代码为:
1
2
3
4
|
if ((a ! = 10 ) || (b ! = 20 )) { a = a + b; } |
对应的ARM指令为:
1
2
3
|
CMP R0, #10 ;比较R0是否为10 CMPEQ R1, #20 ;若R0值为10,则比较R1是否为20 ADDNE R0,R0,R1 ;若R0不为 10 或R1不为 20 ,则执行 R0 = R0 + R1 |
4.分支(跳转)允许我们跳转到另一个代码段,比较两个初始值并返回最大值,C语代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
int main() { int max = 0 ; int a = 2 ; int b = 3 ; if (a < b) { max = b; } else { max = a; } return max ; } |
对应的ARM指令为:
1
2
3
4
5
6
7
8
9
10
11
12
|
main: MOV R1, #2 ;设置初始变量a的值为2 MOV R2, #3 ;设置初始变量b的值为3 CMP R1, R2 ;比较a和b值看哪个更大 BLT lower ;因为a < b 跳转到lower程序段 MOV R0, R1 ;如果a > b,则将a的值存储到R0 B end ;跳转到程序末尾 lower: MOV R0, R2 ;因为 a < b跳转到此处继续执行, 将b的值存储到R0 B end ;跳转到程序末尾 end: BX LR ;程序执行结束,返回值由R0返回 |
5.怎么使用条件分支实现循环,C语言伪代码:
1
2
3
4
5
6
7
8
9
|
int main() { int count = 0 ; while (count < 10 ) { count + + ; } return count; } |
对应的ARM指令为:
1
2
3
4
5
6
7
8
9
|
main: MOV R0, #0 ;设置初始变量count loop: CMP R0, #10 ;比较 count==10 BEQ end ;如果 count = = 10 ,循环执行结束 ADD R0, R0, #1 ;否则将R0中的值递增1 B loop ;跳转到loop开始位置 end: BX LR ;程序执行结束 |
3.2.3 函数调用
函数调用对于熟悉编程的读者来说都不陌会生,即调用者向被调用者传递一些参数,然后执行被调用者的代码,并获取执行结果的过程。任何语言的函数调用都是发生在栈上,如果调用者要在被调用函数返回后继续正常执行,那就需要在跳转到被调用的函数之前保存当前函数的堆栈信息,以便被调用函执行结束后,返回到调用函数时将其运行所需的堆栈信息恢复。
要理解ARM中的函数,必须要先了解ARM中函数的构成。此处为了方便大家理解函数暂且将ARM函数分为三部分函数头、函数体和函数尾。
函数头,该部分主要功能就是保存当前函数的执行环境,设置栈帧的起始位置,并在栈上为程序中使用的变量开辟存储空间。示例代码如下图所示:
1
2
3
|
STMFD SP!, {FP, LR} ;将栈帧指针FP和LR压入栈中,保存当前函数执行环境 ADD R11, SP, #0 ;设置栈帧的起始位置 SUB SP, SP, #16 ;在栈中为程序中的变量分配存储空间 |
函数体,即该函数内部真正的逻辑部分,示例代码如下图所示:
1
2
3
4
5
6
7
|
MOV R3, #5 ;设置sum函数的第5个参数暂存入到R3寄存器 STR R3, [SP] ;将 sum 的第 5 个参数值存储到栈空间 MOV R0, #1 ;将sum函数的第1个参数,存入R0寄存器 MOV R1, #2 ;将sum函数的第2个参数,存入R1寄存器 MOV R2, #3 ;将sum函数的第3个参数,存入R2寄存器 MOV R3, #4 ;将sum函数的第4个参数,存入R3寄存器 BL SUM ;跳转到 sum 函数执行 |
上面的代码的含义设置SUM参数具体的数值,并跳转到SUM函数的过程。同时还展示了通过栈为函数SUM递参数的过程。函数中接收的形参数量少于或等于4,参数通过R0,R1,R2,R3四个寄存器进行传递。若要传递的参数超过4个时,超出的部分参数需要通过堆栈进行传递。
函数尾,即函数的最后部分,用于将函数体的执行结果返回给的调用者,同时还原到函数初始的状态,这样就可以继续函数被调用的地方执行。这个过程需要在被调用函数中调整栈指针SP,通过加减帧指针寄存器FP来实现的。重新调整栈指针后,将之前保存的寄存器值从堆栈弹出到相应的寄存器来还原这些寄存器值。根据函数类型,一般LDMFD/POP指令是函数最后结束的指令 。示例代码如下图所示:
1
2
3
4
|
MOV R0, R0 ;获取 sum 函数的返回值 MOV R0, R3 ;设置调用函数的返回值 SUB SP, FP, #4 ; 恢复原来的栈指针 LDMFD SP!, {FP, PC} ;将栈帧指针和LR出栈,用于恢复现场 |
函数通过情况是使用寄存器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
|
SUM : STR FP, [SP, #-4]! ;设置SUM函数空间中栈底指针 ADD FP, SP, #0 ;设置栈帧指针 SUB SP, SP, #20 ;为传入的参数在栈上分配存储空间 STR R0, [FP, #-8] ;将传入的参数1存储到栈上分配的空间 STR R1, [FP, #-12] ;将传入的参数2存储到栈上分配的空间 STR R2, [FP, #-16] ;将传入的参数3存储到栈上分配的空间 STR R3, [FP, #-20] ;将传入的参数4存储到栈上分配的空间 LDR R2, [FP, #-8] ;将传入的参数1存储到寄存器R2 LDR R3, [FP, #-12] ;将传入的参数2存储到寄存器R3 ADD R2, R2, R3 ;将参数 1 和参数 2 相加,结果存储到寄存器R2 LDR R3, [FP, #-16] ;将传入的参数3存储到寄存器R3 ADD R2, R2, R3 ;将参数 3 和和之前的结果进行累加 LDR R3, [FP, #-20] ;将传入的参数4存储到寄存器R3 ADD R2, R2, R3 ;将参数 4 和和之前的结果进行累加 LDR R3, [FP, #4] ;将传入的参数5存储到寄存器R3 ADD R3, R2, R3 ;将参数 5 和和之前的结果进行累加 MOV R0, R3 ;将执行结果作为函数 sum 的返回值返回 SUB SP, FP, #0 ;调整栈指针地址 LDR FP, [SP], #4 ;恢复原来的栈指针 BX LR ;函数执行完跳转回main函数 MAIN: STMFD SP!, {FP, LR} ;将栈帧指针和LR压入栈中,用于现场保护 ADD FP, SP, #4 ;设置栈底指针 SUB SP, SP, #8 ;在栈中为程序中的变量分配存储空间 MOV R3, #5 ;设置SUM函数的第5个参数暂存入到R3寄存器 STR R3, [SP] ;将 SUM 的第 5 个参数值存储到栈空间 MOV R0, #1 ;将SUM函数的第1个参数,存入R0寄存器 MOV R1, #2 ;将SUM函数的第2个参数,存入R1寄存器 MOV R2, #3 ;将SUM函数的第3个参数,存入R2寄存器 MOV R3, #4 ;将SUM函数的第4个参数,存入R3寄存器 BL SUM ;跳转到 SUM 函数执行 MOV R0, R0 ;获取 SUM 函数的返回值 MOV R0, R3 ;设置MAIN函数的返回值 SUB SP, FP, #4 ;恢复原来的栈指针 LDMFD SP!, {FP, PC} ;将栈帧指针和LR出栈,用于恢复现场 |
汇编代码对应的C代码
1
2
3
4
5
6
7
8
9
|
int sum ( int arg1, int arg2, int arg3, int arg4, int arg5) { return arg1 + arg2 + arg3 + arg4 + arg5; } int main() { sum ( 1 , 2 , 3 , 4 , 5 ); return ; } |
通过对ARM函数调用的分析可以知道ARM中函数调用过程主要可以总结为四部分:
- 进入调用函数时通过STMFD/PUSH指令将栈帧指针和LR压入栈中,用于保护调用函数执行环境。
- 在栈上申请存储空间,用于保存调用函数的局部变量或者被调用函数的参数值。
- 跳转到被调用函数执行,并通过寄存器R0将执行结果返回给调用者。
- 被调用函数返回后,通过LDMFD/POP指令恢复调用函数原始执行环境,继续后续代码执行。
至此,ARM函数的调用过程就讲完了。函数的调用其实不难,只要明白如何保存以及还原FP和LR寄存器,就能明白函数是如何通过栈帧进行调用和返回的了。
3.2.4 ARM64位汇编
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的变动
- 移除了批量加载寄存器指令 LDM/STM,PUSH/POP,使用 加载寄存器指令STP/STP、LDR/LDP代替
- ARM64 里面对栈的操作是16字节对齐
- ARM64指令中没有协处理器的概念,因此也没有协处理器指令 MCR,MRC;
- ARM64中只有条件跳转和少数数据处理这类指令才有条件执行。
ARM64的指令相对于ARM32的汇编指令没发生什么变化,此C程序反编译后可以验证。
1
2
3
4
5
6
7
8
|
int sum ( int arg1, int arg2) { return arg1 + arg2; } int main() { return sum ( 1 , 2 ); } |
对应的ARM64位汇编代码,由反编译得出的汇编代码可以发现,相对于ARM32为的汇编代码主要变动是使用的寄存器由32为的更改为ARM64中寄存器,其它操作指令变化不大。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
SUM : SUB SP, SP, #16 STR W0, [SP, 12 ] STR W1, [SP, 8 ] LDR W1, [SP, 12 ] LDR W0, [SP, 8 ] ADD W0, W1, W0 ADD SP, SP, 16 RET MAIN: STP X29, X30, [SP, - 16 ]! ADD X29, SP, 0 MOV W0, 1 MOV W1, 2 BL SUM LDP X29, X30, [SP], 16 RET |
需要注意ARM64位参数调用规则遵循AAPCS64,规定堆栈为满递减堆栈。前8个参数是通过x0~x7传递,大于8个的参数通过栈来传递(第8个参数需要通过sp访问,第9个参数需要通过sp + 8 访问,第n个参数需要通过sp + 8*(n-8)访问)。
原文始发于看雪社区(FIGHTING安):[原创]移动应用安全与风控——汇编基础