CTF Wiki smali:https://ctf-wiki.org/android/basic_operating_mechanism/java_layer/smali/smali/

深入理解 Dalvik 字节码指令及 Smali 文件:https://blog.csdn.net/dd864140130/article/details/52076515

初识smali:https://www.52pojie.cn/thread-1701353-1-1.html

吾爱破解安卓逆向入门教程(二、三、四)

超详细的 Dalvik 指令:https://blog.csdn.net/qq_32113133/article/details/8524614

Dalvik opcodes 查询 smali 语法大全:http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html

Android Killer 反编译 apk 得到的 smali

教我兄弟学Android逆向系列课程:https://www.52pojie.cn/thread-742703-1-1.html

上面有两个 smali 指令:一个是 if-eqz,另一个是 if-nez 这两条指令是相对的

  • (1) if-eqz vA, vB, :cond_**"   如果vA等于vB则跳转到:cond_**
  • (2) if-nez vA, vB, :cond_**"   如果vA不等于vB则跳转到:cond_**

如果不让程序跳转到 :cond_0位置,让程序每次在 if-eqz v0, :cond_0 这条关键指令上继续往下执行,有三种思路:

  • 第一种 程序有两个 if-eqz 分别是用来判断用户名和密码是否正确的 把这两个if-eqz都改成if-nez
  • 第二种 直接把这两条指令删除掉
  • 第三种也是最省事最懒的一种,直接用goto语句直接一条指令让程序执行跳转登录成的代码 也是可以达到程序破解的目的

修改完 smali 代码后一定要ctrl+s保存 不然程序还是拿原来的smali代码编译。

Java 和 smali 相互转换

Android Studio 或者 IDEA 中安装 java2smaliJadx Android Decompiler插件,

  • java2smali:把 java 代码转成 smali 代码
  • Jadx Android Decompiler:把 smali 代码转成 java 代码

java 转 smali:新建一个 java 类,然后在 build 中找到 Compile to Smali 并点击 即可。

smali 代码转 java :在 smali 文件里 " 右键 ---> 在 jadx GUI 中反编译 " 即可。

参考

smali语法官方:http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html
Android APP静态分析-Smali语法详解:http://nickycc.lofter.com/post/23e2a6_17d6a07
Smali语法官方:http://bbs.pediy.com/showthread.php?t=151769
Google wiki:http://code.google.com/p/smali/wiki/TypesMethodsAndFields

1、Android Dalvik 虚拟机

特点:

  • 体积小,占用内存空间小。
  • 专有 DEX 可执行文件。
  • 常量池采用32位索引值,寻址类方法名,字段名,常量更快。
  • 基于寄存器架构,并拥有一套完整的指令系统。
  • 提供生命周期管理、堆栈管理、线程管理、安全和异常管理以及垃圾回收等重要功能。
  • 所有的 Android 程序都运行在 Android 系统进程里,每个进程对应一个 Dalvik 虚拟机实例。

1.1 Dalvik虚拟机 与 Java虚拟机 的区别

  • Java虚拟机 JVM 是基于栈的,JVM 执行的是 java 字节码,
  • Dalvik 虚拟机运行的是 Dalvik 字节码。Dalvik字节码 由 Java字节码 转换而来,并打包成一个 DEX 可执行文件,通过虚拟机解释执行。【 Dalvik VM 是 基于寄存器的,Dalvik 有专属的文件执行格式dex (dalvik executable)。Dalvik vm 比 JVM 速度更快,占用空间更少 】

Dalvik 字节码是什么?

  • Dalvik 是 google 专门为 Android 操作系统设计的一个虚拟机,经过深度优化,虽然 Android 上的程序使用 Java 开发的,但是 Dalvik 和 标准的 Java 虚拟机JVM 是两回事。通过 Dalvik 字节码不能直接看到原来的逻辑代码,需要借助 apktool、dex2jar+jd_gui、jadx 等工具帮助查看。

注意:修改 apk 需要操作的是 smali 文件,而不是导出来的 java 文件。修改按 smali 文件后,需要重新编译打包

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."<init>":()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 <init>()V
    .registers 1
 
    .prologue
    .line 3
    invoke-direct {p0}, Ljava/lang/Object;-><init>()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字节码。

