文章目录
从一个hello world开始
smali代码和dex之间有着千丝万缕的联系,先从一个最简单的dex文件hello world开始,学习整个dex文件的结构
.class public LHelloWorld;
.super Ljava/lang/Object;
.method public static main([Ljava/lang/String;)V
.registers 2
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
const-string v1, "Hello World!"
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
return-void
.end method
以上面这段smali代码为例
D:\Android\tools>java -jar smali.jar -o classes.dex HelloWorld.smali
然后将smali代码转成dex文件,并打成压缩包,命名为HelloWorld.zip
确保adb和模拟器连接成功
再将打包好的zip文件上传到模拟器
接着执行dex文件,可以看到成功打印了Hello World
Dex文件结构
Dex文件主要可以分为三大块:
- Dex文件头
- 各种数据的数组,包括字符串 类型 方法原型 字段 方法
- 类数据
文件头
首先来看文件头的部分
总共占0x70个字节大小,比较重要的字段有四个
- dex_magic:表示dex文件的文件标识,特征字符串
- checksum:校验和,对文件求了32位的哈希值(从字段3开始到文件末尾)
- signature:表示sha1,对文件求哈希值(从字段4开始到文件末尾)
- file_size:表示文件大小
除了这四个字段以外,文件头部还有其他一些字段
- header_size:dex文件头大小
- endian_tag:数据排列方式–小端方式
各种表的大小及偏移
- string_ids_size和string_ids_off,字符串表的大小和偏移
- type_ids_size和type_ids_off,类型表的大小和偏移
- proto_ids_size和proto_ids_off,字段表的大小和偏移
- class_defs_size和class_defs_off,类数据表的大小和偏移
各种数据的数组
dex文件第二部分是各种数据的数组,包括字符串 类型 方法原型 字段 方法
字符串表
字符串表项,是一个字符串数据的偏移,偏移指向的是一个string_data结构。string_data结构中有两个字段
字段1:字符串长度,数据类型是uleb128,安卓中特有的变长的数据类型
字段2:存储数据,字符串以0结尾
类型表
类型表,保存的是一个索引值,指向的是字符串表
例如:索引值为3表示的是字符串表的下标为3的位置指向的是L/java/lang/Object
这个字符串。
原型表
原型表中存储的是函数原型的各部分描述信息。包括短类型(shorty_idx),返回类型(return_type_idx),参数的类型(parameters_off),最终还是一个指向字符串表的数组下标。
注意:字段为返回类型(return_type_idx)的值,是类型表中的索引
字段表
存储的是字段信息,包括字段所在类(class_idx),字段的类型(type_idx),字段的名称(name_idx)。
class_idx是类型表中的索引,type_idx是类型表中的索引,字段名称的索引是字符串的数组下标
方法表
方法表中存储的是方法的信息,包括方法所在的类(class_dex),方法的原型(proto_idx),方法的名称(name_idx)。
其中class_idx是类型表的索引,proto_idx是原型表的索引,方法名称的索引(name_idx)是字符串表的数组下标
类数据
类数据也是一个数组,每一个元素就是一个类的相关信息。现在所分析的这个文件因为只有一个类,所以只有一个类的信息。
在表项中的class_data中存储的是类数据,包括类名索引,访问属性,父类索引,接口偏移,源码索引,注解偏移,类数据偏移
其中,整个类的数据在class_data_item这个结构中。
method_list是类内所有方法的列表,因为当前这个文件只有一个Main方法,所以列表内只有一个结构体。结构体中有方法的基本信息,包括方法索引,访问标志,代码偏移,代码信息
其中code_item是整个代码的信息,里面有两个字段特别重要
ins_size:指令长度
ushort insns[8]:指令数组
这个数组存放的就是被翻译成smali代码的虚拟机指令,也就是OpCode
手工解析Smali代码
62 00 00 00 1A 01 00 00 6E 20 01 00 10 00 0E 00
接下来复制这一段十六进制,手工将这段代码解析成Smali代码。
这里还需要借助一个文档《(中文)Dalvik操作码》,里面有所有的Opcode和对应的操作码以及示例。
首先找到62,62代表的指令含义是根据字段ID读取静态对象引用字段到vx,接着,还需要看懂旁边的示例
6201 0C00
解析为smali代码是
sget‐object v1, Test3.os1:Lja va/lang/Object; // field@000c
读取 Object 的静态对象引用字段 os1(字段表#CH 条目)到 v1。
也就说这条指令一共4个字节
62 代表操作码 sget‐object
01 代表的是序号为1的寄存器v1
000C 代表字段表索引为0xC的字段
接着再来看我们要解析的Opcode
62 00 00 00 1A 01 00 00 6E 20 01 00 10 00 0E 00
先解析前四个字节
62 00 00 00
具体含义如下
62 代表操作码 sget‐object
00 代表的是序号为0的寄存器v0
0000 代表字段表索引为00的字段
接下来在字段表中找到第0个字段
java.io.PrintStream java.lang.System.out
第0个字段就是out这个对象,将这个字段翻译为smali代码
Ljava.lang.System;->out:java.io.PrintStream
那么前四个字节
62 00 00 00
解析为smali代码就是
sget‐object v0, Ljava.lang.System;->out:java.io.PrintStream
接着来看1A
1A08 0000
解析为
const‐string v8, "" // string @0000
存入 string@0000(字符串表#0 条目)的引用到 v 8
需要解析的Opcode
1A 01 00 00
接着找到字符串表索引为0的字符串
那么这条指令解析为Smali代码就是
const‐string v1, "Hello World!"
接下来查找6E
直接看例子
6E53 0600 0421 ‐ invoke‐virtual { v4, v0, v1, v2, v3}, Test2.method5:(IIII)V // me thod@0006
这条指令比较复杂,一共6个字节,其中
6E---> invoke‐virtual
5---> 参数数量
3---> v3
0600--->method@0006
0421--->v4, v0, v1, v2
调用参数表的编译比较诡异,如果参数的数量大于4,第5个参数将编译在指令字节的下一个字节的4个最低位
那么我们要解析的Opcode
6E 20 01 00 10 00
就可以解析为
6E---> invoke‐virtual
2---> 参数数量
01 00--->method@0001
10 00--->v1 v0
翻译出来就是
invoke‐virtual {v0,v1},method@01
找到函数表下标为1的方法
void java.io.PrintStream.println(java.lang.String)
转为Smali代码
Ljava/io/PrintStream;->println(Ljava/lang/String;)V
完整的smali代码就是
invoke‐virtual {v0,v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
最后看0E
表示返回值为空
62 00 00 00 1A 01 00 00 6E 20 01 00 10 00 0E 00
解析出来完整的smali代码就是
sget‐object v0, Ljava.lang.System;->out:java.io.PrintStream;
const‐string v1, "Hello World!"
invoke‐virtual {v0,v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
return‐void
和HelloWorld.smali的源码没有区别