爱玩科技网
您的当前位置:首页smali基础

smali基础

来源:爱玩科技网
Smali基础

1. 背景知识

使用Apktool反编译apk 文件后,会在反编译工程目录下生成一个smali 文件夹,里面存放着所有反编译出的smali 文件,这些文件会根据程序包的层次结构生成相应的目录,程序中所有的类都会在相应的目录下生成的smali 文件。smali 文件的代码通常情况下比较长,而且指令繁多,在阅读时很难用肉眼捕捉到重点。无论是普通类、抽象类、接口类或者内部类,在反编译出的代码中,它们都以单独的smali文件来存放。每个smali 文件都由若干条语句组成,所有的语句都遵循着一套语法规范。 2. 文件头

在smali 文件的头3 行描述了当前类的一些信息,格式如下。 .class < 访问权限> [ 修饰关键字] < 类名> .super < 父类名> .source <源文件名> 2.1. 数据类型

在smali中,数据类型和Android中的一样,只是对应的符号有变化: B---byte C---char D---double F---float I---int J---long S---short V---void Z---boolean [XXX---array

Lxxx/yyy---object

这里解析下最后两项,数组的表示方式是:在基本类型前加上前中括号“[”,例如int数组和float数组分别表示为:[I、[F;对象的表示则以L作为开头,格式是

LpackageName/objectName;(注意必须有个分号跟在最后),例如String对象在smali中为:Ljava/lang/String;,其中java/lang对应java.lang包,String就是定义在该包中的一个对象。

或许有人问,既然类是用LpackageName/objectName;来表示,那类里面的内部类又如何在smali中引用呢?答案是:LpackageName/objectName$subObjectName;。也就是在内部类前加“$”符号,关于“$”符号更多的规则将在后面谈到。 2.2. Class

.class public Lcom/droider/crackme0502/MainActivity;

第1 行“.class”指令指定了当前类的类名。在本例中,类的访问权限为 public ,类名为“Lcom/droider/crackme0502/MainActivity;”,类名开头的 L 是遵循 Dalvik 字节码的相关约定,表示后面跟随的字符串为一个类。 2.3. Super

.super Landroid/app/Activity;

第2 行的“.super ”指令指定了当前类的父类。本例中的

Lcom/droider/crackme0502MainActivity;”的父类为“Landroid/app/Activity;”。 2.4. source

.source \"MainActivity.java\"

第3 行的“.source ”指令指定了当前类的源文件名。注意 经过混淆的 dex 文件,反编译出来的 smali 代码可能没有源文件信息,因此,“.source” 行的代码可能为空。 2.5. 声明内部类

8] # annotations

9] .annotation system Ldalvik/annotation/MemberClasses; 10] value = {

11] Lcom/disney/WMW/WMWActivity$MessageHandler;, 12] Lcom/disney/WMW/WMWActivity$FinishActivityArgs; 13] }

14] .end annotation

8-14行定义的则是内部类:它有两个成员内部类——MessageHandler和FinishActivityArgs,内部类将在后面小节中会有提及。 3. 主体内容

前3 行代码过后就是类的主体部分了,一个类可以由多个字段或方法组成。 3.1. 字段(fields、属性、成员变量)

smali文件中字段的声明使用“.field”指令。字段有静态字段与实例字段两种。 3.1.1. 声明静态字段

静态字段的声明格式如下。 # static fields

.field < 访问权限> static [ 修饰关键字] < 字段名>:< 字段类型>

baksmali 在生成smali 文件时,会在静态字段声明的起始处添加“static fields ”注释,smali文件中的注释与Dalvik 语法一样,也是以井号“# ”开头。“.field”指令后面跟着的是访问权限,可以是public、private 、protected 之一。修饰关键字描述了字段的其它属性,如synthetic 。指令的最后是字段名与字段类型,使用冒号“:”分隔,语法上与Dalvik 也是一样的。 3.1.2. 声明实例字段

实例字段的声明与静态字段类似,只是少了static关键字,它的格式如下。 # instance fields

.field < 访问权限> [ 修饰关键字] < 字段名>:< 字段类型>

比如以下的实例字段声明。 # instance fields

.field private btnAnno:Landroid/widget/Button;

第1 行的“instance fields ”是 baksmali 生成的注释,第 2 行表示一个私有字段 btnAnno,它的类型为“Landroid/widget/Button; ”。 3.1.3. 使用字段

一般来说,获取的指令有:iget、sget、iget-boolean、sget-boolean、iget-object、sget-object等,操作的指令有:iput、sput、iput-boolean、sput-boolean、iput-object、sput-object等。没有“-object”后缀的表示操作的成员变量对象是基本数据类型,带“-object”表示操作的成员变量是对象类型,特别地,boolean类型则使用带“-boolean”的指令操作。

例子:

(1)、获取static fields的指令类似是:

sget-object v0,

Lcom/disney/WMW/WMWActivity;->PREFS_INSTALLATION_ID:Ljava/lang/String;

Java代码:v0=com.disney.WMW.WMWActivity.PREFS_INSTALLATION_ID

复制代码

sget-object就是用来获取变量值并保存到紧接着的参数的寄存器中,在这里,把上面出现的PREFS_INSTALLATION_ID这个String成员变量获取并放到v0这个寄存器中,注意:前面需要该变量所属的类的类型,后面需要加一个冒号和该成员变量的类型,中间是“->”表示所属关系。

(2)、获取instance fields的指令与static fields的基本一样,只是由于不是static变量,不能仅仅指出该变量所在类的类型,还需要该变量所在类的实例。看例子:

iget-object v0, p0,

Lcom/disney/WMW/WMWActivity;->_view:Lcom/disney/common/WMWView; 复制代码

可以看到iget-object指令比sget-object多了一个参数,就是该变量所在类的实例,在这里就是p0即“this”。

(3)、获取array的还有aget和aget-object,指令使用和上述类似,不细述。 (4)、put指令的使用和get指令是统一的,直接看例子不解释:

