Smali语法小记
介绍
在执行 Android Java 层的代码时,其实就是 Dalvik(ART) 虚拟机(使用 C 或 C++ 代码实现)在解析 Dalvik 字节码,从而模拟程序的执行过程。
自然,Dalvik 字节码晦涩难懂,研究人员们给出了 Dalvik 字节码的一种助记方式:smali 语法。通过一些工具(如 apktool),我们可以把已有的 dex 文件转化为若干个 smali 文件(一般而言,一个 smali 文件对应着一个类),然后进行阅读。对于不同的工具来说,其转换后的 smali 代码一般都不一样,毕竟这个语法不是官方的标准。
下文说明
<> 中的内容必须存在,[] 的内容是可选的
结构
- 声明语句
- 执行语句
声明语句
声明语句一般都是以 . 开始
寄存器
Dalvik 最多支持 65536 个寄存器 (编号从 0~65535),但是 ARM 架构的 cpu 中只有 37 个寄存器。那 Dalvik 是怎么做的呢?其实,每个 Dalvik 虚拟机维护了一个调用栈,该调用栈用来支持虚拟寄存器和真实寄存器相互映射的。
寄存器命名规则
- v命名
-
- 局部变量 v0 ~ vm-1
-
- 函数参数vm ~ vm+n
- P命名
-
- 局部变量 v0 ~ vm-1
-
- 函数参数pm ~ pm+n
变量
java | smali |
---|---|
boolean | Z |
byte | B |
short | S |
char | C |
int | I |
long | L |
float | F |
double | D |
void | V |
object | L |
array | [ |
对象类型
对象类型可以表示 Java 代码中的所有类
例如:
- Lpackage/name/ObjectName;
- Ljava/lang/String;
数组
比如说 int 数组 int [] 在 smali 中的表示形式为 [I 。
比如说数组类型 String[][] 在 smali 中的表示形式为 [[Ljava/lang/String;
字段声明
#instance fields
.field <访问权限修饰符> [非权限修饰符] <字段名>:<字段类型>
访问权限修饰符
- public
- private
- protected
非权限修饰符
- final
- volidate
- transient
综上举例:
- private java.lang.String str1; == .field private str1:Ljava/lang/String;
- public static java.lang.String str2; == .field public static str2:Ljava/lang/String;
方法
# 描述方法类型
.method <访问权限修饰符> [修饰符] <方法原型>
<.locals>(.locals 会指定方法使用的局部变量。)
[.parameter](参数就正常用 p0 开始)
[.prologue](程序的开始处)
[.line](第几行)
<代码逻辑>
[.line]
<代码逻辑>
.end
方法类型
一般是反编译工具加上去的
- 直接方法,direct method
- 虚方法,virtual method
方法原型
方法原型一般为方法名(参数类型描述符)返回值类型描述符 。
类
.class <访问权限修饰符> [非权限修饰符] <类名>
.super <父类名>
.source <源文件名称>
例如:
.class public final Lcom/a/b/c;
.super Ljava/lang/Object;
.source "Demo.java"
类的引用
this$[层数]
例如
public class MainActivity extends Activity { //this$0
public class firstinner //this$1
{
public class secondinner //this$2
{
public class thirdinner //this$3
{
}
}
}
}
接口
#interfaces
.implements <接口名称>
注解
#annotations
.annotation [注解的属性] <注解范围>
[注解字段=值]
...
.end
位描述元素
- 一个 op,8 位指令码
- 若干个字符,每一个字符表示 4 位
- 若干个 | ,进行分割,方便阅读。
- 若干个 ∅同样也是 4 个字符,表示该部分位为 0。
例如:
指令 B|A|op CCCC 包含 2 个 word,一共 32 位。其中,第一个字的低 8 位是操作码,中间 4 位是 A,高 4 位是 B。第二个字是单独的 16 位的数值
格式ID
指令格式,根据 ID 的不同,仍然可以表示不同的指令含义
- 第一个数字表示 word 的数量
- 第二个数字的话表示指令包含的寄存器的最大数量
- 第二个如果是r 的话,表示使用了一定范围内的寄存器 (range)。
- 第三个字符表示指令使用到的额外数据的类型
- 第四个s表示静态链接
- 第四个i表示内联
指令句语法
- 指令以操作码 op 开始
- op后面跟参数
- 参数间以逗号分隔。
- 参数Vx表示寄存器,如 v0、v1 等。
- 参数 #+X 表示常量数字。
- 参数 +X 表示相对指令的地址偏移。
- 参数 kind@X 表示常量池索引值,其中 kind 表示常量池类型,可以是string type field meth
例如:
指令 op vAA, type@BBBB 为例,指令使用了 1 个寄存器 vAA,一个 32 位的类型常量池索引。
指令后缀
- 32 位运算不标记。
- 64 位运算以 -wide 为后缀。
- 特定类型的运算码以其类型(或简单缩写)为后缀,这些类型包括-boolean、-byte、-char、-short、-int、-long、-float、-double、-object、-string、-class 和 -void。
空指令
nop 无任何操作
数据定义指令
主要就是const
举例
byte b = 1; ==》 const/4 v0, 0x1
short s = 2;==》const/4 v8, 0x2
String str = "test"; ==》 const-string v9, "test"
Class c = Object.class; const-class v1, Ljava/lang/Object;
数据移动指令
主要就是move
- move、move-result 用于处理小于等于 32 位的基本类型。
- move-wide、move-result-wide用于处理 64 位类型
- move-object系列指令和move-result-object用于处理对象
- 后缀(/from16、/16)只影响字节码的位数和寄存器的范围
数据转换指令
明显的两种类型的单词组合
举个例子int-to-short v0,v1 即将寄存器 v1 的值强制转换为 short 类型,并放入 v0 中。
运算指令
运算类型如下
类型 | 说明 |
---|---|
add-type | vBB + vCC |
sub-type | vBB - vCC |
mul-type | vBB * vCC |
div-type | vBB / vCC |
rem-type | vBB % vCC |
and-type | vBB & vCC |
or-type | vBB | vCC |
xor-type | vBB ^ vCC |
shl-type | vBB << vCC |
shr-type | vBB >> vCC |
ushr-type | vBB >>> vCC无符号 |
- type 可以是 - int,-long, -float,-double。
举例:
int a = 5, b = 2;
a += b;
a -= b;
a *= b;
a /= b;
a %= b;
a &= b;
a |= b;
a ^= b;
a <<= b;
a >>= b;
a >>>= b;
smali
const/4 v0, 0x5
const/4 v1, 0x2
add-int/2addr v0, v1
sub-int/2addr v0, v1
mul-int/2addr v0, v1
div-int/2addr v0, v1
rem-int/2addr v0, v1
and-int/2addr v0, v1
or-int/2addr v0, v1
xor-int/2addr v0, v1
shl-int/2addr v0, v1
shr-int/2addr v0, v1
ushr-int/2addr v0, v1
数组操作
指令 | 说明 |
---|---|
array-length vA, vB | 获取给定 vB 寄存器中数组的长度并赋给 vA 寄存器 |
new-array vA, vB, type@CCCC | 新建数组 |
new-array/jumbo vAAAA, vBBBB,type@CCCCCCCC | 新建数组范围更大 |
filled-new-array {vC, vD, vE, vF, vG},type@BBBB | 新建数组指定内容 |
filled-new-array/range {vCCCC …vNNNN}, type@BBBB | 新建数组指定范围 |
fill-array-data vAA, +BBBBBBBB | 填充数组 |
例如:
int[] arr = new int[10];
int[] arr = {1, 2, 3, 4, 5};
==
const/4 v1, 0xa
new-array v0, v1, I
const/4 v1, 0x1
const/4 v2, 0x2
const/4 v3, 0x3
const/4 v4, 0x4
const/4 v5, 0x5
filled-new-array {v1, v2, v3, v4, v5}, I
move-result v0
寄存器若连续,则可改为
filled-new-array {v1...v5}, I
实例操作
指令 | 说明 |
---|---|
check-cast vAA, type@BBBB、check-cast/jumbo vAAAA, type@BBBBBBBB | 转成B |
instance-of vA, vB, type@CCCC、instance-of/jumbo vAAAA, vBBBB, type@CCCCCCCC | if能转成B,vA=1 else vA=0 |
new-instance vAA, type@BBBB、new-instance/jumbo vAAAA, type@BBBBBBBB | 构造新实例 |
例如:
Object obj = new Object();
String s = "test";
boolean b = s instanceof String;
String s = "test";
Object o = (Object)s;
==
new-instance v0, Ljava/lang/Object;
invoke-direct-empty {v0}, Ljava/lang/Object;-><init>()V
const-string v0, "test"
instance-of v1, v0, Ljava/lang/String;
const-string v0, "test"
check-cast v0, Ljava/lang/Object;
move-object v1, v0
字段操作指令
字段操作指令主要是对实例的字段进行读写操作
- 读=get
- 写=put
- 普通字段+i
- 静态字段+s
例如:iget,iget-wide,iget-object,iget-boolean,iget-byte,iget-char,iget-short
例如:
int[] arr = new int[2];
int b = arr[0];
arr[1] = b;
==
const/4 v0, 0x2
new-array v1, v0, I
const/4 v0, 0x0
aget-int v2, v1, v0
const/4 v0, 0x1
aput-int v2, v1, v0
比较指令
cmp(l/g)-kind(kind可以选float、long、double) vAA(结果放这里), vBB, vCC
如果 vBB 寄存器大于 vCC 寄存器,结果为 - 1,相等则结果为 0,小于的话结果为 1
跳转指令
- goto,无条件跳转
- switch,分支跳转
-
- packed-switch vAA,+BBBBBBBB 规律递增跳转
-
- sparse-switch vAA,+BBBBBBBB 无规律递增跳转
- if,条件跳转
-
- if-eq vA,vB,target 如果 vA=vB,跳转。
-
- if-ne vA,vB,target 如果 vA!=vB,跳转。
-
- if-lt vA,vB,target 如果 vA<vB,跳转。
-
- if-gt vA,vB,target 如果 vA>vB,跳转。
-
- if-ge vA,vB,target 如果 vA>=vB,跳转。
-
- if-le vA,vB,target 如果 vA<=vB,跳转。
-
- if-eqz vAA,target 如果 vA=0,跳转(加z则是和0比较)
例如
int a = 10
if(a > 0)
a = 1;
else
a = 0;
==
const/4 v0, 0xa
if-lez v0, :cond_0 # if 块开始
const/4 v0, 0x1
goto :cond_1 # if 块结束
:cond_0 # else 块开始
const/4 v0, 0x0
:cond_1 # else 块结束
锁指令
- monitor-enter vAA 为指定的对象获取锁
- monitor-exit vAA 释放指定的对象的锁
方法调用指令
- invoke-kind {vC, vD, vE, vF, vG},meth@BBBB
- invoke-kind/range {vCCCC … vNNNN},meth@BBBB 两类
kind类型:
- invoke-direct 直接调用
- invoke-super 调父类
- invoke-virtual 虚方法(对于protected或者public方法都叫做虚方法。)
- invoke-static 静态
- invoke-interface 接口
异常指令
try-catch
int a = 10;
try {
callSomeMethod();
} catch (Exception e) {
a = 0;
}
callAnotherMethod();
==
const/16 v0, 0xa
:try_start_0 # try 块开始
invoke-direct {p0}, Lnet/flygon/myapplication/SubActivity;->callSomeMethod()V
:try_end_0 # try 块结束
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0
:goto_0
invoke-direct {p0}, Lnet/flygon/myapplication/SubActivity;->callAnotherMethod()V
return-void
:catch_0 # catch 块开始
move-exception v1
const/4 v0, 0x0
goto :goto_0 # catch 块结束
返回指令
return-kind