美文网首页
Smali 语法解析 —— 类

Smali 语法解析 —— 类

作者: 秉心说 | 来源:发表于2018-12-17 23:17 被阅读23次

    上一篇学习了 Smali 的数学运算,条件判断和循环,接下来学习类的基本使用,包括接口,抽象类,内部类等等。

    直接上代码吧,抽象类 Car.java :

    public abstract class Car {
    
        protected String brand;
    
        abstract void run();
    }
    

    接口 IFly.java

    public interface IFly {
        void fly();
    }
    

    BMW.java

    public class BMW extends Car implements IFly{
    
        private String brand = "BMW";
    
        @Override
        void run() {
            System.out.println(brand + " run!");
        }
    
        @Override
        public void fly() {
            System.out.println("I can fly!");
        }
    
        public static void main(String[] args){
            BMW bmw=new BMW();
            bmw.run();
            bmw.fly();
        }
    }
    

    javac 编译之后使用 dx 生成 dex 文件,和以往不同的是,这次是三个 class 文件生成一个 dex,具体命令如下:

    dx --dex --output=BMWCar.dex Car.class IFly.class BMW.class
    

    最后再使用 baksmali 生成 Smali 文件:

    baksmali d BMWCar.dex
    

    可以看到在当前目录 out 文件夹下生成了三个文件 Car.smali IFly.smaiiBMW.smali。下面逐一进行分析。

    抽象类

    Car.smali :

    .class public abstract LCar; // 表明为抽象类
    .super Ljava/lang/Object;
    .source "Car.java"
    
    
    # instance fields
    .field protected brand:Ljava/lang/String; // proteced String brand
    
    
    # direct methods
    .method public constructor <init>()V
        .registers 1
    
        .prologue
        .line 1
        invoke-direct {p0}, Ljava/lang/Object;-><init>()V
    
        return-void
    .end method
    
    
    # virtual methods
    .method abstract run()V // abstract void run()
    .end method
    

    抽象类的表示没有什么特殊的地方,第一行中会使用 abstract 来声明。

    接口

    IFly.smali

    .class public interface abstract LIFly; // 表明是接口
    .super Ljava/lang/Object;
    .source "IFly.java"
    
    
    # virtual methods
    .method public abstract fly()V // abstract void fly()
    .end method
    

    接口使用 interface 声明。从上面的 smali 代码也可以看到接口中的方法默认是 abstract 修饰的。

    实现类

    BMW.smali :

    .class public LBMW;
    .super LCar;    // 父类是 Car
    .source "BMW.java"
    
    # interfaces
    .implements LIFly; // 实现了接口 IFly
    
    
    # instance fields
    .field private brand:Ljava/lang/String; // priva String brand
    
    
    # direct methods
    .method public constructor <init>()V
        .registers 2
    
        .prologue
        .line 1
        invoke-direct {p0}, LCar;-><init>()V
    
        .line 3
        const-string v0, "BMW"
    
        iput-object v0, p0, LBMW;->brand:Ljava/lang/String; // String brand = "BMW";
    
        return-void
    .end method
    
    .method public static main([Ljava/lang/String;)V // main 方法
        .registers 2
    
        .prologue
        .line 16
        new-instance v0, LBMW; // 新建 BMW 对象,并将其引用存入 v0
    
        invoke-direct {v0}, LBMW;-><init>()V // 执行对象的构造函数
    
        .line 17
        invoke-virtual {v0}, LBMW;->run()V // 执行 run() 方法
    
        .line 18
        invoke-virtual {v0}, LBMW;->fly()V // 执行 fly() 方法
    
        .line 19
        return-void
    .end method
    
    
    # virtual methods
    .method public fly()V  // fly() 方法
        .registers 3
    
        .prologue
        .line 12
        // 下面三行字节码执行了 System.out.println("I can fly!");
        sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
    
        const-string v1, "I can fly!"
    
        invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    
        .line 13
        return-void
    .end method
    
    .method run()V  // run() 方法
        .registers 4
    
        .prologue
        .line 7
        sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream; // 获取 Sysem.out 对象
    
        new-instance v1, Ljava/lang/StringBuilder; // 新建 StringBuilder 对象,并将其引用存到 v1
    
        invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V // 初始化
    
        iget-object v2, p0, LBMW;->brand:Ljava/lang/String; // 获取当前类的 brand 对象
    
        invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; // append()
    
        move-result-object v1 // 将上一步中执行 append() 返回的对象赋给 v1,这里指的是 StringBuilder 对象
    
        const-string v2, " run!"
    
        invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; // append()
    
        move-result-object v1 // 这里的 v1 存储的仍然是 StringBuilder 对象的引用
    
        invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; // toString()
    
        move-result-object v1 // 这里 v1 存储的是 StringB.toString() 执行的结果
    
        invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V // 打印语句
    
        .line 8
        return-void
    .end method
    

    BMW 类中出现了几个还没遇到过的指令,逐一分析一下:

    1. invoke-direct {p0}, LCar;-><init>()V

    p0 存储的是当前类的引用,这句话表示执行当前类中 Car 对象的 init() 方法。

    通用表示:

    invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB

    表示调用指定的方法,之后通常会跟一句 move-result* 来获取方法的返回值。例如:

    invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
    
    move-result-object v1
    

    第一句中执行了 StringBuilder.toString() 方法,返回值是一个 String 对象,紧接着后面的 move-result-object 将返回的 String 对象引用存在了 v1 中。

    invoke-virtual 指调用正常的虚方法(不是 private,static,final,也不是构造函数)。除此之外,还有一些方法调用指令,在下面的表格中列举出来:

    指令 说明
    invoke-virtual 调用正常的虚方法
    invoke-super 调用最近超类的虚方法
    invoke-direct 调用 private 方法或构造函数
    invoke-static 调用 static 方法
    invoke-interface 调用 interface 方法
    1. iput-object v0, p0, LBMW;->brand:Ljava/lang/String;

    v0 中存储的是 BMW 字符串,赋给 p0 的 brand 字段。

    通用用法:

    iinstanceop vA, vB, field@CCCC
    

    vA 可以是源寄存器,也可以是目的寄存器。当使用 iput 命令时, vA 就是源寄存器,使用 iget 命令时, vA 就是目的寄存器。

    此命令还有一个同样用法的变种 sputsget,从名字就可以看出来是对静态字段的操作,i 是对实例字段的操作。

    1. new-instance v1, Ljava/lang/StringBuilder;

    创建一个 StringBuilder 对象,并将其引用存在 v1 寄存器。

    通用用法:

    new-instance vAA, type@BBBB
    

    注意这里的 type 不能是数组类型。数组使用的是 new-array 指令:

    new-array vA, vB, type@CCCC
    

    run() 方法的 smali 代码中我们也学习到了一个小知识点。我们都知道在 Java 中,使用 = 进行字符串拼接是很低效的,run() 方法中执行的是 System.out.println(brand + " run!");,然而虚拟机并没有傻傻的使用 = 去拼接,而是自动使用 StringBuilder 去拼接,提高运行效率。

    内部类

    内部类大家应该都不陌生,在 Android 开发中使用最多的要数匿名内部类了,除此之后,还有静态内部类,成员内部类等。下面的 Outer.java 中便使用了这三种内部类:

    public class Outer {
        
        // 成员内部类
        private class Inner {
            private void in() {
                System.out.println("I am inner class.");
            }
        }
    
        // 静态内部类
        private static class StaticInner {
            private void staticIn() {
                System.out.println("I am static inner class.");
            }
        }
    
        public static void main(String[] args) {
    
            Outer outer = new Outer();
    
            Inner inner = outer.new Inner();
            inner.in();
    
            StaticInner staticInner = new StaticInner();
            staticInner.staticIn();
    
            // 匿名内部类
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            }).start();
        }
    }
    

    还记得内部类的使用方法吗?静态内部类可以直接使用 new 关键字进行实例化,如 new StaticInner()。而成员内部类则不行,必须通过外部类来初始化,如 out.new Inner()javac 编译之后会生成如下四个 class 文件:

    • OUter.class
    • Outer$Inner.clss
    • Outer$StaticInner.class
    • Outer$1.class

    同样,使用 baksmali 反汇编之后也会生成四个 smali 文件。

    首先来看外部类 Outer.smali

    .class public LOuter;
    .super Ljava/lang/Object;
    .source "Outer.java"
    
    
    # annotations 系统自动添加的注解,表示内部类列表
    .annotation system Ldalvik/annotation/MemberClasses;
        value = {
            LOuter$StaticInner;,
            LOuter$Inner;
        }
    .end annotation
    
    
    # direct methods
    .method public constructor <init>()V  // 构造函数
        .registers 1
    
        .prologue
        .line 1
        invoke-direct {p0}, Ljava/lang/Object;-><init>()V
    
        return-void
    .end method
    
    .method public static main([Ljava/lang/String;)V // main() 方法
        .registers 4
    
        .prologue
        const/4 v2, 0x0
    
        .line 17
        new-instance v0, LOuter; // 创建外部类 Outer 对象,存入 v0
    
        invoke-direct {v0}, LOuter;-><init>()V // 执行 Outer 对象的构造函数
    
        .line 19
        new-instance v1, LOuter$Inner; // 创建内部类 Outer$Inner 对象,存入 v1
    
        // 获取外部类对象的引用
        invoke-virtual {v0}, Ljava/lang/Object;->getClass()Ljava/lang/Class;
    
        // 执行内部类 Out$Inner 的构造函数,注意这里执行的不是无参构造,后面具体说明
        invoke-direct {v1, v0, v2}, LOuter$Inner;-><init>(LOuter;LOuter$1;)V
    
        .line 20
        # invokes: LOuter$Inner;->in()V 
        // 执行 Outer$Inner 的 access$100() 方法,这个方法是自动生成的,
        // 实际执行的就是 in() 方法
        invoke-static {v1}, LOuter$Inner;->access$100(LOuter$Inner;)V
    
        .line 22
        new-instance v0, LOuter$StaticInner; // 创建静态内部类 Out$StaticInner 对象
    
        // 执行静态内部类的构造函数,这里也是有参构造
        invoke-direct {v0, v2}, LOuter$StaticInner;-><init>(LOuter$1;)V
    
        .line 23
        # invokes: LOuter$StaticInner;->staticIn()V
        // 执行 Outer$StaticInner 的 access$300() 方法,这个方法是自动生成的,
        // 实际执行的就是 staticIn() 方法
        invoke-static {v0}, LOuter$StaticInner;->access$300(LOuter$StaticInner;)V
    
        .line 25
        new-instance v0, Ljava/lang/Thread; // 创建 Thread 对象
    
        new-instance v1, LOuter$1; // 创建 Out$1 对象
    
        invoke-direct {v1}, LOuter$1;-><init>()V // 执行 Out$1 对象的构造函数
    
        invoke-direct {v0, v1}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V // 执行 Thread 对象的构造函数
    
        .line 30
        invoke-virtual {v0}, Ljava/lang/Thread;->start()V // 执行 Thread.start() 方法
    
        .line 31
        return-void
    .end method
    

    看完外部类 Outer 的 smali 代码,我们发现每个内部类初始化时执行的都是有参构造,但是我们并没有显示的声明任何有参构造。我们从内部类的 smali 代码中找找答案。

    成员内部类

    Out$Inner.smali

    .class LOuter$Inner;
    .super Ljava/lang/Object;
    .source "Outer.java"
    
    
    # annotations 
    # EnclosingClass 注解,系统自动生成,value 值代表其作用范围
    .annotation system Ldalvik/annotation/EnclosingClass;
        value = LOuter;
    .end annotation
    
    # InnerClass 注解,系统自动生成,表示内部类
    .annotation system Ldalvik/annotation/InnerClass;
        accessFlags = 0x2 // private
        name = "Inner"
    .end annotation
    
    
    # instance fields
    # synthetic 表示 this$0 是“合成” 的,并非源码中就有的
    .field final synthetic this$0:LOuter;
    
    
    # direct methods
    # 有参构造
    .method private constructor <init>(LOuter;)V
        .registers 2
    
        .prologue
        .line 3
        iput-object p1, p0, LOuter$Inner;->this$0:LOuter;
    
        invoke-direct {p0}, Ljava/lang/Object;-><init>()V
    
        return-void
    .end method
    
    # 有参构造
    .method synthetic constructor <init>(LOuter;LOuter$1;)V
        .registers 3
    
        .prologue
        .line 3
        invoke-direct {p0, p1}, LOuter$Inner;-><init>(LOuter;)V
    
        return-void
    .end method
    
    # 编译器生成的 access$100() 方法
    .method static synthetic access$100(LOuter$Inner;)V
        .registers 1
    
        .prologue
        .line 3
        invoke-direct {p0}, LOuter$Inner;->in()V // 调用 in() 方法
    
        return-void
    .end method
    
    // in() 方法
    .method private in()V
        .registers 3
    
        .prologue
        .line 5
        sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
    
        const-string v1, "I am inner class."
    
        invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    
        .line 6
        return-void
    .end method
    

    重点看一下成员内部类的有参构造函数:

    .method private constructor <init>(LOuter;)V
        .registers 2
    
        .prologue
        .line 3
        iput-object p1, p0, LOuter$Inner;->this$0:LOuter;
    
        invoke-direct {p0}, Ljava/lang/Object;-><init>()V
    
        return-void
    .end method
    

    有参构造函数的参数是 LOuter;p0 存储的是当前类的引用,p1 存储的是构造函数的参数,即外部类的引用。这个构造函数一共执行了两步:

    1. 把外部类的引用赋值给 this$0
    2. 执行内部类的 init() 方法

    由此可以,成员内部类是持有外部类的引用的,这也就解释了为什么成员内部类可以调用外部类的属性和方法。

    再回头看一下 Outer.smali 中成员内部类 Outer$Inner 的初始化过程:

    const/4 v2, 0x0
    invoke-direct {v1, v0, v2}, LOuter$Inner;-><init>(LOuter;LOuter$1;)V
    

    发现调用的并不是上面那个构造函数,而是一个两个参数的构造函数:

    .method synthetic constructor <init>(LOuter;LOuter$1;)V
        .registers 3
    
        .prologue
        .line 3
        invoke-direct {p0, p1}, LOuter$Inner;-><init>(LOuter;)V
    
        return-void
    .end method
    

    这个构造函数中还是调用了 init(LOuter;) 函数,并且也没有使用 LOuter$1 参数。这个 LOuter$1 参数是什么呢?看到最后就知道,这是匿名内部类的引用。但是在实际调用中传入的是 0x0, 关于传递这个参数的意义,不知道有没有读者知道,可以讨论一下。

    紧接着编译器为成员内部类自动生成了一个静态方法:

    .method static synthetic access$100(LOuter$Inner;)V
        .registers 1
    
        .prologue
        .line 3
        invoke-direct {p0}, LOuter$Inner;->in()V // 调用 in() 方法
    
        return-void
    .end method
    

    方法参数是 p0 存储的当前类的引用,然后通过 p0 直接调用 in() 方法。同样,在外部类中调用成员内部类的 in() 方法,是通过 invoke-static 调用这个静态方法来执行的,如下所示:

    # invokes: LOuter$Inner;->in()V
    invoke-static {v1}, LOuter$Inner;->access$100(LOuter$Inner;)V
    

    静态内部类

    Outer$StaticInner.smali

    .class LOuter$StaticInner;
    .super Ljava/lang/Object;
    .source "Outer.java"
    
    
    # annotations
    # EnclosingClass 注解,系统自动生成,value 值代表其作用范围
    .annotation system Ldalvik/annotation/EnclosingClass;
        value = LOuter;
    .end annotation
    
    # InnerClass 注解,系统自动生成,表示内部类
    .annotation system Ldalvik/annotation/InnerClass;
        accessFlags = 0xa # private static
        name = "StaticInner"
    .end annotation
    
    
    # direct methods
    # 无参构造
    .method private constructor <init>()V
        .registers 1
    
        .prologue
        .line 9
        invoke-direct {p0}, Ljava/lang/Object;-><init>()V
    
        return-void
    .end method
    
    # 有参构造
    .method synthetic constructor <init>(LOuter$1;)V
        .registers 2
    
        .prologue
        .line 9
        invoke-direct {p0}, LOuter$StaticInner;-><init>()V
    
        return-void
    .end method
    
    # 编译器生成的 static 方法
    .method static synthetic access$300(LOuter$StaticInner;)V
        .registers 1
    
        .prologue
        .line 9
        invoke-direct {p0}, LOuter$StaticInner;->staticIn()V
    
        return-void
    .end method
    
    // staticIn() 方法
    .method private staticIn()V
        .registers 3
    
        .prologue
        .line 11
        sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
    
        const-string v1, "I am static inner class."
    
        invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    
        .line 12
        return-void
    .end method
    

    结构与成员内部类基本相似,有两点区别。

    第一, accessFlags 的不同。在系统生成的 InnerClass 注解中有 accessFlags 值。成员内部类 Outer$Inner 值为 0x2,表示 private,静态内部类 Outer$StaticInner 值为 0xa,表示 private static。关于 accessFlags 的表示方式,在我的另一篇文章 Class 文件格式详解 中有具体介绍。

    第二,静态内部类不持有外部类的引用。Outer$StaticInner.smali 中并没有定义 this$0 字段,有参构造中的参数也不包含外部类引用。所以,这也验证了静态内部类只能调用外部类的静态属性和静态方法。

    外部类对静态内部类方法的调用也是自动生成了一个静态方法,再通过这个静态方法来调用,如下所示:

    # invokes: LOuter$StaticInner;->staticIn()V
    invoke-static {v0}, LOuter$StaticInner;->access$300(LOuter$StaticInner;)V
    

    匿名内部类

    Outer$1.smali

    .class final LOuter$1; // 匿名内部类是 final 的
    .super Ljava/lang/Object;
    .source "Outer.java"
    
    # interfaces
    .implements Ljava/lang/Runnable; // 实现了 Runnable 接口
    
    
    # annotations
    # 作用域在 main() 方法中
    .annotation system Ldalvik/annotation/EnclosingMethod;
        value = LOuter;->main([Ljava/lang/String;)V
    .end annotation
    
    .annotation system Ldalvik/annotation/InnerClass;
        accessFlags = 0x8
        name = null
    .end annotation
    
    
    # direct methods
    # 无参构造
    .method constructor <init>()V
        .registers 1
    
        .prologue
        .line 25
        invoke-direct {p0}, Ljava/lang/Object;-><init>()V
    
        return-void
    .end method
    
    
    # virtual methods
    # run() 方法
    .method public run()V
        .registers 3
    
        .prologue
        .line 28
        sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
    
        invoke-static {}, Ljava/lang/Thread;->currentThread()Ljava/lang/Thread;
    
        move-result-object v1
    
        invoke-virtual {v1}, Ljava/lang/Thread;->getName()Ljava/lang/String;
    
        move-result-object v1
    
        invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    
        .line 29
        return-void
    .end method
    

    看起来并没有什么特别的地方,run() 方法中执行的 System.out.println(Thread.currentThread().getName()); 也很容易看懂。只有一个无参构造。对,只有一个无参构造,如果你的 Java 基础学的还可以的话,应该记得匿名内部类会持有外部类的引用,可以这里为什么只有一个无参构造呢?别忘了,这里是 main() 方法,是 static 方法。静态方法中只能引用类中的静态属性。如果换成一个普通方法,生成的 smali 代码中肯定会有 this$0 字段和有参构造。在 Outer.java 中添加如下代码:

        public void test(){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            }).start();
        }
    

    生成的 smali 代码中多了一个文件 Outer$2.smali

    .class LOuter$2;
    .super Ljava/lang/Object;
    .source "Outer.java"
    
    # interfaces
    .implements Ljava/lang/Runnable;
    
    
    # annotations
    .annotation system Ldalvik/annotation/EnclosingMethod;
        value = LOuter;->test()V
    .end annotation
    
    .annotation system Ldalvik/annotation/InnerClass;
        accessFlags = 0x0
        name = null
    .end annotation
    
    
    # instance fields
    .field final synthetic this$0:LOuter; // 外部类引用
    
    
    # direct methods
    # 有参构造
    .method constructor <init>(LOuter;)V
        .registers 2
    
        .prologue
        .line 34
        iput-object p1, p0, LOuter$2;->this$0:LOuter;
    
        invoke-direct {p0}, Ljava/lang/Object;-><init>()V
    
        return-void
    .end method
    
    
    # virtual methods
    .method public run()V
        .registers 3
    
        .prologue
        .line 37
        sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
    
        invoke-static {}, Ljava/lang/Thread;->currentThread()Ljava/lang/Thread;
    
        move-result-object v1
    
        invoke-virtual {v1}, Ljava/lang/Thread;->getName()Ljava/lang/String;
    
        move-result-object v1
    
        invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    
        .line 38
        return-void
    .end method
    

    注意看两处添加注释的地方,这就验证了匿名内部类持有外部类的引用的说法。

    关于 Smali 的学习就到这里了,在这个过程中,也验证了一些我们曾经熟知的知识点,加深了我们对 Java 的理解。当然在 Android 逆向过程中,我们碰到的要比这里说的复杂的多,这就需要我们积累足够的经验了。下一篇,正式进入 Android 领域了,介绍一些常用的 Android 逆向工具。

    文中所有示例代码地址: https://github.com/lulululbj/android-reverse

    相关文章

      网友评论

          本文标题:Smali 语法解析 —— 类

          本文链接:https://www.haomeiwen.com/subject/gdbhkqtx.html