提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
导语:学习一门新技术不仅要知其然,更要知其所然。在学习Smali
语法之前,首先要明白其学习目的,学习用途,怎么样才能从能达到事半功倍的效果。所以本文章打算用循序渐进的方式去讲述该知识点。
一、了解基础知识
图片来源处
命令行形式表示:
- 使用
javac
命令将Java
源文件编译成.class
文件:
javac YourJavaFile.java
- 使用
Android SDK
中的dx
工具将.class
文件打包成DEX
文件:
java -jar dx.jar --dex --output=YourApp.dex YourJavaFile.class
- 使用
baksmali
工具将DEX
文件反编译成smali
代码:
java -jar baksmali.jar YourApp.dex
APK
的基础知识
- 在学习
Smali
语法之前,建议先学习掌握Android
应用程序的APK
构建流程和Android
应用程序的打包流程。APK构建流程可以看这一篇
Android应用程序的打包流程建议看这一篇 Smali
语法的学习是为了更好的反编译这一块的内容,而Android
反编译不得不提及到Dalvik
汇编语言(Smali
是Dalvik
汇编语言的一种文本表示形式。Dalvik
是Android
操作系统用于执行.apk
文件中的应用程序代码的中间表示形式,而Smali
提供了一种人类可读的方式来查看和编辑Dalvik
代码),包括其与Java
字节码的区别。Dalvik汇编语言的基本概念
Dalvik
的基础知识
Dalvik
是 Android
操作系统中使用的虚拟机,专为 Android
设计。它有自己的指令集和执行环境
,与 Java 虚拟机(JVM)
不同。
Dalvik
虚拟机基于寄存器架构
,这意味着它使用一组寄存器来存储数据,而不是像 JVM
那样基于栈
。
1 .Dalvik
的表示方法
注:Dalvik
字节码有自己的类型
,方法
及字段
的表示方法,这些内容与Dalvik虚拟机指令集
一起组成了Dalvik汇编代码
。
1.Dalvik
类型
Dalvik
字节码只有两种类型,分别是基本类型
和引用类型
。Dalvik
使用这两种类型来表示 Java
语言的全部类型。除了对象和数组属于引用对象,其它的 Java 类型都属于基本类型。Java
语言的类型与 Dalvik
字节码类型描述符的对应关系如下表所示:
Smali类型描述符 Java类型
V void
Z Boolean
B byte
S string
C char
I int
J long
F float
D double
L Java对象类型
[ 数组类型
对于上述的类型对照表,这里通过下表中几个实际的例子进一步来理解对应关系:
Dalvik汇编代码 | Java代码 |
---|---|
Lpackage/name/ObjectName; | package.name.ObjectName |
Ljava/lang/String; | java.lang.String |
[I | int[ ] |
[[ | int[ ] [ ](最大维数为255) |
[Ljava/lang/String; | int [ ] |
每个Dalvik寄存器都是32位的。对长度小于或等于32位的类型来说,只用一个寄存器就可以存放该类型的值,而对 J 、D 等64位的类型来说,它们的值要使用相邻的两个寄存器来存储,例如 v0 与 v1、v3 与 v4。
2.Dalvik
方法
Dalvik
使用方法名、类型参数与返回值来描述一个方法。格式如下:
二、学习Smali
语法(重点)
前言:学习一门技术,最好的方式就是拿现有的知识和技术,在此基础上去研究新内容,把自己现有的知识体系作为切入点,最好的实现方式就是做一个入门demo,扩展自己的思维和动手能力。
Smali语法主要从三个方面进行学习,分别是smali文件解读
,Smali基础语法
,Smali指令集
Java源代码
smali文件
1.smali文件
解读
1.1头文件
.class public Lcom/cxj/smalitest/MainActivity;
.super Landroidx/appcompat/app/AppCompatActivity;
.source "MainActivity.java"
第一行表示的含义:当前class的包名和类名
第二行表示的是:父类的包名和类名
第三行表示的是:源文件的名称
头文件格式:
.class <访问权限修饰符> [非权限修饰符] <类名>
.super <父类名>
.source <源文件名称>
1.2静态字段
Smali文件类的主体部分:一个类由字段
和方法
组成。.field
字段声明,字段分两类:静态字段和实例字段
Java静态字段:
private static final String STR = "hello"; //常量
private static final int TAKE_PHOTO = 1; //常量
private static int a;
Smali静态字段:
# static fields #是Davlik自动生成的注释,表明以下为静态字段,
# 可以看出final常量需要额外用等于号赋值
.field private static final STR:Ljava/lang/String; = "hello"
.field private static final TAKE_PHOTO:I = 0x1
.field private static a:I
Smali静态字段格式:
#static fields
.field <访问权限> static [修饰词] <字段名>:<变量类型>
.field <访问权限> static final <常量名>:<常量类型> = 常量值
1.3实例字段
实例字段相比于静态字段少了一个static的静态声明,其他均相同
Java实例字段:
Smali实例字段:
Smali实例字段格式:
#instance fields
.field <访问权限修饰符> [非权限修饰符] <变量名>:<变量类型>
1.4直接方法
smali
的方法声明使用的.method
指令,方法分为直接方法和虚方法
两种。
直接方法就是不能被覆写的方法,包括用static,private
修饰的方法
虚方法表示可以被覆写的方法,包括public,protected
修饰的方法。
Java直接方法:
public int add(int a, int b) {
return a + b;
}
Smali直接方法:
#direct methods
.method public add(II)I
.locals 2
.prologue
iadd v0, p1, p2
return v0
.end method
解读:(大家有不懂的可以借助GPT)
Smali直接方法格式:
两者在smali中的注释分别是直接方法(#direct methods
),虚方法(#virtual methods
),一般直接方法在smali文件的前半部分
,虚方法在后半部分
。
#direct methods/#virtual methods
.method <访问权限修饰符> [非访问权限修饰符] <方法名> (Para-Type1Para-Type2Para-Type3...)Return-Type
<.locals>
[.parameter]
[.prologue]
[.line] #对应Java源码的一行代码
<代码逻辑>
.end method
1.5虚方法
虚方法:是指从父类中继承的方法或者实现的接口的方法,它的声明跟直接方法相同,只是起始的初始为virtual methods
Java虚方法:
public int getNumber() {
return 42;
}
Java 中的虚方法可以被继承的子类重写(override)
Smali虚方法:
#virtual methods
.method public virtual getNumber()I
.locals 1
.prologue
# 返回一个整数
const v0, 42
return v0
.end method
# 调用一个虚方法
invoke-virtual {v1}, Lcom/example/MyClass;->getNumber()I
解读:
Smali虚方法格式:
#virtual methods
.method <访问权限修饰符> [非访问权限修饰符] <方法名> (Para-Type1Para-Type2Para-Type3...)Return-Type
<.locals>
[.parameter]
[.prologue]
[.line] #对应Java源码的一行代码
<代码逻辑>
.end method
1.6接口
Java接口:
public interface Vehicle {
int WHEELS = 4; // 常量
void start(); // 抽象方法
default void stop() { // 默认方法
System.out.println("Stopping the vehicle.");
}
}
Smali接口:
# 类 MyClass 实现了接口 Vehicle
.class public Lcom/example/MyClass;
.super Ljava/lang/Object;
# 实现接口 Vehicle
.implements Lcom/example/Vehicle;
# 抽象方法 start 必须在实现类中提供具体实现
.method public start()V
.locals 0
# 实现细节...
return-void
.end method
# 默认方法 stop 可以选择在实现类中重写或使用接口中的默认实现
.method public stop()V
.locals 0
# 如果不重写,将使用 Vehicle 接口中的默认实现
invoke-interface {p0}, Lcom/example/Vehicle;->stop()V
# 或者,如果提供了自己的实现
# 实现细节...
return-void
.end method
Smali接口格式:
如果一个类实现了一个接口,那么会在smali文件中用.implements
指令指出,而且在文件前面就已经指出
#interfaces
.implements <接口名称>
2.smali文件
基础语法
2.1寄存器
Android
的变量都存储于寄存器中。变量分为 v 与 p
两种格式
对于dalviks字节码寄存器都是32位的,它能够表示任何类型,2个寄存器用于表示64位的类型**(Long and Double)**
- 寄存器的命名方法有两种:
v命名法
和p命名法
:
v命名法:局部变量寄存器用v开头数字结尾的符号来表示,如v0、 v1、v2。
p命名法:函数参数寄存器则使用p开头数字结尾的符号来表示,如p0、p1、p2。
v命名法:局部变量寄存器V0-Vn,参数寄存器是Vn-Vn+m。
p命名法:(局部和成员)变量寄存器V0-Vn,参数寄存器P0-Pn,
特别注意的是,p0不一定是函数中的第一个参数,在非static函数
中,p0代指“this"
,p1表示函数的第一个 参数,p2代表函数中的第二个参数。而在static函数中
p0才对应第一个参数(因为Java的static方法中没有this方法)
2.2数据类型
Smali的数据类型其实就是Davikd的数据类型
Dalvik
字节码只有两种类型,分别是基本类型
和引用类型
。Dalvik
使用这两种类型来表示 Java
语言的全部类型。除了对象和数组属于引用对象,其它的 Java 类型都属于基本类型。Java
语言的类型与 Dalvik
字节码类型描述符的对应关系如下表所示:
基本数据类型
Smali类型描述符 Java类型
V void
Z Boolean
B byte
S string
C char
I int
J long
F float
D double
L Java对象类型
[ 数组类型
数组类型
[.... --- array
在基本类型前加上前中括号“[”
即表示该类型的数组,如: [B 表示byte数组 , [i 表示int数组
对象类型的数组为”[“
加上对象类型表示符来表示,如String
类型表示为:[Ljava/lang/String
对象类型
L ..../....; --- object
如果是对象类型,则以L作为开头,格式是LpackageName / objectName; packageName/
表示该对象所在的包,ObjectName
是对象的名字,“;”
表示对象名称的结束。相当于 java 中的 packageName.ObjectName
。
如:Ljava/lang/String; :表示 String 对象。其中 java/lang 对应 java.lang 包, String 是该包的一个对象。类对象中的内部类则使用“$”来连接
2.3字段和方法的表示
字段描述
在JAVA中假设有一个类:com.test.Person
,类有一个属性:int id
,在smali中则用下面的方法表示:
Lcom/test/Person;->id:I
语法为:类名->属性名:属性类型
方法描述
在JAVA中假设有一个类:com.test.Person
,类有一个方法:void setId(int id)
;,在smali中则用下面的方法表示:
Lcom/test/Person;->setId(I)V
语法为:完整类名->方法名(参数类型)返回类型
2.4 Smali文件关键词
.registers 和 .locals 基本区别
指令.registers指定了在这个方法中有多少个可用的寄存器(包括存方法参数的寄存器与非参寄存器
)
指令.locals指明了在这个方法中非参(non-parameter) 寄存器
的数量
3.smali文件
指令集
3.1空操作指令
空操作指令的助记符为nop
,它的值为00
,通常nop
指令被用来作对齐代码之用,无实际操作。
3.2数据操作指令
字符串数据
格式:
const-string 变量名,”字符串”
字节码数据
格式:
const-class 变量名,字节码对象;
数值类型数据 (默认定义最高位为符号位)
#占用一个寄存器(32位)
const/4 变量名,#十六进制数
const/16 变量名,#十六进制数
const 变量名,#十六进制数
const/high16 变量名,#十六进制数
#占用相邻两个容器(64位)
const-wide 变量名,#十六进制数
const-wide/16 变量名,#十六进制数
const-wide/32 变量名,#十六进制数
const-wide/high16 变量名,#十六进制数
1.const-wide只用写出一个寄存器,另一个寄存器默认为其下一个,即const-wide vx,寄存器为 vx 与 v(x+1)如const-wide v2,#FF763D33,其寄存器为 v2 和 v3
2.在使用const/high16时,数值补齐32位,即十六进制8位,不足的末尾+0
如用const/ high16给 v0 赋0xFF7F,代码为const/ high16 v0,0xFF7F0000(补满8位)只取最高16位,即将 #0xFF7F 赋给v0
3.3变量操作指令
用于将源寄存器的值移动到目标寄存器中,此类操作常用于赋值。
基础格式:move 目标寄存器
源寄存器
3.4返回指令
3.5方法调用指令
invoke-virtual或invoke-virtual/range :调用实例的虚方法(普通方法)
invoke-super或invoke-super/range :调用实例的父类/基类方法
invoke-direct或invoke-direct/range :调用实例的直接方法(私有方法和构造方法)
invoke-static或invoke-static/range :调用实例的静态方法
invoke-interface或invoke-interface/range :调用实例的接口方法
格式如下:
invoke-xxxxx {参数}, 方法所属类(全包名路径);->方法名称(方法参数描述符)方法返回值类型描述符
xxxxx 为direct,virtual,static,super,interface其中一种
例1:
Java格式
Smali格式
.class public LTest; #声明类
.super Ljava/lang/Object; #声明父类
.method public constructor <init>(Ljava/lang/String;)v
invoke-direct {p0}, Ljava/lang/Object;-><init>()v #调用父类构造方法
invoke-virtual {p0}, LTest;->getName()Ljava/lang/String; #调用getName方法
return-void
.end method
#声明getName方法
.method public getName()Ljava/lang/String;
const-String v0,"hello" #定义局部字符常量
return-object v0 #返回常量
.end method
例2:
Java格式
public class Test(){
public Test(String a){
String b=getName();
System.out.println(b);
}
private static String getName(){
return "hello";
}
}
Smali格式
# 类定义
.class public Lcom/example/Test; # 假设包名为 com.example
# 构造函数
.method public <init>(Ljava/lang/String;)V
.locals 1
.prologue
.line 1
invoke-direct {p0, p1}, Ljava/lang/Object;-><init>()V # 调用父类构造函数
.line 2
const-string v0, "hello" # 调用静态方法前,需要先获取字符串 "hello"
invoke-static {}, Lcom/example/Test;->getName()Ljava/lang/String; # 调用静态方法 getName
move-result-object v0 # 将静态方法的返回值移动到 v0 寄存器
.line 3
invoke-virtual {p0, v0}, Lcom/example/Test;->print(Ljava/lang/String;)V # 假设有一个 print 方法
return-void
.end method
# 静态方法定义
.method private static getName()Ljava/lang/String;
.locals 1
.prologue
const-string v0, "hello" # 静态方法返回字符串 "hello"
return-object v0
.end method