smali 语法

smali是什么

Smali是Dalvik的寄存器语言,它与Java的关系,简单理解就是汇编之于C。

smali文件是哪来的,获取方法

Smali代码是安卓APK反编译而来的。Smali文件和Java文件一一对应。获取Smali文件,我们需要下载一个辅助工具:ApkTool 。apktool这个命令行工具,最常用的命令有:

  • 反编译decode:
    apktool d xxx.apk
  • 打包build:
    apktool b

Smali语法

基本数据类型

V void (只能用于返回值类型) 
Z boolean
B byte
S short
C char
I int
J long(64位)
F float
D double(64位)

对象类型

Lpackage/ObjectName; 相当于java中的package.ObjectName;
L 表示这是一个对象类型
package 该对象所在的包
ObjectName 对象名称
; 标识对象名称的结束
例如:Ltestdemo/hpp/cn/test/MainActivity;Ljava/lang/String;

数组类型

[I :表示一个整形的一维数组,相当于java的int[];
对于多维数组,只要增加[ 就行了,[[I = int[][];注:每一维最多255个;

对象数组的表示形式:
[Ljava/lang/String 表示一个String的对象数组;

寄存器与变量

android变量都是存放在寄存器中的,寄存器为32位,可以支持任何类型,其中long和double是64为的,需要使用两个寄存器保存。
寄存器采用v和p来命名,v表示本地寄存器,p表示参数寄存器。

例如:

//===================================================================
private void print(String string) {
    Log.d(TAG, string);
}
//===================================================================
.method private print(Ljava/lang/String;)V
    .registers 3
    .param p1, "string"    # Ljava/lang/String;

    .prologue
    .line 29
    const-string v0, "MainActivity"

    invoke-static {v0, p1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

    .line 30
    return-void
.end method
//===================================================================

.registers 3 说明该方法有三个寄存器,其中一个本地寄存器v0,两个参数寄存器p0,p1,细心的人可能会注意到没有看到p0,原因是p0存放的是this。如果是静态方法的话就只有2个寄存器了,不需要存this了。

基本指令

smali字节码是类似于汇编,如果有汇编基础,理解起来是非常容易的。
move v0, v3 把v3寄存器的值移动到寄存器v0上
const-string v0, “MainActivity” 把字符串”MainActivity”赋值给v0寄存器
invoke-super  调用父函数
return-void  函数返回void
new-instance  创建实例
iput-object  对象赋值
iget-object  调用对象
invoke-static  调用静态函数
invoke-direct  调用函数

例如:

//===================================================================
@Override
public void onClick(View view) {
    String str = "Hello World!";
    print(str);
}
//===================================================================
# virtual methods
# 参数类型为Landroid/view/View,返回类型为V
.method public onClick(Landroid/view/View;)V
    # 表示有三个寄存器
    .registers 3
    # 参数View类型的view变量对应的是寄存器p1
    .param p1, "view"    # Landroid/view/View;

    .prologue
    .line 24
    #将"Hello World!"字符串放到寄存器v0中
    const-string v0, "Hello World!"

    .line 25
    # 定义一个Ljava/lang/String类型的str变量对应本地寄存器v0
    .local v0, "str":Ljava/lang/String;
    # 调用该类的print方法,该方法的参数类型为Ljava/lang/String,返回值为V
    # 调用print方法传入的参数为{p0, v0},及print(p0, v0),p0为this,v0为"Hello World!"字符串
    invoke-direct {p0, v0}, Ltestdemo/hpp/cn/annotationtest/MainActivity;->print(Ljava/lang/String;)V

    .line 26
    return-void
.end method
//===================================================================

if判断语句

if判断一共有12条指令:

if-eq vA, VB, cond_** 如果vA等于vB则跳转到cond_**。相当于if (vA==vB)
if-ne vA, VB, cond_** 如果vA不等于vB则跳转到cond_**。相当于if (vA!=vB)
if-lt vA, VB, cond_** 如果vA小于vB则跳转到cond_**。相当于if (vA<vB)
if-le vA, VB, cond_** 如果vA小于等于vB则跳转到cond_**。相当于if (vA<=vB)
if-gt vA, VB, cond_** 如果vA大于vB则跳转到cond_**。相当于if (vA>vB)
if-ge vA, VB, cond_** 如果vA大于等于vB则跳转到cond_**。相当于if (vA>=vB)

if-eqz vA, :cond_** 如果vA等于0则跳转到:cond_** 相当于if (VA==0)
if-nez vA, :cond_** 如果vA不等于0则跳转到:cond_**相当于if (VA!=0)
if-ltz vA, :cond_** 如果vA小于0则跳转到:cond_**相当于if (VA<0)
if-lez vA, :cond_** 如果vA小于等于0则跳转到:cond_**相当于if (VA<=0)
if-gtz vA, :cond_** 如果vA大于0则跳转到:cond_**相当于if (VA>0)
if-gez vA, :cond_** 如果vA大于等于0则跳转到:cond_**相当于if (VA>=0)

循环语句

常用的循环结构有:迭代器循环,for循环,do while循环。

import java.util.*;

public class demo{
    public static void main(String[]args)
    {
        Scanner s=new Scanner(System.in);
        int[] arr=new int[5];
        for(int i=0;i<5;i++)
        {
            arr[i]=s.nextInt();
        }  
        for (int i:arr)
        {
            System.out.println(i);
        }

    }
}

.class public Ldemo;
.super Ljava/lang/Object;
.source "demo.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 7

    .prologue
    const/4 v5, 0x5

    const/4 v0, 0x0

    .line 6
    new-instance v2, Ljava/util/Scanner;

    sget-object v1, Ljava/lang/System;->in:Ljava/io/InputStream;

    invoke-direct {v2, v1}, Ljava/util/Scanner;-><init>(Ljava/io/InputStream;)V

    .line 7
    new-array v3, v5, [I

    move v1, v0

    .line 8
    :goto_c
    if-ge v1, v5, :cond_17

    .line 10
    invoke-virtual {v2}, Ljava/util/Scanner;->nextInt()I

    move-result v4

    aput v4, v3, v1

    .line 8
    add-int/lit8 v1, v1, 0x1

    goto :goto_c

    .line 12
    :cond_17
    array-length v1, v3

    :goto_18
    if-ge v0, v1, :cond_24

    aget v2, v3, v0

    .line 14
    sget-object v4, Ljava/lang/System;->out:Ljava/io/PrintStream;

    invoke-virtual {v4, v2}, Ljava/io/PrintStream;->println(I)V

    .line 12
    add-int/lit8 v0, v0, 0x1

    goto :goto_18

    .line 17
    :cond_24
    return-void
.end method

switch分支语句

1、case值递增的有规律 switch

private String packedSwitch(int i) {  
    String str = null;  
    switch (i) {  
        case 0:  
            str = "she is a baby";  
            break;  
        case 1:  
            str = "she is a girl";  
            break;  
        case 2:  
            str = "she is a woman";  
            break;  
        case 3:  
            str = "she is an obasan";  
            break;  
        default:  
            str = "she is a person";  
            break;  
    }  
    return str;  
}  

.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区域  
    .line 36  
    const-string v0, "she is a person"  #default分支  
    .line 39  
    :goto_0      #所有case的出口  
    return-object v0 #返回字符串v0  
    .line 24  
    :pswitch_0    #case 0  
    const-string v0, "she is a baby"  
    .line 25  
    goto :goto_0  #跳转到goto_0标号处  
    .line 27  
    :pswitch_1    #case 1  
    const-string v0, "she is a girl"  
    .line 28  
    goto :goto_0  #跳转到goto_0标号处  
    .line 30  
    :pswitch_2    #case 2  
    const-string v0, "she is a woman"  
    .line 31  
    goto :goto_0  #跳转到goto_0标号处  
    .line 33  
    :pswitch_3    #case 3  
    const-string v0, "she is an obasan"  
    .line 34  
    goto :goto_0  #跳转到goto_0标号处  
    .line 22  
    nop  
    :pswitch_data_0  
    .packed-switch 0x0    #case  区域,从0开始,依次递增  
        :pswitch_0  #case 0  
        :pswitch_1  #case 1  
        :pswitch_2  #case 2  
        :pswitch_3  #case 3  
    .end packed-switch  
.end method

2、无规律的switch

private String sparseSwitch(int age) {  
    String str = null;  
    switch (age) {  
        case 5:  
            str = "he is a baby";  
            break;  
        case 15:  
            str = "he is a student";  
            break;  
        case 35:  
            str = "he is a father";  
            break;  
        case 65:  
            str = "he is a grandpa";  
            break;  
        default:  
            str = "he is a person";  
            break;  
    }  
    return str;  
} 

.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区域  
    .line 58  
    const-string v0, "he is a person"  #case default  
    .line 61  
    :goto_0    #case 出口  
    return-object v0  #返回字符串  
    .line 46  
    :sswitch_0    #case 5  
    const-string v0, "he is a baby"  
    .line 47  
    goto :goto_0 #跳转到goto_0标号处  
    .line 49  
    :sswitch_1    #case 15  
    const-string v0, "he is a student"  
    .line 50  
    goto :goto_0 #跳转到goto_0标号处  
    .line 52  
    :sswitch_2    #case 35  
    const-string v0, "he is a father"  
    .line 53  
    goto :goto_0 #跳转到goto_0标号处  
    .line 55  
    :sswitch_3    #case 65  
    const-string v0, "he is a grandpa"  
    .line 56  
    goto :goto_0 #跳转到goto_0标号处  
    .line 44  
    nop  
    :sswitch_data_0  
    .sparse-switch            #case 区域  
        0x5 -> :sswitch_0     #case 5(0x5)  
        0xf -> :sswitch_1     #case 15(0xf)  
        0x23 -> :sswitch_2    #case 35(0x23)  
        0x41 -> :sswitch_3    #case 65(0x41)  
    .end sparse-switch  
.end method 

try/catch语句

private void throw2() {
    try {
        throw new Exception("test throw runtime exception");
    } catch (Exception e) {
        e.printStackTrace();
    }
}

.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

头信息——类的主体信息

在打开smali文件的时候,它的头三行描述了当前类的一些信息。
.class <访问权限> [关键修饰字] <类名>;
.super <父类名>;
.source <源文件名>

例如:

//===================================================================
public class MainActivity extends AppCompatActivity {
    // ......
}
//===================================================================
.class public Ltestdemo/hpp/cn/test/MainActivity;
.super Landroid/support/v7/app/AppCompatActivity;
.source "MainActivity.java"
//===================================================================

.class指令表示当前的类名,类的访问权限是public,类名为Ltestdemo/hpp/cn/test/MainActivity,类开头的L是遵循Dalvik字节码的相关约定,表示后面跟随的字符串是一个类。

.super指定了当前类所继承的父类,后面指的就是这个父类的类名,L表示后面跟的字符串是一个类

.source指定了当前类的源文件名

注意:经过混淆的dex文件,反编译出来的smali代码可能没有源文件信息,因此source行的代码可能为空。

这三行就是类的主体部分了,另外一个类是由多个字段或者方法组成。

接口

如果一个类实现了一个接口,那么会在smali文件中使用.implements指令指出。

#interfaces
.implements <接口名>

同样,#interfaces是注释,.implements是接口关键字。

例如:

//===================================================================
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    // ......
}
//===================================================================
# interfaces
.implements Landroid/view/View$OnClickListener;
//===================================================================

字段

smali文件中,字段的声明使用.field指令,字段分为静态字段和实例字段。

1 、静态字段

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

可以看到,baksmali在生成smali文件时,会在静态字段声明的起始处添加注释”static fields”,注释是以#开头。

访问权限包括:private、protected、public(三者之一)
修饰关键字为字段的其他属性,例如,final
字段名和类型就不用解释了

例如:

//===================================================================
private  static final String TAG = "MainActivity";
//===================================================================
# static fields
.field private static final TAG:Ljava/lang/String; = "MainActivity"
//===================================================================

2、 实例字段

相比于静态自动就少了一个static的静态声明而已,其他都一样。

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

例如:

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

方法

smali的方法声明使用的.method指令,方法分为直接方法和虚方法两种。

1、直接方法
直接方法指的是该类中定义的方法。

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

#direct methods是注释,是baksmali添加的,访问权限和修饰关键字跟字段是一样的。
方法原型描述了方法的名称、参数与返回值。
.registers 指令指定了方法中寄存器的总数,这个数量是参数和本地变量总和。
.param表明了方法的参数,每个.param指令表示一个参数,方法使用了几个参数就有几个.parameter指令。
.prologue指定了代码的开始处,混淆过的代码可能去掉了该指令。
.line指明了该处代码在源代码中的行号,同样,混淆后的代码可能去掉了行号。
.local 使用这个指定表明方法中非参寄存器

//===================================================================
private void print(String string) {
    Log.d(TAG, string);
}
//===================================================================
.method private print(Ljava/lang/String;)V
    .registers 3
    .param p1, "string"    # Ljava/lang/String;

    .prologue
    .line 29
    const-string v0, "MainActivity"

    invoke-static {v0, p1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

    .line 30
    return-void
.end method
//===================================================================

2、虚方法
虚方法指的是从父类中继承的方法或者实现的接口的方法,它的声明跟直接方法相同,只是起始的初始为virtual methods

//===================================================================
@Override
public void onClick(View view) {
    String str = "Hello World!";
    print(str);
}
//===================================================================
# virtual methods
.method public onClick(Landroid/view/View;)V
    .registers 3
    .param p1, "view"    # Landroid/view/View;

    .prologue
    .line 24
    const-string v0, "Hello World!"

    .line 25
    .local v0, "str":Ljava/lang/String;
    invoke-direct {p0, v0}, Ltestdemo/hpp/cn/annotationtest/MainActivity;->print(Ljava/lang/String;)V

    .line 26
    return-void
.end method
//===================================================================

3、静态方法

//===================================================================
public static void setTag(String str) {
    TAG = str;
}
//===================================================================
.method public static setTag(Ljava/lang/String;)V
    .registers 1
    .param p0, "str"    # Ljava/lang/String;

    .prologue
    .line 64
    sput-object p0, Ltestdemo/hpp/cn/annotationtest/MainActivity;->TAG:Ljava/lang/String;

    .line 65
    return-void
.end method
//===================================================================

注解

如果一个类使用了注解,那么smali中会使用.annotation指令。

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

注解的作用范围可以是类、方法或者字段。如果注解的作用范围是类,.annotation指令会直接定义在smali文件中,如果是方法或者字段,.annotation指令则会包含在方法或者字段的定义中。

1、 注解类

//===================================================================
@BindInt(100)
public class MainActivity extends AppCompatActivity {

}
//===================================================================
# annotations
.annotation build Ltestdemo/hpp/cn/annotationtest/BindInt;
    value = 0x64
.end annotation
//===================================================================

2、 注解字段

//===================================================================
@BindView(R.id.button)
public Button mButton;
//===================================================================
# instance fields
.field public mButton:Landroid/widget/Button;
    .annotation build Lbutterknife/BindView;
        value = 0x7f0c0050
    .end annotation
.end field
//===================================================================

3、 注解方法

//===================================================================
@OnClick(R.id.button)
public void click() {
    String str = "Hello World!";
    print(str);
}
//===================================================================
# virtual methods
.method public click()V
    .registers 2
    .annotation build Lbutterknife/OnClick;
        value = {
            0x7f0c0050
        }
    .end annotation

    .prologue
    .line 29
    const-string v0, "Hello World!"

    .line 30
    .local v0, "str":Ljava/lang/String;
    invoke-direct {p0, v0}, Ltestdemo/hpp/cn/annotationtest/MainActivity;->print(Ljava/lang/String;)V

    .line 31
    return-void
.end method
//===================================================================

smali插桩

插桩的原理就是静态的修改apk的samli文件,然后重新打包。

1、使用上面的方法得到一个apk的smali文件

2、在关键部位添加自己的代码,需要遵循smili语法,例如在关键地方打log,输出关键信息

3、重新进行打包签名

具体例子参考文章

代码安全,防解密

完全避免破解是不可能的,尽最大可能提高破解成本。

  • 混淆代码。代码混淆后,Smali更加晦涩难懂,逻辑也更难掌握。
  • 解读汇编比解读Smali难度大的多得多。重要的逻辑可以放到C/C++层去处理就不要放在Java层上去处理。
  • 多用连续调用的方式。这样出来的效果是Java只有一行,Smali可能有好几十行,增加查看难度。在一些关键的点上,比如支付,多绕一下。不要直接在Java内用中文显示标注等

### 参考链接