1.2 Dalvik可执行文件体积更小,执行速度更快

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,它属于JVM(Java虚拟机)指令集中load系列找那个的一条,i是指令前缀,表示操作类型为int类型,load表示将局部变量存入java栈,与之类似的还有lload、fload、dload分别表示将long,float,double类型的数据进栈;
  • 第二部分为下划线有变动的 数字,表示要操作具体哪个局部变量,索引值从0开始计算,iload_1表示将第二个int类型的局部变量进栈,这里第二个局部变量是存放在局部变量区foo()函数的第二参数。

第二条指令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
  • 第一条指令将 v3 和 v4 寄存器的值相加,保存到v0寄存器,v3和v4分表表示foo()函数的第一个参数和第二个参数,它们是Dalvik字节码参数表示法之一v命名法,另一种是p命名法。
  • 第二条指令sub-int将v3减去v4的值保存到v1寄存器。
  • 第三条指令mul-int/a2ddr将v0乘以v1的值保存到v0寄存器。
  • 第四条指令返回v0的值。
  • 通过比较,Dalvik虚拟机比Java虚拟机执行速度快。

1.3 Dalvik虚拟机Java虚拟机 的架构不同

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_1可分成两部分,第一部分为下划线左边的iload,属于JVM(Java虚拟机)指令集中load系列中的一条,i是指令前缀,表示操作类型为int类型,load表示将局部变量存入Java栈。第二部分为下划线右边的数字,表示要操作具体哪个变量,索引值从0开始计数,iload_1表示将第二个int类型的局部变量进栈参数。

Dalvik虚拟机的架构及参数传递

Dalvik虚拟机基于寄存器架构,数据访问通过寄存器间直接传递。比起Java虚拟机字节码要简洁很多。

以第一条指令为例简单分析一下。指令add-int将v3与v4寄存器的值相加,然后保存到v0寄存器,v3和v4代表调用函数的所使用的两个参数。这里用到的Dalvik字节码参数表示法是v命名法,另一种是p命名法。

Dalvik虚拟机运行时同样为每个线程维护一个PC计数器与调用栈,与Java虚拟机不同的是,这个调用栈维护一份寄存器列表,寄存器的数量在方法结构体的registers字段中给出,Dalvik虚拟机会根据这个值来创建一份虚拟的寄存器列表。

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 提供三种

  1. fork(),创建一个Zygote进程
  2. forkAndSpecialize(),创建一个非Zygote进程(不能再fork)
  3. forkSystemService(),创建一个系统服务进程(子进程跟随父进程终止)

fork之后,执行的工作交给Dalvik虚拟机。虚拟机通过loadClassFromDex()函数完成类的装载工作,每个类被成功解析后会拥有一个ClassObject类型的数据结构存储在运行时环境中,虚拟机使用gDvm.loadedClasses全局哈希表来存储与查询所有装载进来的类,随后,字节码验证器使用dvmVerifyCodeFlow()函数对装入的代码进行校验,接着虚拟机调用FindClass()函数查找并装载main方法类,随后调用dvmInterpret()函数初始化解释器并执行字节码流。

Dalvik 虚拟机是如何执行程序的 ?

  当进程fork成功后:
                      Dalvik虚拟机
                          |   通过loadClassFromDex()函数完成类的装载工作,完成后会每个类会拥有一个ClassObject类型的数据结构存储在运行时环境中
                          |   虚拟机使用gDvm.loadedClasses全局哈希表来存储与查询所有装载进来的类
                      装载程序类
                          |    字节验证码器使用dvmVerifyCodeFlow()函数对装入的代码进行校验
                          |
                      验证字节码
                          |    调用FindClass()函数查找并装载main方法类
                          |
                      查找主类
                          |    调用dvmInterpret()函数初始化解释器并执行字节码流
                          |
                      执行字节码流
                          |
                          |
                        结束

Dalvik虚拟机执行程序流程:

虚拟机线程   --->   装载程序类   --->   验证字节码   --->   查找主类   --->   执行字节码流   --->   结束

关于 Dalvik 虚拟机 JIT

