完整Smali语法指南 & 一些个人理解
参考来源:CTF Wiki - Smali
目录
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 10
const-string v0, "MainActivity"
iput-object v0, p0, Lcom/social_touch/demo/MainActivity;->TAG:Ljava/lang/String;
.line 13
const/4 v0, 0x0
iput-boolean v0, p0, Lcom/social_touch/demo/MainActivity;->running:Z
return-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 # 仅需

1万+

被折叠的 条评论
为什么被折叠?



