smali文件格式
smali文件的头3行描述了当前类的一些信息,格式如下:
.class<访问权限>[修饰关键字]<类名>
.super<父类名>
.source<源文件名>
打开MainAcivity.smali,头三行代码:
.class public Lcom/droider/crackme0502/MainActivity;
.super Landroid/app/Activity;
.source "MainActivity.java"
第一行权限public
,类名为Lcom/droider/crackme0502/MainActivity;
,类名开头的L
是遵循Dalvik字节码的相关约定,表示后面跟随的字符串为一个类。
第二行,Lcom/droider/crackme0502/MainActivity;
的父类是 Landroid/app/Activity;
第三行,.source
指令指定了当前类的源文件名
经过混淆的dex文件,反编译出来的smali代码可能没有源文件信息,因此,.source
可能为空。
前三行代码过后是类的主体部分,一个类可以由多个字段或方法组成。smali文件中字段的声明使用.field
指令。字段有静态字段与实例字段两种。静态字段的声明格式如下:
# static fields
.field<访问权限>[修饰关键字]<字段名>:<字段类型>
实例字段的声明:
# instance field
.field private btnAnno:Landroid/widget/Button;
private
表示私有;字段btnAnno
,它的类型是Landroid/widget/Button;
直接方法的声明:
# direct methods
.method <访问权限> <方法名>(参数原型) <方法原型>
[.prologue] // 指定代码开始位置
[.param] // 指定方法参数
[.line] // 指定代码在源代码中的行数,混淆后可能不存在
[.locals] // 使用的局部变量个数
<代码体>
.end method
虚方法的声明和直接方法相同,只是起始处的注释为virtual methods
如果一个类实现了接口,会在.smali文件中使用.implement
指令指出。声明入下:
# interface
.implements<接口名>
注解格式声明:
# annotations
.annotation[注解属性]<注解类名>
[注解字段=值]
.end annotation
注解的作用范围可以是类、方法和字段。如果作用范围是类,指令会直接定义在smali文件中,如果是方法或者字段,指令会包含在方法或字段的定义中。例如:
# instace fields
.field public sayWhat:Ljava/lang/String;
.annotation runtime Lcom/droider/anno/MyAnnoField;
info = "Hello my friend"
.end annotation
.end field
转换成Java代码:
@ com.droid.anno MyAnnoField(info = "Hello my friend")
public String sayWhat;
原始类型
B—byte
C—char
D—double
F—float
I—int
J—long
S—short
V—void
Z—boolean
[XXX—array
Lpackage/name/ObjName—object; // 前面表示对象所在包路径,分号表示类结束
寄存器操作
p命名法和v命名法:
假设一个函数中用到M个寄存器,实际传入的参数是N个。
根据传参规则,参数使用后N个寄存器,局部变量使用0到M-N个寄存器。
假如用到5个寄存器,2个局部参数,3个传入参数。
v命名法:
v0,v1,v2,v3,v4;
p命名法:
v0,v1,p0,p1,p2;
只改变传入参数寄存器名。
常量赋值
const v0, 0x7F030018 # R.layout.activity_challenge #从R中取出静态值
const/4 v3, 0x2 #4也可以换成16或者high16,表示取整数值
const-string v2, "Challenge" # 取字符串
const-class v2, Context #把类对象取出
变量赋值
move vx,vy # 将vy的值赋值给vx,也可以是move-object等
move-result vx # 将上个方法调用后的结果赋值给vx,也可以是move-result-object
return-object vx # 将vx的对象作为函数返回值
new-instance v0, ChallengePagerAdapter # 实例化一个对象存入v0中
对象赋值
iput-object a,(this),b 将a的值给b,一般用于b的初始化
iget-object a,(this),b 将b的值给a,一般用于获取b的地址,接着调用它
# eg.
iput-object v0, p0, ChallengeActivity->actionBar:ActionBar
iget-object v0, p0, ChallengeActivity->actionBar:ActionBar
函数操作
最基础的函数操作一般有以下四个:
1.private:invoke-direct
2.public|protected: invoke-virtual
3.static:invoke-static
4.parent: invoke-super
基本调用形式:invoke-xxx {参数},类;->函数(参数原型)
# eg.
invoke-super {p0, p1}, Landroid/support/v4/app/FragmentActivity;->onCreate(Landroid/os/Bundle;)V
<=对应源码=>
super.onCreate(savedInstanceState); // 其中p0是this,其父类是FragmentActivity,p1,是savedInstanceState,其原型是Bundle;即调用p0->onCreate(p1)
程序相关语法
判断语句
if-eq vA, vB, :cond_X 如果vA等于vB则跳转到:cond_X
if-ne vA, vB, :cond_X 如果vA不等于vB则跳转到:cond_X
if-lt vA, vB, :cond_X 如果vA小于vB则跳转到:cond_X
if-ge vA, vB, :cond_X 如果vA大于等于vB则跳转到:cond_X
if-gt vA, vB, :cond_X 如果vA大于vB则跳转到:cond_X
if-le vA, vB, :cond_X 如果vA小于等于vB则跳转到:cond_X
if-eqz vA, :cond_X 如果vA等于0则跳转到:cond_X
if-nez vA, :cond_X 如果vA不等于0则跳转到:cond_X
if-ltz vA, :cond_X 如果vA小于0则跳转到:cond_X
if-gez vA, :cond_X 如果vA大于等于0则跳转到:cond_X
if-gtz vA, :cond_X 如果vA大于0则跳转到:cond_X
if-lez vA, :cond_X 如果vA小于等于0则跳转到:cond_X
循环语句
public void encrypt(String str) {
String ans = "";
for (int i = 0 ; i < str.length();i++){
ans += str.charAt(i);
}
Log.e("ans:",ans);
}
<=对应smali=>
.method public encrypt(Ljava/lang/String;)V # 方法:public void encrypt(String str)
.locals 4 # 四个变量
.param p1, "str" # 方法参数:Ljava/lang/String;
.prologue # 代码起始处
const-string v0, "" # 赋值给ans
.local v0, "ans":Ljava/lang/String;
const/4 v1, 0x0 # 赋值给 i
.local v1, "i":I
:goto_0 # 循环的地方
invoke-virtual {p1}, Ljava/lang/String;->length()I # 调用虚函数(参数p1)String类中的length方法,返回int
move-result v2 #把前一步的结果放在v2中
if-ge v1, v2, :cond_0 # 如果v1<v2,即i<str.length(),就跳到:cond_0
new-instance v2, Ljava/lang/StringBuilder; # 创建实例 v2 ,类型是Ljava/lang/StringBuilder;
invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()V # v2初始化
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; # v2.append(v0)
move-result-object v2 # v2.append(v0) => v2 这里 v2是v0的值,v2=ans
invoke-virtual {p1, v1}, Ljava/lang/String;->charAt(I)C # str.charAt(i)
move-result v3 # str.charAt(i) => v3
invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(C)Ljava/lang/StringBuilder; # ans + v3
move-result-object v2 # ans + v3 =>v2
invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; # v2.toString()
move-result-object v0 # v2=>v0
add-int/lit8 v1, v1, 0x1 # i++
goto :goto_0 # 跳转指令
:cond_0
const-string v2, "ans:" # 常量赋值 v2 = "ans:"
invoke-static {v2, v0}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I # Log.e(v2,v0)
return-void
.end method
switch语句
public void encrypt(int flag) {
String ans = null;
switch (flag){
case 0:
ans = "ans is 0";
break;
default:
ans = "noans";
break;
}
Log.v("ans:",ans);
}
<=对应smali=>
.method public encrypt(I)V # 方法 public void encrypt(int flag)
.locals 2 # 两个变量
.param p1, "flag" # 一个参数 flag
.prologue
const/4 v0, 0x0 # v0赋值,
.local v0, "ans":Ljava/lang/String; # String ans = null; v0就是ans
packed-switch p1, :pswitch_data_0 # pswitch_data_0指定case区域的开头及结尾
const-string v0, "noans" # 默认 赋值 ans="noans"
:goto_0
const-string v1, "ans:" # 赋值 v1 = "ans:"
invoke-static {v1, v0}, Landroid/util/Log;->v(Ljava/lang/String;Ljava/lang/String;)I # Log.v
return-void
:pswitch_0 #pswitch_<case的值> case 0: ans="ans is 0"
const-string v0, "ans is 0"
goto :goto_0 # break
nop
:pswitch_data_0 #case区域的结束
.packed-switch 0x0 #定义case的情况
:pswitch_0 #case 0
.end packed-switch
.end method
其中case定义情况有两种:
1.从0开始递增
packed-switch p1, :pswitch_data_0
...
:pswitch_data_0
.packed-switch 0x0
:pswitch_0
:pswitch_1
2.无规则switch
sparse-switch p1,:sswitch_data_0
...
sswitch_data_0
.sparse-switch
0xa -> : sswitch_0
0xb -> : sswitch_1 # 字符会转化成数组
try-catch语句
public void encrypt(int flag) {
String ans = null;
try {
ans = "ok!";
} catch (Exception e){
ans = e.toString();
}
Log.d("error",ans);
}
<=对应smali=>
.method public encrypt(I)V # public void encrypt(int flag) {
.locals 3 # 3个变量
.param p1, "flag" # 参数
.prologue # 代码开始
const/4 v0, 0x0
.line 20
.local v0, "ans":Ljava/lang/String; # String ans = null;
:try_start_0 # 第一个try开始,
const-string v0, "ok!" # ans = "ok"
:try_end_0 # 第一个try结束(主要是可能有多个try)
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0
:goto_0
const-string v2, "error"
invoke-static {v2, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
return-void
:catch_0 #第一个catch
move-exception v1
.local v1, "e":Ljava/lang/Exception;
invoke-virtual {v1}, Ljava/lang/Exception;->toString()Ljava/lang/String;
move-result-object v0
goto :goto_0
.end method
类
baksmali在反编译时,为每个类单独生成一个smali文件,内部类作为一个独立类,也拥有自己独立的smali文件,只是内部类的文件名形式为[外部类]$[内部类].smali
class Outer{
class Inner{}
}
上述代码生成两个文件:Outer.smali
和Out$Inner.smali
this$0 是什么? 是内部类自动保留的一个指向所在外部类的引用。左边的this表示为父类的引用,右边的0表示引用层数。
public class Outer{ //this$0
public class FirstInner{ //this$1
public class SecondInner{ //this$2
public class ThirdInner{
//在ThirdInner中访问FirstInner类的引用为this$1
}
}
}
}
this$X型字段都被指定了synthetic属性,表明他们是被编译器合成的虚构的,开发者并没有声明该字段。
Reference
https://www.anquanke.com/post/id/85035
《Android软件安全与逆向分析》