Android逆向工程:Smali语法解析完整指南
完整Smali语法指南 & 一些个人理解
参考来源:CTF Wiki - Smali
目录
- 1. Smali基础介绍
- 2. 数据类型系统
- 3. 字节码语法
- 4. 寄存器概念
- 5. 方法语法结构
- 6. 字段语法结构
- 7. 指令语法详解
- 8. 完整代码示例
- 9. 编译与运行
- 10. 实战技巧
1. Smali基础介绍
1.1 什么是Smali
Smali 是 Android 应用程序反编译后的一种中间表示形式:
- dex文件 → baksmali工具 → smali文件(人类可读)
backSmail 工具按照一定格式解析的dex二进制文件,再转成的smail格式文件~ - smali文件 → smali工具 → dex文件(虚拟机执行)
2. 数据类型系统
2.1 基础数据类型
Smali标识 | Java类型 | 描述 | 示例 |
---|---|---|---|
V | void | 空类型 | 方法返回值 |
Z | boolean | 布尔类型 | true/false |
B | byte | 8位字节 | -128~127 |
S | short | 16位短整型 | -32768~32767 |
C | char | 16位字符 | Unicode字符 |
I | int | 32位整型 | 标准整数 |
J | long | 64位长整型 | 大整数 |
F | float | 32位浮点 | 单精度小数 |
D | double | 64位浮点 | 双精度小数 |
2.2 对象类型
格式 | 说明 | Java等价 | Smali示例 |
---|---|---|---|
Lpackage/Class; | 完整类名 | package.Class | Ljava/lang/String; |
[type | 数组类型 | type[] | [I (int数组) |
[[type | 二维数组 | type[][] | [[Ljava/lang/String; |
2.3 方法签名格式
方法名(参数类型...)返回类型
示例对照:
// Java方法
public String test(int a, boolean b) { ... }
# Smali签名
test(IZ)Ljava/lang/String;
3. 字节码语法
3.1 字段语法
.field [访问修饰符] [字段名]:[类型]
示例:
.field private TAG:Ljava/lang/String;
.field private running:Z
3.2 方法语法
.method [访问修饰符] [方法名](参数类型)返回类型[.registers N] # 寄存器声明[.parameter "参数名"] # 参数注释[.locals N] # 本地变量声明.prologue # 方法开始标记[.line 行号] # 源码行号对应# 方法体指令[指令 寄存器, 参数...][return指令] # 返回语句
.end method
示例:
.method public constructor <init>()V.locals 1.prologue.line 8# 调用Activity中的init()方法invoke-direct {p0}, Landroid/app/Activity;-><init>()V.line 10const-string v0, "MainActivity"iput-object v0, p0, Lcom/social_touch/demo/MainActivity;->TAG:Ljava/lang/String;.line 13const/4 v0, 0x0iput-boolean v0, p0, Lcom/social_touch/demo/MainActivity;->running:Zreturn-void
.end method
4. 寄存器概念
4.1 寄存器声明
.registers N # 声明使用N个寄存器(v0 ~ vN-1)
.locals M # 声明M个本地变量寄存器
4.2 寄存器类型
本地寄存器 (v0-vN)
.registers 4 # 可用寄存器:v0, v1, v2, v3
const/4 v0, 0x5 # v0 = 5
const/4 v1, 0x3 # v1 = 3
add-int v2, v0, v1 # v2 = v0 + v1 = 8
参数寄存器 (p0-pN)
静态方法:
# Java: public static void test(int a, String b)
.method public static test(ILjava/lang/String;)V.registers 3# p0 = 第一参数(int a)# p1 = 第二参数(String b)
.end method
实例方法:
# Java: public void test(int a, String b)
.method public test(ILjava/lang/String;)V.registers 4# p0 = this (当前对象)# p1 = 第一参数(int a) # p2 = 第二参数(String b)
.end method
5. 方法语法结构
5.1 方法声明语法
.method [访问修饰符] [方法名](参数类型)返回类型[.registers N] # 寄存器声明[.parameter "参数名"] # 参数注释[.locals N] # 本地变量声明.prologue # 方法开始标记[.line 行号] # 源码行号对应# 方法体指令[指令 寄存器, 参数...][return指令] # 返回语句
.end method
5.2 访问修饰符
修饰符 | 说明 | 示例 |
---|---|---|
public | 公开访问 | .method public test()V |
private | 私有访问 | .method private test()V |
protected | 受保护访问 | .method protected test()V |
static | 静态方法 | .method public static test()V |
final | 最终方法 | .method public final test()V |
abstract | 抽象方法 | .method public abstract test()V |
synchronized | 同步方法 | .method public synchronized test()V |
5.3 特殊方法
方法名 | 作用 | Java等价 |
---|---|---|
<init> | 构造方法 | public ClassName() { ... } |
<clinit> | 静态初始化块 | static { ... } |
6. 字段语法结构
6.1 字段声明语法
.field [访问修饰符] [字段名]:[类型]
示例:
.field private TAG:Ljava/lang/String;
.field private running:Z
6.2 访问修饰符
修饰符 | 说明 | 示例 |
---|---|---|
public | 公开访问 | .field public TAG:Ljava/lang/String; |
private | 私有访问 | .field private TAG:Ljava/lang/String; |
protected | 受保护访问 | .field protected TAG:Ljava/lang/String; |
static | 静态字段 | .field public static TAG:Ljava/lang/String; |
final | 最终字段 | .field public final TAG:Ljava/lang/String; |
7. 指令语法详解
7.1 常量指令
整数常量
const/4 vAA, #+B # 4位整数 (-8~7)
const/16 vAA, #+BBBB # 16位整数 (-32768~32767)
const vAA, #+BBBBBBBB # 32位整数
const/high16 vAA, #+BBBB0000 # 高16位# 示例
const/4 v0, 0x5 # v0 = 5
const/16 v1, 0x1000 # v1 = 4096
const v2, 0x12345678 # v2 = 0x12345678
字符串常量
const-string vAA, "string" # 字符串常量
const-class vAA, Ltype; # 类对象常量# 示例
const-string v0, "Hello World"
const-class v1, Ljava/lang/String;
7.1.1 const/high16 指令深度解析
7.1.1.1 什么是"高位"和"低位"?
想象一个32位数字 0x12345678:
高位(High Bits):值较大的部分 → 0x1234(前16位)
低位(Low Bits):值较小的部分 → 0x5678(后16位)高16位 低16位┌────────┬────────────┐│ 0x1234 │ 0x5678 │ ← 32位数值└────────┴────────────┘▲ ▲MSB LSB
更直观的字节视图:
高位 低位
┌────┬────┬────┬────┐
│ 12 │ 34 │ 56 │ 78 │ ← 字节序列
└────┴────┴────┴────┘
▲ ▲
MSB LSB (最高位 ←→ 最低位)
7.1.1.2 字节序(Endianness):数据在内存中的存储顺序
1. 小端序(Little-Endian) → Android/Intel 使用
规则:低位字节在前(低地址),高位字节在后(高地址)
示例:0x12345678 在内存中的存储:
内存地址: 0x1000 0x1001 0x1002 0x1003┌─────┬─────┬─────┬─────┐
存储内容: │ 78 │ 56 │ 34 │ 12 │ ← 低位到高位└─────┴─────┴─────┴─────┘
2. 大端序(Big-Endian) → 网络传输/部分嵌入式系统使用
规则:高位字节在前(低地址),低位字节在后(高地址)
示例:0x12345678 存储为:
内存地址: 0x1000 0x1001 0x1002 0x1003┌─────┬─────┬─────┬─────┐
存储内容: │ 12 │ 34 │ 56 │ 78 │ ← 高位到低位└─────┴─────┴─────┴─────┘
7.1.1.3 为什么需要 const/high16?
Android 指令设计需节省空间 → 用 16 位指令加载 32 位常量的高半部分:
const/high16 v0, 0x10000000 # 仅加载高16位:0x1000 → v0 = 0x10000000
const/16 v0, 0x0000 # 加载低16位:0x0000 → 需后续合并
合并逻辑(伪代码):
uint32_t value = (high16_value << 16) | low16_value;
// 0x10000000 | 0x0000 = 0x10000000
实战:小端序下的数据解析
假设内存中存储 78 56 34 12(小端序):
按字节读取:
地址0: 0x78
地址1: 0x56
地址2: 0x34
地址3: 0x12
组合为32位值:
value = (byte3 << 24) | (byte2 << 16) | (byte1 << 8) | byte0= (0x12 << 24) | (0x34 << 16) | (0x56 << 8) | 0x78= 0x12345678 // 正确值
7.1.1.4 32位常量加载完整原理(以 0x12345678 为例)
步骤 1:拆分高低位
原始值:0x12345678
高16位:0x1234
低16位:0x5678
步骤 2:两条指令分别加载
# 加载高16位 → 存入寄存器 v0
const/high16 v0, 0x12340000 # 注意:必须补零到32位格式!# 加载低16位 → 存入寄存器 v1
const/16 v1, 0x5678
步骤 3:合并操作(编译器自动插入)
# 编译器生成的隐藏指令(实际字节码)
or-int/lit16 v0, v0, 0x5678 # v0 = v0 | 0x5678 → 0x12345678
关键点:const/high16 加载的值末尾 16 位是 0(0x12340000),后续通过 or-int/lit16 将低16位(0x5678)合并进去。
7.1.1.5 为什么要这样的设计?
问题:高位在前,低位在后,在Android中的dex文件二进制是小端序列,也就是需要从后往前读,大端序列就是从前往后读,这一个理解了,但是为什么要这样做呢?另外读取了16位的指令加载了32位常量的前面的一半部分,那么后面剩下的一半呢?
解答:
1. 指令空间压缩
- 32 位常量需 4 字节存储 → 但 Dalvik 指令长度仅 2 字节
- 解决方案:拆成两条 2 字节指令(const/high16 + const/16)
2. 性能平衡
- 全量加载(如 const v0, 0x12345678)需 6 字节(操作码 2字节 + 常量 4字节)
- 分片加载 仅需 4 字节(两条 2 字节指令) → 节省 33% 空间
核心理解:
- const/high16 不是独立操作 → 它只是 32 位常量加载的第一步
- 后续必跟合并指令(or-int/lit16 等)→ 将低16位"焊接"到高位值的尾部
- 设计本质:牺牲少量指令数(2→3条),换取存储空间优化(6字节→4字节)
7.1.1.6 空间优化的深度分析
疑问:这样操作好处是存储空间的优化,分批加载,然后再合并,可是合并之后的长度不依然是很大吗?
解答:加载const指令的两种方式对比 — 空间优化的本质(指令长度和常量长度)
1. 传统全量加载(const 指令)
const v0, 0x12345678 # 指令长度:6字节
- 结构:操作码(2字节) + 常量值(4字节)
- 总长:6 字节(恒定)
2. 分片加载(const/high16 + const/16 + 合并)
const/high16 v0, 0x12340000 # 2字节
const/16 v1, 0x5678 # 2字节
or-int/lit16 v0, v0, 0x5678 # 2字节(合并指令)
- 总长:6 字节(3条指令 × 2字节)
- 空间未减少?表面看无优势?
7.1.1.7 分片加载的隐藏收益
1. 高频小值的极致压缩
对常见小数字(如 0、1、-1):
全量加载(浪费)
const v0, 0x1 # 6字节 → 操作码2字节 + 常量4字节(其中3.75字节为0)
分片加载(高效)
const/4 v0, 0x1 # 仅需2字节!压缩率 300%
2. 常量池共享优化
- 全量加载:每个 const 指令独占 4 字节常量存储
- 分片加载:const/16 的 16 位立即数嵌入指令内**(不占常量池)**
DEX 文件结构对比
全量加载:常量池条目 [0x12345678] → 4字节
分片加载:无额外常量池开销(立即数直接存指令)
3. 具体案例分析
案例:加载 0x0000FFFF(常见掩码)
方式 | 指令序列 | 总字节数 | 常量池占用 |
---|---|---|---|
全量加载 | const v0, 0x0000FFFF | 6 | 4 |
分片加载 | const/16 v0, 0xFFFF | 2 | 0 |
节省 | 4字节 | 4字节 |
优势:单条指令完成 → 省去合并操作(因高16位为0)
7.1.1.8 总结
分片加载的合并结果(如 0x12345678)在内存中仍是 4 字节,但:
- DEX 文件存储:通过消除常量池条目 + 短指令嵌入,显著压缩体积
- 内存执行:短指令提升缓存命中率 → 加速虚拟机解释执行
本质:这是一种智能的指令设计,在不同场景下自动选择最优的编码方式:
- 小值 → 超级压缩(const/4)
- 中值 → 适度压缩(const/16)
- 大值 → 分片处理(const/high16 + 合并)
7.2 移动指令
move vA, vB # 移动32位值
move-wide vA, vB # 移动64位值
move-object vA, vB # 移动对象引用move-result vAA # 获取方法调用结果
move-result-wide vAA # 获取64位方法结果
move-result-object vAA # 获取对象方法结果move-exception vAA # 获取异常对象# 示例
move v1, v0 # v1 = v0
move-object v2, v1 # v2 = v1 (对象引用)
7.3 返回指令
return-void # 返回void
return vAA # 返回32位值
return-wide vAA # 返回64位值
return-object vAA # 返回对象引用# 示例
return-void # return;
return v0 # return v0;
return-object v1 # return v1;
7.4 算术指令
# 32位整数运算
add-int vAA, vBB, vCC # vAA = vBB + vCC
sub-int vAA, vBB, vCC # vAA = vBB - vCC
mul-int vAA, vBB, vCC # vAA = vBB * vCC
div-int vAA, vBB, vCC # vAA = vBB / vCC
rem-int vAA, vBB, vCC # vAA = vBB % vCC# 浮点运算
add-float vAA, vBB, vCC # 浮点加法
sub-double vAA, vBB, vCC # 双精度减法# 示例
add-int v0, v1, v2 # v0 = v1 + v2
7.5 比较指令
cmp-long vAA, vBB, vCC # 比较long值
cmpl-float vAA, vBB, vCC # 比较float值(NaN时返回-1)
cmpg-float vAA, vBB, vCC # 比较float值(NaN时返回1)# 示例
cmp-long v0, v1, v2 # if (v1 > v2) v0=1; else if (v1==v2) v0=0; else v0=-1
7.6 条件跳转指令
# 与零比较跳转
if-eq vA, vB, :label # if (vA == vB) goto label
if-ne vA, vB, :label # if (vA != vB) goto label
if-lt vA, vB, :label # if (vA < vB) goto label
if-ge vA, vB, :label # if (vA >= vB) goto label
if-gt vA, vB, :label # if (vA > vB) goto label
if-le vA, vB, :label # if (vA <= vB) goto label# 与零比较跳转
if-eqz vAA, :label # if (vAA == 0) goto label
if-nez vAA, :label # if (vAA != 0) goto label
if-ltz vAA, :label # if (vAA < 0) goto label
if-gez vAA, :label # if (vAA >= 0) goto label
if-gtz vAA, :label # if (vAA > 0) goto label
if-lez vAA, :label # if (vAA <= 0) goto label# 无条件跳转
goto :label # 直接跳转
7.7 字段操作指令
# 静态字段
sget vAA, Lclass;->field:Ltype; # 获取静态字段
sput vAA, Lclass;->field:Ltype; # 设置静态字段# 实例字段
iget vA, vB, Lclass;->field:Ltype; # 获取实例字段
iput vA, vB, Lclass;->field:Ltype; # 设置实例字段# 示例
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
iget-object v1, p0, Lcom/example/MainActivity;->TAG:Ljava/lang/String;
7.8 方法调用指令
# 虚方法调用
invoke-virtual {参数寄存器}, Lclass;->method(参数类型)返回类型# 静态方法调用
invoke-static {参数寄存器}, Lclass;->method(参数类型)返回类型# 直接方法调用(构造方法、私有方法)
invoke-direct {参数寄存器}, Lclass;->method(参数类型)返回类型# 接口方法调用
invoke-interface {参数寄存器}, Linterface;->method(参数类型)返回类型# 父类方法调用
invoke-super {参数寄存器}, Lclass;->method(参数类型)返回类型# 示例
invoke-virtual {v1, v0}, Ljava/io/PrintStream;->println(I)V
invoke-static {v0}, Lcom/example/MainActivity;->log(I)V
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
7.9 数组操作指令
# 创建数组
new-array vA, vB, [type # vA = new type[vB]# 数组长度
array-length vA, vB # vA = vB.length# 数组元素读取
aget vAA, vBB, vCC # vAA = vBB[vCC]
aget-object vAA, vBB, vCC # 对象数组读取# 数组元素写入
aput vAA, vBB, vCC # vBB[vCC] = vAA
aput-object vAA, vBB, vCC # 对象数组写入# 示例
new-array v0, v1, [I # v0 = new int[v1]
array-length v2, v0 # v2 = v0.length
7.10 对象操作指令
# 创建对象实例
new-instance vAA, Ltype; # vAA = new type()# 类型检查
instance-of vA, vB, Ltype; # vA = (vB instanceof type)# 类型转换
check-cast vAA, Ltype; # vAA = (type)vAA# 示例
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
7.11 同步指令
monitor-enter vAA # synchronized(vAA) {
monitor-exit vAA # }# 示例
monitor-enter p0 # 进入同步块
# ... 同步代码 ...
monitor-exit p0 # 退出同步块
7.12 空指令
nop # 空操作指令,不执行任何操作
7.13 移动指令详细分类
# 基本移动指令
move vA, vB # 移动32位值:vA = vB
move/from16 vAA, vBBBB # 从16位索引移动:vAA = vBBBB
move/16 vAAAA, vBBBB # 16位索引移动:vAAAA = vBBBB# 宽度移动指令(64位)
move-wide vA, vB # 移动64位值:vA = vB
move-wide/from16 vAA, vBBBB # 从16位索引移动64位
move-wide/16 vAAAA, vBBBB # 16位索引移动64位# 对象移动指令
move-object vA, vB # 移动对象引用:vA = vB
move-object/from16 vAA, vBBBB # 从16位索引移动对象
move-object/16 vAAAA, vBBBB # 16位索引移动对象# 结果移动指令
move-result vAA # 获取方法调用结果(32位)
move-result-wide vAA # 获取方法调用结果(64位)
move-result-object vAA # 获取方法调用结果(对象)# 异常移动指令
move-exception vAA # 获取异常对象
7.14 返回指令详细分类
return-void # 返回void
return vAA # 返回32位值
return-wide vAA # 返回64位值
return-object vAA # 返回对象引用
7.15 常量指令详细分类
# 4位常量(-8到7)
const/4 vA, #+B # vA = B(4位立即数)# 16位常量(-32768到32767)
const/16 vAA, #+BBBB # vA = BBBB(16位立即数)# 32位常量
const vAA, #+BBBBBBBB # vA = BBBBBBBB(32位立即数)# 高16位常量
const/high16 vAA, #+BBBB0000 # vAA = BBBB0000(高16位)# 64位常量
const-wide/16 vAA, #+BBBB # vAA = BBBB(64位,符号扩展)
const-wide/32 vAA, #+BBBBBBBB # vAA = BBBBBBBB(64位,符号扩展)
const-wide vAA, #+BBBBBBBBBBBBBBBB # vAA = BBBBBBBBBBBBBBBB(64位)
const-wide/high16 vAA, #+BBBB000000000000 # 高16位(64位)# 字符串常量
const-string vAA, string@BBBB # vAA = 字符串引用
const-string/jumbo vAA, string@BBBBBBBB # 大字符串引用# 类常量
const-class vAA, type@BBBB # vAA = 类引用
7.16 类型检查和转换指令
# 类型检查
instance-of vA, vB, type@CCCC # vA = (vB instanceof type)# 类型转换
check-cast vAA, type@BBBB # vAA = (type)vAA# 数组长度
array-length vA, vB # vA = vB.length
7.17 对象创建指令
# 创建新实例
new-instance vAA, type@BBBB # vAA = new type()# 创建新数组
new-array vA, vB, type@CCCC # vA = new type[vB]# 创建并填充数组
filled-new-array {vC, vD, vE, vF, vG}, type@BBBB
filled-new-array/range {vCCCC .. vNNNN}, type@BBBB# 填充数组数据
fill-array-data vAA, +BBBBBBBB # 使用数组数据填充
7.18 数组操作指令详细分类
# 获取数组元素(aget = array get)
aget vAA, vBB, vCC # vAA = vBB[vCC]
aget-wide vAA, vBB, vCC # 64位数组元素获取
aget-object vAA, vBB, vCC # 对象数组元素获取
aget-boolean vAA, vBB, vCC # boolean数组元素获取
aget-byte vAA, vBB, vCC # byte数组元素获取
aget-char vAA, vBB, vCC # char数组元素获取
aget-short vAA, vBB, vCC # short数组元素获取# 设置数组元素(aput = array put)
aput vAA, vBB, vCC # vBB[vCC] = vAA
aput-wide vAA, vBB, vCC # 64位数组元素设置
aput-object vAA, vBB, vCC # 对象数组元素设置
aput-boolean vAA, vBB, vCC # boolean数组元素设置
aput-byte vAA, vBB, vCC # byte数组元素设置
aput-char vAA, vBB, vCC # char数组元素设置
aput-short vAA, vBB, vCC # short数组元素设置
7.19 实例字段操作指令详细分类
# 获取实例字段(iget = instance get)
iget vA, vB, field@CCCC # vA = vB.field
iget-wide vA, vB, field@CCCC # 64位实例字段获取
iget-object vA, vB, field@CCCC # 对象实例字段获取
iget-boolean vA, vB, field@CCCC # boolean实例字段获取
iget-byte vA, vB, field@CCCC # byte实例字段获取
iget-char vA, vB, field@CCCC # char实例字段获取
iget-short vA, vB, field@CCCC # short实例字段获取# 设置实例字段(iput = instance put)
iput vA, vB, field@CCCC # vB.field = vA
iput-wide vA, vB, field@CCCC # 64位实例字段设置
iput-object vA, vB, field@CCCC # 对象实例字段设置
iput-boolean vA, vB, field@CCCC # boolean实例字段设置
iput-byte vA, vB, field@CCCC # byte实例字段设置
iput-char vA, vB, field@CCCC # char实例字段设置
iput-short vA, vB, field@CCCC # short实例字段设置
7.20 静态字段操作指令详细分类
# 获取静态字段(sget = static get)
sget vAA, field@BBBB # vAA = 类.静态字段
sget-wide vAA, field@BBBB # 64位静态字段获取
sget-object vAA, field@BBBB # 对象静态字段获取
sget-boolean vAA, field@BBBB # boolean静态字段获取
sget-byte vAA, field@BBBB # byte静态字段获取
sget-char vAA, field@BBBB # char静态字段获取
sget-short vAA, field@BBBB # short静态字段获取# 设置静态字段(sput = static put)
sput vAA, field@BBBB # 类.静态字段 = vAA
sput-wide vAA, field@BBBB # 64位静态字段设置
sput-object vAA, field@BBBB # 对象静态字段设置
sput-boolean vAA, field@BBBB # boolean静态字段设置
sput-byte vAA, field@BBBB # byte静态字段设置
sput-char vAA, field@BBBB # char静态字段设置
sput-short vAA, field@BBBB # short静态字段设置
7.21 方法调用指令详细分类
# 虚方法调用(多态)
invoke-virtual {vC, vD, vE, vF, vG}, meth@BBBB
invoke-virtual/range {vCCCC .. vNNNN}, meth@BBBB# 父类方法调用
invoke-super {vC, vD, vE, vF, vG}, meth@BBBB
invoke-super/range {vCCCC .. vNNNN}, meth@BBBB# 直接方法调用(私有方法、构造函数)
invoke-direct {vC, vD, vE, vF, vG}, meth@BBBB
invoke-direct/range {vCCCC .. vNNNN}, meth@BBBB# 静态方法调用
invoke-static {vC, vD, vE, vF, vG}, meth@BBBB
invoke-static/range {vCCCC .. vNNNN}, meth@BBBB# 接口方法调用
invoke-interface {vC, vD, vE, vF, vG}, meth@BBBB
invoke-interface/range {vCCCC .. vNNNN}, meth@BBBB
7.22 一元操作指令(unop)
# 类型转换
int-to-long vA, vB # vA = (long)vB
int-to-float vA, vB # vA = (float)vB
int-to-double vA, vB # vA = (double)vB
long-to-int vA, vB # vA = (int)vB
long-to-float vA, vB # vA = (float)vB
long-to-double vA, vB # vA = (double)vB
float-to-int vA, vB # vA = (int)vB
float-to-long vA, vB # vA = (long)vB
float-to-double vA, vB # vA = (double)vB
double-to-int vA, vB # vA = (int)vB
double-to-long vA, vB # vA = (long)vB
double-to-float vA, vB # vA = (float)vB
int-to-byte vA, vB # vA = (byte)vB
int-to-char vA, vB # vA = (char)vB
int-to-short vA, vB # vA = (short)vB# 取反操作
neg-int vA, vB # vA = -vB
not-int vA, vB # vA = ~vB
neg-long vA, vB # vA = -vB (long)
not-long vA, vB # vA = ~vB (long)
neg-float vA, vB # vA = -vB (float)
neg-double vA, vB # vA = -vB (double)
7.23 二元操作指令(binop)
# 整数运算
add-int vAA, vBB, vCC # vAA = vBB + vCC
sub-int vAA, vBB, vCC # vAA = vBB - vCC
mul-int vAA, vBB, vCC # vAA = vBB * vCC
div-int vAA, vBB, vCC # vAA = vBB / vCC
rem-int vAA, vBB, vCC # vAA = vBB % vCC
and-int vAA, vBB, vCC # vAA = vBB & vCC
or-int vAA, vBB, vCC # vAA = vBB | vCC
xor-int vAA, vBB, vCC # vAA = vBB ^ vCC
shl-int vAA, vBB, vCC # vAA = vBB << vCC
shr-int vAA, vBB, vCC # vAA = vBB >> vCC
ushr-int vAA, vBB, vCC # vAA = vBB >>> vCC# 长整数运算
add-long vAA, vBB, vCC # vAA = vBB + vCC (long)
sub-long vAA, vBB, vCC # vAA = vBB - vCC (long)
mul-long vAA, vBB, vCC # vAA = vBB * vCC (long)
div-long vAA, vBB, vCC # vAA = vBB / vCC (long)
rem-long vAA, vBB, vCC # vAA = vBB % vCC (long)
and-long vAA, vBB, vCC # vAA = vBB & vCC (long)
or-long vAA, vBB, vCC # vAA = vBB | vCC (long)
xor-long vAA, vBB, vCC # vAA = vBB ^ vCC (long)
shl-long vAA, vBB, vCC # vAA = vBB << vCC (long)
shr-long vAA, vBB, vCC # vAA = vBB >> vCC (long)
ushr-long vAA, vBB, vCC # vAA = vBB >>> vCC (long)# 浮点运算
add-float vAA, vBB, vCC # vAA = vBB + vCC (float)
sub-float vAA, vBB, vCC # vAA = vBB - vCC (float)
mul-float vAA, vBB, vCC # vAA = vBB * vCC (float)
div-float vAA, vBB, vCC # vAA = vBB / vCC (float)
rem-float vAA, vBB, vCC # vAA = vBB % vCC (float)# 双精度浮点运算
add-double vAA, vBB, vCC # vAA = vBB + vCC (double)
sub-double vAA, vBB, vCC # vAA = vBB - vCC (double)
mul-double vAA, vBB, vCC # vAA = vBB * vCC (double)
div-double vAA, vBB, vCC # vAA = vBB / vCC (double)
rem-double vAA, vBB, vCC # vAA = vBB % vCC (double)
7.24 二元操作指令(binop/2addr)
# 地址模式二元操作(目标和源操作数相同)
add-int/2addr vA, vB # vA += vB
sub-int/2addr vA, vB # vA -= vB
mul-int/2addr vA, vB # vA *= vB
div-int/2addr vA, vB # vA /= vB
rem-int/2addr vA, vB # vA %= vB
and-int/2addr vA, vB # vA &= vB
or-int/2addr vA, vB # vA |= vB
xor-int/2addr vA, vB # vA ^= vB
shl-int/2addr vA, vB # vA <<= vB
shr-int/2addr vA, vB # vA >>= vB
ushr-int/2addr vA, vB # vA >>>= vB# 长整数地址模式
add-long/2addr vA, vB # vA += vB (long)
sub-long/2addr vA, vB # vA -= vB (long)
mul-long/2addr vA, vB # vA *= vB (long)
div-long/2addr vA, vB # vA /= vB (long)
rem-long/2addr vA, vB # vA %= vB (long)
and-long/2addr vA, vB # vA &= vB (long)
or-long/2addr vA, vB # vA |= vB (long)
xor-long/2addr vA, vB # vA ^= vB (long)
shl-long/2addr vA, vB # vA <<= vB (long)
shr-long/2addr vA, vB # vA >>= vB (long)
ushr-long/2addr vA, vB # vA >>>= vB (long)# 浮点地址模式
add-float/2addr vA, vB # vA += vB (float)
sub-float/2addr vA, vB # vA -= vB (float)
mul-float/2addr vA, vB # vA *= vB (float)
div-float/2addr vA, vB # vA /= vB (float)
rem-float/2addr vA, vB # vA %= vB (float)# 双精度浮点地址模式
add-double/2addr vA, vB # vA += vB (double)
sub-double/2addr vA, vB # vA -= vB (double)
mul-double/2addr vA, vB # vA *= vB (double)
div-double/2addr vA, vB # vA /= vB (double)
rem-double/2addr vA, vB # vA %= vB (double)
7.25 字面量二元操作指令(binop/lit16)
# 16位字面量操作
add-int/lit16 vA, vB, #+CCCC # vA = vB + CCCC
rsub-int vA, vB, #+CCCC # vA = CCCC - vB(反向减法)
mul-int/lit16 vA, vB, #+CCCC # vA = vB * CCCC
div-int/lit16 vA, vB, #+CCCC # vA = vB / CCCC
rem-int/lit16 vA, vB, #+CCCC # vA = vB % CCCC
and-int/lit16 vA, vB, #+CCCC # vA = vB & CCCC
or-int/lit16 vA, vB, #+CCCC # vA = vB | CCCC
xor-int/lit16 vA, vB, #+CCCC # vA = vB ^ CCCC
7.26 字面量二元操作指令(binop/lit8)
# 8位字面量操作
add-int/lit8 vAA, vBB, #+CC # vAA = vBB + CC
rsub-int/lit8 vAA, vBB, #+CC # vAA = CC - vBB(反向减法)
mul-int/lit8 vAA, vBB, #+CC # vAA = vBB * CC
div-int/lit8 vAA, vBB, #+CC # vAA = vBB / CC
rem-int/lit8 vAA, vBB, #+CC # vAA = vBB % CC
and-int/lit8 vAA, vBB, #+CC # vAA = vBB & CC
or-int/lit8 vAA, vBB, #+CC # vAA = vBB | CC
xor-int/lit8 vAA, vBB, #+CC # vAA = vBB ^ CC
shl-int/lit8 vAA, vBB, #+CC # vAA = vBB << CC
shr-int/lit8 vAA, vBB, #+CC # vAA = vBB >> CC
ushr-int/lit8 vAA, vBB, #+CC # vAA = vBB >>> CC
7.27 比较指令详细分类
# 长整数比较
cmp-long vAA, vBB, vCC # 比较vBB和vCC(long)# 结果:vAA = 1(>), 0(=), -1(<)# 浮点比较(NaN偏向)
cmpl-float vAA, vBB, vCC # 比较vBB和vCC(float)# NaN时返回-1(less bias)
cmpg-float vAA, vBB, vCC # 比较vBB和vCC(float)# NaN时返回1(greater bias)# 双精度浮点比较
cmpl-double vAA, vBB, vCC # 比较vBB和vCC(double)# NaN时返回-1
cmpg-double vAA, vBB, vCC # 比较vBB和vCC(double)# NaN时返回1
7.28 控制流指令详细分类
# 无条件跳转
goto +AA # 跳转到相对地址
goto/16 +AAAA # 16位相对跳转
goto/32 +AAAAAAAA # 32位相对跳转# 打包switch语句
packed-switch vAA, +BBBBBBBB # 连续值switch
sparse-switch vAA, +BBBBBBBB # 稀疏值switch# 条件跳转(两寄存器比较)
if-eq vA, vB, +CCCC # if (vA == vB) goto +CCCC
if-ne vA, vB, +CCCC # if (vA != vB) goto +CCCC
if-lt vA, vB, +CCCC # if (vA < vB) goto +CCCC
if-ge vA, vB, +CCCC # if (vA >= vB) goto +CCCC
if-gt vA, vB, +CCCC # if (vA > vB) goto +CCCC
if-le vA, vB, +CCCC # if (vA <= vB) goto +CCCC# 条件跳转(与零比较)
if-eqz vAA, +BBBB # if (vAA == 0) goto +BBBB
if-nez vAA, +BBBB # if (vAA != 0) goto +BBBB
if-ltz vAA, +BBBB # if (vAA < 0) goto +BBBB
if-gez vAA, +BBBB # if (vAA >= 0) goto +BBBB
if-gtz vAA, +BBBB # if (vAA > 0) goto +BBBB
if-lez vAA, +BBBB # if (vAA <= 0) goto +BBBB
8. 完整代码示例
8.1 MainActivity.smali 完整示例
.class public Lcom/social_touch/demo/MainActivity;
.super Landroid/app/Activity;
.source "MainActivity.java"# instance fields
.field private TAG:Ljava/lang/String;
.field private running:Z# direct methods
.method public constructor <init>()V.locals 1.prologue.line 8# 调用Activity中的init()方法invoke-direct {p0}, Landroid/app/Activity;-><init>()V.line 10const-string v0, "MainActivity"iput-object v0, p0, Lcom/social_touch/demo/MainActivity;->TAG:Ljava/lang/String;.line 13const/4 v0, 0x0iput-boolean v0, p0, Lcom/social_touch/demo/MainActivity;->running:Zreturn-void
.end method# 静态方法log()
.method public static log(I)V.locals 3.parameter "result" # 表示result参数.prologue.line 42# v0寄存器中赋值为"MainActivity"const-string v0, "MainActivity"# 创建StringBuilder对象,并将其引用赋值给v1寄存器new-instance v1, Ljava/lang/StringBuilder;# 调用StringBuilder中的构造方法invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V# v2寄存器中赋值为"the result:"const-string v2, "the result:"# {v1,v2}大括号中v1寄存器中存储的是StringBuilder对象的引用# 调用StringBuilder中的append(String str)方法,v2寄存器则是参数寄存器invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;# 获取上一个方法的执行结果,此时v1中存储的是append()方法执行后的结果# 此处之所以仍然返回v1的原因在于append()方法返回的就是自身的引用move-result-object v1# 继续调用append方法(),p0表示第一个参数寄存器,即上面提到的result参数invoke-virtual {v1, p0}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;# 同上move-result-object v1# 调用StringBuilder对象的toString()方法invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;# 获取上一个方法执行结果,toString()方法返回了一个新的String对象# 因此v1中此时存储了String对象的引用move-result-object v1# 调用Log类中的静态方法d()。因为d()是静态方法,因此{v0,v1}中的都成了参数寄存器invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I.line 43# 调用返回指令,此处没有返回任何值return-void
.end method# virtual methods
.method public add(II)I.locals 1.parameter "x" # 第一个参数.parameter "y" # 第二个参数.prologue.line 34# 调用add-int指令求和之后将结果赋值给v0寄存器add-int v0, p1, p2# 返回v0寄存器中的值return v0
.end method.method public onClick(Landroid/view/View;)V.locals 4.parameter "view" # 参数view.prologueconst/4 v3, 0x4 # v3寄存器中赋值为4.line 23 # java源文件中的第23行const/4 v1, 0x5 # v1寄存器中赋值为5# 调用add()方法invoke-virtual {p0, v3, v1}, Lcom/social_touch/demo/MainActivity;->add(II)I# 从v0寄存器中获取add方法的执行结果move-result v0.line 24 # java源文件中的24行.local v0, result:I# v1寄存器中赋值为PrintStream对象的引用outsget-object v1, Ljava/lang/System;->out:Ljava/io/PrintStream;# 执行out对象的println()方法invoke-virtual {v1, v0}, Ljava/io/PrintStream;->println(I)V.line 26const/16 v1, 0x9 # v1寄存器中赋值为9const/4 v2, 0x3 # v2寄存器中赋值为3# 调用sub()方法,{p0,v1,v2},p0指的是this,即当前对象,v1,v2则是参数invoke-virtual {p0, v1, v2}, Lcom/social_touch/demo/MainActivity;->sub(II)I# 从v0寄存器中获取sub()方法的执行结果move-result v0.line 28# 如果v0寄存器的值小于等于v3寄存器中的值,则跳转到cond_0处继续执行if-le v0, v3, :cond_0.line 29# 调用静态方法log()invoke-static {v0}, Lcom/social_touch/demo/MainActivity;->log(I)V.line 31:cond_0return-void
.end method.method protected onCreate(Landroid/os/Bundle;)V.locals 1.parameter "savedInstanceState" # 参数savedInstanceState.prologue.line 17# 调用父类方法onCreate()invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V.line 18const v0, 0x7f04001a # v0寄存器赋值为0x7f04001a# 调用方法setContentView()invoke-virtual {p0, v0}, Lcom/social_touch/demo/MainActivity;->setContentView(I)V.line 19return-void
.end method# declared-synchronized表示该方法是同步方法
.method public declared-synchronized sub(II)I.locals 1.parameter "x".parameter "y".prologue.line 38monitor-enter p0 # 为该方法添加锁对象p0sub-int v0, p1, p2 # v0 = p1 - p2monitor-exit p0 # 释放锁对象return v0
.end method
8.2 对应的Java源码
public class MainActivity extends Activity {private String TAG = "MainActivity";private boolean running = false;public MainActivity() {// 构造方法}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(0x7f04001a);}public void onClick(View view) {int result = add(4, 5);System.out.println(result);result = sub(9, 3);if (result > 4) {log(result);}}public int add(int x, int y) {return x + y;}public synchronized int sub(int x, int y) {return x - y;}public static void log(int result) {String message = "the result:" + result;Log.d("MainActivity", message);}
}
9. 编译与运行
9.1 smali2dex编译
将smali文件编译为dex文件:
# 使用smali工具编译
java -jar smali.jar assemble src.smali -o src.dex
工具下载:
- smali.jar: https://bitbucket.org/JesusFreke/smali/overview
9.2 运行smali
步骤1:推送dex文件到设备
adb push main.dex /sdcard/
步骤2:使用dalvikvm运行
adb shell dalvikvm -cp /sdcard/main.dex main
参数说明:
dalvikvm
: Dalvik虚拟机命令-cp
: classpath路径,指定dex文件位置main
: 要执行的类名
9.3 实用工具
工具 | 功能 | 用途 |
---|---|---|
baksmali | dex → smali | 反编译dex文件 |
smali | smali → dex | 编译smali文件 |
apktool | apk ↔ 资源+smali | APK完整解包/重打包 |
jadx | dex → java | 直接反编译为Java代码 |
10. 实战技巧
10.1 常见分析模式
查找字符串
const-string v0, "关键字符串"
查找方法调用
invoke-virtual {v0, v1}, Lcom/target/Class;->targetMethod(I)V
查找跳转逻辑
if-eqz v0, :bypass_check # 如果v0==0则跳过检查
# 正常检查逻辑
return-void:bypass_check
# 绕过检查的逻辑
10.2 修改技巧
绕过检查
# 原代码:检查失败则退出
if-eqz v0, :exit_app# 修改为:检查失败也继续
# if-eqz v0, :exit_app # 注释掉
修改返回值
# 原代码:返回检查结果
return v0# 修改为:总是返回true
const/4 v0, 0x1
return v0
10.3 调试技巧
添加日志输出
# 在关键位置添加调试日志
const-string v1, "DEBUG"
const-string v2, "执行到此处"
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
11. 参考资源
11.1 官方文档
- Android官方文档
- Dalvik字节码格式
11.2 工具资源
- smali/baksmali工具
- apktool
- jadx反编译器
11.3 学习资源
- CTF Wiki - Smali