JIT是即时编译(动态编译),是通过在运行时将字节码翻译为机器码的技术,使得程序的执行速度更快。

主流的JIT包含两种字节码编译方式:

  • method方式:以函数或方法为单位进行编译。
  • trace方式:以trace为单位进行编译。

执行代码分为冷路径(在实践运行过程中很少被执行的)和热路径(执行比较频繁的路径),method会编译整个方法,trace编译的是获取的热路径的代码,节省内存。

Dalvik 运行在 Arm 的 CPU 上,那么有必要了解一下 ARM

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 库。

2、Dalvik 寄存器

Dalvik 中用的寄存器都是 32位,64位类型数据则用两个相邻的32位寄存器表示,也就是对于double这种64位类型的数据,需要用到两个32位寄存器来存储

2.1 虚拟 寄存器

        Dalvik 最多支持 65536 个寄存器 ( 编号从0~65535 ),但是在 ARM 架构的 cpu 中只存在 37 个寄存器,那么这种不对称是怎么解决的呢 ?
        Dalvik 中的寄存器是虚拟寄存器, 通过映射真实的寄存器来实现。我们知道每个 Dalvik 维护了一个调用栈,该调用栈就是用来支持虚拟寄存器 和 真实寄存器相互映射的。在执行具体函数时,Dalvik会根据 .registers 指令来确定该函数要用到的寄存器数目。具体的原理,可以自行参考 Davilk 的实现。

下面说的 寄存器 都是 虚拟寄存器。

