Android反编译知识点

smali文件格式

来自《Android 软件安全与逆向分析》

使用 Apktool 反编译 apk 文件后,会在反编译工程目录下生成一个smali 文件夹,里面存放着所有反编译出的 smali 文件,这些文件会根据 程序包的层次结构生成相应的目录,程序中所有的类都会在相应的目录下生成独立smali 文件。

如上一节中程序的主 Activity 名为com.droider.crackme0502.MainActivity,就会在 smali 目录下依次生成 com\droider\ crackme0502 目录结构,然后在这个目录下生成MainActivity.smali 文件。

smali 文件来存放。每个 smali 文件都由若干条语句组成,所有的语句都遵循着一套语法规范。在 smali 文件的头 3 行描述了当前类的一些信息,格式如下。

.class <访问权限> [修饰关键字] <类名>
.super <父类名>
.source <源文件名>
打开 MainActivity.smali 文件,头 3 行代码如下。
.class public Lcom/droider/crackme0502/MainActivity;.super Landroid/app/Activity;
.source "MainActivity.java"

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

第 2 行的“.super” 指令指定了当前类的父类。本例中的“Lcomdroider/crackme0502/ MainActivity;” 的父类为“Landroid/app/Activity;” 。

第 3 行的“.source” 指令指定了当前类的源文件名。

回想一下,在上一章中讲解 dex 文件格式时介绍的 DexClassDef 构,这个结构描述了一个类的详细信息,该结构的第 1 个字段 classIdx 就是类的类型索引,第 3 个字段superclassIdx 就是指向类的父类类型索引,第 5 个字段 sourceFileIdx 就是指向类的源文件名的字符串索引。 baksmali 在解析 dex 文件时,也是通过这 3 个字段来获取相应的类的值。

注意 经过混淆的 dex 文件,反编译出来的 smali 代码可能没有源文件信息,因此,“.source” 行的代码可能为空。前 3 行代码过后就是类的主体部分了,一个类可以由多个字段或方法组成。 smali 文件中字段的声明使用“.field” 指令。字段有静态字段与实例字段两种。静态字段的声明格式如下。

#static fields

.field <访问权限> static [修饰关键字] <字段名>:<字段>baksmali 在生成 smali 文件时,会在静态字段声明的起始处添加“staticfields” 注释, smali 文件中的注释与 Dalvik 语法一样,也是以井号“#” 开头。

“.field” 指令后面跟着的是访问权限,可以是 public、 private、 protected 之一。修饰关键字描述了字段的其它属性,如 synthetic。指令的最后是字段名与字段类型,使用冒 号“: ” 分隔,语法上与Dalvik 也是一样的。

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

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

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

 #instance fields
.field private btnAnno:Landroid/widget/Button;

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

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

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

“direct methods” 是 baksmali 添加的注释,访问权限和修饰关键字与字段的描述相同,方法原型描述了方法的名称、参数与返回值。

“.locals” 指 定了使用的局部变量的个数。 “.parameter” 指定了方法的参数,与 Dalvik 语法中使用“.parameters” 指定参数个数不同,每个 “.parameter” 指令表明使用一个参数,比如方法中有使用到 3 个参数,那么就会出现 3 条“.parameter” 指令。

“.prologue” 指 定了代码的开始处,混淆过的代码可能去掉了该指令。 “.line” 指定了该处指令在源代码中的行号,同样的,混淆过的代码可能去除了行号信息。

虚方法的声明与直接方法相同,只是起始处的注为“virtualmethods” 。

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

 #interfaces
.implements <接口名>

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

如果一个类使用了注解,会在 smali 文件中使用“.annotation” 指令指出。注解的格式声明如下。

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

