码迷,mamicode.com
首页 > 其他好文 > 详细

dalvik字节码问答

时间:2016-05-12 16:09:36      阅读:309      评论:0      收藏:0      [点我收藏+]

标签:

参考资料

https://source.android.com/devices/tech/dalvik/dalvik-bytecode.html

https://source.android.com/devices/tech/dalvik/instruction-formats.html

http://www.milk.com/kodebase/dalvik-docs-mirror/docs/dalvik-bytecode.html



什么是Dalvik字节码?

dalvik 的虚拟机指令代码。dalvik字节码之于dalvik VM,就像可执行程序的机器码之于CPU一样。

dalvik 字节码是一种基于寄存器的虚拟机指令系统。它的含义是,每一个可执行函数都有一个Frame,Frame的大小是固定,包括固定数目大小的虚拟寄存器。
dalvik VM 会为这个函数的参数、本地变量、临时变量都分配一个虚拟寄存器。所有的运算都是在寄存器之间完成的。
当然,我说的,变量与寄存器不是一对一的关系,而是多对一的关系。因为变量的生命周期并不完全一样,有些变量之间生命周期不重合,就会出现共用一个寄存器的情况。

在下面的描述中,我们用v0, v1, ... vN 表示寄存器,一共有N+1寄存器。

除了常用的寄存器外,还有两个隐含的寄存器:result和exception。当函数返回时,返回值被写入到result寄存器,而抛出异常时,异常对象会被放入exception寄存器。
在dalvik的实现中,这两个寄存器都是放在Thread对象中的。一个Thread对象代表一个java线程,一个java线程同时只需要一个result和exception寄存器。

dalvik字节码指令分成几大部分:
  • const 指令:负责将常量值放入到虚拟寄存器中;
  • mov指令:在虚拟寄存器之间拷贝数据;
  • 比较指令:比较两个寄存器的值,有部分指令是直接与0比较的;
  • 分支指令:包括if系列指令如if-eq, if-ge等等,以及switch指令,goto指令。switch指令作为一个单独指令存在的;
  • invoke指令:函数调用指令,分成invoke-direct, invoke-static, invoke-native, invoke-virtual和invoke-interface几大系列;
  • field操作指令:包括put/get,针对静态域和非静态域;
  • class相关的指令:比如new-instance, instance-of,checkcast等等
  • 异常处理指令:包括throw, move-exception这样的指令;
  • 算术、位移等指令:不多说了;
  • 其他指令

字节码的格式

dalvik字节码是以一个字(两字节)为单位,必须是一个字的整数倍。所以,你看到dexdump出的字节指令,其长度都必须x2才能得到其字节长度。

不同指令的字节码长度不一样,相同操作类型的指令长度是一样的,根据指令操作类型,就可以确定指令的长度。比如 mov v1, v2,指令长度是1字。

字节码在dex文件中存储时,都是小端的。因此,第一个字节必须是操作类型(操作符),是一个0~255范围内的一个数字,根据操作符,我们可以确定后续有多少个字以及表示什么意思。


每个操作符都有一个格式(format),根据格式,我们可以知道该指令的长度,参数有哪些、每个参数的长度、参数是如何存储的,这些重要的信息。不同的指令可以有相同的格式,因此,dalvik将他们分类,并给每个格式以不同的描述。每种格式被称为Format ID。