2.2 寄存器的命名( V命名法 P命名法 

寄存器使用规则

对于一个使用 m个寄存器 (m局部变量寄存器个数 x  + 参数寄存器个数 y) 的方法而言:

  • 局部寄存器 使用从 v0 开始的 x 个寄存器,
  • 参数寄存器 则使用 最后的 y 个寄存器

示例说明:假设实例方法 test(String a,String b) 一共使用了5个寄存器:0,1,2,3,4,那么参数寄存器是能使用 2,3,4 这三个寄存器,如图:

寄存器 有两种不同的命名方法(这两种命名法仅仅是影响了字节码的可读性):

  1. v 字命名法。v 表示本地寄存器(局部变量寄存器)。( v 表示 var,即 变量 )
  2. p 字命名法。p 表示参数寄存器(一般都用 p 命名法)。( p 表示 param,即 参数)。

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 命名法对比:

  • v 命名法:v 命名法用到 v0、v1、v2、v3、v4 五个寄存器。v0 和 v1 用来表示函数的局部变量寄存器,v2 表示被传入的 Hello 对象的引用,v3 和 v4 分别表示两个传入的整形参数。( 参数寄存器 使用 最后的 y 个寄存器。 )
  • p 命名法:p 命名法 对 函数的 局部变量寄存器 命名 没有影响,对于函数中引入的参数命名从 p0 开始,依次递增。p 命名法用到了 v0,v1,p0,p1,p2 五个寄存器,v0 和 v1 用来表示函数的局部变量寄存器,p0 表示被传入的 Hello 对象的引用,p1 和p2 分别表示两个传入的整形参数。

使用 p 命名法表示的 Dalvik 汇编代码,通过寄存器的前缀更容易判断寄存器到底是局部变量寄存器还是参数寄存器,在 Dalvik 汇编代码较长,使用寄存器较多的情况下,这种优势更加明显。

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 为参数能够使用的寄存器。

p 命名法:寄存器采用 v 和 p 来命名,

如果一个非静态方法有两个本地变量,有三个参数,需要的寄存器关系如下:

  • v0   第一个本地寄存器
  • v1   第二个本地寄存器
  • p0   // 指代调用这个方法的this对象。
  • p1   第一个参数
  • p2   第二个参数
  • p3   第三个参数

如果是静态方法,那么就不需要 this 对象了,需要的寄存器是v0, v1, p0, p1, p2。

  •  .registers     使用这个指令指定方法中寄存器的总数。寄存器的数量可以多,但是不能少。
  •  .locals          使用这个指定表明方法中非参寄存器的总数,放在方法的第一行。

3、Dalvik 描述符

与 JVM 类似,Dalvik 字节码中同样有一套用于描述 类型、方法、字段 的方法,这些方法结合 Dalvik 的指令便形成了完整的汇编代码。

3.1 字节码数据类型

Dalvik 字节码 只有 2 种类型:

  1. 基本类型。
  2. 引用类型。

除了 对象数组 属于 引用类型 外,其他的 Java 类型都是基本类型。

Dalvik 使用这2种类型来表示 Java 语言的全部类型。

Davilk 中对字节码类型的描述 和 JVM 中的描述符规则一致:

  • 对于 基本类型无返回值的void类型 都是用一个大写字母表示。
  • 对象类型 则 用 字母 L 加 对象的 全限定名 来表示。
  • 数组类型[ 来表示。

全限定名 是 什么?

                以 String 为例,其完整名称是 java.lang.String,那么其 全限定名 就是 java/lang/String;
                即 java.lang.String 的  "." 用 "/" 代替,并在末尾添加分号 ”;” 做结束符。

Java类型 和 类型描述符 

java 类型类型描述符
byteB
shortS
intI
longJ
oatF
doubleD
charC
booleanZ
voidV
数组[
objectL + / 分割的全类名路径

每个 Dalvik 寄存器都是 32 位,对于小于或等于 32 位长度的类型来说,一个寄存器就可以存放该类型的值;像 J、D 等 64位 类型的值,它们的值使用相邻两个寄存器来存储,如 v0 和 v1。

示例解释:

  • L  可以表示 Java 类型中 任何类,在 Java 中以 package.name.ObjectName 表示。
            在 Dalvik 汇编代码中,以 Lpackage/name/ObjectName;  形式来表示,最后有个分号。
            如  Ljava/lang/String; 相当于 java.lang.String。
  • [   表示 所有基本类型 的 数组 
            [I  表示一个整型数组,相当于  Java  中的  int[]。
            [[I  表示  int[][] ,多维数组最大为 255 个。
            [  与  L  可以同时表示对象数组,如  [Ljava/lang/String  表示  Java  中的字符串数组。

图 示:

这里重点解释 对象类型数组类型。

3.1.1 对象类型

L 可以表示 java 类型中 的 任何类

  • 在 java 代码中以 package.name.ObjectName 的方式引用。( L 即上面定义的 java类 类型,表示后面跟着的是 类 的 全限定名。比如:java 中的 java.lang.String 对应的描述是 Ljava/lang/String; . )
  • 而在 Davilk 中其描述则是以 Lpackage/name/ObjectName; 的形式表示。

对象类型

形式:Lxxx/yyy/zzz;

  • L  表示这是一个对象类型
  • xxx/yyy 是该对象所在的包
  • zzz 是对象名称
  • 标识对象名称的结束

如:Ljava/lang/String;

3.1.2 数组类型

[  用来表示 所有基本类型 数组,[ 后跟着是基本类型的描述符。每一维度使用一个前置的 [

比如:java 中的 int[] 用汇编码表示便是 [I; 。二维数组 int[][] 为 [[I; ,三维数组则用 [[[I; 表示。

对于对象数组来说,[ 后跟着对应类的全限定符。比如 java 当中的 String[] 对应的是 [java/lang/String; 

数组类型

形式:[XXX

  • [I 表示一个 int 型的一维数组,相当于int[]
  • 增加一个维度增加一个 [,如 [[I 表示 int[][]
  • 数组每一个维度最多 255 个;
  • 对象数组表示也是类似

如:String 数组的表示是 [Ljava/lang/String

3.2 字段 的 描述(字段 即 变量

Dalvik 中 对字段的描述 分为两种:

  1. 基本类型字段描述
  2. 引用类型描述

但 两者 的 描述格式 一样:

对象类型描述符; -> 字段名:类型描述符;

比如 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;

  • 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;

  • 类型:Lpackage/name/ObjectName;
  • 字段名:ObjectName
  • 字段类型:FieldName:Ljava/lang/String;


 

3.3 方法 的 描述

Java 中方法的签名包括 方法名参数 及 返回值。在 Davilk 相应的描述规则为:

对象类型描述符 -> 方法名(参数类型描述符)返回值类型描述符

方法格式:Lpackage/name/ObjectName;->MethodName(III)Z

  • Lpackage/name/ObjectName;    是一个类型,
  • MethodName    为具体方法的方法名,
  • 括号内的三个III为方法的参数( 3 个 int 类型 )
  • Z 表示方法的返回类型(boolean)。

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

  • Lxxx/yyy/zzz       表示对象的类型
  • methodName    表示方法名
  • Lxxx/yyy/zzz;Lxxx/yyy/zzz;I   表示三个参数。两个对象类型,一个整型(方法的参数是一个接一个的,中间没有隔开)
  • Z返回值类型,这里是boolean类型

如:Log.i("xxx", msg); 转换成smali语句之后是:Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I

4、Dalvik 指令集

掌握以上的 字段方法 描述,只能说我们懂了如何描述一个 字段方法,而关于方法中具体的逻辑则需要了解 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语言的调用约定。

官网地址:

指令语法与助词有如下特点:

  1. 采用从目标(destination)到源(source)的方法
  2. 根据字节码的大小与类型不同,一些字节码添加了名称后缀已消除歧义
        2.1   32位常规类型的字节码未添加任何后缀
        2.2   64位常规类型的字节码添加 -wide后缀
        3.3   特殊类型的字节码根据具体类型添加后缀,-boolean、-byte、-char、-short、-int、-long、-float、-double、-object、-string、-class、-void 之一。
  3. 根据字节码的布局和选项不同,一些字节码添加了字节后缀消除歧义,后缀与字节码直接用 / 分割
  4. 在指令集的描述中,宽度值中每个字母表示宽度为 4 位。( 16进制表示,一个字母代表 4 位 2进制

如:

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
类声明: .class + 权限修饰符 + 类名;
字段声明: .field 权限修饰符 + 静态修饰符 + 变量名: 全类名路径;
常量声明: .field 权限修饰符 + [静态修饰符] + final + 变量名: 全类名路径; = 常量值
方法/函数的声明: 
        .method 权限修饰符 + [静态修饰符] + 方法名(参数类型)返回值类型
        # 方法体
        .end method # 方法结束标记
方法返回值关键字
        return-void
        return-object
        return
        return-wide
构造函数的声明
        .method 权限修饰符 + constructor <init>(参数类型)V
        # 方法体
        .end method
静态代码块的声明
        .method static + constructor <clinit>()V
        # 方法体
        .end method
方法调用        
        invoke-virtual 非私有实例方法的调用
        invoke-direct 构造方法以及私有方法的调用
        invoke-static 静态方法的调用
        invoke-super 父类方法的调用
        invoke-interface 接口方法的调用
非私有实例方法的调用
        invoke-virtual {参数}, 方法所属类名;->方法名(参数类型)返回值类型;
私有方法或者构造函数的调用
        invoke-direct {参数}, 方法所属类名;->方法名(参数类型)返回值类型;
静态方法的调用
        invoke-static {参数}, 方法所属类名;->方法名(参数类型)返回值类型;
父类方法的调用
        invoke-super {参数}, 方法所属类名;->方法名(参数类型)返回值类型;
接口的调用
        invoke-interface {参数}, 方法所属类名;->方法名(参数类型)返回值类型;

空操作 指令

  • 空操作指令的助记符为 nop。它的值为 00,通知被用来作对齐代码之用,无实际操作。

指令

锁指令多用在多线程程序中对同一对象的操作。Dalvik 指令集中有两条锁指令。

  • monitor-enter vAA     为指定的对象获取锁
  • monitor-exit vAA        释放指定的对象的锁

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

实例操作 指令

与实例相关的操作包括实例的类型转换、检查及新建等

  • check-cast  vAA,  type@BBBB 
    将vAA寄存器中的对象引用转换成指定的类型,如果失败会抛出 ClassCastException 异常。如果类型B 指定的是基本类型,对于非基本类型的A来说,运行时始终会失败。
  • instance-of  vA,  vB,  type@CCCC 
    判断vB寄存器中的对象引用是否可以转换成指定的类型,如果可以vA寄存赋值为1,否则vA寄存器为0
  • new-instance  vAA,  type@BBBB 
    构造一个指定类型对象的新实例,并将对象引用赋值给vAA寄存器,类型符号type指定的类型不能是数组类。
  • check-cast/jumbo      vAAAA,   type@BBBBBBBB    
    instance-of vAAAA,   vBBBB,   type@CCCCCCCC   
    new-instance/jumbo  vAAAA,   type@BBBBBBBB  

    这三个指令功能分别与 上面三个指令分对应 相同,只是寄存器值与指令的索引取值范围坑大(Android4.0中新增的命令)

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;-><init>()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;-><init>()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;-><init>(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;-><init>()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 中设置专门的指令用于数组操作。

数组操作包括:读取数组长度、新建数组、数组赋值、数组元素取值与赋值、等 操作。

  • array-length vA,vB 
    获取给定vB寄存器中数组的长度并将值赋给vA寄存器,数组长度指的是数组的条目个数。
  • new-array vA,vB,type@CCCC 
    构造指定类型(type@CCCC)与大小(vB)的数组,并将值赋给vA寄存器。
  • new-array/jumbo vAAAA,vBBBB,type@CCCCCCCC 
    指令功能与上一条指令相同,只是寄存器与指令的索引取值范围更大(Android4.0中新增的指令)
  • filled-new-array {vC,vD,vE,vF,vG},type@BBBB
    构造指定类型(type@BBBB)与 大小(vA)的数组并填充数组内容。vA寄存器是隐含使用的,除了指定数组的大小外还制订了参数的个数,vC~vG是使用到的参数寄存器序列
  • filled-new-array/range {vCCCC, ... ,vNNNN},type@BBBB
    指定功能与上一条指令相同,只是参数寄存器使用range字节码后缀指定了取值范围,vC是第一个参数寄存器, N=A+C-1。
  • filled-new-array/jumbo {vCCCC, ... ,vNNNN},type@BBBBBBBB 
    指令功能与上一条指令相同,只是寄存器与指令的索引取值范围更大(Android4.0中新增的指令)
  • 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 条比较指令。

  • type 表示比较数据的类型,如 -long、-float 等;
  • kind 则代表操作类型,因此有 cmplcmpgcmp 三种比较指令。

    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 中有效的数据类型,

        对 普通字段静态字段 操作的两中指令集:

  • iinstanceop vA,vB,field@CCCC
            普通字段指令的指令前缀为 i,如对普通字段读操作使用 iget 指令,写操作使用 iput 指令。
            普通字段操作指令有:iget、iget-wide、iget-object、iget-boolean、iget-byte、iget-char、iget-short、iput、iput-wide、iput-object、iput-boolean、iput-byte、iput-char、iput-short。
  • sstaticop vAA,field@BBBB
            静态字段的指令前缀为 s,如对静态字段读操作使用 sget 指令,写操作使用 sput 指令。
            静态字段操作指令有:sget、sget-wide、sget-object、sget-boolean、sget-byte、sget-char、sget-short、sput、sput-wide、sput-object、sput-boolean、sput-byte、sput-char、sput-short。

基本指令是 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;-><init>()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;-><init>()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;-><init>()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@BBBBinvoke-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;-><init>(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;-><init>(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;-><init>(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指令集大全  (Dalvik opcodes

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

Dalvik hello world

首先写一个基本框架

.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

我们去官网下载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;-><init>()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

5、详解 smali 文件

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

5.1 基础介绍

怎么得到 smali 文件 ?

        apk 文件通过 apktool 反编译出来的都有一个 smali 文件夹,里面都是以 .smali 结尾的文件。

Smali 是什么 ?

        Smali 是安卓系统里的Java虚拟机(Dalvik)所使用的一种.dex格式文件的汇编器,而 Baksmali 是反汇编器。其语法是一种宽松式的Jasmin/dedexer语法。所谓的 smali 就是 Dalvik VM 内部执行的核心代码。

        Smali 语言其实就是一种面向Dalvik的汇编语言(汇编语言是一种用于电子计算机、微处理器、微控制器,或其他可编程器件的低级语言。在不同的设备中,汇编语言对应着不同的机器语言指令集。使用汇编语言编写的源代码,然后通过相应的汇编程序将它们转换成可执行的机器代码。这一过程被称为汇编过程。),用 smali 语言写的代码通过汇编器就转换成Dalvik可以执行的dex文件。

        对应的有两个工具:

  • smali.jar 将 Smali 文件转换为 dex 文件,
  • baksmali.jar 则是将 dex 文件转换为 smali 文件。

apktool 能反编译出 smali 文件就是因为它里面用了 baksmali 工具。

为什么要学习Smali ?

        smali 语法是逆向的基础,它能帮助我们理解别人代码的逻辑(当然我们也可以用 dex2jar, 更方便查看代码),但是当我们要修改别人的代码逻辑,或者加上自己的逻辑,最终是要修改 smali 文件的,如果我们不懂 smali 语法,就无从下手。目前android盗版软件中,大部分都是二次打包党,他们基本上就是反编译->注入广告->再打包签名->上传市场,其中注入广告或者破解游戏等都是通过修改smali来达到的。

        Android 系统有自己的虚拟机 Dalvik,代码编译最终不是采用 Java 的 class,而是使用的 smali。反编译得到的代码,如果是 jar 的话可能很多地方无法正确的解释出来,如果反编译得到的是 smali 则可以正确的理解程序的意思。因此,我们有必要熟悉 smali 语法 。

如何学习 ?

        smali的语法细节太多了,各种指令,看的人头疼,我们其实也不需要全都学会,因为现在有工具帮我们了。

        AndroidStudio 有个插件: Java2Smali,方便快捷把 java 代码转换为smali代码。有了这个工具,我们可以结合它在实践中学习 smali 语法,同时它也可以帮我们省很多事,当我们想在别人的逻辑中增加代码时,我们可以把自己的代码先用这个工具转成 smali 语句, 然后做微调就行了。(  Java2Smali 安装完成之后重启 AS,然后 Build ---> Compile to 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 文件的结构:

5.2 文件头描述( smali 文件的 头 3 行 )

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"

5.2 文件正文

在文件头之后便是文件的正文,即 类 的 主体部分,包括:类实现的接口描述、注解描述、字段描述和方法描述四部分。

下面我们就分别看看 字段 和 方法的结构。( 别忘了我们在 Davilk 中说过的 方法 和 字段 的表示 )

接口描述

如果一个类实现了某个接口,会在 smali 文件中使用 “.implements ” 指令指出,相应的格式声明如下:

#interfaces
.implements <接口名称>

# 下面 smali 代码表明了实现了 TestParent 这个接口。
# interfaces
.implements Lcom/example/administrator/myapplication/TestParent;  

举例说明:

# interfaces
.implements Landroid/view/View$OnClickListener;

smali 为其添加了 #Interface  注释

注解描述

如果一个类使用了注解,会在 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 <init>()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
.end method

需要注意 smali 为其添加了 #direct method 注释

2. 虚方法( 即 java 中的普通方法 )

虚方法的定义会和直接方法唯一的不同就是注释不同:#virtual methods,其格式如下:

#virtual methods
.method <访问权限> [修饰关键词] <方法原想>
      <.locals>
      [.parameter1]
      [.parameter2]
      [.prologue]
      [.line]
      <代码逻辑>
.end

java 代码:

package com.spark.jvm;

public class Hello {
    public static String a;
    public void myPrint(){
        System.out.println("this is a test");
    }

    public static void main(String[] args){
        Hello h = new Hello();
        h.myPrint();
    }
}

使用 java2smali 插件 转成 smail 代码如下:

.class public Lcom/spark/jvm/Hello;
.super Ljava/lang/Object;
.source "Hello.java"


# static fields
.field public static a:Ljava/lang/String;


# direct methods
.method public constructor <init>()V
    .registers 1

    .prologue
    .line 3
    invoke-direct {p0}, Ljava/lang/Object;-><init>()V

    return-void
.end method

.method public static main([Ljava/lang/String;)V
    .registers 2
    .param p0, "args"    # [Ljava/lang/String;

    .prologue
    .line 10
    new-instance v0, Lcom/spark/jvm/Hello;

    invoke-direct {v0}, Lcom/spark/jvm/Hello;-><init>()V

    .line 11
    .local v0, "h":Lcom/spark/jvm/Hello;
    invoke-virtual {v0}, Lcom/spark/jvm/Hello;->myPrint()V

    .line 12
    return-void
.end method


# virtual methods
.method public myPrint()V
    .registers 3

    .prologue
    .line 6
    sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;

    const-string v1, "this is a test"

    invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V

    .line 7
    return-void
.end method

5.4 内部类的 smali 文件结构

内部类:内部类可以有 成员内部类静态嵌套类方法内部类匿名内部类
      格式: 外部类$内部类.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 <init>(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;-><init>()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
}

每往里一层,右边的数值就加 1,如 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 <init>(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;-><init>()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;-><init>()V

调用默认的构造函数。

匿名内部类演示

java 代码:

package com.spark.jvm;

public class Hello {
    public static String a;
    public void myPrint(){
        System.out.println("this is a test");
    }

    public static void main(String[] args){
        Hello h = new Hello();
        h.myPrint();

        class A {}
        class B {}
        class C {}
    }
}

如果安装完 java2smali 插件后,即可在 build 里面把 java 代码转成 smali 代码。

转换后,可以看到 4 个 smali 文件,带 $ 的是匿名内部类的 smali 文件

监听器

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;-><init>(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 <init>(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;-><init>()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

下面是 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 等等。

更详细的说明见下文。

5.5 实例演示

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 <init>()V
    .locals 1    # 表示函数中使用了一个局部变量

    .prologue    # 表示方法中代码正式开始
    .line 8      # 表示对应与java源文件的低8行
    # 调用Activity中的init()方法
    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
.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;-><init>()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

5.6 各种语句

1)switch语句

.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;
    }

2)循环语句

常用的循环结构有:迭代器循环,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;-><init>()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 实现跳出循环。

3)try/catch语句

.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 的标号
    .catch Ljava/lang/IndexOutOfBoundsException; {:try_start_7 .. :try_end_8} :catch_11  
    .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"

    # index 不能是 String 类型! 
    const-string v4, "index\u4e0d\u80fd\u662fString\u7c7b\u578b\uff01"   
 
    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<异常类型>{<try起始标号>...<try结束标号>}<catch标号>

      对于代码中的汉字,在反编译的时候使用Unicode进行编码,因此,在阅读前需要使用相关的编码转换工具进行转换。

整理为 java 代码为:

public void test(int index){
        int[] numbers = {1,4,5,6,3,34,645};
 
        try {
            for(int i=0;i<numbers.length;i++){
                if(i==10){
 
                }
            }
        }catch (IndexOutOfBoundsException e){
            Log.i("log","数组越界!");
        }catch (IllegalArgumentException e){
            Log.i("log","index不能是String类型!");
        }
    }

说明:

app 完全避免破解是不可能的,我们能做的工作就是尽最大可能去妨碍破解者破解游戏,提高破解成本。

  • 一定要使用混淆。不单单是第三方 SDK,你的代码也是。破解游戏很重要一点就是要抓住游戏的逻辑。代码混淆后,Smali更加晦涩难懂,逻辑也更难掌握。
  • 解读汇编比解读 Smali 难度大的多得多。所以重要的逻辑可以放到 C/C++ 层去处理就不要放在 Java 层上去处理。
  • 多用连续调用的方式。这样出来的效果是 Java 只有一行,Smali 可能有好几十行,看着都蛋疼。当然这对熟练的破解老手无效~
  • 在一些关键的点上,比如支付,多绕一下。而不是像《消灭小星星》这样,直接在 Java 内用中文显示 “支付成功”,同时去调用 JNI 方法。用 dex2jar 看一眼就暴露了。

6、RDP 的使用

RDP 是一个基于平头哥可以对 apk 进行源码级别修改的模块。

使用流程

1. rdp 工程的产生
2. rdp 修改 smali 
3. rdp 重新编译生成 apk

生成 smali 代码:./ratel.sh -D xxx.apk

上面命令执行完成后,会生成一个目录,可以直接使用 Android Studio 打开,修改 smali 代码,修改完成后,进入 ratel_resource 目录下( 生成的目录中会有一个 ratel_resource 目录 ),执行下面命令重新编译成 apk,卸载老的apk,安装新的apk并运行。

重新编译生成 apk:./ratel_resource/rdp.sh

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