深入理解 Dalvik 字节码指令及 Smali 文件:https://blog.csdn.net/dd864140130/article/details/52076515
安卓逆向入门教程(二)--- 初识 APK、Dalvik 字节码以及 Smali:https://www.52pojie.cn/thread-395689-1-1.html
超详细的 Dalvik 指令:https://blog.csdn.net/qq_32113133/article/details/8524614
哔哩哔哩 视频:https://www.bilibili.com/video/BV1UE411A7rW?p=28
smali语法官方
Android APP静态分析-Smali语法详解
Smali语法官方
Google wiki
特点:
Java 虚拟机运行的是 Java 字节码,Dalvik 虚拟机运行的是 Dalvik 字节码。
Dalvik字节码 由 Java字节码 转换而来,并打包成一个 DEX 可执行文件,通过虚拟机解释执行。
Hello.java文件:
public class Hello {
public static void main(String[] args){
//System.out.print("Hello");
int i=2;
int j=3;
}
}
通过 javac Hello.java 得到 Hello.class 文件
JDK 中提供了 javap 命令反汇编可以查看 class文件字节码:javap -c Hello -> demo.txt
将 Hello.class 字节码中的内容存放到 demo.txt 中,如下:
Compiled from "Hello.java"
public class Hello {
public Hello();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_2
1: istore_1
2: iconst_3
3: istore_2
4: return
}
上面就是 java 字节码的内容。
通过 dx 工具可以将 Hello.class 文件转为 dex 文件:
dx --dex --output=Hello.dex Hello.class
Dalvik字节码我们无法直接查看,最终需要查看的是 samli 格式的文件,通过将 Hello.dex 文件转为 smali文件,smali 文件是Dalvik 可以识别的核心代码,可以使用 AndroidStudio 自带的插件实现(或者使用 baksmali 也是可以的)。
smali 有自己的一套语法,可以参考:https://blog.csdn.net/qq_32113133/article/details/85163277
以下就是相对应的 smali 文件格式:
.class public Lcom/example/administrator/myapplication/Hello;
.super Ljava/lang/Object;
.source "Hello.java"
# direct methods
.method public constructor ()V
.registers 1
.prologue
.line 3
invoke-direct {p0}, Ljava/lang/Object;->()V
return-void
.end method
.method public static main([Ljava/lang/String;)V
.registers 3
.param p0, "args" # [Ljava/lang/String;
.prologue
.line 7
const/4 v0, 0x2
.line 8
.local v0, "i":I
const/4 v1, 0x3
.line 9
.local v1, "j":I
return-void
.end method
通过比较可知:.class字节码 中的内容格式 和 Dalvik 中的字节码是不一样的,所以 Android 虚拟机加载的是 dex 字节码,而不能加载 java 字节码。即 Dalvik 不会去加载 .class字节码。
Android SDK中的 dx 工具在转换字节码时会消除类文件的冗余信息,避免虚拟机在初始化时出现重复的文件加载与解析过程。另外,dx工具会将所有的 Java类文件中的常量池进行分解,消除其中的冗余信息,组合成一个新的共享常量池。这使得文件体积和解析文件的效率都得到了提高。所以,Dalvik虚拟机比Java虚拟机执行速度快。
通过实例来对比 Java字节码 和 Dalvik字节码 的不同
源码如下:
public class Hello {
public static void main(String[] args){
Hello hello = new Hello();
System.out.println(hello.foo(5,3));
}
public int foo(int a,int b){
return (a+b) * (a-b);
}
}
Java字节码如下:
public int foo(int, int);
Code:
0: iload_1
1: iload_2
2: iadd
3: iload_1
4: iload_2
5: isub
6: imul
7: ireturn
iload_1分为两部分:
第二条指令iload_2取第三个参数。
第三条指令iadd从栈顶弹出两个int类型值,将值相加,然后把结果押回栈顶。
第四,第五条指令分别再次压入第二个参数与第三个参数。
第六条指令isub从栈顶弹出两个int类型值,然后相减,然后把结果压回栈顶。这时求值栈上有两个int值了。
第七条指令imul从栈顶弹出两个int类型值,将值相乘,然后把结果压回栈顶。
第八条指令ireturn函数返回一个int值。
参考Java字节码指令列表:https://en.wikipedia.org/wiki/Java_bytecode_instruction_listings
那如何查看生成的 Dalvik字节码呢?
可以使用 dexdump.exe (位于SDK下的platform-tools目录,新版本可能在build-tools下的版本目录下),前提是得到了Hello.dex文件,执行如下命令:dexdump -d Hello.dex ->Hello.txt
整理如下:
0000: add-int v0, v3, v4
0002: sub-int v1, v3, v4
0004: mul-int/2addr v0, v1
0005: return v0
Java虚拟机的架构及参数传递
Java虚拟机基于栈架构。需要频繁的从栈上读写数据,在这个过程中需要更多的指令分派与内存访问次数,耗费CPU时间,消耗手机资源。
Java虚拟机的指令集被称为零地址,是指指令的源参数与目标参数都是隐含的,它通过Java虚拟机中提高的一种数据结构“求值栈”来传递的。
对于Java程序来说,每个线程在执行时都有一个PC计数器与一个Java栈。PC计数器以字节为单位记录当前运行位置距离方法开头的偏移量,与ARM架构和x86架构类似,通过栈帧对栈中的数据进行操作。
Java栈用于记录Java方法调用的“活动记录”,Java栈以帧为单位保存线程的运行状态,每调用一个方法就会分配一个新的栈帧压入Java栈上,每从一个方法返回则弹出并撤销响应的栈帧。
每个栈帧包括局部变量区、求值栈(JVM规范中将其称为“操作数栈”)和其他一些信息。局部变量区用于存储方法的参数与局部变量,其中参数按源码中从左到右顺序保存在局部变量区开头的几个slot中。
求栈值用于保存求值的中间结果和调用别的方法的参数等,JVM运行时它的状态如下图
每条指令占用一个字节空间,foo()函数Java字节码左边的偏移量就是程序执行到每一行代码时PC的值,并且Java虚拟机最多只支撑0xff条指令。
iload_1可分成两部分,第一部分为下划线左边的iload,属于JVM(Java虚拟机)指令集中load系列中的一条,i是指令前缀,表示操作类型为int类型,load表示将局部变量存入Java栈。第二部分为下划线右边的数字,表示要操作具体哪个变量,索引值从0开始计数,iload_1表示将第二个int类型的局部变量进栈参数。
Dalvik虚拟机基于寄存器架构,数据访问通过寄存器间直接传递。比起Java虚拟机字节码要简洁很多。
以第一条指令为例简单分析一下。指令add-int将v3与v4寄存器的值相加,然后保存到v0寄存器,v3和v4代表调用函数的所使用的两个参数。这里用到的Dalvik字节码参数表示法是v命名法,另一种是p命名法。
Dalvik虚拟机运行时同样为每个线程维护一个PC计数器与调用栈,与Java虚拟机不同的是,这个调用栈维护一份寄存器列表,寄存器的数量在方法结构体的registers字段中给出,Dalvik虚拟机会根据这个值来创建一份虚拟的寄存器列表。
Dalvik虚拟机由于生成的代码指令减少了,程序执行速度会更快一些。
Android系统的架构采用分层思想,这样的好处是拥有减少各层之间的依赖性、便于独立分发、容易收敛问题和错误等优点。
Android系统由linux内核、函数库、Android运行时、应用程序框架以及应用程序组成。
Android系统加载完内核后,第一个执行的是init进程,init进程对设备进行初始化,读取init.rc文件并启动系统中重要外部程序Zygote。
Zygote进程是Android所有进程的孵化器,启动后首先初始化Dalvik虚拟机,然后启动system_server并进入Zygote模式,通过socket等待命令。
当执行一个Android应用程序时,system_server进程通过socket方式发送命令给Zygote,Zygote收到命令后通过fork自身创建一个Dalvik虚拟机的实例来执行应用程序的入口函数,这就是程序启动的流程。
Zygote 提供三种
fork之后,执行的工作交给Dalvik虚拟机。虚拟机通过loadClassFromDex()函数完成类的装载工作,每个类被成功解析后会拥有一个ClassObject类型的数据结构存储在运行时环境中,虚拟机使用gDvm.loadedClasses全局哈希表来存储与查询所有装载进来的类,随后,字节码验证器使用dvmVerifyCodeFlow()函数对装入的代码进行校验,接着虚拟机调用FindClass()函数查找并装载main方法类,随后调用dvmInterpret()函数初始化解释器并执行字节码流。
Dalvik 虚拟机是如何执行程序的 ?
当进程fork成功后:
Dalvik虚拟机
| 通过loadClassFromDex()函数完成类的装载工作,完成后会每个类会拥有一个ClassObject类型的数据结构存储在运行时环境中
| 虚拟机使用gDvm.loadedClasses全局哈希表来存储与查询所有装载进来的类
装载程序类
| 字节验证码器使用dvmVerifyCodeFlow()函数对装入的代码进行校验
|
验证字节码
| 调用FindClass()函数查找并装载main方法类
|
查找主类
| 调用dvmInterpret()函数初始化解释器并执行字节码流
|
执行字节码流
|
|
结束
虚拟机线程 ---> 装载程序类 ---> 验证字节码 ---> 查找主类 ---> 执行字节码流 ---> 结束
JIT是即时编译(动态编译),是通过在运行时将字节码翻译为机器码的技术,使得程序的执行速度更快。
主流的JIT包含两种字节码编译方式:
method方式:以函数或方法为单位进行编译。
trace方式:以trace为单位进行编译。
执行代码分为冷路径(在实践运行过程中很少被执行的)和热路径(执行比较频繁的路径),method会编译整个方法,trace编译的是获取的热路径的代码,节省内存。
Arm:一家 1990 年成立的英国公司,主要进行 Cpu 授权,是一种知识产权公司,占据移动 95% 的市场,主要靠设备抽成盈利。
产权领域:指令集架构、微处理器、GPU、互连架构等
特点:低功耗、低成本,使用RISC(Reduced Instruction Set Computer,通常仅执行1或2个指令即可完成意图),
和big.LITTLE架构(适配高性能-64位的内核和低性能-32位的内核切换),方便其他品牌贴标生产
授权方式:架构、内核、使用三种形式,分别针对大、中、小型公司
合作伙伴:高通、联发科等蚂蚁联盟
合作形式:实行OEM(Original Equipment Manufacture)
对手劣势:1968年成立的Intel使用CISC(Complex Instruction Set Computer,通常仅执行3或4个指令即可完成意图)架构的x86功耗高、掉电快
适配执行:从上面介绍可以看出,Arm占据绝对多数市场,因此mips和x86基础不需要考虑适配,而且它们会主动解析Arm指令成自己能使用的指令(使得自身效率更低,不得不赞叹规模效应的强大),big.LITTLE的高低性能内核切换,可以进行有效省电(计算量小用低性能、大用高性能,性能越高越耗电)。而64位寄存器的使用,减少内存存取的次数,提高CPU处理效率,用于RISC架构高性能的处理器。
Mips:1998 年成立,同样依据 RISC 架构,中科院设计的 “龙芯” 与它 95% 类似,涉及侵权,而准备收购其估值1亿的20%股份。
在 Android Studio 下,则可以通过以下的构建方式指定需要类型的 SO 库。
Dalvik 中用的寄存器都是 32位,64位类型数据则用两个相邻的32位寄存器表示,也就是对于double这种64位类型的数据,需要用到两个32位寄存器来存储。
Dalvik 最多支持 65536 个寄存器 ( 编号从0~65535 ),但是在 ARM 架构的 cpu 中只存在 37 个寄存器,那么这种不对称是怎么解决的呢?
Dalvik 中的寄存器是虚拟寄存器, 通过映射真实的寄存器来实现。我们知道每个 Dalvik 维护了一个调用栈,该调用栈就是用来支持虚拟寄存器 和 真实寄存器相互映射的。在执行具体函数时,Dalvik会根据 .registers 指令来确定该函数要用到的寄存器数目。具体的原理,可以自行参考 Davilk 的实现。
下面我们谈到的寄存器都是虚拟寄存器.
对于一个使用 m个寄存器 (m = 局部变量寄存器个数 x + 参数寄存器个数 y) 的方法而言:
示例说明:假设实例方法 test(String a,String b) 一共使用了5个寄存器:0,1,2,3,4,那么参数寄存器是能使用 2,3,4 这三个寄存器,如图:
寄存器 有两种不同的命名方法(这两种命名法仅仅是影响了字节码的可读性):
v 表示本地寄存器(局部变量寄存器),p表示参数寄存器。
V命名法 和 P命名法
public class Hello {
public static void main(String[] args){
Hello hello = new Hello();
System.out.println(hello.foo(5,3));
}
public int foo(int a,int b){
return (a+b) * (a-b);
}
}
main 函数 1个 形参,foo 函数 2 个形参,总共需要 3 个参数寄存器。针对这个代码,v 命名法 和 p 命名法对比:
使用 p 命名法表示的 Dalvik 汇编代码,通过寄存器的前缀更容易判断寄存器到底是局部变量寄存器还是参数寄存器,在 Dalvik 汇编代码较长,使用寄存器较多的情况下,这种优势更加明显。
以小写字母 v 开头的方式:表示 方法中 使用的 局部变量 和 参数。
对于上面实例方法 test(String a, String b)来说:v0,v1 为局部变量能够使用的寄存器。v2,v3,v4 为参数能够使用的寄存器:
以小写字母 p 开头的方式:表示 参数,参数名称从 p0 开始,依次增大。局部变量能够使用的寄存器仍然是以 v 开头。
对于上面实例方法 test(String a, String b)来说:v0,v1 为局部变量能够使用的寄存器。p0,p1,p2 为参数能够使用的寄存器。
p 命名法:寄存器采用 v 和 p 来命名,
如果一个非静态方法有两个本地变量,有三个参数,需要的寄存器关系如下:
如果是静态方法,那么就不需要 this 对象了,需要的寄存器是v0, v1, p0, p1, p2。
与 JVM 类似,Dalvik 字节码中同样有一套用于描述 类型、方法、字段 的方法,这些方法结合 Dalvik 的指令便形成了完整的汇编代码。
Dalvik 字节码 只有 两种 类型( 除了 对象 和 数组 属于 引用类型 外,其他的 Java 类型都是基本类型 ):
Dalvik 使用这两种类型来表示 Java 语言的全部类型。注意:对象 和 数组 都是 引用类型。
Davilk 中对字节码类型的描述 和 JVM 中的描述符规则一致:
全限定名 是 什么?
以 String 为例,其完整名称是 java.lang.String,那么其 全限定名 就是 java/lang/String;
即 java.lang.String 的 "." 用 "/" 代替,并在末尾添加分号 ”;” 做结束符。
Java类型 和 类型描述符
每个 Dalvik 寄存器都是 32 位,对于小于或等于 32 位长度的类型来说,一个寄存器就可以存放该类型的值;像 J、D 等 64位 类型的值,它们的值使用相邻两个寄存器来存储,如 v0 和 v1。
示例解释:
图 示:
这里重点解释 对象类型 和 数组类型。
L 可以表示 java 类型中 的 任何类。
对象类型
形式:Lxxx/yyy/zzz;
L
表示这是一个对象类型xxx/yyy
是该对象所在的包zzz
是对象名称;
标识对象名称的结束
如:Ljava/lang/String;
[ 类型 用来表示 所有基本类型 的 数组,[ 后跟着是基本类型的描述符。每一维度使用一个前置的 [ 。
比如:java 中的 int[] 用汇编码表示便是 [I; 。二维数组 int[][] 为 [[I; ,三维数组则用 [[[I; 表示。
对于对象数组来说,[ 后跟着对应类的全限定符。比如 java 当中的 String[] 对应的是 [java/lang/String; 。
数组类型
形式:[XXX
[I
表示一个 int 型的一维数组,相当于int[]
- 增加一个维度增加一个
[
,如[[I
表示int[][]
- 数组每一个维度最多 255 个;
- 对象数组表示也是类似
如:String 数组的表示是 [Ljava/lang/String
Dalvik 中 对字段的描述 分为两种:
但 两者 的 描述格式 一样:
对象类型描述符; -> 字段名:类型描述符;
比如 com.sbbic.Test 类 中存在 String类型的 name字段 及 int 类型的 age 字段,那么其描述为:
Lcom/sbbic/Test;->name:Ljava/lang/String;
Lcom/sbbic/test;->age:I
字段 的 描述
形式:Lxxx/yyy/zzz;->FieldName:Lxxx/yyy/zzz;
例如:ff = "aa"; 把 字符串 aa 赋值给 变量 ff。 转换后是 Lcom/example/reforceapk/MyLog;->ff:Ljava/lang/String
示例: Lpackage/name/ObjectName;->FieldName:Ljava/lang/String;
Java 中方法的签名包括 方法名、参数 及 返回值。在 Davilk 相应的描述规则为:
对象类型描述符 -> 方法名(参数类型描述符)返回值类型描述符
方法格式:Lpackage/name/ObjectName;->MethodName(III)Z
BakSmali 生成的方法以 .method 指令开始,以 .end method 指令结束。使用 #来注释。
下面我们通过几个例子来说明。以 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
图示:
方法 的 描述
形式:Lxxx/yyy/zzz;->methodName(Lxxx/yyy/zzz;Lxxx/yyy/zzz;I)Z
如:Log.i("xxx", msg); 转换成smali语句之后是:Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I
掌握以上的 字段 和 方法 的 描述,只能说我们懂了如何描述一个 字段 和 方法,而关于方法中具体的逻辑则需要了解 Dalvik 中的指令集。因为 Dalvik 是基于 寄存器 的架构的,因此 指令集 和 JVM 中的指令集区别较大。反而更类似 x86 的中的汇编指令。
Dalvik 指令集中,大多数指令用到了寄存器作为目的操作数 或 源操作数,其中 A/B/C/D/E/F/G/H 代表一个4位的数值, 可用来表示 v0~v15 的寄存器。 AA/BB/.../HH代表一个8位的数值。 AAAA/BBBB/.../HHHH 代表一个16位的数值。( 16进制表示,一个字母代表 4 位 2进制 )
Dalvik 在调用格式上模仿了C语言的调用约定,官方地址,指令语法与助词有如下特点:
如:
move-wide/from16 vAA, vBBBB
move-wide/from16 v18, v0
move: 基础字节码(base opcode),标示是基本操作
wide: 标示指令操作的数据宽度为64位宽度
from16: 字节码后缀(opcode suffix),标示源(vBBBB)为一个16的寄存器引用变量
vAA: 目的寄存器,v0~v255
vBBBB: 源寄存器,v0~v65535
锁指令多用在多线程程序中对同一对象的操作。Dalvik 指令集中有两条锁指令。
Java 代码:
private void callSynchronizeClassMethod() {
synchronized (MainActivity.class) {
Log.d("TAG","synchronized class");
}
}
private void callSynchronizeMethod() {
synchronized (this) {
Log.d("TAG","synchronized this");
}
}
private synchronized void callLockMethod() {
Log.d("TAG","synchronized method");
}
对应代码:
.method private declared-synchronized callLockMethod()V
.locals 2
.prologue
.line 43
monitor-enter p0
:try_start_0
const-string v0, "TAG"
const-string v1, "synchronized method"
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
:try_end_0
.catchall {:try_start_0 .. :try_end_0} :catchall_0
.line 44
monitor-exit p0
return-void
.line 43
:catchall_0
move-exception v0
monitor-exit p0
throw v0
.end method
.method private callSynchronizeClassMethod()V
.locals 3
.prologue
.line 31
const-class v1, Lcom/woblog/testsmali/MainActivity;
monitor-enter v1
.line 32
:try_start_0
const-string v0, "TAG"
const-string v2, "synchronized class"
invoke-static {v0, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 33
monitor-exit v1
.line 34
return-void
.line 33
:catchall_0
move-exception v0
monitor-exit v1
:try_end_0
.catchall {:try_start_0 .. :try_end_0} :catchall_0
throw v0
.end method
.method private callSynchronizeMethod()V
.locals 2
.prologue
.line 37
monitor-enter p0
.line 38
:try_start_0
const-string v0, "TAG"
const-string v1, "synchronized this"
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 39
monitor-exit p0
.line 40
return-void
.line 39
:catchall_0
move-exception v0
monitor-exit p0
:try_end_0
.catchall {:try_start_0 .. :try_end_0} :catchall_0
throw v0
.end method
与实例相关的操作包括实例的类型转换、检查及新建等
Java 代码:
CharSequence cs = new String();
Object o = cs;
String s = (String) cs;
//实例检测
if (s instanceof CharSequence) {
Log.d("TAG", "ok");
} else {
Log.d("TAG","no");
}
//创建实例
StringBuilder sb = new StringBuilder();
sb.append("Ok");
String s1 = new String("new string");
String s2 = "string";
对应代码:
new-instance v1, Ljava/lang/String;
invoke-direct {v1}, Ljava/lang/String;->()V
.line 33
.local v1, "cs":Ljava/lang/CharSequence;
move-object v7, v1
.local v7, "o":Ljava/lang/CharSequence;
move-object v8, v1
.line 35
check-cast v8, Ljava/lang/String;
.line 38
.local v8, "s":Ljava/lang/String;
instance-of v12, v8, Ljava/lang/CharSequence;
if-eqz v12, :cond_0
.line 39
const-string v12, "TAG"
const-string v13, "ok"
invoke-static {v12, v13}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 46
:goto_0
new-instance v11, Ljava/lang/StringBuilder;
invoke-direct {v11}, Ljava/lang/StringBuilder;->()V
.line 47
.local v11, "sb":Ljava/lang/StringBuilder;
const-string v12, "Ok"
invoke-virtual {v11, v12}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
.line 49
new-instance v9, Ljava/lang/String;
const-string v12, "new string"
invoke-direct {v9, v12}, Ljava/lang/String;->(Ljava/lang/String;)V
.line 50
.local v9, "s1":Ljava/lang/String;
const-string v10, "string"
.line 51
.local v10, "s2":Ljava/lang/String;
return-void
.line 41
.end local v9 # "s1":Ljava/lang/String;
.end local v10 # "s2":Ljava/lang/String;
.end local v11 # "sb":Ljava/lang/StringBuilder;
:cond_0
const-string v12, "TAG"
const-string v13, "no"
invoke-static {v12, v13}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
goto :goto_0
数据定义指令用于定义代码中使用的常量、类 等数据,基础指令是 const
数据定义指令
数据定义指令用来定义程序中用到的常量、字符串、类等数据。它的基础字节码为 const。
const/4 vA,#+B 将数值符号扩展为 32位 后赋给寄存器 vA
const/16 vAA,#+BBBB 将数值符号扩展为 64位 后赋给寄存器 vAA
const vAA,#+BBBBBBBB 将数值赋给寄存器vAA
const/high16 vAA,#+BBBB0000 将数值右边 0 扩展为32位后赋给寄存器vAA
const-wide/16 vAA,#+BBBB 将数值符号扩展64位后赋给寄存器对vAA
const-wide vAA,#+BBBBBBBBBBBBBBBB 将数值赋给寄存器对vAA
const-wide/high16 vAA,#+BBBB000000000000 将数值右边 0 扩展为64位后付给寄存器对 vAA
const-string vAA,string@BBBB 通过字符串索引构造一个字符串并赋给寄存器对 vAA
const-string/jumbo vAA,string@BBBBBBBB 通过字符串索引(较大) 构造一个字符串并付给寄存器对vAA
const-class vAA,type@BBBB 通过类型索引获取一个类引用并付给寄存器 vAA
通过给定的类型那个索引获取一个类索引并付给寄存器vAAAA
(这条指令占用两个字节,值为0x00ff,是Android4.0中新增的指令)
const-class/jumbo vAAAA,type@BBBBBBBB
Java 示例代码:
private void testConst() {
int a = 1;
int b = 7;
int c = 254;
int d = 2345;
int d1 = 65538;
long e = 12435465657677L;
float f = 123235409234.09097945F;
double g = 111343333454999999999.912384375;
}
对应代码:
//-8到7用4,大于255小于等于65535用16
const/4 v0, 0x1
.line 25
.local v0, "a":I
const/4 v1, 0x7
.line 26
.local v1, "b":I
const/16 v2, 0xfe
.line 27
.local v2, "c":I
const/16 v3, 0x929
.line 28
.local v3, "d":I
const v4, 0x10002 //65538,大于65535用const v4
//long用const-wide
.line 30
.local v4, "d1":I
const-wide v6, 0xb4f5b835d4dL
.line 31
.local v6, "e":J
const v5, 0x51e58b39
.line 32
.local v5, "f":F
const-wide v8, 0x441824cbef6b9491L # 1.11343333455E20
move 指令用于数据操作,其表示 move destination, source,即数据数据从 source 寄存器(源寄存器) 移动到 destionation 寄存器(源寄存器),可以理解 java 中变量间的赋值操作。根据 字节码 和 类型 的不同,move 指令后会跟上不同的后缀。
数据操作指令
数据操作指令为 move。指令格式: move 目标,源
move 指令根据字节码大小与类型不同,后面会跟上不同的后缀。
move-object/from16 vAA, vBBBB 为对象赋值。源寄存器为16位,目的寄存器为8位。
move-object/16 vAAAA,vBBBB 为对象复制。源寄存器与目的寄存器都为16位
move-result-wide vAA 将上一个invoke类型指令操作的双(没有-wide则是 单 )字非对象结果赋给vAA寄存器
move-result-object vAA 将上一个invoke类型指令操作的非对象结果赋给vAA寄存器
保存一个运行时发生的异常到vAA寄存器。
这条指令必须是异常发生是的异常处理器的一条指令。否则的话,指令无效。
move-exception vAA
Java 示例代码:
private void testMove() {
int a = 100;
long b = 100000000000000000L;
int c = a;
long d = b;
Log.d(TAG,c+"");
Log.d(TAG,d+"");
int e = getIntResult();
Log.d(TAG,e+"");
try {
int f = e/c;
} catch (ArithmeticException e1) {
e1.printStackTrace();
}catch (Exception e1) {
e1.printStackTrace();
}finally {
}
}
对应代码:
//move-result-object
invoke-direct {v7}, Ljava/lang/StringBuilder;->()V
invoke-virtual {v7, v1}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
move-result-object v7
const-string v8, ""
invoke-virtual {v7, v8}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v7
//move-result
invoke-direct {p0}, Lcom/woblog/testsmali/MainActivity;->getIntResult()I
move-result v6
//move exception
.line 35
:try_start_0
div-int v8, v6, v1
:try_end_0
.catch Ljava/lang/ArithmeticException; {:try_start_0 .. :try_end_0} :catch_0
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_1
.catchall {:try_start_0 .. :try_end_0} :catchall_0
.line 43
:goto_0
return-void
.line 36
:catch_0
move-exception v7
.line 37
.local v7, "e1":Ljava/lang/ArithmeticException;
:try_start_1
invoke-virtual {v7}, Ljava/lang/ArithmeticException;->printStackTrace()V
:try_end_1
.catchall {:try_start_1 .. :try_end_1} :catchall_0
goto :goto_0
.line 40
.end local v7 # "e1":Ljava/lang/ArithmeticException;
:catchall_0
move-exception v8
throw v8
.line 38
:catch_1
move-exception v7
.line 39
.local v7, "e1":Ljava/lang/Exception;
:try_start_2
invoke-virtual {v7}, Ljava/lang/Exception;->printStackTrace()V
:try_end_2
.catchall {:try_start_2 .. :try_end_2} :catchall_0
goto :goto_0
与对象实例相关的操作,比如对象创建、对象检查等。
在实例操作指令中我们并没有发现创建对象的指令。Davilk 中设置专门的指令用于数组操作。
数组操作包括:读取数组长度、新建数组、数组赋值、数组元素取值与赋值、等 操作。
fill-array-data vAA, +BBBBBBBB
用指定的数据来填充数组,vAA寄存器为数组引用,引用必须为基础类型的数组,在指令后面会紧跟一个数据表
arrayop vAA, vBB, vCC
对 vBB 寄存器指定的数组元素进入取值与赋值。vCC 寄存器指定数组元素索引,vAA 寄存器用来寄放读取的或需要设置的数组元素的值。读取元素使用 aget 类指令,元素赋值使用 aput 指令,元素赋值使用 aput 类指令,根据数组中存储的类型指令后面会紧跟不同的指令后缀,指令列表有 aget、aget-wide、aget-object、aget-boolean、aget-byte、aget-char、aget-short、aput、aput-wide、aput-boolean、aput-byte、aput-char、aput-short。
Java 代码:
private void testArray() {
int[] ints = new int[2];
int[] ints1 = null;
int[] ints2 = {1,2,3};
Integer[] integers = new Integer[]{1,2,4};
int[] strings = {1,2,3,4,5,6,5,6,6,6,6,6,6,7,7,8,8,8,8,8,1,1,1,3,3,5,6,54,5,6,56,567,67,6,34,45,45,6,56,57,45,45,5,56,56,7,34,543,543,6,56,56,45,4,54,5,45,56};
//数组长度
int length = ints.length;
int length1 = ints2.length;
int length2 = strings.length;
//获取数组元素
int string = strings[30];
int string1 = ints2[1];
//赋值
strings[30] = length;
ints2[1] = length2;
}
对应代码:
.method private testArray()V
.locals 15
.prologue
const/16 v14, 0x1e
const/4 v10, 0x3
const/4 v13, 0x2
const/4 v12, 0x1
.line 27
new-array v1, v13, [I
.line 28
.local v1, "ints":[I
const/4 v2, 0x0
.line 29
.local v2, "ints1":[I
new-array v3, v10, [I
fill-array-data v3, :array_0
.line 31
.local v3, "ints2":[I
new-array v0, v10, [Ljava/lang/Integer;
const/4 v10, 0x0
invoke-static {v12}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;
move-result-object v11
aput-object v11, v0, v10
invoke-static {v13}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;
move-result-object v10
aput-object v10, v0, v12
const/4 v10, 0x4
invoke-static {v10}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;
move-result-object v10
aput-object v10, v0, v13
.line 33
.local v0, "integers":[Ljava/lang/Integer;
const/16 v10, 0x3a
new-array v9, v10, [I
fill-array-data v9, :array_1
.line 36
.local v9, "strings":[I
array-length v4, v1
.line 37
.local v4, "length":I
array-length v5, v3
.line 38
.local v5, "length1":I
array-length v6, v9
.line 41
.local v6, "length2":I
aget v7, v9, v14
.line 42
.local v7, "string":I
aget v8, v3, v12
.line 45
.local v8, "string1":I
aput v4, v9, v14
.line 46
aput v6, v3, v12
.line 47
return-void
.line 29
:array_0
.array-data 4
0x1
0x2
0x3
.end array-data
.line 33
:array_1
.array-data 4
0x1
0x2
0x3
0x4
0x5
0x6
0x5
0x6
0x6
0x6
0x6
0x6
0x6
0x7
0x7
0x8
0x8
0x8
0x8
0x8
0x1
0x1
0x1
0x3
0x3
0x5
0x6
0x36
0x5
0x6
0x38
0x237
0x43
0x6
0x22
0x2d
0x2d
0x6
0x38
0x39
0x2d
0x2d
0x5
0x38
0x38
0x7
0x22
0x21f
0x21f
0x6
0x38
0x38
0x2d
0x4
0x36
0x5
0x2d
0x38
.end array-data
.end method
数据运算 主要包括 两种:
数据运算指令有如下四类(数据运算时可能在寄存器或寄存器对间进行,下面的指令作用讲解时使用寄存器来描述):
binop vAA,vBB,vCC 将vBB寄存器与vCC寄存器进行运算,结果保存到vAA寄存器
binop/2addr vA,vB 将vA寄存器与vB寄存器进行运算,结果保存到vA寄存器
binop/lit16 vA,vB,#+CCCC 将vB寄存器与常量CCCC进行运算,结果保存到vA寄存器
binop/lit8 vAA,vBB,#+CC 将vBB寄存器与常量CC进行运算,结果保存到vAA寄存器
后面 3 类指令比第 1 类指令分别多了 addr、lit16、lit8 等指令后缀。
这四类指令中的基础字节码后面加上数据类型后缀,如 -int 或 -long 分别表示操作的数据类型那个为整型与长整型。
第 1 类指令可归类如下:
add-type vBB寄存器与vCC寄存器值进行加法运算(vBB + vCC)
sub-type vBB寄存器与vCC寄存器值进行减法运算(vBB - vCC)
mul-type vBB寄存器与vCC寄存器值进行乘法运算(vBB * vCC)
div-type vBB寄存器与vCC寄存器值进除法运算(vBB / vCC)
rem-type vBB寄存器与vCC寄存器值进行模运算(vBB % vCC)
and-type vBB寄存器与vCC寄存器值进行与运算(vBB & vCC)
or-type vBB寄存器与vCC寄存器值进行或运算(vBB | vCC)
xor-type vBB寄存器与vCC寄存器值进行异或运算(vBB ^ vCC)
shl-type vBB寄存器(有符号数)左移vCC位(vBB << vCC)
shr-type vBB寄存器(有符号数)右移vCC位(vBB >> vCC)
ushr-type vBB寄存器(无符号数)右移vCC位(vBB >> vCC)
其中基础字节码后面的 -type 可以是 -int、-long、-float、-double。后面3类指令与之类似。
1. 算术运算指令
2. 逻辑元算指令
3. 位移指令
上面的 -type 表示操作的寄存器中数据的类型,可以是 -int、-float、-long、-double 等。
比较指令用于比较两个寄存器的值(浮点型或长整型)的大小,其基本格式格式是:cmp+kind-type vAA,vBB,vCC
格式为: cmpkind vAA,vBB,vCC,其中vBB寄存器与vCC寄存器是需要比较的两个寄存器或者两个寄存器对,比较的结果放到vAA寄存器。Dalvik 指令集中共有 5 条比较指令。
cmpl 是 compare less 的缩写,因此 cmpl 表示 vBB 小于 vCC 中的值这个条件是否成立,是则返回1,否则返回-1,相等返回0;cmpg 是 compare greater 的缩写,因此 cmpg 表示 vBB 大于vCC 中的值这个条件是否成立,是则返回 1,否则返回 -1,相等返回 0。cmp 和 cmpg 的语意一致,即表示 vBB 大于 vCC 寄存器中的值是否成立,成立则返回 1,否则返回 -1,相等返回 0
来具体看看 Davilk 中的指令:
比较指令
cmpl-float 比较两个单精度浮点数。如果vBB寄存器小于vCC寄存器,则结果为1,相等则结果为0,大于的话结果为-1。
cmpg-float 比较两个单精度浮点数。如果vBB寄存器大于vCC寄存器,则结果为1,相等则结果为0,小于的话结果为-1。
cmpl-double 比较两个双精度浮点数。如果vBB寄存器小于vCC寄存器,则结果为1,相等则结果为0,大于的话结果为-1。
cmpg-double 比较两个双精度浮点数。如果vBB寄存器大于vCC寄存器,则结果为1,相等则结果为0,小于的话结果为-1。
cmp-long 比较两个长整型数。如果vBB寄存器大于vCC寄存器,则结果为1,相等则结果为0,小于的话结果为-1。
Java 代码:
private void testCmpLong() {
long a = 13;
long b = 12;
if (a < b) {
Log.d("TAG", "<");
} else if (a > b) {
Log.d("TAG", ">");
} else {
Log.d("TAG", "=");
}
}
private void testCmpDouble() {
double a = 13.4;
double b = 11.4;
if (a < b) {
Log.d("TAG", "<");
} else if (a > b) {
Log.d("TAG", ">");
} else {
Log.d("TAG", "=");
}
}
private void testCmpFloat() {
float a = 13.4F;
float b = 10.4F;
if (a < b) {
Log.d("TAG", "<");
} else if (a > b) {
Log.d("TAG", ">");
} else {
Log.d("TAG", "=");
}
}
对应代码:
.method private testCmpDouble()V
.locals 6
.prologue
.line 46
const-wide v0, 0x402acccccccccccdL # 13.4
.line 47
.local v0, "a":D
const-wide v2, 0x4026cccccccccccdL # 11.4
.line 48
.local v2, "b":D
cmpg-double v4, v0, v2
if-gez v4, :cond_0
.line 49
const-string v4, "TAG"
const-string v5, "<"
invoke-static {v4, v5}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 55
:goto_0
return-void
.line 50
:cond_0
cmpl-double v4, v0, v2
if-lez v4, :cond_1
.line 51
const-string v4, "TAG"
const-string v5, ">"
invoke-static {v4, v5}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
goto :goto_0
.line 53
:cond_1
const-string v4, "TAG"
const-string v5, "="
invoke-static {v4, v5}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
goto :goto_0
.end method
.method private testCmpFloat()V
.locals 4
.prologue
.line 58
const v0, 0x41566666 # 13.4f
.line 59
.local v0, "a":F
const v1, 0x41266666 # 10.4f
.line 60
.local v1, "b":F
cmpg-float v2, v0, v1
if-gez v2, :cond_0 #>=
.line 61
const-string v2, "TAG"
const-string v3, "<"
invoke-static {v2, v3}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 67
:goto_0
return-void
.line 62
:cond_0
cmpl-float v2, v0, v1
if-lez v2, :cond_1 #<=
.line 63
const-string v2, "TAG"
const-string v3, ">"
invoke-static {v2, v3}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
goto :goto_0
.line 65
:cond_1
const-string v2, "TAG"
const-string v3, "="
invoke-static {v2, v3}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
goto :goto_0
.end method
.method private testCmpLong()V
.locals 6
.prologue
.line 34
const-wide/16 v0, 0xd
.line 35
.local v0, "a":J
const-wide/16 v2, 0xc
.line 36
.local v2, "b":J
cmp-long v4, v0, v2
if-gez v4, :cond_0
.line 37
const-string v4, "TAG"
const-string v5, "<"
invoke-static {v4, v5}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 43
:goto_0
return-void
.line 38
:cond_0
cmp-long v4, v0, v2
if-lez v4, :cond_1
.line 39
const-string v4, "TAG"
const-string v5, ">"
invoke-static {v4, v5}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
goto :goto_0
.line 41
:cond_1
const-string v4, "TAG"
const-string v5, "="
invoke-static {v4, v5}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
goto :goto_0
.end method
字段操作指令用来对对象实例的字段进入读写操作( 即 字段操作指令 表示 对 对象字段进行 赋值 和 取值 操作,就像是你在代码中的 set 和 get 方法。 )。
字段的类型可以是 Java 中有效的数据类型,
对 普通字段 与 静态字段 操作的两中指令集:
基本指令是 iput-type、iget-type、sput-type、sget-type。type 表示数据类型。
根据访问的字段类型不同,字段操作指令后面会紧跟字段类型的后缀,如 iget-byte 指令表示读写实例字段的值类型为字节类型,iput-short 指令表示设置实例字段的值类型为短整型。两类指令操作结果都是一样的,只是指令前缀与操作的字段类型不同。
在 Android4.0 系统中,Dalvik 指令集中增加了 instanceop/jumbo vAAAA,vBBBB,field@CCCCCCCC 与 sstaticop/jumbo vAAAA,field@BBBBBBBB 两类指令,它们与上面介绍的两类指令作用相同,只是在指令中增加了 jumbo 字节码后缀,且寄存器值与指令的索引取值范围更大。
前缀是 i 的 iput-type 和 iget-type 指令用于字段的读写操作。
前缀是 s 的 sput-type 和 sget-type 指令用于静态字段的读写操作。
Java 代码:
private void testInstanceFieldOperator() {
//write
InstanceObject instanceObject = new InstanceObject();
instanceObject.aInt=1;
instanceObject.aLong=12454L;
instanceObject.aFloat=12344.45F;
instanceObject.aDouble=123546.2;
instanceObject.object=new Object();
instanceObject.aBoolean=true;
instanceObject.aByte=3;
instanceObject.aChar='c';
instanceObject.aShort=1;
Log.d("TAG",String.valueOf(instanceObject.aInt));
Log.d("TAG",String.valueOf(instanceObject.aLong));
Log.d("TAG",String.valueOf(instanceObject.aFloat));
Log.d("TAG",String.valueOf(instanceObject.aDouble));
Log.d("TAG",String.valueOf(instanceObject.object));
Log.d("TAG",String.valueOf(instanceObject.aBoolean));
Log.d("TAG",String.valueOf(instanceObject.aByte));
Log.d("TAG",String.valueOf(instanceObject.aChar));
Log.d("TAG",String.valueOf(instanceObject.aShort));
}
private void testStatusFieldOperator() {
//write
StatusObject.aInt=1;
StatusObject.aLong=12454L;
StatusObject.aFloat=12344.45F;
StatusObject.aDouble=123546.2;
StatusObject.object=new Object();
StatusObject.aBoolean=true;
StatusObject.aByte=3;
StatusObject.aChar='c';
StatusObject.aShort=1;
Log.d("TAG",String.valueOf(StatusObject.aInt));
Log.d("TAG",String.valueOf(StatusObject.aLong));
Log.d("TAG",String.valueOf(StatusObject.aFloat));
Log.d("TAG",String.valueOf(StatusObject.aDouble));
Log.d("TAG",String.valueOf(StatusObject.object));
Log.d("TAG",String.valueOf(StatusObject.aBoolean));
Log.d("TAG",String.valueOf(StatusObject.aByte));
Log.d("TAG",String.valueOf(StatusObject.aChar));
Log.d("TAG",String.valueOf(StatusObject.aShort));
}
对应代码:
.method private testInstanceFieldOperator()V
.locals 5
.prologue
const/4 v4, 0x1
.line 30
new-instance v0, Lcom/woblog/testsmali/InstanceObject;
invoke-direct {v0}, Lcom/woblog/testsmali/InstanceObject;->()V
.line 31
.local v0, "instanceObject":Lcom/woblog/testsmali/InstanceObject;
iput v4, v0, Lcom/woblog/testsmali/InstanceObject;->aInt:I
.line 32
const-wide/16 v2, 0x30a6
iput-wide v2, v0, Lcom/woblog/testsmali/InstanceObject;->aLong:J
.line 33
const v1, 0x4640e1cd
iput v1, v0, Lcom/woblog/testsmali/InstanceObject;->aFloat:F
.line 34
const-wide v2, 0x40fe29a333333333L # 123546.2
iput-wide v2, v0, Lcom/woblog/testsmali/InstanceObject;->aDouble:D
.line 35
new-instance v1, Ljava/lang/Object;
invoke-direct {v1}, Ljava/lang/Object;->()V
iput-object v1, v0, Lcom/woblog/testsmali/InstanceObject;->object:Ljava/lang/Object;
.line 36
iput-boolean v4, v0, Lcom/woblog/testsmali/InstanceObject;->aBoolean:Z
.line 37
const/4 v1, 0x3
iput-byte v1, v0, Lcom/woblog/testsmali/InstanceObject;->aByte:B
.line 38
const/16 v1, 0x63
iput-char v1, v0, Lcom/woblog/testsmali/InstanceObject;->aChar:C
.line 39
iput-short v4, v0, Lcom/woblog/testsmali/InstanceObject;->aShort:S
.line 41
const-string v1, "TAG"
iget v2, v0, Lcom/woblog/testsmali/InstanceObject;->aInt:I
invoke-static {v2}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v2
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 42
const-string v1, "TAG"
iget-wide v2, v0, Lcom/woblog/testsmali/InstanceObject;->aLong:J
invoke-static {v2, v3}, Ljava/lang/String;->valueOf(J)Ljava/lang/String;
move-result-object v2
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 43
const-string v1, "TAG"
iget v2, v0, Lcom/woblog/testsmali/InstanceObject;->aFloat:F
invoke-static {v2}, Ljava/lang/String;->valueOf(F)Ljava/lang/String;
move-result-object v2
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 44
const-string v1, "TAG"
iget-wide v2, v0, Lcom/woblog/testsmali/InstanceObject;->aDouble:D
invoke-static {v2, v3}, Ljava/lang/String;->valueOf(D)Ljava/lang/String;
move-result-object v2
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 45
const-string v1, "TAG"
iget-object v2, v0, Lcom/woblog/testsmali/InstanceObject;->object:Ljava/lang/Object;
invoke-static {v2}, Ljava/lang/String;->valueOf(Ljava/lang/Object;)Ljava/lang/String;
move-result-object v2
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 46
const-string v1, "TAG"
iget-boolean v2, v0, Lcom/woblog/testsmali/InstanceObject;->aBoolean:Z
invoke-static {v2}, Ljava/lang/String;->valueOf(Z)Ljava/lang/String;
move-result-object v2
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 47
const-string v1, "TAG"
iget-byte v2, v0, Lcom/woblog/testsmali/InstanceObject;->aByte:B
invoke-static {v2}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v2
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 48
const-string v1, "TAG"
iget-char v2, v0, Lcom/woblog/testsmali/InstanceObject;->aChar:C
invoke-static {v2}, Ljava/lang/String;->valueOf(C)Ljava/lang/String;
move-result-object v2
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 49
const-string v1, "TAG"
iget-short v2, v0, Lcom/woblog/testsmali/InstanceObject;->aShort:S
invoke-static {v2}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v2
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 50
return-void
.end method
.method private testStatusFieldOperator()V
.locals 4
.prologue
const/4 v2, 0x1
.line 54
sput v2, Lcom/woblog/testsmali/StatusObject;->aInt:I
.line 55
const-wide/16 v0, 0x30a6
sput-wide v0, Lcom/woblog/testsmali/StatusObject;->aLong:J
.line 56
const v0, 0x4640e1cd
sput v0, Lcom/woblog/testsmali/StatusObject;->aFloat:F
.line 57
const-wide v0, 0x40fe29a333333333L # 123546.2
sput-wide v0, Lcom/woblog/testsmali/StatusObject;->aDouble:D
.line 58
new-instance v0, Ljava/lang/Object;
invoke-direct {v0}, Ljava/lang/Object;->()V
sput-object v0, Lcom/woblog/testsmali/StatusObject;->object:Ljava/lang/Object;
.line 59
sput-boolean v2, Lcom/woblog/testsmali/StatusObject;->aBoolean:Z
.line 60
const/4 v0, 0x3
sput-byte v0, Lcom/woblog/testsmali/StatusObject;->aByte:B
.line 61
const/16 v0, 0x63
sput-char v0, Lcom/woblog/testsmali/StatusObject;->aChar:C
.line 62
sput-short v2, Lcom/woblog/testsmali/StatusObject;->aShort:S
.line 64
const-string v0, "TAG"
sget v1, Lcom/woblog/testsmali/StatusObject;->aInt:I
invoke-static {v1}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v1
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 65
const-string v0, "TAG"
sget-wide v2, Lcom/woblog/testsmali/StatusObject;->aLong:J
invoke-static {v2, v3}, Ljava/lang/String;->valueOf(J)Ljava/lang/String;
move-result-object v1
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 66
const-string v0, "TAG"
sget v1, Lcom/woblog/testsmali/StatusObject;->aFloat:F
invoke-static {v1}, Ljava/lang/String;->valueOf(F)Ljava/lang/String;
move-result-object v1
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 67
const-string v0, "TAG"
sget-wide v2, Lcom/woblog/testsmali/StatusObject;->aDouble:D
invoke-static {v2, v3}, Ljava/lang/String;->valueOf(D)Ljava/lang/String;
move-result-object v1
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 68
const-string v0, "TAG"
sget-object v1, Lcom/woblog/testsmali/StatusObject;->object:Ljava/lang/Object;
invoke-static {v1}, Ljava/lang/String;->valueOf(Ljava/lang/Object;)Ljava/lang/String;
move-result-object v1
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 69
const-string v0, "TAG"
sget-boolean v1, Lcom/woblog/testsmali/StatusObject;->aBoolean:Z
invoke-static {v1}, Ljava/lang/String;->valueOf(Z)Ljava/lang/String;
move-result-object v1
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 70
const-string v0, "TAG"
sget-byte v1, Lcom/woblog/testsmali/StatusObject;->aByte:B
invoke-static {v1}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v1
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 71
const-string v0, "TAG"
sget-char v1, Lcom/woblog/testsmali/StatusObject;->aChar:C
invoke-static {v1}, Ljava/lang/String;->valueOf(C)Ljava/lang/String;
move-result-object v1
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 72
const-string v0, "TAG"
sget-short v1, Lcom/woblog/testsmali/StatusObject;->aShort:S
invoke-static {v1}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v1
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 73
return-void
.end method
方法调用指令负责调用类实例的方法。它的基础指令为 invoke 。
方法调用指令有 invoke-kind {vC,vD,vE,vF,vG}, meth@BBBB 与 invoke-kind/range {vCCCC, ... ,vNNNN},meth@BBBB 两类。
两类指令在作用上并无不同,只是后者在设置参数寄存器时使用了 range 来指定寄存器的范围。
Davilk 中的方法指令和 JVM 的中指令大部分非常类似。根据方法类型的不同,目前共有五条指令集:
invoke-virtual 或 invoke-virtual/range 调用实例的虚方法
invoke-super 或 invoke-super/range 调用实例的父类方法
invoke-direct 或 invoke-direct/range 调用实例的直接方法
invoke-static 或 invoke-static/range 调用实例的静态方法
invoke-interface 或 invoke-interface/range 调用实例的接口方法
在 Android4.0 系统中,Dalvik 指令集中增加了 invoke-kind/jumbo {vCCCC, ... ,vNNNN},meth@BBBBBBBB 这类指令,它与上面介绍的两类指令作用相同,只是在指令中增加了 jumbo 字节码后缀,且寄存器值与指令的索引取值范围更大。
方法调用的指令的返回值必须使用 move-result-* 指令来获取。如下两条指令:
invoke-static {},Landroid/os/Parcel;->obtain()Landroid/osParcel;
move-result-object v0
图示:
这 5 种指令是基本指令,除此之外,你也会遇到 invoke-direct/range、invoke-static/range、invoke-super/range、invoke-virtual/range、invoke-interface/range 指令,该类型指令和以上指令唯一的区别就是后者可以设置方法参数可以使用的寄存器的范围,在参数多于四个时候使用。
再此强调一遍对于非静态方法而言{}的结构是{当前实例对象,参数1,参数2,…参数n},而对于静态方法而言则是{参数1,参数2,…参数n}
需要注意,如果要获取方法执行有返回值,需要通过上面的 move-result 指令获取执行结果。
在方法调用者我们可以看到有:
invoke-super {p0, p1}, Lcom/woblog/testsmali/BaseActivity;->onCreate(Landroid/os/Bundle;)V
invoke-virtual {p0, v0}, Lcom/woblog/testsmali/MainActivity;->setContentView(I)V
invoke-direct {p0}, Lcom/woblog/testsmali/MainActivity;->initMove()V
invoke-static {}, Lcom/woblog/testsmali/TimeUtil;->getCurrentTime()J
invoke-interface {v0}, Lcom/woblog/testsmali/ICallback;->onSuccess()V
在 java 中,很多情况下我们需要通过 Return 返回方法的执行结果,在 Davilk 中同样提供的 return 指令来返回运行结果。
返回指令指的是函数结尾时运行的最后一条指令。共有以下四条返回指令:
Java 代码:
private String returnObject() {
return new String("");
}
private float returnFloat() {
return 12333334.00234345F;
}
private double returnDouble() {
return 3425465767.9345865;
}
private long returnLong() {
return 12445657999999L;
}
private int returnInt() {
return 1024;
}
private void returnVoid() {
int a = 3;
}
对应代码:
.method private returnDouble()D
.locals 2
.prologue
.line 40
const-wide v0, 0x41e9858eb4fde822L # 3.4254657679345865E9
return-wide v0
.end method
.method private returnFloat()F
.locals 1
.prologue
.line 36
const v0, 0x4b3c3116 # 1.2333334E7f
return v0
.end method
.method private returnInt()I
.locals 1
.prologue
.line 48
const/16 v0, 0x400
return v0
.end method
.method private returnLong()J
.locals 2
.prologue
.line 44
const-wide v0, 0xb51bb062a7fL
return-wide v0
.end method
.method private returnObject()Ljava/lang/String;
.locals 2
.prologue
.line 32
new-instance v0, Ljava/lang/String;
const-string v1, ""
invoke-direct {v0, v1}, Ljava/lang/String;->(Ljava/lang/String;)V
return-object v0
.end method
.method private returnVoid()V
.locals 1
.prologue
.line 52
const/4 v0, 0x3
.line 53
.local v0, "a":I
return-void
.end method
同步一段指令序列通常是由 java 中的 synchronized 语句块表示,则 JVM 中是通过 monitorenter 和 monitorexit 的指令来支持synchronized 关键字的语义的,而在 Davilk 中同样提供了两条类似的指令来支持 synchronized 语义。
很久以前,JVM 也是用过 jsr 和 ret 指令来实现异常的,但是现在的 JVM 中已经抛弃原先的做法,转而采用异常表来实现异常。而 Davilk 仍然使用指令来实现:
Java 代码:
private void throw2() {
try {
throw new Exception("test throw runtime exception");
} catch (Exception e) {
e.printStackTrace();
}
}
private void throw1() {
throw new RuntimeException("test throw runtime exception");
}
对应代码:
.method private throw1()V
.locals 2
.prologue
.line 38
new-instance v0, Ljava/lang/RuntimeException;
const-string v1, "test throw runtime exception"
invoke-direct {v0, v1}, Ljava/lang/RuntimeException;->(Ljava/lang/String;)V
throw v0
.end method
.method private throw2()V
.locals 3
.prologue
.line 31
:try_start_0
new-instance v1, Ljava/lang/Exception;
const-string v2, "test throw runtime exception"
invoke-direct {v1, v2}, Ljava/lang/Exception;->(Ljava/lang/String;)V
throw v1
:try_end_0
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0
.line 32
:catch_0
move-exception v0
.line 33
.local v0, "e":Ljava/lang/Exception;
invoke-virtual {v0}, Ljava/lang/Exception;->printStackTrace()V
.line 35
return-void
.end method
跳转指令用于从当前地址跳转到指定的偏移处,在 if,switch 分支中使用的居多。Davilk 中提供了 goto、packed-switch、if-test 指令用于实现跳转操作。
Dalvik指令集中有三种跳转指令:无条件跳转(goto)、分支跳转(switch)与条件跳转(if)。
在条件比较中,if-test 中的 test 表示比较规则。该指令用的非常多,因此我们简单的坐下说明:
除了以上指令之外,Davilk 还提供可一个零值条件指令,该指令用于 和 0 比较,可以理解为将上面指令中的 vB 寄存器 的值固定为 0。
附:上面我们说道两张偏移表 packed-switch-payload 和 spare-switch-payload,两者唯一的区别就是表中的值是否有序,后面我们会在下文中进行详细的说明。
goto +AA 无条件跳转到指定偏移处,偏移量AA不能为0
goto/16 +AAAA 无条件跳转到指定偏移处,偏移量AAAA不能为0。
goto/32 +AAAAAAAA 无条件跳转到指定偏移处。
// 分支跳转指令。vAA寄存器为switch分支中需要判断的值,BBBBBBBB指向
// 一个packed-switch-payload格式的偏移表,表中的值是有规律递增的。
packed-switch vAA,+BBBBBBBB
// 分支跳转指令。vAA寄存器为switch分支中需要判断的值,BBBBBBBB指向
// 一个sparse-switch-payload格式的偏移表,表中的值是无规律的偏移表,表中的值是无规律的偏移量。
sparse-switch vAA,+BBBBBBBB
if-test vA,vB,+CCCC 条件跳转指令。比较vA寄存器与vB寄存器的值,如果比较结果满足就跳转到CCCC指定的偏移处。偏移量CCCC不能为0。if-test类型的指令有以下几条:
● if-eq 如果vA不等于vB则跳转。Java语法表示为 if(vA == vB)
● if-ne 如果vA不等于vB则跳转。Java语法表示为 if(vA != vB)
● if-lt 如果vA小于vB则跳转。Java语法表示为 if(vA < vB)
● if-le 如果vA小于等于vB则跳转。Java语法表示为 if(vA <= vB)
● if-gt 如果vA大于vB则跳转。Java语法表示为 if(vA > vB)
● if-ge 如果vA大于等于vB则跳转。Java语法表示为 if(vA >= vB)
if-testz vAA,+BBBB 条件跳转指令。拿vAA寄存器与 0 比较,如果比较结果满足或值为0时就跳转到BBBB指定的偏移处。偏移量BBBB不能为0。 if-testz类型的指令有一下几条:
● if-nez 如果vAA为 0 则跳转。Java语法表示为 if(vAA == 0)
● if-eqz 如果vAA不为 0 则跳转。Java语法表示为 if(vAA != 0)
● if-ltz 如果vAA小于 0 则跳转。Java语法表示为 if(vAA < 0)
● if-lez 如果vAA小于等于 0 则跳转。Java语法表示为 if(vAA <= 0)
● if-gtz 如果vAA大于 0 则跳转。Java语法表示为 if(vAA > 0)
● if-gez 如果vAA大于等于 0 则跳转。Java语法表示为 if(vAA >= 0)
Java 代码:
private void testIfz() {
int a = 3;
if (a == 0) {
} else {
}
if (a != 0) {
} else {
}
if (a < 0) {
} else {
}
if (a > 0) {
} else {
}
if (a <= 0) {
} else {
}
if (a >= 0) {
} else {
}
if (a < 5) {
Log.d("TAG", "<5");
} else if (a > 5) {
Log.d("TAG", ">5");
} else {
Log.d("TAG", "=5");
}
}
private void testIf() {
int a = 2;
int b = 3;
if (a == b) {
} else {
}
if (a != b) {
} else {
}
if (a < b) {
} else {
}
if (a > b) {
} else {
}
if (a <= b) {
} else {
}
if (a >= b) {
} else {
}
}
对应代码:
.method private testIf()V
.locals 2
.prologue
.line 69
const/4 v0, 0x2
.line 70
.local v0, "a":I
const/4 v1, 0x3
.line 71
.local v1, "b":I
if-ne v0, v1, :cond_0
.line 76
:cond_0
if-eq v0, v1, :cond_1
.line 81
:cond_1
if-ge v0, v1, :cond_2
.line 86
:cond_2
if-le v0, v1, :cond_3
.line 91
:cond_3
if-gt v0, v1, :cond_4
.line 96
:cond_4
if-lt v0, v1, :cond_5
.line 102
:cond_5
return-void
.end method
.method private testIfz()V
.locals 3
.prologue
const/4 v1, 0x5
.line 27
const/4 v0, 0x3
.line 28
.local v0, "a":I
if-nez v0, :cond_0
.line 33
:cond_0
if-eqz v0, :cond_1
.line 38
:cond_1
if-gez v0, :cond_2
.line 43
:cond_2
if-lez v0, :cond_3
.line 48
:cond_3
if-gtz v0, :cond_4
.line 53
:cond_4
if-ltz v0, :cond_5
.line 59
:cond_5
if-ge v0, v1, :cond_6
.line 60
const-string v1, "TAG"
const-string v2, "<5"
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 66
:goto_0
return-void
.line 61
:cond_6
if-le v0, v1, :cond_7
.line 62
const-string v1, "TAG"
const-string v2, ">5"
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
goto :goto_0
.line 64
:cond_7
const-string v1, "TAG"
const-string v2, "=5"
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
goto :goto_0
.end method
数据转换指令用于将一种类型的数值转换成另一种类型,它的格式为 unop vA,vB 。 vB寄存器或vB寄存器对存放需要转换的数据,转换后的结果保存在vA寄存器或vA寄存器对中。
数据类型转换 对任何 java 开发者都是非常熟悉的,用于实现两种不同数据类型的相互转换。其基本指令格式是:unop vA,vB,表示对 vB 寄存器中的值进行操作,并将结果保存在 vA 寄存器中。
neg-int 对整型数求补
not-int 对整型数求反
neg-long 对长整型求补
not-long 对长整型求反
neg-float 对单精度浮点型数求补
neg-double 对双精度浮点型数求补
int-to-long 将整型数转换为长整型
int-to-float 将整型数转换为单精度浮点型
int-to-double 将整型数转换为双精度浮点型
long-to-int 将长整型数转换为整型
long-to-float 将长整型数转换为单精度浮点型
long-to-double 将长整型数转换为双精度浮点型
float-to-int 将单精度浮点型数转换为整型
float-to-long 将单精度浮点型数转换为长整型
float-to-double 将单精度浮点型数转换为双精度浮点型
double-to-int 将双精度浮点型数转换为整型
double-to-long 将双精度浮点型数转换为长整型
double-to-float 将双精度浮点型数转换为单精度浮点型
int-to-byte 将整型转换为字节型
int-to-char 将整型转换为字符串
int-to-short 将整型转换为短整型
图示:
到现在为止,我们对 Davilk 中的指令做了简单的说明。Davilk 的指令在很大程度上结合了 x86 指令 和 JVM 的指令结构和语意,因此总体来说 Davilk 中的指令还是非常容易学习。
更多更详细的指令参考请参考:Davilk指令集大全 (http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html)
Vx values in the table denote a Dalvik register. Depending on the instruction, 16, 256 or 64k registers can be accessed. Operations on long and double values use two registers, e.g. a double value addressed in the V0 register occupies the V0 and V1 registers.
Boolean values are stored as 1 for true and 0 for false. Operations on booleans are translated into integer operations.
All the examples are in hig-endian format, e.g. 0F00 0A00 is coded as 0F, 00, 0A, 00 sequence.
表中的 Vx 值表示 Dalvik 寄存器。根据指令,可以访问16、256或64k寄存器。对长值和双值的操作使用两个寄存器,例如,在V0寄存器中寻址的双值占用V0和V1寄存器。
布尔值存储为1表示真,0表示假。对布尔值的操作被转换为整数操作。
所有的例子都是hig-endian格式,例如0F00 0A00编码为0F, 00, 0A, 00序列。
Note there are no explanation/example at some instructions. This means that I have not seen that instruction "in the wild" and its presence/name is only known from Android opcode constant list.
Consider using the dedexer tool to observe the Dalvik opcodes in real-life dex files!
注意:有些说明没有解释/举例。这意味着我还没有看到“在野外”的指令,它的存在/名称只知道从Android操作码常量列表。
考虑使用 dedexer 工具观察现实生活中的 dex 文件中的 Dalvik 操作码 !
Java 代码:
private void testConvert() {
int i1=13;
//int 转其他类型
long l1 = i1;
float f1 = i1;
double d1 = i1;
byte b1 = (byte) i1;
char c1 = (char) i1;
short s1 = (short) i1;
//long 转其他类型
long l2 = 234444556576L;
int i2 = (int) l2;
float f2 = l2;
double d2 = l2;
//float 转其他类型
float f10 =234399.9F;
int i10 = (int) f10;
long l10 = (long) f10;
double d10 = f10;
//double 转其他类型
double d20 = 123344445.324;
int i20 = (int) d20;
long l20 = (long) d20;
float f20 = (float) d20;
}
对应代码:
.method private testConvert()V
.locals 29
.prologue
.line 30
const/16 v16, 0xd
.line 33
.local v16, "i1":I
move/from16 v0, v16
int-to-long v0, v0
move-wide/from16 v20, v0
.line 34
.local v20, "l1":J
move/from16 v0, v16
int-to-float v12, v0
.line 35
.local v12, "f1":F
move/from16 v0, v16
int-to-double v4, v0
.line 37
.local v4, "d1":D
move/from16 v0, v16
int-to-byte v2, v0
.line 38
.local v2, "b1":B
move/from16 v0, v16
int-to-char v3, v0
.line 39
.local v3, "c1":C
move/from16 v0, v16
int-to-short v0, v0
move/from16 v28, v0
.line 42
.local v28, "s1":S
const-wide v24, 0x3695fc0920L
.line 43
.local v24, "l2":J
move-wide/from16 v0, v24
long-to-int v0, v0
move/from16 v18, v0
.line 44
.local v18, "i2":I
move-wide/from16 v0, v24
long-to-float v14, v0
.line 45
.local v14, "f2":F
move-wide/from16 v0, v24
long-to-double v8, v0
.line 48
.local v8, "d2":D
const v13, 0x4864e7fa # 234399.9f
.line 49
.local v13, "f10":F
float-to-int v0, v13
move/from16 v17, v0
.line 50
.local v17, "i10":I
float-to-long v0, v13
move-wide/from16 v22, v0
.line 51
.local v22, "l10":J
float-to-double v6, v13
.line 54
.local v6, "d10":D
const-wide v10, 0x419d6858f54bc6a8L # 1.23344445324E8
.line 55
.local v10, "d20":D
double-to-int v0, v10
move/from16 v19, v0
.line 56
.local v19, "i20":I
double-to-long v0, v10
move-wide/from16 v26, v0
.line 57
.local v26, "l20":J
double-to-float v15, v10
.line 58
.local v15, "f20":F
return-void
.end method
首先写一个基本框架
.class public LHelloWorld; #定义类名
.super Ljava/lang/Object; #定义父类
.method public static main([Ljava/lang/String;)V #声明静态的main函数
.locals 4 #使用的寄存器个数,包括一个参数寄存器
.param p0, "args" #一个参数
.prologue #代码起始指令
# 这里是代码主体
return-void
.end method
完整版如下:
.class public LHelloWorld; #定义类名
.super Ljava/lang/Object; #定义父类
.method public static main([Ljava/lang/String;)V #声明静态的main函数
.locals 4 #使用的寄存器个数,包括一个参数寄存器
.param p0, "args" #一个参数
.prologue #代码起始指令
const-string v1, "Hello World"
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
invoke-virtual {v0,v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
return-void
.end method
我们去官网下载smali.jar,然后运行
java -jar smali.jar -o classes.dex HelloWorld.smali
编译完后我们把classes.dex push到手机里面
adb push classes.dex /data/local/
dalvikvm -cp /data/local/classes.dex HelloWorld
加强版本
.class public LHelloWorld; #定义类名
.super Ljava/lang/Object; #定义父类
.method public static main([Ljava/lang/String;)V #声明静态的main函数
.locals 10 #使用的寄存器个数,包括一个参数寄存器
.param p0, "args" #一个参数
.prologue #代码起始指令
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
# 空指令
nop
nop
nop
# 数据定义指令
const/4 v2, 0x3
const/16 v3, 0xffff ##不能大于65535
#大于65535用-wide
const-wide v4, 0x10000
# 定义一个类 类型
const-class v5, Ljava/lang/String;
# 数据操作指令
move v6, v5
new-instance v7, Ljava/lang/StringBuilder;
invoke-direct {v7}, Ljava/lang/StringBuilder;->()V
const-string v8, "\u8fd9\u662f\u624b\u5199\u7684\u0073\u006d\u0061\u006c\u0069\u0044\u0065\u006d\u006f"
invoke-virtual {v7, v12}, Ljava/java/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilber;
invoke-direct {v7}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v9
invoke-virtual {v0, v9}, Ljava/io/PrintStream;->println(Ljava/java/String;)V
# 打印字符串
const-string v1, "Hello World"
invoke-virtual {v0,v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
return-void
.end method
哔哩哔哩 :https://www.bilibili.com/video/BV1UE411A7rW?p=32
反编译 - 超详细的 smali 文件解读:https://blog.csdn.net/qq_32113133/article/details/85163277
Smali 语法入门教程:https://blog.csdn.net/pinksofts/article/details/82791806
逆向入门教程(五)---Smali实战分析:https://www.52pojie.cn/thread-397987-1-1.html
[Android 分享] 逆向之Smali入门学习:https://www.52pojie.cn/thread-687375-1-1.html
Android逆向之动态分析smali篇:https://www.freebuf.com/column/188728.html
Android逆向——smali复杂类解析:https://www.cnblogs.com/ichunqiu/p/9077723.html
Android 工程师,如何简单高效的学会 smali 语法:https://www.jianshu.com/p/b23782460f61
逆向小米 rom 层应用做碎片化适配:https://www.jianshu.com/p/6f313b4876ab
Smali 语法:https://www.jianshu.com/p/730c6e3e21f6
Smali 基础语法总结:https://www.cnblogs.com/bmjoker/p/10506623.html
smali 语法小结:https://www.cnblogs.com/zhen-android/p/7259434.html
Android逆向从入门到入土(smali修改,so修改):https://segmentfault.com/a/1190000012669267
apk文件通过apktool反编译出来的都有一个smali文件夹,里面都是以.smali结尾的文件。
Smali 是安卓系统里的Java虚拟机(Dalvik)所使用的一种.dex格式文件的汇编器,而 Baksmali 是反汇编器。其语法是一种宽松式的Jasmin/dedexer语法。所谓的 smali 就是 Dalvik VM 内部执行的核心代码。
Smali 语言其实就是一种面向Dalvik的汇编语言(汇编语言是一种用于电子计算机、微处理器、微控制器,或其他可编程器件的低级语言。在不同的设备中,汇编语言对应着不同的机器语言指令集。使用汇编语言编写的源代码,然后通过相应的汇编程序将它们转换成可执行的机器代码。这一过程被称为汇编过程。),用 smali 语言写的代码通过汇编器就转换成Dalvik可以执行的dex文件。
对应的有两个工具:
apktool 能反编译出 smali 文件就是因为它里面用了 baksmali 工具。
smali 语法是逆向的基础,它能帮助我们理解别人代码的逻辑(当然我们也可以用 dex2jar, 更方便查看代码),但是当我们要修改别人的代码逻辑,或者加上自己的逻辑,最终是要修改 smali 文件的,如果我们不懂 smali 语法,就无从下手。目前android盗版软件中,大部分都是二次打包党,他们基本上就是反编译->注入广告->再打包签名->上传市场,其中注入广告或者破解游戏等都是通过修改smali来达到的。
Android 系统有自己的虚拟机 Dalvik,代码编译最终不是采用 Java 的 class,而是使用的 smali。反编译得到的代码,如果是 jar 的话可能很多地方无法正确的解释出来,如果反编译得到的是 smali 则可以正确的理解程序的意思。因此,我们有必要熟悉 smali 语法 。
smali的语法细节太多了,各种指令,看的人头疼,我们其实也不需要全都学会,因为现在有工具帮我们了。
AndroidStudio 有个插件: Java2Smali,方便快捷把 java 代码转换为smali代码。有了这个工具,我们可以结合它在实践中学习 smali 语法,同时它也可以帮我们省很多事,当我们想在别人的逻辑中增加代码时,我们可以把自己的代码先用这个工具转成 smali 语句, 然后做微调就行了。
当然我们还是需要学习一些基础语法,了解一些常用指令。
如果想完整学习smali语法,可以看看 《Android软件安全与逆向分析》这本书,网上的很多资料也是来源于这本书。
PDF 链接: https://pan.baidu.com/s/1rEKkBJnKhRVvwRTALZBuCg 提取码: 9a2t
Dalvik 虚拟机加载的是 dex 文件,Dex 文件是 Dalvik 虚拟机的可执行文件格式,dex 文件很难看懂,baksmali 和 smali 是对 Dex 文件的反汇编器和汇编器,通过对 Dex 文件反编译得到 smali 文件,smali 文件是对 Dalvik 虚拟机字节码的一种解释(也可以说是翻译),并非一种官方标准语言。通过对 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 文件,所有的指令都以 ”.” 开头。
常用的指令如下:
在这里很多人对 .local 和 .register 感到困惑,如果你也是请重新看上面的有关寄存器的点。
下面我们就简单的说明一下 smali 文件的结构:
smali 文件的前三行描述了当前类的信息:
.class <访问权限修饰符> [非权限修饰符] <类名>
.super <父类名>
.source <源文件名称>
<> 中的内容表示必不可缺的,[] 表示的是可选择的。
# 指定了当前类的类名,访问权限为public,类名开头的L是遵循 Dalvik 字节码的相关约定
.class public Lcom/example/administrator/myapplication/Demo;
# super指令指定了当前类的父类,父类为Object
.super Ljava/lang/Object;
# 指定了当前类的源文件名。
注意:经过混淆的dex文件,反编译出来的smali代码可能没有源文件信息,source可能为空。
.source "Demo.java"
访问权限修饰符即所谓的 public、protected、private 即 default。而非权限修饰符则指的是 final、abstract。
举例说明:
.class public final Lcom/sbbic/demo/Device;
.super Ljava/lang/Object;
.source "Device.java"
在文件头之后便是文件的正文,即 类 的 主体部分,包括:类实现的接口描述、注解描述、字段描述和方法描述四部分。
下面我们就分别看看 字段 和 方法的结构。( 别忘了我们在 Davilk 中说过的 方法 和 字段 的表示 )
如果一个类实现了某个接口,会在 smali 文件中使用 “.implements ” 指令指出,相应的格式声明如下:
#interfaces
.implements <接口名称>
# 下面 smali 代码表明了实现了 TestParent 这个接口。
# interfaces
.implements Lcom/example/administrator/myapplication/TestParent;
举例说明:
# interfaces
.implements Landroid/view/View$OnClickListener;
smali 为其添加了 #Interface 注释
如果一个类中使用注解,会用 .annotation 定义。其格式如下:
#annotations
.annotation [注解的属性] <注解类名>
[注解字段=值]
...
.end
如果一个类使用了注解,会在 smali 文件中使用“.annotation ”指令指出,注解的格式声明如下:
# annotations
.annotation [ 注解属性] < 注解类名>
[ 注解字段 = 值]
.end annotation
注解的作用范围可以是类、方法或字段。如果注解的作用范围是类,“.annotation ”指令会直接定义在smali 文件中,如果是方法或字段,“.annotation ”指令则会包含在方法或字段定义中。
.field public sayWhat:Ljava/lang/String;
.annotation runtime Lcom/droider/anno/MyAnnoField;
info = ”Hello my friend”
.end annotation
.end field
如上String 类型 它使用了 com.droider.anno.MyAnnoField 注解,注解字段info 值 为“Hello my friend”
转换成java代码为:
@com.droider.anno.MyAnnoField(info = ”Hello my friend”)
public String sayWhat;
smali 文件的字段的声明使用 ".field" 指令。字段有 静态字段 和 实例字段 两种:
smali 中使用 .field 描述字段,我们知道 java 中分为 静态字段(类属性) 和 普通字段(实例属性),它们在 smali 中的表示如下:
1. 普通字段:
实例字段格式: .field 访问权限 修饰关键字 字段名 字段类型
#instance fields
.field <访问权限修饰符> [非权限修饰符] <字段名>:<字段类型>
访问权限修饰符相比各位已经非常熟了,而此处非权限修饰符则可是 final、volidate、transient 。
举例说明:
.field private button:Landroid/widget/Button;
.field public number:I
上面的smali代码转为java代码为:
private Button button;
public int number =5;
# 示例 2
# instance fields
.field private TAG:Ljava/lang/String;
获取 instance fields 的指令与 static fields 的类似,需要指明对象所属的实例。示例:
iget-object v0, p0, Lcom/example/reforceapk/MyLog;->ff:Ljava/lang/String;
上句iget-object指令比sget-object多了一个参数p0,就是该变量所在类的实例,在这里就是p0即“this”。
2. 静态字段
静态字段格式: .field 访问权限 static 修饰关键字 字段名 字段类型
静态字段知识在普通字段的的定义中添加了static,其格式如下:
#static fields
.field <访问权限> static [修饰词] <字段名>:<字段类型>
举例说明:
.field public static HELLO:Ljava/lang/String;
上面smali代码转为java代码为:
public static String HELLO = "hello";
# static fields
.field private static final pi:F = 3.14f
需要注意:smali 文件还为 静态字段、普通字段 分别添加 #static field 和 #instan filed 注释。
获取 static fields的指令示例:
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
上句中sget-object指令把out这个变量获取并放到v0寄存器中。
使用".method"指令,分为直接方法(用private修饰的)和虚方法(用public和protected修饰的),直接方法和虚方法的声明是一样的。在调用函数时,有invoke-direct,invoke-virtual,invoke-static、invoke-super以及invoke-interface等几种不同的指令。还有invoke-XXX/range 指令的,这是参数多于4个的时候调用的指令,比较少见:
.method private static getCount(II)V
.registers 2
.param p0, "x"
.param p1, "y"
.prologue
.line 28
return-void
.end method
第一行为方法的开始处,此处方法的修饰符是static,访问权限是private,方法名是getCount,有两个参数,都是int类型的(I代表int),V代表无返回值。
第二行指定寄存器的大小。
第三行和第四行为方法的参数,每有一个参数,就写一个参数,此处有两个参数。
第五行为 方法的主体部分(.prologue)
第六行指定了该处指令在源代码中的行号,这里是从java源码中底28行开始的
第七行return-void表示无返回值
第八行(.end method)方法结束
上面的smali转为java代码为:
private static void getCount(int x,int y){
}
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;->()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
.end method
需要注意 smali 为其添加了 #direct method 注释
2. 虚方法
虚方法的定义会和直接方法唯一的不同就是注释不同:#virtual methods,其格式如下:
#virtual methods
.method <访问权限> [修饰关键词] <方法原想>
<.locals>
[.parameter1]
[.parameter2]
[.prologue]
[.line]
<代码逻辑>
.end
内部类:内部类可以有成员内部类,静态嵌套类,方法内部类,匿名内部类。
格式: 外部类$内部类.smali
内部类的 smali 文件稍有不同,具体表现在内部类对应的 smali 文件的的文件名为 [外部类名称$内部类名称.smali]
class Outer{
class Inner{}
}
baksmali反编译上面的代码后会生成两个文件:Outer.smali和Outer$Inner.smali
Inner.smali 文件如下:
.class public Lcom/example/myapplication/Outer$Inner;
.super Ljava/lang/Object;
.source "Outer.java"
# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
value = Lcom/example/myapplication/Outer;
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x1
name = "Inner"
.end annotation
# instance fields
.field final synthetic this$0:Lcom/example/myapplication/Outer;
# direct methods
.method public constructor (Lcom/example/myapplication/Outer;)V
.registers 2
.param p1, "this$0" # Lcom/example/myapplication/Outer;
.prologue
.line 4
iput-object p1, p0, Lcom/example/myapplication/Outer$Inner;->this$0:Lcom/example/myapplication/Outer;
invoke-direct {p0}, Ljava/lang/Object;->()V
return-void
.end method
this$0是Outer类型,syntheitc关键字表明它是“合成的”。
.field final synthetic this$0:Lcom/example/myapplication/Outer;
this$0是什么东西呢?
this$0是内部类自动保留的一个指向所在外部类的隐隐个,左边的this表示为父类的引用,右边的数值0表示引用的层数:
public class Outer {
public class FirstInner{} //this$0
public class SecondInner{} //this$1
public class ThirdInner{} //this$2
}
没往里一层右边的数值就加一,如ThirdInner类访问FirstInner类的引用为this$1.在生成的反汇编代码中,this$X型字段都被指定了synthetic属性,表明它们是被编译器合成的,虚构的,代码的作者并没有生命该字段。
紧接着来看Inner.smali的构造函数:
从下面的构造函数中可以看到.param指定了一个参数,却使用了p0和p1两个寄存器,因为Dalvik虚拟机对于一个非静态的方法而言,会隐含的使用p0寄存器当做类的this使用,因此,这里的确是使用了2个寄存器(.registers 2),p0表示Outer$Inner.smali自身的引用,p1表示this$0,也就是Outer的引用。
# direct methods
.method public constructor (Lcom/example/myapplication/Outer;)V
.registers 2
.param p1, "this$0" # Lcom/example/myapplication/Outer;
.prologue
.line 4
iput-object p1, p0, Lcom/example/myapplication/Outer$Inner;->this$0:Lcom/example/myapplication/Outer;
invoke-direct {p0}, Ljava/lang/Object;->()V
return-void
.end method
分析如下:
.param p1, "this$0" # Lcom/example/myapplication/Outer;
这个是默认的构造函数,没有参数,但是有一个默认的参数就是this$0,他是Outer的应用,如果构造函数有参数的话,会在
.param p1,"this$0"下面继续列出。
iput-object p1, p0, Lcom/example/myapplication/Outer$Inner;->this$0:Lcom/example/myapplication/Outer;
将Outer引用赋值给this$0
invoke-direct {p0}, Ljava/lang/Object;->()V
调用默认的构造函数。
Android程序开发中使用了大量的监听器,比如Button的点击事件OnClickListener等等,由于监听器只是临时使用一次,没有什么服用价值,因此,编写代码中多使用匿名内部类的形式来实现。
java源码:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
相应的 smali 代码:
.method protected onCreate(Landroid/os/Bundle;)V
.registers 4
.param p1, "savedInstanceState" # Landroid/os/Bundle;
.prologue
.line 12
invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V
.line 13
const v1, 0x7f09001c
invoke-virtual {p0, v1}, Lcom/example/myapplication/MainActivity;->setContentView(I)V
.line 15
const v1, 0x7f070022
invoke-virtual {p0, v1}, Lcom/example/myapplication/MainActivity;->findViewById(I)Landroid/view/View;
move-result-object v0
check-cast v0, Landroid/widget/Button;
.line 16
.local v0, "button":Landroid/widget/Button;
#新建一个MainActivity$1实例
new-instance v1, Lcom/example/myapplication/MainActivity$1;
invoke-direct {v1, p0}, Lcom/example/myapplication/MainActivity$1;->(Lcom/example/myapplication/MainActivity;)V
#设置按钮点击事件监听器
invoke-virtual {v0, v1}, Landroid/widget/Button;->setOnClickListener(Landroid/view/View$OnClickListener;)V
.line 22
return-void
.end method
MainActivity$1代码如下:
.class Lcom/example/myapplication/MainActivity$1;
.super Ljava/lang/Object;
.source "MainActivity.java"
# interfaces
.implements Landroid/view/View$OnClickListener;
# annotations
.annotation system Ldalvik/annotation/EnclosingMethod;
value = Lcom/example/myapplication/MainActivity;->onCreate(Landroid/os/Bundle;)V
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x0
name = null
.end annotation
# instance fields
.field final synthetic this$0:Lcom/example/myapplication/MainActivity;
# direct methods
.method constructor (Lcom/example/myapplication/MainActivity;)V
.registers 2
.param p1, "this$0" # Lcom/example/myapplication/MainActivity;
.prologue
.line 16
iput-object p1, p0, Lcom/example/myapplication/MainActivity$1;->this$0:Lcom/example/myapplication/MainActivity;
invoke-direct {p0}, Ljava/lang/Object;->()V
return-void
.end method
# virtual methods
.method public onClick(Landroid/view/View;)V
.registers 2
.param p1, "v" # Landroid/view/View;
.prologue
.line 20
return-void
.end method
在MainActivity$1.smali文件的开头使用了“,implements”指令指定该类实现了按钮点击事件的监听器接口,因此,这个类实现了它的OnClick()方法,这时在分析程序时关心的地方。程序中的注解与监听器的构造函数都是编译器为我们自己生成的,实际分析过程中不必关心。
下面是R.java文件的一部分:
public final class R {
public static final class anim {
public static final int abc_fade_in=0x7f010000;
}
}
由于这些资源文件类都是R类的内部类,因此他们都会独立生成一个类文件,在反编译出的代码中,可以发现有R.smali,R$attr.smali,R$dimen.smali,R$drawable.smali等等。
更详细的说明见下文。
smali 文件的结构也是非常清晰明了的,熟悉之后读起来也是非常不错的。下面我们来看个简单的 smali 文件。
为了方便理解,我们首先贴一段 java 代码:
public class MainActivity extends Activity implements View.OnClickListener {
private String TAG = "MainActivity";
private static final float pi = (float) 3.14;
public volatile boolean running = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public void onClick(View view) {
int result = add(4, 5);
System.out.println(result);
result = sub(9, 3);
if (result > 4) {
log(result);
}
}
public int add(int x, int y) {
return x + y;
}
public synchronized int sub(int x, int y) {
return x + y;
}
public static void log(int result) {
Log.d("MainActivity", "the result:" + result);
}
}
接下来我们来看该段代码反编译出来的 smali 文件。需要注意的是不同的反编译工具反编译的文件可能稍有不同,比如使用 .param
而不是使用 .paramter,
不存在 .register
等,但是总体来说含义是相同的。
#文件头描述
.class public Lcom/social_touch/demo/MainActivity;
.super Landroid/app/Activity;#指定MainActivity的父类
.source "MainActivity.java"#源文件名称
#表明实现了View.OnClickListener接口
# interfaces
.implements Landroid/view/View$OnClickListener;
#定义float静态字段pi
# static fields
.field private static final pi:F = 3.14f
#定义了String类型字段TAG
# instance fields
.field private TAG:Ljava/lang/String;
#定义了boolean类型的字段running
.field public volatile running:Z
#构造方法,如果你还纳闷这个方法是怎么出来的化,就去看看jvm的基础知识吧
# direct methods
.method public constructor ()V
.locals 1#表示函数中使用了一个局部变量
.prologue#表示方法中代码正式开始
.line 8#表示对应与java源文件的低8行
#调用Activity中的init()方法
invoke-direct {p0}, Landroid/app/Activity;->()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
.end method
#静态方法log()
.method public static log(I)V
.locals 3
.parameter "result"#表示result参数
.prologue
.line 42
#v0寄存器中赋值为"MainActivity"
const-string v0, "MainActivity"
#创建StringBuilder对象,并将其引用赋值给v1寄存器
new-instance v1, Ljava/lang/StringBuilder;
#调用StringBuilder中的构造方法
invoke-direct {v1}, Ljava/lang/StringBuilder;->()V
#v2寄存器中赋值为ther result:
const-string v2, "the result:"
#{v1,v2}大括号中v1寄存器中存储的是StringBuilder对象的引用.
#调用StringBuilder中的append(String str)方法,v2寄存器则是参数寄存器.
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
#获取上一个方法的执行结果,此时v1中存储的是append()方法执行后的结果,此处之所以仍然返回v1的 #原因在与append()方法返回的就是自身的引用
move-result-object v1
#继续调用append方法(),p0表示第一个参数寄存器,即上面提到的result参数
invoke-virtual {v1, p0}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
#同上
move-result-object v1
#调用StringBuilder对象的toString()方法
invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
#获取上一个方法执行结果,toString()方法返回了一个新的String对象,因此v1中此时存储了String对象的引用
move-result-object v1
#调用Log类中的静态方法e().因为e()是静态方法,因此{v0,v1}中的成了参数寄存器
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 43
#调用返回指令,此处没有返回任何值
return-void
.end method
# virtual methods
.method public add(II)I
.locals 1
.parameter "x"#第一个参数
.parameter "y"#第二个参数
.prologue
.line 34
#调用add-int指令求和之后将结果赋值给v0寄存器
add-int v0, p1, p2
#返回v0寄存器中的值
return v0
.end method
.method public onClick(Landroid/view/View;)V
.locals 4
.parameter "view" #参数view
.prologue
const/4 v3, 0x4 #v3寄存器中赋值为4
.line 23#java源文件中的第23行
const/4 v1, 0x5#v1寄存器中赋值为5
#调用add()方法
invoke-virtual {p0, v3, v1}, Lcom/social_touch/demo/MainActivity;->add(II)I
#从v0寄存器中获取add方法的执行结果
move-result v0
.line 24#java源文件中的24行
.local v0, result:I
#v1寄存器中赋值为PrintStream对象的引用out
sget-object v1, Ljava/lang/System;->out:Ljava/io/PrintStream;
#执行out对象的println()方法
invoke-virtual {v1, v0}, Ljava/io/PrintStream;->println(I)V
.line 26
const/16 v1, 0x9#v1寄存器中赋值为9
const/4 v2, 0x3#v2寄存器中赋值为3
#调用sub()方法,{p0,v1,v2},p0指的是this,即当前对象,v1,v2则是参数
invoke-virtual {p0, v1, v2}, Lcom/social_touch/demo/MainActivity;->sub(II)I
#从v0寄存器中获取sub()方法的执行结果
move-result v0
.line 28
if-le v0, v3, :cond_0#如果v0寄存器的值小于v3寄存器中的值,则跳转到cond_0处继续执行
.line 29
#调用静态方法log()
invoke-static {v0}, Lcom/social_touch/demo/MainActivity;->log(I)V
.line 31
:cond_0
return-void
.end method
.method protected onCreate(Landroid/os/Bundle;)V
.locals 1
.parameter "savedInstanceState" #参数savedInstancestate
.prologue
.line 17
#调用父类方法onCreate()
invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V
.line 18
const v0, 0x7f04001a#v0寄存器赋值为0x7f04001a
#调用方法setContentView()
invoke-virtual {p0, v0}, Lcom/social_touch/demo/MainActivity;->setContentView(I)V
.line 19
return-void
.end method
#declared-synchronized表示该方法是同步方法
.method public declared-synchronized sub(II)I
.locals 1
.parameter "x"
.parameter "y"
.prologue
.line 38
monitor-enter p0#为该方法添加锁对象p0
add-int v0, p1, p2
#释放锁对象
monitor-exit p0
return v0
.end method
.method public testSwitch(I)Ljava/lang/String;
.registers 3
.param p1, "index" # I
.prologue
.line 21
const-string v0, ""
.line 22
.local v0, "name":Ljava/lang/String;
packed-switch p1, :pswitch_data_14 #packed-switch分支,pswitch_data_14指定case区域
.line 36 #default
const-string v0, "This index is 100"
.line 38
:goto_7 #所有case出口
return-object v0
.line 24
:pswitch_8 #case 0
const-string v0, "This index is 0"
.line 25
goto :goto_7 #跳转到goto_7出口
.line 27
:pswitch_b #case 1
const-string v0, "This index is 1"
.line 28
goto :goto_7
.line 30
:pswitch_e #case 2
const-string v0, "This index is 2"
.line 31
goto :goto_7
.line 33
:pswitch_11 #case 3
const-string v0, "This index is 3"
.line 34
goto :goto_7
.line 22
:pswitch_data_14
.packed-switch 0x0 #case区域,从0开始,依次递增
:pswitch_8
:pswitch_b
:pswitch_e
:pswitch_11
.end packed-switch
.end method
代码中的switch分支使用的是packed-switch指令,p1为传递进来的int类型的数值,pswitch_data_10为case区域,在case区域中,第一条指令".packed-switch"指定了比较的初始值为0。
:pswitch_8, :pswitch_b,:pswitch_e,:pswitch_11 分别是比较结果为"case 0"到"case 3"时要跳转的地址,标号的命名采用pswitch_开关,后面的数值为case分支需要判断的值,并且它的值依次递增。
每个标号处都使用v0寄存器初始化一个字符串,然后跳转到了goto_7标号处,可见goto_7是所有case分支的出口。
整理为java代码如下:
public String testSwitch(int index) {
String name = "";
switch (index) {
case 0:
name = "This index is 0";
break;
case 1:
name = "This index is 1";
break;
case 2:
name = "This index is 2";
break;
case 3:
name = "This index is 3";
break;
default:
name = "This index is 100";
}
return name;
}
以上是有规律的switch分支,下面是无规律的分支。
.method public testSwitch(I)Ljava/lang/String;
.registers 3
.param p1, "index" # I
.prologue
.line 27
const-string v0, ""
.line 28
.local v0, "name":Ljava/lang/String;
sparse-switch p1, :sswitch_data_14
.line 42
const-string v0, "This index is 100"
.line 44
:goto_7 #所有case的出口
return-object v0
.line 30
:sswitch_8 #case 5
const-string v0, "This index is 0"
.line 31
goto :goto_7 #跳转到goto_7标号处
.line 33
:sswitch_b #case 10
const-string v0, "This index is 1"
.line 34
goto :goto_7
.line 36
:sswitch_e #case 15
const-string v0, "This index is 2"
.line 37
goto :goto_7
.line 39
:sswitch_11 #case 25
const-string v0, "This index is 3"
.line 40
goto :goto_7
.line 28
:sswitch_data_14
.sparse-switch #case区域
0x5 -> :sswitch_8 #case5
0xa -> :sswitch_b #case10
0xf -> :sswitch_e #case15
0x19 -> :sswitch_11 #case25
.end sparse-switch
.end method
直接查看sswitch_data_14标号处的内容,可以看到“.sparse-switch”指令并没有给出初始化case的值,所有的case值都使用"case值->case标号"的形式给出,此处共有4个case,它们的内容都是构造一个字符串,然后跳转到goto_7标号处,加码架构上与packed-switch方式的switch分支一样。
整理为java代码如下:
public String testSwitch(int index) {
String name = "";
switch (index) {
case 5:
name = "This index is 0";
break;
case 10:
name = "This index is 1";
break;
case 15:
name = "This index is 2";
break;
case 25:
name = "This index is 3";
break;
default:
name = "This index is 100";
}
return name;
}
常用的循环结构有:迭代器循环,for/foreach循环,while/do-while循环。
# virtual methods
.method public statementFor()Z
.locals 4
.prologue
.line 17
const/4 v0, 0x0
.local v0, "a":I
:goto_0
const/16 v1, 0x64
if-ge v0, v1, :cond_0 // 结束循环条件
.line 18
const-string v1, "StateLoop"
new-instance v2, Ljava/lang/StringBuilder;
invoke-direct {v2}, Ljava/lang/StringBuilder;->()V
const-string v3, ""
invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v2
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
move-result-object v2
invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 17
add-int/lit8 v0, v0, 0x1
goto :goto_0 // 通过goto实现循环
.line 20
:cond_0
const/4 v1, 0x0
return v1
.end method
for/foreach循环,while/do-while 几种循环结构smali语法均通过goto实现,内部嵌入if-xxx 实现跳出循环。
.method public test(I)V
.registers 7
.param p1, "index" # I
.prologue
.line 20
const/4 v3, 0x7
new-array v2, v3, [I
fill-array-data v2, :array_24
.line 23
.local v2, "numbers":[I
const/4 v1, 0x0
.local v1, "i":I
:goto_7
:try_start_7 #第一个try开始
array-length v3, v2
:try_end_8 #第一个try结束
.catch Ljava/lang/IndexOutOfBoundsException; {:try_start_7 .. :try_end_8} :catch_11 #指定处理到的异常类型和catch的标号
.catch Ljava/lang/IllegalArgumentException; {:try_start_7 .. :try_end_8} :catch_1a
if-ge v1, v3, :cond_19
.line 24
const/16 v3, 0xa
if-ne v1, v3, :cond_e
.line 23
:cond_e
add-int/lit8 v1, v1, 0x1
goto :goto_7
.line 28
:catch_11
move-exception v0
.line 29
.local v0, "e":Ljava/lang/IndexOutOfBoundsException;
const-string v3, "log"
const-string v4, "\u6570\u7ec4\u8d8a\u754c\uff01" #数组越界!
invoke-static {v3, v4}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I
.line 33
.end local v0 # "e":Ljava/lang/IndexOutOfBoundsException;
:cond_19
:goto_19
return-void
.line 30
:catch_1a
move-exception v0
.line 31
.local v0, "e":Ljava/lang/IllegalArgumentException;
const-string v3, "log"
const-string v4, "index\u4e0d\u80fd\u662fString\u7c7b\u578b\uff01" #index不能是String类型!
invoke-static {v3, v4}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I
goto :goto_19
.line 20
nop
:array_24
.array-data 4
0x1
0x4
0x5
0x6
0x3
0x22
0x285
.end array-data
.end method
代码中的try语句块使用try_start_开头的标号注明,以try_end_开头的标号结束。本例中只有一个try语句块,捕获了两个异常,使用多个try语句块时标号名称后面的数值依次递增。
在try_end_8标号下面使用“catch”指令指定处理到的异常类型与catch的标号,格式如下:
.catch<异常类型>{...}
对于代码中的汉字,在反编译的时候使用Unicode进行编码,因此,在阅读前需要使用相关的编码转换工具进行转换。
整理为java代码为:
public void test(int index){
int[] numbers = {1,4,5,6,3,34,645};
try {
for(int i=0;i
完全避免破解是不可能的,我们能做的工作就是尽最大可能去妨碍破解者破解游戏,提高破解成本。