[color=rgb(72,72,72)][font=Verdana, sans-serif] const/4 v3, 0x0

sput-object v3,

Lcom/disney/WMW/WMWActivity;->globalIapHandler:Lcom/disney/config/GlobalPurchaseHandler;[/font][/color]

复制代码

相当于:this.globalIapHandler = null;(null = 0x0) 3.2. 方法(methods)

如果一个类中含有方法,那么类中必然会有相关方法的反汇编代码,smali 文件中方法的声明使用“.method ”指令。方法有直接方法与虚方法两种。 3.2.1. 声明直接方法

直接方法的声明格式如下。 # direct methods

.method <访问权限> [ 修饰关键字] < 方法原型> <.locals> [.parameter] [.prologue] [.line] <代码体> .end method

“direct methods”是baksmali 添加的注释,访问权限和修饰关键字与字段的描述相同,方法原型描述了方法的名称、参数与返回值。“.locals ”指定了使用的局部变量的个数。“.parameter”指定了方法的参数,与Dalvik 语法中使用“.parameters ”指定参数个数不同,每个“.parameter”指令表明使用一个参数,比如方法中有使用到 3 个参数,那么就会出现 3条“.parameter”指令。“.prologue ”指定了代码的开始处,混淆过的

代码可能去掉了该指令。“.line ”指定了该处指令在源代码中的行号,同样的,混淆过的代码可能去除了行号信息。 3.2.2. 声明虚方法

虚方法的声明与直接方法相同,只是起始处的注释为“virtual methods”。 3.2.3. 声明方法例子

注意参数与参数之间没有任何分隔符,同样举几个例子就容易明白了: 1). foo ()V

没错,这就是void foo()。 2). foo (III)Z

这个则是boolean foo(int, int, int)。