内部类

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 = 0x1name = "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
<init>(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 0MainActivitysyntheticthis 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 1this X 型字段都被指定了 synthetic 属性,表明它们是被编译器合成的、虚构的,代码的作者并没有声明该字段。

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

#direct methods
.method public constructor
<init>(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;-><init>()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 SNCheckerp1MainActivityp2snMainActivity SNChecker 的构造函数可以看出,内部类的初始化共有以下 3 个步骤:

首先是保存外部类的引用到本类的一个 synthetic 字段中,以便内部类的其它方法使用,然后是调用内部类的父类的构造函数来初始化父类,最后是对内部类自身进行初始化

监听器

Android 程序开发中大量使用到了监听器,如 Button 的点击事件响应
OnClickListener、 Button 的长按事件响应 OnLongClickListener、 ListView 列表项的点击事件响应 OnItemSelected- Listener 等。

由于监听器只是临时使用一次,没有什么复用价值,因此,在实际编写代码的过程中,多采用匿名内部类的形式来实现。如下面的按钮点击事 件响应代码。

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

监听器的实质就是接口,在 Android 系统源码的frameworks\base\core\java\android\view\ View.java 文件中可以发现OnClickListener 监听器的代码如下。

public interface OnClickListener {
    /**
    * Called when a view has been clicked.
    *
    * @param v The view that was clicked.
    */
    void onClick(View v);
}

设置按钮点击事件的监听器只需要实现 View.OnClickListener 的 onClick()方法即可。

打开 5.2 节的 MainActivity.smali 文件,在 OnCreate()方法中找到设置按钮点击事件监听器的代码如下。

.method public onCreate(Landroid/os/Bundle;)V
.locals 2
.parameter "savedInstanceState"
……
.line 32
iget-object v0, p0, Lcom/droider/crackme0502/MainActivity;->btnAnno:
Landroid/widget/Button;
new-instance v1, Lcom/droider/crackme0502/MainActivity$1;

 #新建一个MainActivity$1 实例
invoke-direct {v1, p0}, Lcom/droider/crackme0502/MainActivity$1;
-><init>(Lcom/droider/crackme0502/MainActivity;)V 

#初始化实例MainActivity$1
invoke-virtual {v0, v1}, Landroid/widget/Button;
->setOnClickListener(Landroid/view/View$OnClickListener;)V
 #设置按钮点击事件监听器
.line 40
iget-object v0, p0, Lcom/droider/crackme0502/MainActivity;
->btnCheckSN:Landroid/widget/Button;
new-instance v1, Lcom/droider/crackme0502/MainActivity$2; 
#新建一个MainActivity$2 实例
invoke-direct {v1, p0}, Lcom/droider/crackme0502/MainActivity$2
-><init>(Lcom/droider/crackme0502/MainActivity;)V;

#初始化MainActivity$2实例
invoke-virtual {v0, v1}, Landroid/widget/Button;
->setOnClickListener(Landroid/view/View$OnClickListener;)V#设置按钮点击事件监听器
.line 50
return-void
.end method

OnCreate() 方法分别了调用按钮对象的 setOnClickListener()方法来设置点击事件的监听器。

第一个按钮传入了一个 MainActivity 1MainActivity 2 对象的引用,我们到 MainActivity$1.smali 文件中看一下前者的实现,它的代 码大致如下。

.class Lcom/droider/crackme0502/MainActivity$1;
.super Ljava/lang/Object;
.source "MainActivity.java"
# interfaces
.implements Landroid/view/View$OnClickListener;
# annotations
.annotation system Ldalvik/annotation/EnclosingMethod;
value = Lcom/droider/crackme0502/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/droider/crackme0502/MainActivity;
# direct methods
.method constructor <init>(Lcom/droider/crackme0502/MainActivity;)V
……
.end method
# virtual methods
.method public onClick(Landroid/view/View;)V
……
.end method

在 MainActivity$1.smali 文件的开头使用了“.implements” 指令指定该类实现了按钮点击事件的监听器接口,因此,这个 类实现了它的OnClick()方法,这也是我们在分析程序时关心的地方。

另外,程序中的注解与监听器的构造函数都是编译器为我们自己生成的,实际分析过 程中不必关心。

注解类

注解是 Java 的语言特性,在 Android 的开发过程中也得到了广泛的使用。

Android 系统中涉及到注解的包共有两个:一个是dalvik.annotation,该程序包下的注解不对外开放,仅供核心库与代码测试使用,所有的注解声明位于 Android 系统源码的libcore\dalvik\src\ main\java\dalvik\annotation 目录下;

另一个是android.annotation,相应注解声明位于 Android 系统源 码的frameworks\base\core\java\android\annotation 目录下。

在前面介绍的 smali 文件中,可以发现很多代 码都使用到了注解类,首先是 MainActivity.smali 文件,其中有一段代码如下。

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

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

/**
* A "system annotation" used to provide the MemberClasses list.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
@interface MemberClasses {}

从注释可以看出, MemberClasses 注解是一个“系统注解” ,作用是为父类提供一个MemberClasses 列表。

MemberClasses 即子类成员集合,通俗的讲就是一个内部类列
表。

接着是 MainActivity$1.smali 文件,其中有一段代码如下。

# annotations
.annotation system Ldalvik/annotation/EnclosingMethod;
value = Lcom/droider/crackme0502/MainActivity;->onCreate(Landroid/os/
Bundle;)V
.end annotation

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

与 EnclosingMethod 对应的还有 EnclosingClass 注解,在MainActivity$SNChecker.smali 文件中有如下一段代码。

# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
value = Lcom/droider/crackme0502/MainActivity;
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x1
name = "SNChecker"
.end annotation

EnclosingClass 注解表明 MainActivity$SNChecker 作用于一个类,注解的 value 表明这个类是 MainActivity。在 EnclosingClass 注解的下面是 InnerClass,它表明自身是一个内部类,其中的accessFlags 访 问标志是一个枚举值,声明如下。

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

为 1 表明它的属性是 Runtime。 name 为内部类的名称,本例为 SNChecker。 如果注解类在声明时提供了默认值,那么程序中会使用到 AnnotationDefault 注解。打开 5.2小节 smali\com\droider\anno 目录下的 MyAnnoClass.smali 文件,有如下一段代码。

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

可以看到,此处的 MyAnnoClass 类有一个默认值为“MyAnnoClass” 。
除了以上介绍的注解外,还有 Signature 与 Throws 注解。Signature 注解用于验证方法的签名,如下面的代码中, onItemClick()方法的原型与 Signature 注解的 value 值是一致的。

.method public 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;",
"IJ)V"
}
.end annotation
……
.end method

如果方法的声明中使用 throws 关键字抛出异常,则会生成相应的 Throws 注解。示例代码如下。

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

示例的 get()方法抛出了 InterruptedException 与ExecutionException 两个异常,将其转换为 Java 代码如下。

public final Object get() throws InterruptedException,  ExecutionException {
    ……
}

以上介绍的注解都是自动生成的,用户不可以在代码中添加使用。在 Android SDK r17版本中, android.annotation 增加了一个SuppressLint 注解,它的作用是辅助开发人员去除代码检查器(Lint API check)添加的警告信息。

比如在代码中声明了一个常量但在代码中没有使用它,代码检查器检测到后会在变量所在的代码行添加警告信息(通常的表现为在代 码行的最左边添加一个黄色惊叹号的小图标以及在变量名底部会加上一条黄色波线),将鼠标指向变量并停留片刻,代码检查器会给出提示建议,

如图 5-1 所 示。

根据最后一条提示建议添加@SuppressWarnings(“unused”)注解后,警告信息消失。

另外,如果在程序代码中使用到的 API 等级比 AndroidManifest.xml 文件中定义的minSdkVersion 要高,代码检查器会在 API 所在代码行添加错误信息。比如在代码中使用了 File 类的 getUsableSpace()方法,该 API 要求的最低 SDK 版本为 9,如果minSdkVersion 指定的值为 8,那么代码检查器就会报错误提示,解决方法是在方法或方法所在类的前面添加“@TargetApi(9)” 。

除了 SuppressLint 与 TargetApi 注解, android.annotation 包还提供了 SdkConstant与 Widget 两 个注解,这两个注解在注释中被标记为“@hide” ,即在 SDK 中是不可见的。

SdkConstant 注解指定了 SDK 中可以被导出的常量字段 值, Widget 注解指定了哪些类是 UI 类,这两个注解在分析 Android 程序时基本上碰不到,此处就不去探究了。

自动生成的类

使用 Android SDK 默认生成的工程会自动添加一些类。这些类在程序发布后会仍然保留在 apk 文件中,目前最新版本的 Android SDK 为 r20 版,经过笔者研究,发现会自动生成如下的类。

首先是 R 类,这个类开发 Android 程序的读者应该会很熟悉,工程 res 目录下的每个资源都会有一个 id 值,这些资源的类型可以是字符串、图片、样式、颜色等。例如我们常见的 R.java 代码如下。

package com.droider.crackme0502;
public final class R {
    public static final class attr { //属性
    }
    public static final class dimen { //尺寸
        public static final int padding_large=0x7f040002;
        public static final int padding_medium=0x7f040001;
        public static final int padding_small=0x7f040000;
    }
    public static final class drawable { //图片
        public static final int ic_action_search=0x7f020000;public static final int ic_launcher=0x7f020001;
    }
    public static final class id { //id 标识
        public static final int btn_annotation=0x7f080000;
        public static final int btn_checksn=0x7f080002;
        public static final int edt_sn=0x7f080001;
        public static final int menu_settings=0x7f080003;
    }
    public static final class layout { //布局
        public static final int activity_main=0x7f030000;
    }
    public static final class menu { //菜单
        public static final int activity_main=0x7f070000;
    }
    public static final class string { //字符串
        public static final int app_name=0x7f050000;
        public static final int hello_world=0x7f050001;
        public static final int menu_settings=0x7f050002;
        public static final int title_activity_main=0x7f050003;
    }
    public static final class style { //样式
        public static final int AppTheme=0x7f060000;
    }
}

由于这些资源类都是 R 类的内部类,因此它们都会独立生成一个类文件,在反编译出的代码中,可以发现有 R.smali、 R attr.smaliR dimen.smali、R drawable.smaliR id.smali、 R layout.smaliR menu.smali、R string.smaliR style.smali 等几个文件。

接下来是 BuildConfig 类,该类是在 Android SDK r17 版本中添加的,以后版本的Android 程序中都有它的身影。这个类中只有一个 boolean 类型的名为 DEBUG 的字段,用来标识程序发布的版本 类型。它的值默认是 true,即程序以调试版本发布。

由于这个类是自动生成的,如果想将它改为 false,需要先在 Eclipse 开发环境中点击菜单“Project→Build Automatically” 关闭自动构建,然后点击菜单“Project→Clean” ,现在使用右键菜单“Android Tools→Export SignedApplication Package” 导出程序,会发现此时 BuildConfig.DEBUG 的值为 false 了。

然后是注解类,如果在代码中使用了 SuppressLint 或 TargetApi 注解,程序中将会包含相应的注解类,在反编译后会在 smali\android\annotation 目录下生成相应的smali 文件。

Android SDK r20 更新后,会在默认生成的工程中添加 android-support-v4.jar 文件。这个 jar 包是 Android SDK 中提供的兼容包,里面提供了高版本才有的如Fragment、 ViewPager 等控件供低版本的程序调用。关于该包的详细信息请参看Android 官方文档: http://developer. android.com/tools extras/supportlibrary.html。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值