Dalvik学习
Dalvik寄存器
Dalvik中用的寄存器都是32位,64位类型数据则是用两个相邻的32位寄存器表示,也就是对于double这种64位类型的数据,需要用到两个32位寄存器来存储。
虚拟机寄存器
Dalvik最多支持65536个寄存器(编号从0~65536),但是在ARM架构中的cpu中只存在37个寄存器,那么这种不对称的是怎么解决的呢?Dalvik中的寄存器是虚拟寄存器,通过映射真实的寄存器来实现,每个Dalvik维护了一个调用栈,该调用栈就是用来支持虚拟寄存器和真是寄存器互相映射的,在执行具体函数时,Dalvik会根据.registers指令来确定该函数要用到的寄存器数目.具体的原理,可以自行参考Davilk的实现.
下面我们谈到的都是虚拟寄存器
寄存器的使用规则
对于一个使用m个寄存器(m=局部变量寄存器个数l+参数寄存器个数n)的方法而言,局部寄存器使用从v0开始的l给寄存器,而参数寄存器则使用最后n个寄存器,举个例子说明假设实例方法test(String a,String b)一共使用了5个寄存器:0,1,2,3,4,那么参数寄存器是能使用2,3,4这三个寄存器,如图:
寄存器的命名
寄存器有两种不同的命名方法:v字命名法和p字命 名法.这两种命名法仅仅是影响了字节码的可读性.
寄存器的命名–V字命名法
以小写字母v开头的方式表示方法中使用的局部变量和参数.
对于上面实例方法test(String a,String b)来说,v0,v1为局部变量能够使用的寄存器,v2,v3,v4为参数能够使用的寄存器:
寄存器的命名–P字命名法
以小写字母p开头的方式表示参数,参数名称从p0开始,依次增大.局部变量能够使用的寄存器仍然是以v开头.
对于上面实例方法test(String a,String b)来说,v0,v1为局部变量能够使用的寄存器,p0,p1,p2为参数能够使用的寄存器:
Dalivk描述符
与JVM相类似,Davilk字节码中同样有一套用于描述类型,方法,字段的方法,这些方法结合Davilk的指令便形成了完整的汇编代码.
字节码和数据类型
Dalivk字节码只有两种类型:基本类型和引用类型,对于基本类型和无返回值类型都是用一个大写字母表示,对于对象类型则是使用字母L加对象的全限定类名来表示,数组是使用 [ 来表示,具体规则如下图所示
全限定名是什么?以String为例,其完整名称是java.lang.String,那么其全限定名就是java/lang/String;,即java.lang.String的”.”用”/”代替,并在末尾添加分号”;”做结束符.
java类型 | 类型描述符 |
---|---|
boolean | Z |
byte | B |
short | S |
char | C |
int | I |
long | J |
float | F |
double | D |
void | V |
对象类型 | L |
数组类型 | [ |
对象类型
L可以表示java类型中的任何类.在java代码中以package.name.ObjectName的方式引用,而在Davilk中其描述则是以Lpackage/name/ObjectName;的形式表示.L即上面定义的java类类型,表示后面跟着的是累的全限定名.比如java中的java.lang.String对应的描述是Ljava/lang/String;
数组类型
[类型用来表示所有基本类型的数组,[后跟着是基本类型的描述符.每一维度使用一个前置的[.
比如java中的int[] 用汇编码表示便是[I;.二维数组int[][]为[[I;,三维数组则用[[[I;表示.
对于对象数组来说,[后跟着对应类的全限定符.比如java当中的String[]对应的是 [java/lang/String
字段描述
Davilk中对字段的描述分为两种,对基本类型字段的描述和对引用类型的描述,但两者的描述格式一样:
对象类型描述符->字段名:类型描述符;
比如com.sbbic.Test类中存在String类型的name字段及int类型的age字段,那么其描述为:
Lcom/sbbic/Test;->name:Ljava/lang/String;
Lcom/sbbic/test;->age:I
方法描述
java中方法的签名包括方法名,参数及返回值,在Davilk相应的描述规则为:
对象类型描述符->方法名(参数类型描述符)返回值类型描述符
下面我们通过几个例子来说明,以java.lang.String为例:
java方法:public char charAt(int index){...}
Davilk描述:Ljava/lang/String;->charAt(I)C
java方法:public void getChars(int srcBegin,int srcEnd,char dst[],int dstBegin){...}
Davilk描述:Ljava/lang/String;->getChars(II[CI)V
java方法:public boolean equals(Object anObject){...}
Davilk描述:Ljava/lang/String;->equals(Ljava/lang/Object)Z
掌握以上字段和方法的描述,只能说我们懂了如何描述一个字段和方法,而关于方法中的具体逻辑则需要了解Dalvik中的指令集,因为dalvik是基于寄存器架构的,因此指令集和JVM中的指令集区别较大,反而更类似x86的中的汇编指令.
数据定义指令
dalvik指令格式: 基础编码 - 名称后缀/字节码后缀 目的寄存器 源寄存器
Dalvik指令集中大多数指令用到了寄存器作为目的操作数或源操作数,
其中A/B/C/D/E/F/G/H 代表一个四位的数值,AA/BB.../HH代表一个八位的数值,AAAA/BBBB.../HHHH代表一个十六位的数值
数据定义指令用于定义代码中使用的常量,类等数据,基础指令是const
默认是32位
指令 | 描述 |
---|---|
const/4 vA,#+B | 将数值符号扩展为32后赋值给寄存器vA |
const-wide/16 vAA,#+BBBB | 将数值符号扩展为64位后赋值个寄存器对vAA |
const-string vAA,string@BBBB | 通过字符串索引高走字符串赋值给寄存器vAA |
const-class vAA,type@BBBB | 通过类型索引获取一个类的引用赋值给寄存器vAA |
数据操作指令
move指令用于数据操作,其表示move destination,source,即数据数据从source寄存器(源寄存器)移动到destionation寄存器(源寄存器),可以理解java中变量间的赋值操作.根据字节码和类型的不同,move指令后会跟上不同的后缀.
指令 | 描述 |
---|---|
move vA,vB | 将vB寄存器的值赋值给vA寄存器,vA和vB寄存器都是4位 |
move/from16 vAA,VBBBB | 将vBBBB寄存器(16位)的值赋值给vAA寄存器(7位),from16表示源寄存器vBBBB是16位的 |
move/16 vAAAA,vBBBB | 将寄存器vBBBB的值赋值给vAAAA寄存器,16表示源寄存器vBBBB和目标寄存器vAAAA都是16位 |
move-object vA,vB | 将vB寄存器中的对象引用赋值给vA寄存器,vA寄存器和vB寄存器都是4位 |
move-result vAA | 将上一个invoke指令(方法调用)操作的单字(32位)非对象结果赋值给vAA寄存器 |
move-result-wide vAA | 将上一个invoke指令操作的双字(64位)非对象结果赋值给vAA寄存器 |
mvoe-result-object vAA | 将上一个invoke指令操作的对象结果赋值给vAA寄存器 |
move-exception vAA | 保存上一个运行时发生的异常到vAA寄存器 |
对象操作指令(重点)
与对象实例相关的操作,比如对象创建,对象检查等.
指令 | 描述 |
---|---|
new-instance vAA ,type@BBBB | 构建一个指定类型的对象引用赋值给vAA寄存器,此处不包括数组对象 |
instance-of vA ,vB,type@cccc | 判断vB寄存器中的对象引用是否是指定类型,如果是将v1赋值为1,否则赋值为0 |
check-cast vAA,type@BBBB | 将vAA寄存器中对象的引用转成指定类型,成功则将结果赋值给vAA,否则抛出ClassCastException异常. |
数组操作指令
在实例操作指令中我们并没有发现创建对象的指令.Davilk中设置专门的指令用于数组操作.
指令 | 描述 |
---|---|
new-array vA,vB,type@CCCC | 创建指定类型与指定大小(vB寄存器指定)的数组,并将其赋值给vA寄存器 |
fill-array-data vAA,+BBBBBBBB | 用指定的数据填充数组,vAA代表数组的引用(数组的第一个元素的地址) |
数据运算指令
1.算术运算指令
指令 | 说明 |
---|---|
add-type | 加法指令 |
sub-type | 减法指令 |
mul-type | 乘法指令 |
div-type | 除法指令 |
rem-type | 求 |
2.逻辑元算指令
指令 | 说明 |
---|---|
and-type | 与运算指令 |
or-type | 或运算指令 |
xor-type | 异或元算指令 |
3.位移指令
指令 | 说明 |
---|---|
shl-type | 有符号左移指令 |
shr-type | 有符号右移指令 |
ushr-type | 无符号右移指令 |
上面的-type表示操作的寄存器中数据的类型,可以是-int,-float,-long,-double等.
比较指令
比较指令用于比较两个寄存器中值的大小,其基本格式格式是cmp+kind-type vAA,vBB,vCC
,type
表示比较数据的类型,如-long,-float等;kind
则代表操作类型,因此有cmpl,cmpg,cmp三种比较指令.coml
是compare less的缩写,cmpg是compare greater的缩写,因此cmpl表示vBB小于vCC中的值这个条件是否成立,是则返回1,否则返回-1,相等返回0;cmpg表示vBB大于vCC中的值这个条件是否成立,是则返回1,否则返回-1,相等返回0.
cmp和cmpg的语意一致,即表示vBB大于vCC寄存器中的值是否成立,成立则返回1,否则返回-1,相等返回0
来具体看看Davilk中的指令:
指令 | 说明 |
---|---|
cmpl-float vAA,vBB,vCC | 比较两个单精度的浮点数.如果vBB寄存器中的值大于vCC寄存器的值,则返回-1到vAA中,相等则返回0,小于返回1 |
cmpg-float vAA,vBB,vCC | 比较两个单精度的浮点数,如果vBB寄存器中的值大于vCC的值,则返回1,相等返回0,小于返回-1 |
cmpl-double vAA,vBB,vCC | 比较两个双精度浮点数,如果vBB寄存器中的值大于vCC的值,则返回-1,相等返回0,小于则返回1 |
cmpg-double vAA,vBB,vCC | 比较双精度浮点数,和cmpl-float的语意一致 |
cmp-double vAA,vBB,vCC | 等价与cmpg-double vAA,vBB,vCC指令 |
g是大,l是小
字段操作指令
字段操作指令表示对对象字段进行设值和取值操作,就像是你在代码中常写的set和get方法.基本指令是iput-type,iget-type,sput-type,sget-type
.type表示数据类型.
普通字段读写操作
前缀是i的iput-type和iget-type指令用于字段的读写操作.
指令 | 说明 |
---|---|
iget-byte vX,vY,filed_id | 读取vY寄存器中的对象中的filed_id字段值赋值给vX寄存器 |
iput-byte vX,vY,filed_id | 设置vY寄存器中的对象中filed_id字段的值为vX寄存器的值 |
静态字段读写操作
前缀是s的sput-type和sget-type指令用于静态字段的读写操作
指令 | 说明 |
---|---|
sget-byte vX,vY,filed_id | |
sput-byte vX,vY,filed_id |
方法调用指令
Davilk中的方法指令和JVM的中指令大部分非常类似.目前共有五条指令集:
指令 | 说明 |
---|---|
invoke-direct{parameters},methodtocall | 调用实例的直接方法,即private修饰的方法.此时需要注意{}中的第一个元素代表的是当前实例对象,即this,后面接下来的才是真正的参数.比如指令invoke-virtual {v3,v1,v4},Test2.method5:(II)V中,v3表示Test2当前实例对象,而v1,v4才是方法参数 |
invoke-static{parameters},methodtocall | 调用实例的静态方法,此时{}中的都是方法参数 |
invoke-super{parameters},methodtocall | 调用父类方法 |
invoke-virtual{parameters},methodtocall | 调用实例的虚方法,即public和protected修饰修饰的方法 |
invoke-interface{parameters},methodtocall | 调用接口方法 |
再此强调一遍对于非静态方法而言{}的结构是{当前实例对象,参数1,参数2,…参数n},而对于静态方法而言则是{参数1,参数2,…参数n}
需要注意,如果要获取方法执行有返回值,需要通过上面说道的move-result指令获取执行结果.
方法返回指令
在java中,很多情况下我们需要通过Return返回方法的执行结果,在Davilk中同样提供的return指令来返回运行结果:
指令 | 说明 |
---|---|
return-void | 什么也不返回 |
return vAA | 返回一个32位非对象类型的值 |
return-wide vAA | 返回一个64位非对象类型的值 |
return-object vAA | 反会一个对象类型的引用 |
同步指令
同步一段指令序列通常是由java中的synchronized语句块表示,则JVM中是通过monitorenter和monitorexit的指令来支持synchronized关键字的语义的,而在Davilk中同样提供了两条类似的指令来支持synchronized语义:
指令 | 说明 |
---|---|
monitor-enter vAA | 为指定对象获取锁操作 |
monitor-exit vAA | 为指定对象释放锁操作 |
异常指令
很久以前,VM也是用过jsr和ret指令来实现异常的,但是现在的JVM中已经抛出原先的做法,转而采用异常表来实现异常.而Davilk仍然使用指令来实现:
指令 | 说明 |
---|---|
throw vAA | 抛出vAA寄存器中指定类型的异常 |
跳转指令
跳转指令用于从当前地址条状到指定的偏移处,在if,switch分支中使用的居多.Davilk中提供了goto,packed-switch,if-test
指令用于实现跳转操作
指令 | 说明 |
---|---|
goto +AA | 无条件跳转到指定偏移处(AA即偏移量) |
packed-switch vAA,+BBBBBBBB | 分支跳转指令.vAA寄存器中的值是switch分支中需要判断的,BBBBBBBB则是偏移表(packed-switch-payload)中的索引值, |
spare-switch vAA,+BBBBBBBB | 分支跳转指令,和packed-switch类似,只不过BBBBBBBB偏移表(spare-switch-payload)中的索引值 |
if-test vA,vB,+CCCC | 条件跳转指令,用于比较vA和vB寄存器中的值,如果条件满足则跳转到指定偏移处(CCCC即偏移量),test代表比较规则,可以是eq.lt等. |
在条件比较中,if-test中的test表示比较规则.该指令用的非常多,因此我们简单的坐下说明:
指令 | 说明 |
---|---|
if-eq vA,vB,target | vA,vB寄存器中的相等,等价于java中的if(a==b),比如if-eq v3,v10,002c表示如果条件成立,则跳转到current position+002c处.其余的类似 |
if-ne vA,vB,target | 等价与java中的if(a!=b) |
if-lt vA,vB,target | vA寄存器中的值小于vB,等价于java中的if(a<b) |
if-gt vA,vB,target | 等价于java中的if(a>b) |
if-ge vA,vB,target | 等价于java中的if(a>=b) |
if-le vA,vB,target | 等价于java中的if(a<=b) |
除了以上指令之外,Davilk还提供可一个零值条件指令,该指令用于和0比较,可以理解为将上面指令中的vB寄存器的值固定为0.
指令 | 说明 |
---|---|
if-eqz vAA,target | 等价于java中的if(a==0)或者if(!a) |
if-nez vAA,target | 等价于java中的if(a!=0)或者if(a) |
if-ltz vAA,target | 等价于java中的if(a<0) |
if-gtz vAA,target | 等价于java中的if(a>0) |
if-lez vAA,target | 等价于java中的if(a<=0) |
if-gtz vAA,target | 等价于java中的if(a>=0) |
数据转换指令
数据类型转换对任何java开发者都是非常熟悉的,用于实现两种不同数据类型的相互转换.其基本指令格式是:unop vA,vB,表示对vB寄存器的中值进行操作,并将结果保存在vA寄存器中.
指令 | 说明 |
---|---|
int-to-long | 整形转为长整型 |
float-to-int | 单精度浮点型转为整形 |
int-to-byte | 整形转为字节类型 |
neg-int | 求补指令,对整数求补 |
not-int | 求反指令,对整数求反 |
详解smali文件
上面我们介绍了Dalvik的相关指令,下面我们则来认识一下smali文件.尽管我们使用java来写Android应用,但是Dalvik并不直接加载.class文件,而是通过dx工具将.class文件优化成.dex文件,然后交由Dalvik加载.这样说来,我们无法通过分析.class来直接分析apk文件,而是需要借助工具baksmali.jar反编译dex文件来获得对应smali文件,smali文件可以认为是Davilk的字节码文件,但是并两者并不完全等同.
通过baksmali.jar反编译出来每个.smali,都对应与java中的一个类,每个smali文件都是Davilk指令组成的,并遵循一定的结构.smali存在很多的指令用于描述对应的java文件,所有的指令都以”.”开头,常用的指令如下:
指令 | 说明 |
---|---|
.filed | 定义字段 |
.method…end method | 定义方法 |
.annotation…annotation | 定义注解 |
.implement | 定义接口指令 |
.local | 指定了方法局部变量个数 |
.registers | 指定方法内寄存器的个数 |
.prologue | 表示方法中代码的开始处 |
.line | 表示Java源文件中指定行 |
.paramter | 指定了方法的参数 |
.parm | 和.paramter含义一致,但是表达格式不同 |
1. 文件头描述
.class <访问权限修饰符> [非权限修饰符] <类名>
.super <父类名>
.source <源文件名称>
<>中的内容表示必不可缺的,[]表示的是可选择的.
访问权限修饰符即所谓的public,protected,private即default.而非权限修饰符则指的是final,abstract.
举例说明:
.class public final Lcom/sbbic/demo/Device;
.super Ljava/lang/Object;
.source "Device.java"
2. 文件正文
在文件头之后便是文件的正文,即类的主体部分,包括类实现的接口描述,注解描述,字段描述和方法描述四部分.下面我们就分别看看字段和方法的结构.(别忘了我们在Davilk中说过的方法和字段的表示)
接口描述
如果该类实现了某个接口,则会通过.implements定义,其格式如下:
#interfaces
.implements <接口名称>
举例说明:
interfaces
.implements Landroid/view/View$OnClickListener;
smali为其添加了#Interface注释
注解描述
如果一个类中使用注解.会用annotation定义:格式如下:
#annotations
.annotation [注解的属性] <注解类名>
[注解字段=值]
...
.end
字段描述
smail中使用.field描述字段,Java中分为普通字段和静态字段
1.普通字段
#instance dields
.field <访问权限修饰符> [非访问权限修饰符] <字段名>:<字段类型>
访问权限修饰符相比各位已经非常熟了,而此处非权限修饰符则可是final,volidate,transient.
举例说明:
# instance fields
.field private TAG:Ljava/lang/String;
2.静态字段
静态字段知识在普通字段的的定义中添加了static,其格式如下:
#static fields
.field <访问权限> static [修饰词] <字段名>:<字段类型>
举例说明:
# static fields
.field private static final pi:F = 3.14f
需要注意:smali文件还为静态字段,普通字段分别添加#static field和#instan filed注释.
方法描述
smali中使用.method描述方法.具体定义格式如下:
1. 直接方法
直接方法即所谓的direct methods,还记的Davilk中方法调用指令invoke-direct么?忘记的童鞋自行翻看,这里就不做说明了.
#direct methods
.method <访问权限修饰符> [非访问权限修饰符] <方法原型>
<.locals>
[.parameter]
[.prologue]
[.line]
<代码逻辑>
.end
重点解释一下parameter:
parameter的个数和方法参数的数量相对应,即有几个参数便有几个.parameter,默认从1开始,即p1,p2,p2….
熟悉java的童鞋一定会记得该类型的方法有个默认的参数指向当前对象,在smali中,方法的默认对象参数用p0表示.
# direct methods
.method public constructor ()V
.registers 2
.prologue
.line 8
invoke-direct {p0}, Landroid/app/Activity;-><init>()V
.line 10
const-string v0, "MainActivity"
iput-object v0, p0, Lcom/social_touch/demo/MainActivity;->TAG:Ljava/lang/String;
.line 13
const/4 v0, 0x0
iput-boolean v0, p0, Lcom/social_touch/demo/MainActivity;->running:Z
return-void
2.虚方法
虚方法的定义会和直接方法唯一的不同就是注释不同:#virtual methods,其格式如下:
#virtual methods
.method <访问权限> [修饰关键词] <方法原想>
<.locals>
[.parameter1]
[.parameter2]
[.prologue]
[.line]
<代码逻辑>
.end
原始类型
指令 | 说明 |
---|---|
B | byte |
C | char |
D | double |
F | float |
I | int |
J | long |
S | short |
V | void |
Z | boolean |
Smali基本语法
指令 | 说明 |
---|---|
.field private isFlag:z | 定义变量 |
.method | |
.parameter | 方法参数 |
.prologue | 方法开始 |
.line 123 | 此方法位于第123行 |
invoke-super | 调用父函数 |
const/high16 vO, 0x7fo3 | 把0x7fo3赋值给v0 |
invoke-direct | 调用函数 |
return-void | 函数返回void |
.end method | 函数结束 |
new-instance | 创建实例 |
iput-object | 对象赋值 |
iget-object | 调用对象 |
invoke-static | 调用静态函数 |
条件跳转分支 | |
“if-eq vA, vB, :con_**” | 如果vA等于vB则跳转到:cond_ ** |
“if-ne vA, vB, :cond_**” | 如果vA不等于vB则跳转到:cond_ ** |
“if-It vA, vB, :cond_**” | 如果vA小于vB则跳转 到:cond_ ** |
“if-ge vA, vB, :cond_**” | 如果vA大于等于vB则跳转到:cond_ ** |
“if-gt vA, vB, :cond_**” | 如果vA大于vB则跳转到:cond_ ** |
“if-le vA, vB, :cond_ *” | 如果vA小于等于vB则跳转到:cond_ ** |
“if-eqz vA, :cond_**” | 如果vA等于0则跳转到:cond_ ** |
“if-nez vA, :cond_**” | 如果vA不等于0则跳转到:cond_ ** |
“if-Itz vA, :cond_**” | 如果vA小于0则跳转到:cond_** |
“if-gez vA, :cond_**” | 如果vA大于等 于0则跳转到:cond_ ** |
“if-gtz vA, :cond_**” | 如果vA大于0则跳转到:cond_ ** |
参考文章
整理自下面两篇文章
https://blog.csdn.net/dd864140130/article/details/52076515
https://blog.csdn.net/gqv2009/article/details/124812525