3). foo (Z[I[ILjava/lang/String;J)Ljava/lang/String;

看出来这是String foo (boolean, int[], int[], String, long) 了吗? 3.2.4. 调用方法

使用invoke...调用方法,使用move-result...获取函数的返回值。 smali中的函数调用 smali中的函数和成员变量一样也分为两种类型,但是不同成员变量中的static和instance之分,而是direct和virtual之分。那么direct method和virtual method有什么区别呢?直白地讲,direct method就是private函数,其余的public和protected函数都属于virtual method。所以在调用函数时,有invoke-direct,

invoke-virtual,另外还有invoke-static、invoke-super以及invoke-interface等几种不同的指令。当然其实还有invoke-XXX/range 指令的,这是参数多于4个的时候调用的指令,比较少见,了解下即可。

(1)、invoke-static:顾名思义就是调用static函数的,因为是static函数,所以比起其他调用少一个参数,例如:

invoke-static {}, Lcom/disney/WMW/UnlockHelper;->unlockCrankypack()Z 复制代码

这里注意到invoke-static后面有一对大括号“{}”,其实是调用该方法的实例+参数列表,由于这个方法既不需参数也是static的,所以{}内为空,再看一个例子:

复制代码

这个是调用static void System.loadLibrary(String)来加载NDK编译的so库用的方法,同样也是这里v0就是参数\"fmodex\"了。

(2)、invoke-super:调用父类方法用的指令,在onCreate、onDestroy等方法都能看到,略。

(3)、invoke-direct:调用private函数的,例如: invoke-direct {p0},

Lcom/disney/WMW/WMWActivity;->getGlobalIapHandler()Lcom/disney/config/GlobalPurchaseHandler;

复制代码

这里GlobalPurchaseHandler getGlobalIapHandler()就是定义在WMWActivity中的一个private函数,如果修改smali时错用invoke-virtual或invoke-static将在回编译后程序运行时引发一个常见的VerifyError(更多错误汇总可参照APK反编译之番外三:常见错误汇总)。

(4)、invoke-virtual:用于调用protected或public函数,同样注意修改smali时不要错用invoke-direct或invoke-static,例子:

sget-object v0,

Lcom/disney/WMW/WMWActivity;->shareHandler:Landroid/os/Handler;

invoke-virtual {v0, v3},

Landroid/os/Handler;->removeCallbacksAndMessages(Ljava/lang/Object;)V

复制代码

这里相信大家都已经明白了,主要搞清楚v0是shareHandlerandroid/os/Handler,v3是传递给removeCallbackAndMessage方法的Ljava/lang/Object参数就可以了。

(5)、invoke-xxxxx/range:当方法的参数多于5个时(含5个),不能直接使用以上的指令,而是在后面加上“/range”,使用方法也有所不同:

复制代码

这个是电信SDK中的付费接口,需要传递6个参数,这时候大括号内的参数需要用省略形式,且需要连续(未求证是否需要从v0开始)。

有人也许注意到,刚才看到的例子都是“调用函数”这个操作而已,貌似没有取函数返回的结果的操作?

在Java代码中调用函数和返回函数结果是一条语句完成的,而在smali里则需要分开来完成,在使用上述指令后,如果调用的函数返回非void,那么还需要用到move-result(返回基本数据类型)和move-result-object(返回对象)指令:

const/4 v2, 0x0

invoke-virtual {p0, v2},

Lcom/disney/WMW/WMWActivity;->getPreferences(I)Landroid/content/SharedPreferences;

move-result-object v1 复制代码

v1保存的就是调用getPreferences(int)方法返回的SharedPreferences实例。 invoke-virtual {v2}, Ljava/lang/String;->length()I move-result v2 复制代码

v2保存的则是调用String.length()返回的整型。 3.3. 实现接口(interfaces)

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

# interfaces

.implements < 接口名>

“# interfaces ”是baksmali 添加的接口注释,“.implements ”是接口关键字,后面的接口名是DexClassDef结构中interfacesOff 字段指定的内容。 3.4. 注释(annotations、注解)

.annotation [ 注解属性] < 注解类名> [ 注解字段 = 值] .end annotation

注解的作用范围可以是类、方法或字段。如果注解的作用范围是类,“.annotation ”指令会直接定义在smali 文件中,如果是方法或字段,“.annotation ”指令则会包含在方法或字段定义中。例如下面的代码。 # instance fields

.field public sayWhat:Ljava/lang/String;

.annotation runtime Lcom/droider/anno/MyAnnoField; info = \"Hello my friend\" .end annotation .end field

实例字段sayWhat 为String 类型,它使用了 com.droider.anno.MyAnnoField 注解,注解字段info 值为“Hello my friend”。将其转换为 Java 代码为: @ com.droider.anno MyAnnoField(info = \"Hello my friend\") public String sayWhat; 3.4.1. MemberClasses注解

在MainActivity.smali中有如下代码:

# annotations.annotation system Ldalvik/annotation/MemberClasses; value = { Lcom/yiji/test/MainActivity$SNChecker; }.end annotations

MemberClasses是编译时自动加上的,查看MemberClasses注解类源码,如下:

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.ANNOTATION_TYPE)@interface MemberClasses { }

可以看出MemberClasses是“系统注解”,记录一个内部类列表。

3.4.2. EnclosingMethod注解

在MainActivity$1.smali中有一段代码如下:

# annotations.annotation system Lcom/dalvik/annotation/EnclosingMethod; value =

Lcom/yiji/test/MainActivity;->onCreate(Landroid/os/Bundle;)V.end annotation

EnclosingMethod注解用来说明MainActivity$1类的作用范围,其中的Method说明它作用于一个方法,而value表明它位于MainActivity的onCreate()方法中。

3.4.3. EnclosingClass注解

在MainActivity$SNChecker.smali文件中,有如下代码:

# annotations.annotation system Ldalvik/annotation/EnclosingClass; value = Lcom/yiji/test/MainActivity;.end annotation

EnclosingClass表明MainActivity$SNChecker作用于一个类,value表明这个类是MainActivity。

3.4.4. InnerClass注解

.annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x1 name = \"SNChecker\".end annotation

InnerClass表明是一个内部类,name表示内部类的名称,accessFlags访问标志,声明如下:

enum { kDexVisibilityBuild = 0x00, /* annotation visibility */ kDexVisibilityRuntime = 0x01, kDexVisibilitySystem = 0x02,};

3.4.5. AnnotationDefault注解

如果注解在声明时提供了默认值,那么会用到AnnotationDefault注解,示例:

# annotations.annotation system Ldalvik/annotation/AnnotationDefault; value = .subannotation Lcom/yiji/test/anno/MyAnnoClass; value = \"MyAnnoClass\" .end subannotation.end annotation

可以看出MyAnnoClass类有一个默认值”MyAnnoClass”。

3.4.6. Signature注解

用于验证方法的签名

.method pubilc

onItemClick(Landroid/widget/AdapterView;Landroid/view/View;IJ)V .locals 6 .parameter .parameter \"v\" .parameter \"position\" .parameter \"id\" .annotation system

Ldalvik/annotation/Signature; value = { \"(\\"Landroid/widget/AdapterView\\"Landroid/view/View;\annotation ....end method

3.4.7. Throws注解

如果方法抛出异常则生成相应的Throws注解

.method public final get()Ljava/lang/Object; .locals 1 .annotation system Ldalvik/annotation/Throws; value = { Ljava/lang/InterruptedException; Ljava/util/concurrent/ExcutionException; } .end annotation ....end method

3.4.8. 其他注解

- SuppressLint注解:去掉代码检查器的警告信息 - TargetApi注解:去掉代码版本检查的错误信息

- SdkConstant注解:被标记为@hide,指定sdk中可以被导出的常量 - Widget注解:被标记为@hide,表明是UI类

3.5. 内部类

Java 语言允许在一个类的内部定义另一个类,这种在类中定义的类被称为内部类(Inner Class)。内部类可分为成员内部类、静态嵌套类、方法内部类、匿名内部类。前面我们曾经说过,baksmali 在反编译dex 文件的时候,会为每个类单独生成了一个 smali 文件,内部类作为一个的类,它也拥有自己的smali 文件,只是内部类的文件名形式为“[外部类]$[内部类].smali ”,例如下面的类。

class Outer { class Inner{} }

baksmali 反编译上述代码后会生成两个文件:Outer.smali 与Outer$Inner.smali。查看 5.2节生成的smali 文件,发现在 smali\\com\\droider\\crackme0502目录下有一个 MainActivity$ SNChecker.smali 文件,这个SNChecker 就是MainActivity的一个内部类。打开这个文件,代码结构如下。

.class public Lcom/droider/crackme0502/MainActivity$SNChecker; .super Ljava/lang/Object; .source \"MainActivity.java\"

# annotations

.annotation system Ldalvik/annotation/EnclosingClass; value = Lcom/droider/crackme0502/MainActivity; .end annotation

.annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x1 name = \"SNChecker\" .end annotation

# instance fields

.field private sn:Ljava/lang/String;

.field final synthetic this$0:Lcom/droider/crackme0502/MainActivity;

# direct methods

.method public constructor

(Lcom/droider/crackme0502/MainActivity;Ljava/lang/String;)V „„

.end method

# virtual methods

.method public isRegistered()Z „„

.end method

发现它有两个注解定义块“Ldalvik/annotation/EnclosingClass;”与

“Ldalvik/annotation/ InnerClass; ”、两个实例字段sn 与this$0 、一个直接方法 init()、一个虚方法isRegistered() 。注解定义块我们稍后进行讲解。先看它的实例字段,sn 是字符串类型,this$0 是MainActivity类型,synthetic 关键字表明它是“合成”的,那 this$0 到底是个什么东西呢?

其实this$0 是内部类自动保留的一个指向所在外部类的引用。左边的 this 表示为父类的引用,右边的数值0 表示引用的层数。我们看下面的类。

public class Outer { //this$0

public class FirstInner { //this$1 public class SecondInner { //this$2 public class ThirdInner { } } }

每往里一层右边的数值就加一,如 ThirdInner类访问 FirstInner 类的引用为

this$1 。在生成的反汇编代码中,this$X 型字段都被指定了synthetic 属性,表明它们是被编译器合成的、虚构的,代码的作者并没有声明该字段。

我们再看看MainActivity$SNChecker的构造函数,看它是如何初始化的。代码如下。 # direct methods

.method public constructor

(Lcom/droider/crackme0502/MainActivity;Ljava/lang/String;)V .locals 0

.parameter #第一个参数MainActivity引用 .parameter \"sn\" #第二个参数字符串sn .prologue .line 83

iput-object p1, p0, Lcom/droider/crackme0502/MainActivity$SNChecker;

->this$0:Lcom/droider/crackme0502/MainActivity; # 将MainActivity引用赋值 给this$0

invoke-direct {p0}, Ljava/lang/Object;->()V #调用默认的构造函数 .line 84

iput-object p2, p0, Lcom/droider/crackme0502/MainActivity$SNChecker;->

sn:Ljava/lang/String; #将sn字符串的值赋给sn字段 .line 85 return-void .end method

细心的读者会发现,这段代码声明时使用“.parameter ”指令指定了两个参数,而实际上却使用了p0~p2 共3 个寄存器,为什么会出现这种情况呢?在第 3 章介绍 Dalvik 虚拟机时曾经讲过,对于一个非静态的方法而言,会隐含的使用p0寄存器当作类的this 引用。因此,这里的确是使用了3 个寄存器:p0表示MainActivity$SNChecker自身的引用,p1表示MainActivity的引用,p2表示sn 字符串。另外,从 MainActivity$SNChecker的构造函数可以看出,内部类的初始化共有以下 3 个步骤:首先是保存外部类的引用到本类的一个 synthetic字段中,以便内部类的其它方法使用,然后是调用内部类的父类的构造函数来初始化父类,最后是对内部类自身进行初始化。 3.5.1. accessFlags(访问标记)

现在来说下accessFlags在字节码中的计算方式:

access_flags的计算公式为:access_flags = flagA | flagB | flagC ...

java字节码字段访问标志

标志名称 标志值 二进制值 含义

ACC_PUBLIC 0x0001 0000 0000 0000 0001 是否public

ACC_PRIVATE 0x0002 0000 0000 0000 0010 是否private ACC_PROTECTED 0x0004 0000 0000 0000 0100 是否protected ACC_STATIC 0x0008 0000 0000 0000 1000 是否static ACC_FINAL 0x0010 0000 0000 0001 0000 是否final

ACC_VOLATILE 0x0040 0000 0000 0100 0000 是否volatile ACC_TRANSIENT 0x0080 0000 0000 1000 0000 是否transient

ACC_SYNTHETIC 0x1000 0001 0000 0000 0000 是否由编译器自动产生 ACC_ENUM 0x4000 0100 0000 0000 0000 是否enum

例如:

accessFlags = 0x609//

那么现在可以知道上面代码的0x8是指static class Alarm

假设我把他改成0x10

那么就是指private static class Alarm

2|8=10

看不懂再给个例子:

class反编译后的smali代码:

[plain] view plain copy print?

.annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x0 name = \"inner\" .end annotation

[plain] view plain copy print?

.annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x8 name = \"inner2\" .end annotation

[plain] view plain copy print?

.annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x9 name = \"inner3\" .end annotation

java源代码

[java] view plain copy print?

public class test {

class inner{ void echo(){

System.out.println(\"test\"); } }

static class inner2{ void echo2(){

System.out.println(\"test\");

} }

public static class inner3{ void echo3(){

System.out.println(\"test\"); } } }

3.6. 变量和参数

“.locals” 表示局部变量的个数

“.parameter” 方法的参数,肯存在多条

“.prologue” 代码的开始处,混淆的代码可能去掉改条 “.line” 知道在源代码中的行号

在非static函数中p0代表的是“this”,p1,p2...依次表示参数1、参数2...。所有局部变量以V开头。

例子:

.method protected onDestroy()V .locals 0 .prologue .line 277

invoke-super {p0}, Lcom/disney/common/BaseActivity;->onDestroy()V .line 279 return-void .end method

首先看到函数内第一句:.local 0,这句话很重要,标明了你在这个函数中最少要用到的本地寄存器的个数。在这里,由于只需要调用一个父类的onDestroy()处理,所以只需要用到p0,所以使用到的本地寄存器数为0。如果不清楚这个规则,很容易在植入代码后忘记修改.local 的值,那么回编译后运行时将会得到一个VerifyError错误,而且极难发现问题所在。我正是被这个问题困扰了很多次,最后研究发现.local的值有这个规律,于是在文档查证了一下果然是这个问题。例如我往onDestroy()增加一句:

this.existed = true;那么应该改为(注意修改.local的值为1——使用到了v0这一个本地寄存器): 3.7.

btn.setOnClickListener(new android.view.View.OnClickListener() { @Override public void onClick(View v) { ... }});

3.8. 控制条件

\"if-eq vA, vB, :cond_**\" 如果vA等于vB则跳转到:cond_** \"if-ne vA, vB, :cond_**\" 如果vA不等于vB则跳转到:cond_** \"if-lt vA, vB, :cond_**\" 如果vA小于vB则跳转到:cond_**

\"if-ge vA, vB, :cond_**\" 如果vA大于等于vB则跳转到:cond_**

\"if-gt vA, vB, :cond_**\" 如果vA大于vB则跳转到:cond_**

\"if-le vA, vB, :cond_**\" 如果vA小于等于vB则跳转到:cond_** \"if-eqz vA, :cond_**\" 如果vA等于0则跳转到:cond_** \"if-nez vA, :cond_**\" 如果vA不等于0则跳转到:cond_** \"if-ltz vA, :cond_**\" 如果vA小于0则跳转到:cond_**

\"if-gez vA, :cond_**\" 如果vA大于等于0则跳转到:cond_** \"if-gtz vA, :cond_**\" 如果vA大于0则跳转到:cond_**

\"if-lez vA, :cond_**\" 如果vA小于等于0则跳转到:cond_** z 既可以表示zero(0) 也可以是null、或者false;具体看逻辑。 3.9. Switch分支语句

1. 2. 3. 4. 5. 6. 7. 8. 9.

.method private packedSwitch(I)Ljava/lang/String; .locals 1

.parameter \"i\" .prologue .line 21

const/4 v0, 0x0 .line 22

.local v0, str:Ljava/lang/String; #v0为字符串,0表示null

packed-switch p1, :pswitch_data_0 #packed-switch分支,pswitch_data_0指定case区域

10. .line 36

11. const-string v0, \"she is a person\" #default分支 12. .line 39

13. :goto_0 #所有case的出口 14. return-object v0 #返回字符串v0 15. .line 24

16. :pswitch_0 #case 0

17. const-string v0, \"she is a baby\" 18. .line 25

19. goto :goto_0 #跳转到goto_0标号处 20. .line 27

21. :pswitch_1 #case 1

22. const-string v0, \"she is a girl\" 23. .line 28

24. goto :goto_0 #跳转到goto_0标号处 25. .line 30

26. :pswitch_2 #case 2

27. const-string v0, \"she is a woman\" 28. .line 31

29. goto :goto_0 #跳转到goto_0标号处 30. .line 33

31. :pswitch_3 #case 3

32. const-string v0, \"she is an obasan\" 33. .line 34

34. goto :goto_0 #跳转到goto_0标号处 35. .line 22 36. nop

37. :pswitch_data_0

38. .packed-switch 0x0 #case 区域,从0开始,依次递增 39. :pswitch_0 #case 0 40. :pswitch_1 #case 1 41. :pswitch_2 #case 2 42. :pswitch_3 #case 3 43. .end packed-switch 44. .end method

packed-switch 指令。p1为传递进来的 int 类型的数值,pswitch_data_0 为case 区域,在 case 区域中,第一条指令“.packed-switch”指定了比较的初始值为0 ,pswitch_0~ pswitch_3分别是比较结果为“case 0 ”到“case 3 ”时要跳转到的地址。可以发现,标号的命名采用 pswitch_ 开关,后面的数值为 case 分支需要判断的值,并且它的值依次递增。再来看看这些标号处的代码,每个标号处都使用v0 寄存器初始化一个字符串,然后跳转到了goto_0 标号处,可见goto_0 是所有的 case 分支的出口。另外,“.packed-switch”区域指定的case 分支共有4 条,对于没有被判断的 default 分支,会在代码的 packed-switch指令下面给出。

至此,有规律递增的 switch 分支就算是搞明白了。最后,将这段 smali 代码整理为Java代码如下。

[java] view plain copy

1. private String packedSwitch(int i) { 2. String str = null; 3. switch (i) { 4. case 0:

5. str = \"she is a baby\"; 6. break; 7. case 1:

8. str = \"she is a girl\"; 9. break; 10. case 2:

11. str = \"she is a woman\"; 12. break; 13. case 3:

14. str = \"she is an obasan\"; 15. break; 16. default:

17. str = \"she is a person\"; 18. break; 19. }

20. return str; 21. }

现在我们来看看无规律的case 分支语句代码会有什么不同

[java] view plain copy

1. 2. 3. 4. 5. 6. 7. 8. 9.

.method private sparseSwitch(I)Ljava/lang/String; .locals 1

.parameter \"age\" .prologue .line 43

const/4 v0, 0x0 .line 44

.local v0, str:Ljava/lang/String;

sparse-switch p1, :sswitch_data_0 # sparse-switch分支,sswitch_data_0指定case区域

10. .line 58

11. const-string v0, \"he is a person\" #case default 12. .line 61

13. :goto_0 #case 出口

14. return-object v0 #返回字符串 15. .line 46

16. :sswitch_0 #case 5

17. const-string v0, \"he is a baby\" 18. .line 47

19. goto :goto_0 #跳转到goto_0标号处 20. .line 49

21. :sswitch_1 #case 15

22. const-string v0, \"he is a student\" 23. .line 50

24. goto :goto_0 #跳转到goto_0标号处 25. .line 52

26. :sswitch_2 #case 35

27. const-string v0, \"he is a father\" 28. .line 53

29. goto :goto_0 #跳转到goto_0标号处 30. .line 55

31. :sswitch_3 #case 65

32. const-string v0, \"he is a grandpa\" 33. .line 56

34. goto :goto_0 #跳转到goto_0标号处 35. .line 44 36. nop

37. :sswitch_data_0

38. .sparse-switch #case 区域 39. 0x5 -> :sswitch_0 #case 5(0x5) 40. 0xf -> :sswitch_1 #case 15(0xf) 41. 0x23 -> :sswitch_2 #case 35(0x23) 42. 0x41 -> :sswitch_3 #case 65(0x41) 43. .end sparse-switch 44. .end method

按照分析packed-switch 的方法,我们直接查看 sswitch_data_0 标号处的内容。可以看到“.sparse-switch ”指令没有给出初始case 的值,所有的case 值都使用“case 值 -> case 标号”的形式给出。此处共有4 个case ,它们的内容都是构造一个字符串,然后跳转到goto_0 标号处,代码架构上与packed-switch 方式的 switch 分支一样。 最后,将这段smali 代码整理为Java 代码如下。

[java] view plain copy

1. private String sparseSwitch(int age) { 2. String str = null; 3. switch (age) { 4. case 5:

5. str = \"he is a baby\"; 6. break; 7. case 15:

8. str = \"he is a student\"; 9. break; 10. case 35:

11. str = \"he is a father\"; 12. break; 13. case 65:

14. str = \"he is a grandpa\"; 15. break; 16. default:

17. str = \"he is a person\"; 18. break; 19. }

20. return str; 21. }

3.10. Try/catch语句

.method private tryCatch(ILjava/lang/String;)V .locals 10

.parameter \"drumsticks\" .parameter \"peple\" .prologue const/4 v9, 0x0

.line 19

try_start_0 # 第1个try开始

invoke-static {p2}, Ljava/lang/Integer;->parseInt(Ljava/lang/String;)I #将第2个参数转换为int 型

:try_end_0 # 第1个try结束

.catch Ljava/lang/NumberFormatException; {:try_start_0 .. :try_end_0} : catch_1 # catch_1

move-result v1 #如果出现异常这里不会执行,会跳转到catch_1标号处

.line 21

.local v1, i:I #.local声明的变量作用域在.local声明与.end local 之间

:try_start_1 #第2个try 开始 div-int v2, p1, v1 # 第1个参数除以第2个参数

.line 22

.local v2, m:I mul-int #m * i

sub-int #v3 = p1 - v5

.line 23 .local v3, n:I

const-string v5, \"\共\有%d\只\鸡\腿\,%d

\个\人\平\分\,\每\人\可\分\得%d \只\,\还\剩\下%d\只\" # 格式化字符串

const/4 v6, 0x4

new-array v6, v6, [Ljava/lang/Object; const/4 v7, 0x0 .line 24

invoke-static

Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;

move-result-object v8 aput-object v8, v6, v7 const/4 v7, 0x1

invoke-static

Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;

move-result-object v8 aput-object v8, v6, v7 const/4 v7, 0x2

{v1}, {p1},

v5,

v2,

v1

v3, p1, v5

invoke-static

Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;

move-result-object v8 aput-object v8, v6, v7 const/4 v7, 0x3

invoke-static

Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;

move-result-object v8

aput-object v8, v6, v7 .line 23

invoke-static {v5, v6}, Ljava/lang/String;

->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;

move-result-object v4 .line 25

.local v4, str:Ljava/lang/String; const/4 v5, 0x0

invoke-static {p0, v4, v5}, Landroid/widget/Toast;

->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)

Landroid/widget/Toast; move-result-object v5 invoke-virtual {v5}, # 使用Toast 显示格式化后的结果

{v2},

{v3},

Landroid/widget/Toast;->show()V

:try_end_1 #第2个try 结束

.catch Ljava/lang/ArithmeticException; {:try_start_1 .. :try_end_1} : catch_0 # catch_0

.catch Ljava/lang/NumberFormatException; {:try_start_1 .. :try_end_1} : catch_1 # catch_1

.line 33

.end local v1 #i:I .end local v2 #m:I .end local v3 #n:I

.end local v4 #str:Ljava/lang/String; :goto_0

return-void # 方法返回

.line 26

.restart local v1 #i:I :catch_0 move-exception v0 .line 27

.local v0, e:Ljava/lang/ArithmeticException;

:try_start_2 #第3个try 开始 const-string #―人数不能为0‖

const/4 v6, 0x0

invoke-static {p0, v5, v6}, Landroid/widget/Toast;

v5,

\"\人\数\不\能\为0\"

->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)

Landroid/widget/Toast; move-result-object v5

invoke-virtual {v5}, Landroid/widget/Toast;->show()V #使用Toast 显示异常原因

:try_end_2 #第3个try 结束

.catch Ljava/lang/NumberFormatException; {:try_start_2 .. :try_end_2} :catch_1

goto :goto_0 #返回 .line 29

.end local v0 #e:Ljava/lang/ArithmeticException; .end local v1 #i:I :catch_1

move-exception v0 .line 30

.local v0, e:Ljava/lang/NumberFormatException; const-string

\"\无\效\的\数\值\字\符\串\"

#―无效的数值字符串‖

invoke-static {p0, v5, v9}, Landroid/widget/Toast;

->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)

Landroid/widget/Toast; move-result-object v5

v5,

invoke-virtual {v5}, Landroid/widget/Toast;->show()V # 使用Toast 显示异 常原因

goto :goto_0 #返回 .end method

代码中的try语句块使用try_start_开头的标号注明,以try_end_开头的标号结束。第一个try语句的开头标号为try_start_0,结束标号为 try_end_0。使用多个try语句块时标号名称后面的数值依次递增,本实例代码中最多使用到了try_end_2。

在try_end_0 标号下面使用―.catch‖指令指定处理到的异常类型与catch的标号,格式如下。

.catch < 异常类型> { .. }

查看catch_1标号处的代码发现,当转换 String 到int 时发生异常会弹出―无效的数值字符串‖的提示。对于代码中的汉字,baksmali 在反编译时将其使用Unicode进行编码,因此,在阅读前需要使用相关的编码转换工具进行转换。

仔细阅读代码会发现在try_end_1标号下面使用―.catch‖指令定义了 catch_0与catch_1两个catch。catch_0标号的代码开头又有一个标号为try_start_2的try 语句块,其实这个try语句块是虚构的,假如下面的代码。

[plain] view plain copy

private void a() { try {

…… try { ……

} catch (XXX) { …… }

} catch (YYY) { …… } }

当执行内部的try语句时发生了异常,如果异常类型为XXX,则内部catch就会捕捉到并执行相应的处理代码,如果异常类型不是 XXX,那么就会到外层的 catch中去查找异常处理代码,这也就是为什么实例的try_end_1标号下面会有两个catch的原因,另外,如果在执行XXX异常的处理代码时又发生了异常,这个时候该怎么办?此时这个异常就会扩散到外层的catch中去,由于XXX异常的外层只有一个YYY的异常处理,这时会判断发生的异常是否为YYY类型,如果是就会进行处理,不是则抛给应用程序。回到本实例中来,如果在执行内部的ArithmeticException异常处理时再次发生别的异常,就会调用外层的 catch进行异常捕捉,因此在try_end_2标号下面有一个 catch_1就很好理解了。

最后,将这段 smali 代码整理为Java 代码如下。 [java] view plain copy

private void tryCatch(int drumsticks, String peple) { try {

int i = Integer.parseInt(peple);

try {

int m = drumsticks / i; int n = drumsticks - m * i;

String str = String.format(\"共有%d只鸡腿,%d个人平分,每人可分得%d只,还剩下%d只\ Toast.makeText(MainActivity.this, str,Toast.LENGTH_SHORT).show();

} catch (ArithmeticException e) {

Toast.makeText(MainActivity.this, \" 人数不能为0\

}

} catch (NumberFormatException e) {

Toast.makeText(MainActivity.this, \" 无效的数值字符串\

} }

finally语句块 源码:

[java] view plain copy

try {

ServerSocket serverSocket= new ServerSocket(10000); Socket socket=serverSocket.accept(); } catch (IOException e) { e.printStackTrace(); }finally{ int abc=5;

Toast.makeText(this, \"sssss \ }

finally 语句块作用:执行一些必要代码。即不管出现异常与否,在finally中的代码都会被执行

执行时机:针对所有catch语句之后,退出方法之前将被执行(即先执行catch里面的代码,但在throw之前将转向finally)。finally中返回的结果将可以覆盖catch中返回的结果

对应的smail代码如下: :try_start_0

new-instance

#ServerSocket v2 = null; const/16 # v3 = 10000;

v2,

Ljava/net/ServerSocket;

v3, 0x2710

invoke-direct {v2, v3}, Ljava/net/ServerSocket;->(I)V # v2 = new ServerSocket(v3);

.line 21

.local v2, serverSocket:Ljava/net/ServerSocket;

invoke-virtual {v2}, Ljava/net/ServerSocket;->accept()Ljava/net/Socket; # v2.accept( );

:try_end_0

.catchall {:try_start_0 .. :try_end_0} :catchall_0 //处理start_0对应的异常块是catchall_0 也就是finally

.catch Ljava/io/IOException; {:try_start_0 .. :try_end_0} :catch_0//处理start_0对应的异常块是catch_0,catch_0异常块先执行,之后再执行catchall_0

想对应的smali代码为:

:try_start_0

new-instance v2, Ljava/net/ServerSocket; const/16 v3, 0x2710

invoke-direct {v2, v3}, Ljava/net/ServerSocket;->(I)V .line 21

.local v2, serverSocket:Ljava/net/ServerSocket; invoke-virtual

Ljava/net/ServerSocket;->accept()Ljava/net/Socket;

:try_end_0

.catchall {:try_start_0 .. :try_end_0} :catchall_0

.catch Ljava/io/IOException; {:try_start_0 .. :try_end_0} :catch_0 .line 27 const/4

#正常流程 即未发生异常

.line 28

v0,

{v2},

0x5

.local v0, abc:I

const-string v3, \"sssss \"

invoke-static {p0, v3, v5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

move-result-object v3

invoke-virtual {v3}, Landroid/widget/Toast;->show()V .line 32

.end local v2 #serverSocket:Ljava/net/ServerSocket; :goto_0 return-void .line 22

.end local v0 #abc:I

:catch_0 #当发生异常时执行

move-exception v1 .line 24

.local v1, e:Ljava/io/IOException;

:try_start_1

invoke-virtual {v1}, Ljava/io/IOException;->printStackTrace()V :try_end_1

.catchall {:try_start_1 #异常部分执行完毕,转而执行finally

..

:try_end_1}

:catchall_0

.line 27

const/4 v0, 0x5 .line 28

.restart local v0 #abc:I const-string v3, \"sssss \"

invoke-static {p0, v3, v5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

move-result-object v3

invoke-virtual {v3}, Landroid/widget/Toast;->show()V goto :goto_0 .line 25

.end local v0 #abc:I

.end local v1 #e:Ljava/io/IOException;

#finally代码定义部分 :catchall_0

move-exception v3 .line 27

const/4 v0, 0x5 .line 28

.restart local v0 #abc:I const-string v4, \"sssss \"

invoke-static {p0, v4, v5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

move-result-object v4

invoke-virtual {v4}, Landroid/widget/Toast;->show()V .line 30 throw v3 3.11. For循环

# virtual methods

.method public onClick(Landroid/view/View;)V .locals 9 .parameter ―v‖

.prologue .line 36

invoke-virtual {p1}, Landroid/view/View;->getId()I # 非静态方法参数中隐含的第一个参数p0为this指针, p1为第一个参数, 即View对象

move-result v6 # 把上次的计算结果给第七个寄存器,v6=p1.getId(), v6中为View对象的id

packed-switch v6, :pswitch_data_0 # switch(v6)

# —————– 程序出口开始 —————— .line 58

:goto_0 # for循环出口 return-void # return;

# —————– 程序出口结束 ——————

# —————– 获取控件内容开始 —————— .line 39 :pswitch_0

iget-object v6, p0, Lcom/sanwho/crackdemo/ForActivity$1;->this$0:Lcom/sanwho/crackdemo/ForActivity; # v6保存this指针

const v7, 0x7f080001 # v7 = txtValue1, 该id保存在public.xml中

invoke-virtual {v6, v7}, Lcom/sanwho/crackdemo/ForActivity;->findViewById(I)Landroid/view/View; # findViewById(txtValue1)

move-result-object v4 # v4为txtValue1对应的View对象

check-cast v4, Landroid/widget/EditText; # 将View对象转换成EditText, 完成后v4中是txtValue1对象, 失败会抛出ClassCastException异常

.line 40

.local v4, txtValue1:Landroid/widget/EditText;

iget-object v6, p0, Lcom/sanwho/crackdemo/ForActivity$1;->this$0:Lcom/sanwho/crackdemo/ForActivity;

const v7, 0x7f080003 # v7 = txtValue2

invoke-virtual {v6, v7}, Lcom/sanwho/crackdemo/ForActivity;->findViewById(I)Landroid/view/View;

move-result-object v5 # v5为txtValue2对应的View对象

check-cast v5, Landroid/widget/EditText; # 将View对象转换成EditText, 完成后v5中是txtValue2对象

.line 41

.local v5, txtValue2:Landroid/widget/EditText;

invoke-virtual {v4}, Landroid/widget/EditText;->getText()Landroid/text/Editable; # 根据.line 39处可知,v4中为txtValue1对象

move-result-object v6 # v6 = txtValue1.getText();

invoke-interface

Landroid/text/Editable;->toString()Ljava/lang/String;

move-result-object v6 # v6 = txtValue1.getText().toString();

invoke-static

Ljava/lang/Integer;->parseInt(Ljava/lang/String;)I

{v6}, {v6},

move-result v1 # v1 = Integer.parseInt(v6); 也就是起始数值 .line 42

.local v1, from:I

invoke-virtual {v5}, Landroid/widget/EditText;->getText()Landroid/text/Editable; # 根据.line 40处可知,v5中为txtValue2对象

move-result-object v6 # v6 = txtValue2.getText();

invoke-interface

Landroid/text/Editable;->toString()Ljava/lang/String;

move-result-object v6 # v6 = txtValue2.getText().toString();

invoke-static

Ljava/lang/Integer;->parseInt(Ljava/lang/String;)I

move-result v0 # v0 = Integer.parseInt(v6); 也就是结束数值

# —————– 获取控件内容结束 —————— .line 43 .local v0, end:I

{v6}, {v6},

if-le v1, v0, :cond_0 # if v1 <= v0, 即起始数值 <= 结束数值, 则跳到cond_0

# —————– 起始数值 > 结束数值时开始 —————— .line 45

iget-object v6, p0, Lcom/sanwho/crackdemo/ForActivity$1;->this$0:Lcom/sanwho/crackdemo/ForActivity;

const-string v7, ―\起\始\数\值\不\能\大\于\结\束\数\\u503c!‖ # 起始数值不能大于结束数值

#calls:

Lcom/sanwho/crackdemo/ForActivity;->MessageBox(Ljava/lang/String;)V invoke-static {v6, v7}, Lcom/sanwho/crackdemo/ForActivity;->access$0(Lcom/sanwho/crackdemo/ForActivity;Ljava/lang/String;)V

goto :goto_0

# —————– 起始数值 > 结束数值时结束 ——————

# —————– 起始数值 <= 结束数值时开始 —————– .line 49 :cond_0

const/4 v3, 0x0 # v3 = 0, 即int sum = 0;

.line 50

.local v3, sum:I

move v2, v1 # v2 = v1, v2即源码中的i变量

.local v2, i:I

:goto_1 # for循环主要入口

if-le v2, v0, :cond_1 # if 当前数值 <= 结束数值, 跳到cond_1; 否则循环结束, 显示累加结果

.line 54

iget-object v6, p0, Lcom/sanwho/crackdemo/ForActivity$1;->this$0:Lcom/sanwho/crackdemo/ForActivity; # v6指向MessageBox方法

new-instance v7, Ljava/lang/StringBuilder; # v7为StringBuilder对象

const-string v8, ―\累\加\结\果\\uff1a‖ # v8 = ―累加结果:‖

invoke-direct {v7, v8}, Ljava/lang/StringBuilder;->(Ljava/lang/String;)V # 以v8为参数调用StringBuilder构造函数

invoke-static {v3}, Ljava/lang/Integer;->toString(I)Ljava/lang/String; # 把int型的sum值转成字符串

move-result-object v8 # v8 = Integer.toString(v3); 此时v8中为sum的值

invoke-virtual {v7, v8}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; # 把累加结果和sum的值进行追加

move-result-object v7 # v7 为 ―累加结果:‖ + Integer.toString(sum)的StringBuilder对象;

invoke-virtual {v7}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; # 将v7转为字符串对象

move-result-object v7 # v7 = ―累加结果:‖ + Integer.toString(sum);

#calls:

Lcom/sanwho/crackdemo/ForActivity;->MessageBox(Ljava/lang/String;)V invoke-static {v6, v7}, Lcom/sanwho/crackdemo/ForActivity;->access$0(Lcom/sanwho/crackdemo/ForActivity;Ljava/lang/String;)V # 调用MessageBox显示字符串

goto :goto_0 # 跳到goto_0

# —————– 起始数值 <= 结束数值时结束 —————– .line 52

:cond_1 # 加1操作入口

add-int/2addr v3, v2 # v3 = v3 + v2, 即sum += i .line 50

add-int/lit8 v2, v2, 0x1 # v2 = v2 + 1, , 即i = i + 1

goto :goto_1 # 跳到for循环入口继续比对 .line 36 nop

:pswitch_data_0

.packed-switch 0x7f080004 :pswitch_0

.end packed-switch .end method 源码解释

[java] view plain copy

Button.OnClickListener onClickListener = new Button.OnClickListener() {

@Override

public void onClick(View v) {

switch (v.getId()) {

case R.id.btnSubmit: EditText findViewById(R.id.txtValue1); EditText findViewById(R.id.txtValue2);

txtValue1

=

(EditText)

txtValue2 = (EditText)

int from Integer.parseInt(txtValue1.getText().toString());

=

int end = Integer.parseInt(txtValue2.getText().toString()); if (from > end){

MessageBox(\"起始数值不能大于结束数值!\"); } else {

int sum = 0;

for (int i = from; i <= end; i++){ sum += i; }

MessageBox(\"累加结果:\" + Integer.toString(sum)); } break;

} } };

private void MessageBox(String str) {

Toast.makeText(this, str, Toast.LENGTH_LONG).show(); } 3.12. 基本语法

.field private isFlag:z 定义变量 .method 方法 .parameter 方法参数 .prologue 方法开始

.line 123 此方法位于第123行 invoke-super 调用父函数

const/high16 v0, 0x7fo3 把0x7fo3赋值给v0 invoke-direct 调用函数 return-void 函数返回void .end method 函数结束 new-instance 创建实例 iput-object 对象赋值 iget-object 调用对象 invoke-static 调用静态函数 3.13. 控制条件 \"if

3.14. 控制条件 \"if

3.15. 控制条件 \"if

3.16. 方法 3.17. 方法 3.18. 方法

4. 主体内容

因篇幅问题不能全部显示,请点此查看更多更全内容