下表详细说明了dalvik字节码的格式(来自https://source.android.com/devices/tech/dalvik/instruction-formats.html)。这个表分为4列。其中前3列分别是Format, ID, Syntax。

Format列

"op" 表示操作符,8位1字节无符号整数。大写字母"A","B","C"等表示4位数;"?" 表示空,占4位,必须为0;
每个部分用‘|‘分割,表示一个逻辑上独立的部分。
排列最小单位是字(两字节),中间不分割。例如B|A|op,这个长度是1个字(两字节),但是包括3部分,op操作符和 A, B两个参数; 而AA|opBBBB 包含两个字(4字节),其中"AA|op"是第一个字,而"BBBB"则是在第二字。

因为在实际存储时,是以小端方式,而在描述时,则以大端方式。阅读时要注意。比如 ??|op , 在实际存储时,“op”在第0字节,而“??”在第1字节。在如
AA|opBBBB 则是 op在第0字节,AA在第1字节,BBBB 占用第2,3字节。

ID列

ID由3部分组成: <长度><寄存器个数><类型描述>。 例如 "10x"  "1"表示指令长度为1, "0"表示用到0个寄存器,"x"表示类型描述; “21t“ 表示指令长度为2,用到1个寄存器,并需要跳转("t"表示分支跳转)。
最后类型描述部分各个字符表示的意思如下表:
Mnemonic Bit Sizes Meaning
b 8 有符号1字节立即数
c 16, 32 常量池索引
f 16 interface相关的常量
h 16 16位有符号立即数,表示32/64位中的最高16位
i 32 32位有符号立即数,或者32位的浮点数
l 64 64位有符号立即数,或者双精度浮点数
m 16 method的常量索引值
n 4 4位有符号立即数
s 16 16位有符号立即数
t 8, 16, 32 分支目标偏移值
x 0 无更多附加数

Syntax列

字节码可以反汇编,形成类似汇编代码的助记符形式,方便人类阅读。一般形式是 op Arg1[, Arg2, [,Arg3 ...]] 这种形式。
你在Format列看到的字符会反映到Syntax列。例如,如果在Foramt列看到了"AA",那么在Syntax列看到的“AA”所指的是同一个概念。

在Syntax,会给它加不同前缀,形成不同的格式,代表不同含义:
  • "vX": 前缀"v"表示这是一个虚拟寄存器,"X"对应Foramt列中的参数,"X"的值就是虚拟寄存器的索引;
  • "#+X":前缀“#+”表示一个有符号的立即数;
  • "+X": 前缀“+"表示一个相对偏移地址;
  • "kind@X" :前缀"kind" 表示类型(只能取"type","meth", "field", "string",分别表示解析表中class,method,feild,string的索引),“X”是索引值
  • 另外[X=N]这样的格式出现在某条指令之前,表示X满足N时有效。例如[A=2] 表示A的值为2时。


Format ID Syntax Notable Opcodes Covered
N/A 00x N/A 针对一些不使用的指令
??|op 10x op  
B|A|op 12x op vA, vB  
11n op vA, #+B  
AA|op 11x op vAA  
10t op +AA goto
??|opAAAA 20t op +AAAA goto/16
AA|opBBBB 20bc op AA, kind@BBBB 目前没有该格式的指令
AA|opBBBB 22x op vAA, vBBBB  
21t op vAA, +BBBB  
21s op vAA, #+BBBB  
21h op vAA, #+BBBB0000
op vAA, #+BBBB000000000000
 
21c op vAA, type@BBBB
op vAA, field@BBBB
op vAA, string@BBBB
check-cast
const-class
const-string
AA|opCC|BB 23x op vAA, vBB, vCC  
22b op vAA, vBB, #+CC  
B|A|opCCCC 22t op vA, vB, +CCCC  
22s op vA, vB, #+CCCC  
22c op vA, vB, type@CCCC
op vA, vB, field@CCCC
instance-of
22cs op vA, vB, fieldoff@CCCC suggested format for statically linked field access instructions of format 22c
??|opAAAAloAAAAhi 30t op +AAAAAAAA goto/32
??|opAAAA BBBB 32x op vAAAA, vBBBB  
AA|opBBBBloBBBBhi 31i op vAA, #+BBBBBBBB  
31t op vAA, +BBBBBBBB  
31c op vAA, string@BBBBBBBB const-string/jumbo
A|G|opBBBB F|E|D|C 35c [A=5op {vC, vD, vE, vF, vG}, meth@BBBB
[A=5op {vC, vD, vE, vF, vG}, type@BBBB
[A=4op {vC, vD, vE, vF}, kind@BBBB
[A=3op {vC, vD, vE}, kind@BBBB
[A=2op {vC, vD}, kind@BBBB
[A=1op {vC}, kind@BBBB
[A=0op {}, kind@BBBB

The unusual choice in lettering here reflects a desire to make the count and the reference index have the same label as in format 3rc.

 
35ms [A=5op {vC, vD, vE, vF, vG}, vtaboff@BBBB
[A=4op {vC, vD, vE, vF}, vtaboff@BBBB
[A=3op {vC, vD, vE}, vtaboff@BBBB
[A=2op {vC, vD}, vtaboff@BBBB
[A=1op {vC}, vtaboff@BBBB

The unusual choice in lettering here reflects a desire to make the count and the reference index have the same label as in format 3rms.

suggested format for statically linked invoke-virtual and invoke-super instructions of format 35c
35mi [A=5op {vC, vD, vE, vF, vG}, inline@BBBB
[A=4op {vC, vD, vE, vF}, inline@BBBB
[A=3op {vC, vD, vE}, inline@BBBB
[A=2op {vC, vD}, inline@BBBB
[A=1op {vC}, inline@BBBB

The unusual choice in lettering here reflects a desire to make the count and the reference index have the same label as in format 3rmi.

suggested format for inline linked invoke-staticand invoke-virtual instructions of format 35c
AA|opBBBB CCCC 3rc op {vCCCC .. vNNNN}, meth@BBBB
op {vCCCC .. vNNNN}, type@BBBB

where NNNN = CCCC+AA-1, that is Adetermines the count 0..255, and Cdetermines the first register

 
3rms op {vCCCC .. vNNNN}, vtaboff@BBBB

where NNNN = CCCC+AA-1, that is Adetermines the count 0..255, and Cdetermines the first register

suggested format for statically linked invoke-virtual and invoke-super instructions of format3rc
3rmi op {vCCCC .. vNNNN}, inline@BBBB

where NNNN = CCCC+AA-1, that is Adetermines the count 0..255, and Cdetermines the first register

suggested format for inline linked invoke-staticand invoke-virtual instructions of format 3rc
AA|opBBBBloBBBB BBBB BBBBhi 51l op vAA, #+BBBBBBBBBBBBBBBB const-wide


常量池与常量解析表是什么?

java的很多操作,都需要类、方法、域和字符串。这些反映在字节码中,都是以字符串的形式存在的。

所有的类名、方法名、域名、域和方法的签名已经常量字符串全部存储在常量池中。常量池是一个字符串池,包含所有字符串。

同时为了访问这些常量字符串,必须通过一些解析表来进行映射。这些解析表包括type表--保存class名字的索引;field表,包括field所属类的解析表索引、field名字的索引、field签名的索引;method表,包括method所属类的索引、method名字的索引、签名的列表;string表保存的则是常量池中的索引。

用下面的图表示的更加清楚:

技术分享


图中的每一项在Dex文件中都是以表的方式存在的。

这些表对程序的运行至关重要。如果指令中含有“string@XXXX”这种格式的参数时,那么就要从StringId表中取出一个StringId项,然后通过其offset,从常量池中得到对应的字符串;对于"method@XXXX"这种格式,就是从MethodId的表中得到一个MethodId项,从而读取到各项信息。


但是要注意一点,上图描绘的是dex文件中存在的关系,不是运行时的关系。当我们执行一条指令,比如 "const/string vA, string@BBBB", 寄存器vA中保存的不是“BBBB”的值,也不是从常量池中得到的字符串地址,而是一个和该字符串对应的java.lang.String对象;同理,const/class vA, class@BBBB这样的指令,vA保存的是BBBB所描述的class的java.lang.Class对象。但是 invoke指令和iput,iget等field指令,在运行时并不取得对应的java.reflect.Method和java.reflect.Field对象,而是Dalvik内部的Method和Field对象。这些内部对象只能在dalvik内部使用,不会反映在java层的代码中,java代码也无法使用。

之所以出现这些差别,是因为string,class这种对象,是必须能够让java层代码访问的,而field和method对java代码是透明的。


Dalvik为了应对这种差别,都会做一个与这些表大小一样,索引一致的cache表,这个cache表内放置了已经解析好的对象,需要的时候直接从cache中取,不需要这样来回查找了。


当然,这些表内防止的是所有java代码能够访问到的类、field、method和字符串。比如,你要访问一个android.content.Content类及其方法,那么他们的名字就会放在表内。


和类、对象操作相关的指令有哪些?

对于java语言中一些特有的表达式,它们对应的指令关系做一个深入的探讨。

new 语句对应的指令


new语句对应的是new-instance指令和一个invoke-direct指令。new-instance负责创建一个新的对象,而invoke-direct指令则是用于调用构造函数。

不是所有的new语句都会产生invoke-direct指令,如果new的时候调用的构造函数没有参数,而且被new的类也没有定义不带参数的构造函数时,它就不会产生invoke-direct指令了。


instance of语句对应的指令


对应instance-of指令


check cast指令



这种情况只发生在down cast的时候。如果你将一个父类类型转换为子类类型,就会默认生成一条check-cast指令。


<Class>.class语句



当我们要取一个类的class时候,就会产生一条 const/class指令。例如


const-class v1, Ljava/lang/Integer;


Field访问指令

Field访问指令包括get和put。Field分为static和instance两种类型。因此,对static的访问,都是sget/sput开头,而对instance类型的访问,都是iget/iput。
根据数据类型的不同,以iget为例,有iget, iget-object, iget-wide, iget-boolean, iget-char, iget-short, iget-byte。虽然有这么多种类型,但是在dalvik的实现中,其实只有iget和iget-wide两种区别。除了iget-wide之外的其他iget指令,都是以4字节为单位,占用一个虚拟寄存器;而iget-wide则是8字节为单位,占用两个寄存器。


例如 iget vA, vB, field@CCCC。表示将vA内容放到vB的CCCCfield中,即便是一个char类型的,也是4字节操作,即vB的数据是4字节长度,Field在object中占用的空间也是4字节长度;而对于iget-wide,则是使用连着的两个寄存器存储数据。按照小端原则。


因为这个缘故,在做odex优化时,iget指令被优化成iget-quick/iget-object-quick和iget-wide-quick。因为object比较特殊,所以将iget-quick分化出一个iget-object-quick来。quick优化后面章节有专门描述。


如何调用和返回函数

在dalvik中,调用函数是通过invoke指令来的。invoke指令按照参数多少,可以分为invoke和invoke/range两种类型。dalvik所有的参数都是通过虚拟寄存器传递的。当参数少时,直接用invoke,这种情况下,将所有用来传递参数的虚拟寄存器都放到指令中取;而参数较多时,就需要用到range方式,这种方式下,invoke指令中包含了参数个数和第一个参数用到的寄存器的标号。

按照调用函数的类型不同,分为:
  • invoke-direct
  • invoke-virtual
  • invoke-super
  • invoke-interface
  • invoke-static
其中,direct类的函数,包括除了static外的非虚函数,例如构造函数、prive函数等。static函数是按照direct函数对待的。那么,native函数是否也是direct函数呢?其实,这是按照实现方法来区分一个函数是native还是非native的,native方法也可以是 virtual,甚至是可以实现interface的一个方法,所以,native方法的调用,可能是上面任意一种指令。对native方法的调用与普通方法是一样的,所不同的是调用时,函数入口所有区别而已; 那么final方法是否也是direct呢?实际上final方法只是为了防止进一步继承,而不能确定它一定就是direct的,即不可能存在继承和覆盖的情况。所以fianl方法是按照virtual方法来对待的。final方法与普通方法的不同不是体现在字节码上,而是体现在类和函数的定义上,dalvik虚拟机会在加载类的时候进行验证(verify),其中一项是检查当前类的父类是否是final的,或者是否包含final方法。



invoke的指令编码是:
6e..72 35c invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB
6e: invoke-virtual
6f: invoke-super
70: invoke-direct
71: invoke-static
72: invoke-interface
A: argument word count (4 bits)
B: method reference index (16 bits)
C..G: argument registers (4 bits each)
invoke的返回值是放在result寄存器中的。要取得结果需要用move-result vN的方法来取得。

74..78 3rc invoke-kind/range {vCCCC .. vNNNN}, meth@BBBB
74: invoke-virtual/range
75: invoke-super/range
76: invoke-direct/range
77: invoke-static/range
78: invoke-interface/range
A: argument word count (8 bits)
B: method reference index (16 bits)
C: first argument register (16 bits)
N = A + C - 1
Call the indicated method. See firstinvoke-kind description above for details, caveats, and suggestions.

在参数传递上,对于非static函数,第一个参数是this对象。



switch相关指令是怎么回事?

switch有两种指令类型:packed-switch和spare-switch。这些是指令和数据放在一起的,case分支信息是放在switch指令指定的payload数据中。
指令的数据是:
2b 31t packed-switch vAA, +BBBBBBBB (with supplemental data as specified below in "packed-switch-payload Format") A: register to test
B: signed "branch" offset to table data pseudo-instruction (32 bits)
B所指出的数据就是 switch的payload数据,记录了case分支信息。如果没有对应的case分支,就会走到下条指令来执行。
下条指令是default分支的数据
2c 31t sparse-switch vAA, +BBBBBBBB (with supplemental data as specified below in "sparse-switch-payload Format") A: register to test
B: signed "branch" offset to table data pseudo-instruction (32 bits)
同上
那么payload的数据格式是什么呢?
Type packed-switch spare-switch
ushort  0x0100 0x0200
ushort size size
  int first_key
int[] targets
int[] keys
int[] targets

packed-switch是一种压缩的switch,它给出一个first_key,用vAA-first_key得到的索引,然后从targets中取得偏移,这个偏移是相对于switch的指令的。用伪代码就是
int idx = value_of(vAA) - first_key;
if (idx >= size)
  target_pc = current_pc + PACKED_SWITCH_SIZE;
else
  target_pc = current_pc + targets[idx];
注意一点: targets中的数据偏移是以16位(2字节)为偏移单位的。

那么,同理,spare-switch有keys和targets两个数组,需要先和keys比较,得到对应的索引,然后再找targets中对应的偏移。用伪代码表示就是
int idx = find_key(keys, value_of(vAA));
if (idx >= size)
  target_pc = current_pc + PACKED_SWITCH_SIZE;
else
  target_pc = current_pc + targets[idx];


quick类型的指令有那些,起到什么作用?

dalvik有一项优化,即将dex文件转换为odex文件。那么,odex与dex有什么区别呢?在文件结构上,odex文件会创建一个hash表,以类名为key,帮助系统快速找到class,从而快速加载;在字节码上,引入了一些quick指令,这些quick指令,能够加快执行速度。

quick指令是针对iget/iput,以及invoke-virtual的。iget/iput指令总共被分成3种:iget/iput-quick, iget/iput-object-quick, iget/iput-wide-quick。它们主要是类型与长度不同(见上一节),实际原理都是一样的。iget/iput的原理,是将原iget/iput中记录field在解析表中的索引,直接换成了这个field在对象中的偏移。虚拟机只要用this对象+偏移就可以获取到field在object对象中的位置,从而进行读写。

另外一个invoke-virtual-quick和invoke-virtual-quick/range,同样的,它将invoke-virtual指令内包含的method在解析表内的索引,换成了对应的虚函数在对象的虚函数表中的偏移,通过 this->class->vtable + offset的方式取得method指针,然后进行调用。

除了这些外,还有一个execute-inline指令。这个指令其实是将很多非常基础的类的函数换成dalvik内部指针。比如String类的很多函数,如charAt等,换成dalvik内部用c语言编写的 java_lang_String_charAt函数,在运行时,直接调用内部函数,速度会提升很多。execute-inline会将对应的内部函数的索引放在指令里面。


字节码的程序结构如何?

在dex文件内部,一个方法的代码数据由2大部分组成:代码数据和Try-catch数据块。代码数据包括:代码和payload数据。每个块之间都必须4字节对齐

用下面的表表示:

代码数据
 align pending (如果不是4字节对齐,增加一个0x0000的pending)
payload数据(switch, fill-array-data指令的数据,如果有)
 align pending (如果不是4字节对齐,增加一个0x0000的pending)
 try-catch数据数组

代码数据部分都是可执行的代码。与我们通常看到的代码不同,davlik字节码使用了大量的goto指令,用于实现各种分支和循环。


try-catch数据结构是什么?

try-catch数据结构,是放在代码的后面。在dalvik用CodeItem这个结构来保存code数据,其中的insns_size_in_code_units_是以2字节为单位保存从Method代码开始位置到TryItem数组的偏移。

try catch是用TryItem数组保存的,一个TryItem的结构如下

  struct TryItem {
    uint32_t start_addr_;
    uint16_t insn_count_;
    uint16_t handler_off_;

   private:
    DISALLOW_COPY_AND_ASSIGN(TryItem);
  };
start_addr_是try块开始位置,相对于代码开始位置的偏移

insn_count_记录try块涉及的代码指令数;

handler_off_ 是catch块的数据,这个偏移是相对与代码开始位置。它的数据是经过LEB128编码的,数据可以被解析成一个CatchHandlerItem结构

CatchHandlerItem结构定义如下

    struct CatchHandlerItem {
      uint16_t type_idx_;  // type index of the caught exception type
      uint32_t address_;  // handler address
    } handler_;
type_idx_ 是exception type的索引,同样是type解析表的索引;

address_ 是处理代码的入口。

HandleItem可能不止一个,会有多个。


TryItem是一个数组,数组的长度可以0,1,... N个。当异常发出时,得到异常发出位置的dex pc,用这个dex pc去匹配TryItem中start_addr_和insn_count_,看dex pc落在哪个TryItem的区间内。如果找到了TryItem块,就找它下面的CatchHandleItem,去匹配抛出的exception类型,与CatchHandlerItem定义的类型是否一致,如果一致就找到了对应的处理,如果不一致,则继续查找。

如果本函数内已经没有可以处理的Try-catch了,就向上找调用者函数,依次取查找。


当然,我们知道,java的异常,是 try-catch-finally结构,所以,如果有 finally的话,它是最后一个CatchHandleItem了。

一个TryItem的CatchHandlerItem信息结构,可以用下表来描述(数据都是经过LEB128加密的)

剩余handler_catch 个数(int型)
catch的exception type index
catch块的addr偏移
剩余handler_catch 个数(int型)
......
 剩余handler_catch (<0 表示还有一个final块, 0表示没有了)
 finally块的addr偏移(如果<0)


(因为个人水平限制和时间限制,内容很乱,没有很好组织,请读者原谅,我力图将dalvik的比较全面的知识告诉大家,如果有什么不正确的,请指出。谢谢)

dalvik字节码问答

标签:

原文地址:http://blog.csdn.net/doon/article/details/51193196

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!