Smail语法

前言

Dalvik字节码Dalvik是google专门为android操作系统设计的一个虚拟机,经过深度的优化。虽然android上的程序是使用java来开发的,但是Dalvik和标准的java虚拟机JVM还是两回事。Dalvik VM是基于寄存器的,而JVM是基于栈的;Dalvik有专属的文件执行格式dex(dalvik executable),而JVM则执行的是java字节码。Dalvik VM比JVM速度更快,占用空间更少。

通过Dalvik的字节码我们不能直接看到原来的逻辑代码,这时需要借助如Apktool或dex2jar+jd-gui工具来帮助查看。但是,注意的是最终我们修改APK需要操作的文件是.smali文件,而不是导出来的Java文件重新编译(况且这基本上不可能)。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.class public Lcom/disney/WMW/WMWActivity;
.super Lcom/disney/common/BaseActivity;
.source "WMWActivity.java"
# interfaces
.implements Lcom/burstly/lib/ui/IBurstlyAdListener;
# annotations
.annotation system Ldalvik/annotation/MemberClasses;
value = {
Lcom/disney/WMW/WMWActivity$MessageHandler;,
Lcom/disney/WMW/WMWActivity$FinishActivityArgs;
}
.end annotation

1-3行定义的是基本信息:这是一个由WMWActivity.java编译得到的smali文件(第3行),它是com.disney.WMW这个package下的一个类(第1行),继承自com.disney.common.BaseActivity(第2行)。 5-6行定义的是接口信息:这个WMWActivity实现了一个com.burstly.lib.ui这个package下(一个广告SDK)的IBurstyAdListener接口。
8-14行定义的则是内部类:它有两个成员内部类——MessageHandler和FinishActivityArgs,内部类将在后面小节中会有提及。

1
2
3
4
5
6
7
8
9
class WMWActivity extends BaseActivity implements IBurstlyAdListener{
//...
class MessageHandler {
//...
}
class FinishActivityArgs{
//...
}
}

一. 数据类型

原始类型:

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

引用类型:

对象 和 数组

对象:L + 包名.对象

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

