Android逆向工程:Smali语法解析完整指南

完整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 | 0x56780x12345678

关键点: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  # 仅需
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值