Dalvik虚拟机加载的是dex文件,Dex文件是Dalvik虚拟机的可执行文件格式,dex文件很难看懂,baksmali和smali是对Dex文件的反汇编器和汇编器,通过对Dex文件反编译得到smali文件,smali文件是对Dalvik虚拟机字节码的一种解释(也可以说是翻译),并非一种官方标准语言。通过对smali文件的解读可以获取源码的信息。
Dalvik指令是Dex文件最主要的组成部分:Dalvik指南
smali文件的头3行
# 指定了当前类的类名,访问权限为public,类名开头的L是遵循Dalvik字节码的相关约定
.class public Lcom/example/administrator/myapplication/Demo;
# super指令指定了当前类的父类,父类为Object
.super Ljava/lang/Object;
//指定了当前类的源文件名。注意:经过混淆的dex文件,反编译出来的smali代码可能没有源文件信息,source可能为空。
.source "Demo.java"
字段的声明:
smali文件的字段的声明使用".field"指令,字段有静态字段和实例字段两种:
静态字段格式: .field 访问权限 static 修饰关键字 字段名 字段类型
.field public static HELLO:Ljava/lang/String;
上面smali代码转为java代码为:
public static String HELLO = "hello";
实例字段格式: .field 访问权限 修饰关键字 字段名 字段类型
.field private button:Landroid/widget/Button;
.field public number:I
上面的smali代码转为java代码为:
private Button button;
public int number =5;
方法的声明
使用".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 文件中使用“.implements ”指令指出,相应的格式声明如下:
# interfaces
.implements Lcom/example/administrator/myapplication/TestParent;
上面smali代码表明了实现了TestParent这个接口。
类使用注解
如果一个类使用了注解,会在 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;
程序中的类
1)内部类:内部类可以有成员内部类,静态嵌套类,方法内部类,匿名内部类。
格式: 外部类$内部类.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
}
没往里一层右边的数值就加一,如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
调用默认的构造函数。
2)监听器
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()方法,这时在分析程序时关心的地方。程序中的注解与监听器的构造函数都是编译器为我们自己生成的,实际分析过程中不必关心。
3)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等等。
各种语句
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)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 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<异常类型>{<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类型!");
}
}