数组:[ + 数据类型

数组的表示方式是:在基本类型前加上前中括号“[”,例如int数组和float数组分别表示为:[I、[F,
[I表示一个整型一维数组,相当于java中的int[]。 对于多维数组,只要增加[就行了。[[I相当于int[][],[[[I相当于int[][][]。注意每一维的最多255个对象数组的表示:[Ljava/lang/String;表示一个String对象数组。

二. 方法

方法定义:

表示形式:Lpackage/name/ObjectName;->MethodName(III)Z

  • Lpackage/name/ObjectName;表示类型,
  • MethodName是方法名。
  • III为参数(在此是3个整型参数),
  • Z是返回类型(bool型)。

方法的参数是一个接一个的,中间没有隔开。一个更复杂的例子:

method(I[[IILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;

在java中则为:String method(int, int[][], int, String, Object[])

内部类:

既然类是用LpackageName/objectName;来表示,那类里面的内部类又如何在smali中引用呢?答案是:LpackageName/objectName$subObjectName;。也就是在内部类前加“$”符号

字段:

表示形式: Lpackage/name/ObjectName;->FieldName:Ljava/lang/String; 即包名,字段名和各字段类型。

标记说明:

# static fields             定义静态变量的标记
# instance fields        定义实例变量的标记

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

sget-object v0, Lcom/disney/WMW/WMWActivity;->PREFS_INSTALLATION_ID:Ljava/lang/String;

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

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

iget-object v0, p0, Lcom/disney/WMW/WMWActivity;->_view:Lcom/disney/common/WMWView;

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

获取array的还有aget和aget-object,指令使用和上述类似,不细述。

put指令的使用和get指令是统一的,直接看例子不解释:

const/4 v3, 0x0
sput-object v3, Lcom/disney/WMW/WMWActivity;->globalIapHandler:Lcom/disney/config/GlobalPurchaseHandler;

相当于:this.globalIapHandler = null;(null = 0x0)

# direct methods       定义静态方法的标记
# virtual methods      定义非静态方法的标记

直白地讲,direct method就是private函数,其余的public和protected函数都属于virtual method。所以在调用函数时,有invoke-direct,invoke-virtual,另外还有invoke-static、invoke-super以及invoke-interface等几种不同的指令。当然其实还有invoke-XXX/range 指令的,这是参数多于4个的时候调用的指令,比较少见,了解下即可。

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

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

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

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

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.

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参数就可以了。

语法:If语句

if-eq p1, v0, :cond_8 表示如果p1和v0相等,则执行cond_8的流程:
    :cond_8
    invoke-direct {p0}, Lcom/paul/test/a;->d()V

调用com.paul.test.a的d()方法

if-ne p1, v0, :cond_b 表示不相等则执行cond_b的流程:
    :cond_b
    const/4 v0, 0x0
    invoke-virtual {p0, v0}, Lcom/paul/test/a;->setPressed(Z)V
    invoke-super {p0, p1, p2}, Landroid/view/View;->onKeyUp(ILandroid/view/KeyEvent;)Z
    move-result v0

大概意思就是调用com.paul.test.a的setPressed方法,然后再调用父类View的onKeyUp方法,最后 return v0

三. 寄存器(重点)

概念:

在dalvik字节码中,寄存器都是32位的,能够支持任何类型。64位类型(Long和Double型)用2个寄存器表示。有两种方式指定一个方法中有多少寄存器是可用的。registers指令指定了方法中寄存器的总数。 [注意:一个方法内,不允许超过15个;如果超过15个,需要做特殊处理,处理方法找google大神,度娘],locals指令表明了方法中非参寄存器的数量。

命名方式

在smali里的所有操作都必须经过寄存器来进行:本地寄存器用v开头数字结尾的符号来表示,如v0、v1、v2、…参数寄存器则使用p开头数字结尾的符号来表示,如p0、p1、p2、…特别注意的是,p0不一定是函数中的第一个参数,在非static函数中,p0代指“this”,p1表示函数的第一个参数,p2代表函数中的第二个参数…而在static函数中p0才对应第一个参数(因为Java的static方法中没有this方法)。本地寄存器没有限制,理论上是可以任意使用的。

const/4 v0, 0x0
iput-boolean v0, p0, Lcom/disney/WMW/WMWActivity;->isRunning:Z

在上面的两句中,使用了v0本地寄存器,并把值0x0存到v0中,然后第二句用iput-boolean这个指令把v0中的值存放到com.disney.WMW.WMWActivity.isRunning这个成员变量中。即相当于:this.isRunning = false;(上面说过,在非static函数中p0代表的是“this”,在这里就是com.disney.WMW.WMWActivity实例)。

Long/Double值:Long和double类型是64位的,需要2个寄存器(切记切记)。
例如,对于非静态方法LMyObject;->MyMethod(IJZ)V,参数分别是LMyObject;,int,long,boolean。故该方法需要5个寄存器来存储参数。

p0  this
p1  I
p2,p3   J
p4  Z

指令

1.If语句

if-nez v0,:cond_0    如果结果不为0,就跳转到cond_0标号处
if-eqz v0,:cond_1    如果结果为0,就跳转到cond_1标号处    

2.数据操作指令

move v0,v1    将v1的值赋给v0 ,两个寄存器都为4位

move-result v0    将上一个invoke类型指令操作的单字非对象结果赋给v0
move-result-object v0    将上一个invoke类型指令操作的对象结果(返回值)赋给v0    

3.返回指令

return-void
return v0
return-object v0    

4.实例操作指令

check-cast v1, Landroid/widget/TextView;  将v1寄存器中的对象引用转化成指定的类型(这里是 TextView)
new-instance v1, Ljava/lang/StringBuilder; 构造一个指定类型对象的新实例    

5.数组操作指令

new-array v0,v0,[I  构造Int类型,大小是v0的数组,并将值赋给v0寄存器
array-length v1,v0  获取v0数组的长度且将值赋给v1    

6.异常指令

throw vAA 抛出vAA寄存器中指定类型的异常    

7.跳转指令

1,goto :goto_0  偏移量goto_0不能为0 
2,If语句
3,switch    
    packed-switch v0, :pswitch_data_0  v0是switch需要判断的值     :pswitch_data_0 偏移表,表中值是有规律递增的。
    sparse-switch v0, :pswitch_data_0  偏移表中值是无规律的。

8,比较指令

cmpl-float v0,v2,v3     比较v2和v3,如果v2>v3,v0=-1;反之,v0=1;相等,v0=0
cmpg-float v0,v2,v3  比较v2和v3,如果v2>v3,v0=1;反之,v0=-1;相等,v0=0
cmpl-double v0,v2,v3  同理
cmpg-double v0,v2,v3  同理
cmp-long v0,v2,v3    比较v2和v3,如果v2>v3,v0=1;反之,v0=-1;相等,v0=0

在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()返回的整型。

smali中函数实体分析

下面开始介绍函数实体,其实没有什么特别的地方,只是在植入代码时有一点需要特别注意,举例说明:

.method protected onDestroy()V     
.locals 0     

.prologue     
.line 277     
invoke-super {p0}, Lcom/disney/common/BaseActivity;->onDestroy()V     

.line 279     
return-void     
.end method

这是onDestroy()函数,它的作用大家都知道。首先看到函数内第一句:.locals 0,这句话很重要,标明了你在这个函数中最少要用到的本地寄存器的个数。在这里,由于只需要调用一个父类的onDestroy()处理,所以只需要用到p0,所以使用到的本地寄存器数为0。如果不清楚这个规则,很容易在植入代码后忘记修改.local 的值,那么回编译后运行时将会得到一个VerifyError错误,而且极难发现问题所在。例如往onDestroy()增加一句:this.existed = true;那么应该改为(注意修改.local的值为1——使用到了v0这一个本地寄存器):

.method protected onDestroy()V
.locals 1
.prologue
.line 277
const/4 v0, 0x1
iput-boolean v0, p0, Lcom/disney/WMW/WMWActivity;->exited:Z
invoke-super {p0}, Lcom/disney/common/BaseActivity;->onDestroy()V
.line 279
return-void
.end method

.line 这个标识,它是标注了该代码在原Java文件中的行数,它也很有用,想想使用IDE开发时,遇到错误崩溃时,在catLog不是有提示哪个文件哪一行崩溃的么?Dalvik VM运行到.line XX时就将这个值存起来,如果在这一行运行时出错了,就往catLog输出这个值,这样我们就能看到具体是哪一行的问题了。jd-gui这个工具也是通过分析这些信息将smali代码还原成我们喜闻乐见的Java代码的。当然,它不是必须的,去掉也没有关系,只不过为了方便调试还是保留一下吧。
以上一些smali语法规则可以点击参照
http://code.google.com/p/smali/wiki/TypesMethodsAndFields

.class public LHelloWorld;
#Ye olde hello world application
#To assemble and run this on a phone or emulator:
#s
#java -jar smali.jar -o classes.dex HelloWorld.smali
#zip HelloWorld.zip classes.dex
#adb push HelloWorld.zip /data/local
#adb shell dalvikvm -cp /data/local/HelloWorld.zip HelloWorld
#
#if you get out of memory type errors when running smali.jar, try
#java -Xmx512m -jar smali.jar HelloWorld.smali
#instead
.super Ljava/lang/Object;
.method public static main([Ljava/lang/String;)V
    .registers 2
    sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
    const-string    v1, "Hello World!"
    invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    return-void
.end method

参考链接:官方语法链接,语法,详解,静态分析

文章目录
  1. 1. 前言
  2. 2. 例子
  3. 3. 一. 数据类型
    1. 3.1. 原始类型:
    2. 3.2. 引用类型:
  4. 4. 二. 方法
    1. 4.1. 方法定义:
    2. 4.2. 内部类:
    3. 4.3. 字段:
    4. 4.4. 标记说明:
    5. 4.5. 语法:If语句
  5. 5. 三. 寄存器(重点)
    1. 5.1. 概念:
    2. 5.2. 命名方式
  6. 6. 指令
    1. 6.1. smali中函数实体分析
,