对于dex字节码,官方提供了三篇文档对其进行比较详尽的介绍,分别是:
- Dalvik bytecode :主要介绍的是Dalvik字节码的总体设计理念,还提供了全部的字节码指令介绍。如果没有一点计算机系统相关知识,还是比较难懂的,有部分的描述也不是太清晰,可参考本人这篇文章的翻译:Dalvik bytecode的总体设计
- Dalvik Executable instruction formats :主要讲解的是
dex指令的二进制格式。 - Dalvik Executable format :介绍的是
Dex文件的格式。
上面三篇文章,通读下来,如果不认真仔细看,很难把它们之间的关系串联起来,在实际阅读分析系统源码或者一些字节码指令时,还是较为困难的。下面就以一个实际例子,分析dex字节码指令的运用和阅读。
源码例子
public class Main {
public static void main(String[] args) {
Hello hello = new Hello();
String sentence = hello.sayHello();
System.out.printf(sentence);
}
public static class Hello {
public String sayHello() {
return "hello!!!!";
}
}
}
我们主要分析 String sentence = hello.sayHello(); 这行代码的字节码指令情况。
使用工具,将上面源码编译成dex文件,用 010编译器 打开 :

从上图看,dex 文件总体的格式,就如 Dalvik Executable format 中介绍的一样。上图中对应字段,和下图dex 文件格式是一致吻合的。

要找到 String sentence = hello.sayHello(); 该语句的对应二进制指令,就要在dex 文件中,首先找到 main 方法。
根据官方文档,它是在 :
dex文件 —>class_defs的class_def_item —> class_data_item —> direct_methods
在本例子中,main 方法隐藏在字节码中,如下图:

main 方法的二进制字节码指令就在 insns 这个18位数组中。
我们看看 insns 这些指令的二进制:

如果仅仅看二进制,很难明白这么多条指令的内容,我们可以借助dexdump,先看看二进制译码后的字节码指令究竟是怎样的,dexdump 如下(只包含 dexdump 的 main 方法的部分):

从上图看到,对于语句 :
String sentence = hello.sayHello(); ,
其对应的dex字节码指令是:
invoke-virtual {v0}, LMain$Hello;.sayHello:()Ljava/lang/String; // method@0001
该指令在二进制中的表示是:
6e10 0100 0000
这么复杂的一个dex字节码指令,对应的二进制表示,是如此的简单。这个编码和译码的关系就是 Dalvik bytecode 和 Dalvik Executable instruction formats 里面说的内容.
接下来,就是详细介绍该如何译码 6e10 0100 0000 这条二进制指令 。
要译码指令,首先要清楚,指令的格式 如何。
从 Dalvik bytecode 知道:
指令 = 指令码op + 指令格式format + 参数
先不要管这几个组成部分在指令中的排列顺序,一条指令就是上面的三部分组成的,三部分确定一条字节码指令。
步骤一 :译码op
要译码,首先,我们要确定op(指令码)是什么?
在 6e10 0100 0000 二进制指令中,op = 6e 。
这里有一个疑问,从 Dalvik bytecode 和 Dalvik Executable instruction formats 中,我们知道一条指令,大概 (仅仅是大概)是下面的类似形式:
B|A|op CCCC
从直观阅读来看, op 在参数 B|A 之后,那么在这里,10 才是 op, 为什么是 6e 呢?
主要原因是,字节码的字节序为 Little Endian 。这样,在16进制表示中,我们用编辑器看到op 在前面,而实际上,指令的格式是op 在后面,这有利于程序按字节序解释指令的。
那么,在6e10 0100 0000 中,6e10 对应到字节码指令,实际是 1|0|6e 的形式。
步骤二 :确立Format
知道 op=6e 后,下一步,就是确定指令format。查阅 Dalvik bytecode 的指令集表格:
| op & format | Mnemonic / Syntax | Arguement |
|---|---|---|
6e 35c | 6e: invoke-virtual {vC, vD, vE, vF, vG}, meth@BBBB | A: argument word count (4 bits) B: method reference index (16 bits) C…G: argument registers (4 bits each) |
从表格中,看到,op=6e 对应的 format=35c(op 和 format 是多对一关系) 。
format=35c ,实际上,这个 35c 是 format 的ID,这个我们可以在 Dalvik Executable instruction formats 中提供的表格中查到:
| Format | ID | Syntax |
|---|---|---|
| A|G|op BBBB F|E|D|C | 35c | [A=5] op {vC, vD, vE, vF, vG}, meth@BBBB [A=5] op {vC, vD, vE, vF, vG}, site@BBBB [A=5] op {vC, vD, vE, vF, vG}, type@BBBB [A=4] op {vC, vD, vE, vF}, kind@BBBB [A=3] op {vC, vD, vE}, kind@BBBB [A=2] op {vC, vD}, kind@BBBB [A=1] op {vC}, kind@BBBB [A=0] op {}, kind@BBBB |
这里,我们就可以确立 6e10 0100 0000 二进制指令对应的字节码指令格式为 :
A|G|op
BBBB
F|E|D|C
步骤三 :分析参数
前面我们已经知道指令的Format,这时候我们需要 代入参数。
6e10 0100 0000 中,6e 后面的10 ,对应Format,可以代入 A=1,G=0,确立,字节码指令语法为:
op {vC}, kind@BBBB
op 实际为 invoke-virtual , C=0 , kind为method,BBBB=0001 (注意 Little Endian )。
最终, 6e10 0100 0000 的字节码指令(其实只是助记符,方便人理解指令) :
invoke-virtual {V0}, method@0001
invoke-virtual {V0}, method@0001 这条指令实际上就是调用一个virtual方法(非private,非static,非final,非构造函数),这个方法的索引由 参数 0001 指定。
那么,0001 方法索引在哪里?这个就要到dex文件中的method_ids中去找。

在method_ids 数组中,准确找到了index=0x0001 的对应方法为 Main$Hello.sayHello() 。
剩下的参数是 {V0} , 其意思是需要使用 V0 寄存器的值作为方法调用的参数,但 sayHello 是不带参数的方法。在 Dalvik bytecode 中提及,对于实例方法(instantce methods)的调用,第一个参数为 调用者本身,所以 V0 存有的肯定是 Hello 类的实例对象,这符合前面指令new-instance 时,V0 寄存器是作为目的寄存器的情况。
总结
上文全部,只是其中一个方法调用的例子,但是实际上,任何二进制指令都是可以按以上步骤进行译码:
指令 = 指令码op + 指令格式format + 参数- 译码步骤:
确立op --> 查 format --> 读参数 --> 最终译码 - 字节码指令实际上不是必然要存在的,它只是反编译
dex文件的产物,帮组人更易理解指令,也可以理解为一种IR。 - 理解字节码指令和
dex文件相关的知识,利于对android虚拟机,AOT等模块的进行深入了解。

本文通过一个实例详细解析了如何从 Dex 字节码的二进制格式逐步解读字节码指令,涉及Dalvik Executable instruction formats和Dalvik bytecode的相关知识。通过译码步骤,包括确定指令码、确立指令格式和分析参数,阐述了字节码指令的解析过程,有助于深入理解Android系统的字节码执行机制。
2721

被折叠的 条评论
为什么被折叠?



