Smali 语法入门与代码拆解
工欲善其事必先利其器,所以要学好smali语法,好的工具是必须的,这里推荐两个工具,一个是Smali2Java,也就是将smali文件转换成java的工具;另一个工具是J2S2J1.3,这个工具可以将简单的java或者smali代码进行转换。
案例
因为我是一开始直接看源码的,所以我们今天也是直接从源码开始:
1 | .super Ljava/lang/Object; |
上面这些代码其实很简单,就是创建一个Test类,定义了一个私有成员属性name,并进行了赋值,然后定义了一个main方法,main方法中进行了Hello World!打印输出操作,对应的java代码:
1 | import java.io.PrintStream; |
代码拆解
下面我们来逐步讲解下smali的语法,方便大家建立它与java代码语法直接的关系。
smali文件,我的理解是它其实就类似于java编译之后的.class文件,也是一种字节码文件。
基本信息
我们先看前三行:
1 | .super Ljava/lang/Object; |
目前没有找到官方的相关文档,只能结合网上的资料和自己的推测进行分析,如果有不合理的地方,希望大家不吝指教。
第一行.super表示当前类的父类,表示继承关系的
第二行.source表示当前字节码文件对应的源码文件名
第三行.field表示定义一个属性(字段),具体语法是
1 | .field 访问权限 字段名:字段类型; |
字段类型的取值范围如下:
| Dalvik字节码类型 | java基本数据类型 |
|---|---|
| V | void |
| Z | boolean |
| B | byte |
| C | char |
| S | short |
| I | int |
| J | long |
| F | float |
| D | double |
| L | java类类型 |
| [ | 数组类型 |
我们这里的Ljava/lang/String就表示String类型的数据,如果是int类型,那语法应该是这样的:
1 | .field private id:I |
这里的对照关系,和class字节码绝大部分都一样
构造方法
紧接着,下面是构造方法对应的smali代码
1 | # direct methods |
.method
其中.method表示方法的开始,具体语法是:
1 | .method 访问权限 方法名()返回结果 |
这里的返回结果和字段的类型取值一样,可以参考上面的表格
.end method表示方法的结束,与.method相对应,两者之间的内容为方法体
.registers
.registers 2用于声明当前方法的寄存器个数,寄存器是用来存放方法的入参和方法的局部变量的,具体语法为:
1 | .registers N |
N代表需要的寄存器的总个数
同时,还有一个关键字.locals,它用于声明非参数的寄存器个数(也就是举报变量的个数,包含在registers声明的个数当中),也叫做本地寄存器,语法是一样的。
.prologue
.prologue表示方法代码的开始处,所以在方法中增加代码,只能在.prologue下面的区域进行
.line
.line 1用于标记java代码中的行数,没有实际含义
.invoke-direct
invoke-direct {p0}, Ljava/lang/Object;-><init>()V表示调用private或init方法,p0表示java中的this,所以这个方法的含义是调用Object的init方法,并把返回值赋给p0,这里需要补充一个小知识点:
在smali里的所有操作都必须经过寄存器来进行,本地寄存器用v开头数字结尾的符号来表示,如v0、v1、v2、…
参数寄存器则使用p开头数字结尾的符号来表示,如p0、p1、p2、…特别注意的是,p0不一定是函数中的第一个参数,在非static函数中, p0代指“this”,p1表示函数的第一个参数,p2代表函数中的第二个参数…,而在static函数中p0才对应第一个参数(因为Java的static方法中没有this方法)。
本地寄存器没有限制,理论上是可以任意使用的。
这里需要注意的是,再使用vx和px的时候一定不要超过定义的限制(也就是.registers和.locals),虽然超过代码编译时不会报错,但是在运行时会报错,而且在代码反编译的时候也会报错:
虽然这种报错不一定是参数个数超了,但如果之前反编译正常,修改之后不正常,这块可以作为一个检查点
const-string
const-string v0, "test"表示定义一个String常量,并把值赋给v0,也就是第一个本地寄存器。
下面再补充一些和const-string相关的内容
const
- 定义常量
1 | const/4`,`4`代表`4`个字节,最大只允许存放4位数值(`4`个二进制位),取值范围为 `-8 and 7 |
const/high16 最大只允许存放高16位数值
1 | #定义一个容器 比如0xFFFF0000末四位补0 存入高四位0XFFFF |
const-wide 占用两个寄存器vx和vx+1,共64位,数值必须以L结尾,否则编译不通过
1 | const-wide v0,30 #占用v0和v1 |
const-wide/16 定义两个相连容器,最大只允许存放16位数据const-wide/32 定义两个相连容器,最大只允许存放32位数据const-wide 定义两个相连容器,最大只允许存放64位数据const-wide/high16 定义两个相连容器,只允许存放高16位数据
iput-object
iput-object v0, p0, LTest;->name:Ljava/lang/String;表示把v0的值(也就是字符串常量test)赋给p0(也就是this)的name字段,Ltest标记的是p0的类型,:Ljava/lang/String是标记name的类型。
具体语法是:
1 | iput-object 要设置的值, 要设置值的对象, 对象类型;->字段名:字段类型; |
这里的iput表示设置非静态成员变量,设置静态变量要用sput,语法如下
1 | sput-object 要设置的值, 类型;->字段名:字段类型; |
这里的-object表示对非基本类型赋值,如果是基本类型可以通过iput、sput、iput-boolean、sput-boolean进行设置,语法是一样的。
iget-obejct
与iput-object对应,iget-object是为了获取数据,具体语法是:
1 | iget-object 接收值的寄存器, 接受值来源对象(谁的字段), 对象类型;->字段名:字段类型; |
例如:
1 | iget-object v0, p0, Lcom/syske/android/Activity;->_view:Lcom/syske/common/View; |
静态变量的字段也是一样的:
1 | sget-object 接收值的寄存器, 静态对象类型;->字段名:字段类型; |
基本类型的字段也是类似的,需要通过iget、sget、iget-boolean、sget-boolean来获取
return-void
return-void表示方法没有返回值,也就是void,同时返回值还可以是:
return vx:返回vx寄存器中的值。return-wide vx:返回在vx,vx+1寄存器的doubl e/long值。return-object vx:返回在 vx 寄存器的对象引用。
| smali方法返回关键字 | java |
|---|---|
| return | byte |
| return | short |
| return | int |
| return-wide | long |
| return | float |
| return-wide | double |
| return | char |
| return | boolean |
| return-void | void |
| return-object | 数组 |
| return-object | object |
静态方法
自此,构造方法相关方法我们算是基本讲解完了,下面我们分析下静态main方法的源码,前面已经分享的内容这里直接忽略了。
1 | .method public static main([Ljava/lang/String;)V |
invoke-virtual
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V表示 调用protected或public方法(非私有实例方法),这里的v0就表示java/lang/System, v1表示方法的入参,具体语法如下:
1 | invoke-virtual {对象实例, 方法入参}, 实例类型;->方法名(入参类别;)返回值 |
如果参数是多参数则直接写多个参数即可,如上面的源码:
1 | invoke-virtual {v0, v2, v2}, LTest;->test(IZ)Ljava/lang/String; |
另外与invoke-virtual类似的语句还有几个在,这里做一个简单的总结:
| 方法 | 说明 |
|---|---|
| invoke-virtual | 用于非私有实例方法的调用 |
| invoke-direct | 用于构造方法以及私有方法的调用 |
| invoke-static | 调用静态方法 |
| invoke-super | 调用父类的方法 |
| invoke-interface | 调用接口方法 |
new-instance
new-instance v0, LTest;表示创建LTest的实例对象,并将对象引用赋值给v0寄存器 紧接着,调用LTets的实例化方法:
1 | invoke-direct {v0}, LTest;-><init>()V`表示调用实例化方法,并把返回值赋给`v0 |
move-result-object
move-result-object v0表示将上一次(紧挨着的)方法执行结果指向v0,也就是接收方法返回值。
扩展知识
好了,至此我们已经把今天示例代码基本分析完了,同时也对smali的基本语法有了一些初步的认识,基于以上这些知识点,我们基本上已经可以阅读smali源码了,但是为了更全面地学习smali的语法,我觉得还是有必要补充一些额外的知识点,比如条件语句的语法:
条件语句
| 语句 | 说明 |
|---|---|
| if-eq vA, vB, :cond_** | 如果vA等于vB则跳转到:cond_** #equal |
| if-ne vA, vB, :cond_** | 如果vA不等于vB则跳转到:cond_** # not equal |
| if-lt vA, vB, :cond_** | 如果vA小于vB则跳转到:cond_** #less than |
| if-ge vA, vB, :cond_** | 如果vA大于等于vB则跳转到:cond_** # greater equal |
| if-gt vA, vB, :cond_** | 如果vA大于vB则跳转到:cond_** # greater than |
| if-le vA, vB, :cond_** | 如果vA小于等于vB则跳转到:cond_** # less equal |
| if-eqz vA, :cond_** | 如果vA等于0则跳转到:cond_** #zero |
| if-nez vA, :cond_** | 如果vA不等于0则跳转到:cond_** |
| if-ltz vA, :cond_** | 如果vA小于0则跳转到:cond_** |
| if-gez vA, :cond_** | 如果vA大于等于0则跳转到:cond_** |
| if-gtz vA, :cond_** | 如果vA大于0则跳转到:cond_** |
| if-lez vA, :cond_** | 如果vA小于等于0则跳转到:cond_** |
其中,代码中:cond_**语句对应if条件中的:cond_**,表示该条件语句结束:
语法关键词
下面是一些常用的语法关键词
| 关键词 | 说明 |
|---|---|
| .class | 定义java类名 |
| .super | 定义父类名 |
| .source | 定义Java源文件名 |
| .filed | 定义字段 |
| .method | 定义方法开始 |
| .end method | 定义方法结束 |
| .annotation | 定义注解开始 |
| .end annotation | 定义注解结束 |
| .implements | 定义接口指令 |
| .local | 指定了方法内局部变量的个数 |
| .registers | 指定方法内使用寄存器的总数 |
| .prologue | 表示方法中代码的开始处 |
| .line | 表示java源文件中指定行 |
| .paramter | 指定了方法的参数 |
| .param | 和.paramter含义一致,但是表达格式不同 |






