ART世界探险(2) - 从java byte code说起

来源:互联网 发布:软件系统调试 编辑:程序博客网 时间:2024/04/30 01:20

ART世界探险(2) - 从java byte code说起

Dalvik时代,如果不做JIT的话,只需要了解java字节码和Dalivk的字节码就够了。但是,到了ART时代,我们可能还要至少学习两种新东西:一个是编译后端的IR中间代码。比如,我们假如使用LLVM做为编译后端的话,需要做从dex到LLVM IR的转换工作。这个IR可能还不只一层,比如分中层的MIR和底层的LIR。
最后,我们还得了解机器指令。仅就ARM来说,现在是64位时代了,我们需要了解的就是AArch64和AArch32两种状态下的A64,A32,Thumb2和Thumb四种指令集,还有NEON指令扩展等。

Java字节码指令集一览

我们先看一下Android提供的Java指令集简表,在这个网址可以看到:http://androidxref.com/6.0.1_r10/xref/dalvik/docs/java-bytecode.html

一共200条指令。不过大家千万别被这么多指令吓到啊,相对来说绝大部分指令还是非常简单的。我们用一讲的时间就可以讲个大概,细节将来遇到再说。倒是后面我们讲ARM指令的时候篇幅搞不好要长一点。

字节码 10进制值(相当于序号) 助记符 0x00 0 nop 0x01 1 aconst_null 0x02 2 iconst_m1 0x03 3 iconst_0 0x04 4 iconst_1 0x05 5 iconst_2 0x06 6 iconst_3 0x07 7 iconst_4 0x08 8 iconst_5 0x09 9 lconst_0 0x0a 10 lconst_1 0x0b 11 fconst_0 0x0c 12 fconst_1 0x0d 13 fconst_2 0x0e 14 dconst_0 0x0f 15 dconst_1 0x10 16 bipush 0x11 17 sipush 0x12 18 ldc 0x13 19 ldc_w 0x14 20 ldc2_w 0x15 21 iload 0x16 22 lload 0x17 23 fload 0x18 24 dload 0x19 25 aload 0x1a 26 iload_0 0x1b 27 iload_1 0x1c 28 iload_2 0x1d 29 iload_3 0x1e 30 lload_0 0x1f 31 lload_1 0x20 32 lload_2 0x21 33 lload_3 0x22 34 fload_0 0x23 35 fload_1 0x24 36 fload_2 0x25 37 fload_3 0x26 38 dload_0 0x27 39 dload_1 0x28 40 dload_2 0x29 41 dload_3 0x2a 42 aload_0 0x2b 43 aload_1 0x2c 44 aload_2 0x2d 45 aload_3 0x2e 46 iaload 0x2f 47 laload 0x30 48 faload 0x31 49 daload 0x32 50 aaload 0x33 51 baload 0x34 52 caload 0x35 53 saload 0x36 54 istore 0x37 55 lstore 0x38 56 fstore 0x39 57 dstore 0x3a 58 astore 0x3b 59 istore_0 0x3c 60 istore_1 0x3d 61 istore_2 0x3e 62 istore_3 0x3f 63 lstore_0 0x40 64 lstore_1 0x41 65 lstore_2 0x42 66 lstore_3 0x43 67 fstore_0 0x44 68 fstore_1 0x45 69 fstore_2 0x46 70 fstore_3 0x47 71 dstore_0 0x48 72 dstore_1 0x49 73 dstore_2 0x4a 74 dstore_3 0x4b 75 astore_0 0x4c 76 astore_1 0x4d 77 astore_2 0x4e 78 astore_3 0x4f 79 iastore 0x50 80 lastore 0x51 81 fastore 0x52 82 dastore 0x53 83 aastore 0x54 84 bastore 0x55 85 castore 0x56 86 sastore 0x57 87 pop 0x58 88 pop2 0x59 89 dup 0x5a 90 dup_x1 0x5b 91 dup_x2 0x5c 92 dup2 0x5d 93 dup2_x1 0x5e 94 dup2_x2 0x5f 95 swap 0x60 96 iadd 0x61 97 ladd 0x62 98 fadd 0x63 99 dadd 0x64 100 isub 0x65 101 lsub 0x66 102 fsub 0x67 103 dsub 0x68 104 imul 0x69 105 lmul 0x6a 106 fmul 0x6b 107 dmul 0x6c 108 idiv 0x6d 109 ldiv 0x6e 110 fdiv 0x6f 111 ddiv 0x70 112 irem 0x71 113 lrem 0x72 114 frem 0x73 115 drem 0x74 116 ineg 0x75 117 lneg 0x76 118 fneg 0x77 119 dneg 0x78 120 ishl 0x79 121 lshl 0x7a 122 ishr 0x7b 123 lshr 0x7c 124 iushr 0x7d 125 lushr 0x7e 126 iand 0x7f 127 land 0x80 128 ior 0x81 129 |lor 0x82 130 ixor 0x83 131 lxor 0x84 132 iinc 0x85 133 i2l 0x86 134 i2f 0x87 135 i2d 0x88 136 l2i 0x89 137 l2f 0x8a 138 l2d 0x8b 139 f2i 0x8c 140 f2l 0x8d 141 f2d 0x8e 142 d2i 0x8f 143 d2l 0x90 144 d2f 0x91 145 i2b 0x92 146 i2c 0x93 147 i2s 0x94 148 lcmp 0x95 149 fcmpl 0x96 150 fcmpg 0x97 151 dcmpl 0x98 152 dcmpg 0x99 153 ifeq 0x9a 154 ifne 0x9b 155 iflt 0x9c 156 ifge 0x9d 157 ifgt 0x9e 158 ifle 0x9f 159 if_icmpeq 0xa0 160 if_icmpne 0xa1 161 if_icmplt 0xa2 162 if_icmpge 0xa3 163 if_icmpgt 0xa4 164 if_icmple 0xa5 165 if_acmpeq 0xa6 166 if_acmpne 0xa7 167 goto 0xa8 168 jsr 0xa9 169 ret 0xaa 170 tableswitch 0xab 171 lookupswitch 0xac 172 ireturn 0xad 173 lreturn 0xae 174 freturn 0xaf 175 dreturn 0xb0 176 areturn 0xb1 177 return 0xb2 178 getstatic 0xb3 179 putstatic 0xb4 180 getfield 0xb5 181 putfield 0xb6 182 invokevirtual 0xb7 183 invokespecial 0xb8 184 invokestatic 0xb9 185 invokeinterface 0xba 186 (unused) 0xbb 187 new 0xbc 188 newarray 0xbd 189 anewarray 0xbe 190 arraylength 0xbf 191 athrow 0xc0 192 checkcast 0xc1 193 instanceof 0xc2 194 monitorenter 0xc3 195 monitorexit 0xc4 196 wide 0xc5 197 multianewarray 0xc6 198 ifnull 0xc7 199 ifnonnull 0xc8 200 goto_w 0xc9 201 jsr_w

反汇编,学指令

如果一个一个指令地讲下来,估计大家都睡着了。所以我们都过反汇编我们写的代码的方式来学习,学得差不多了,我们再把指令串一下。一切以实用为先,我们尝试一下吧。

首先我们还是以上一讲的empty3例子说起。
我们首先用javap工具反汇编一下BuildConfig那个类:

javap -c com.yunos.system.empty3.BuildConfig

反汇编出来的代码如下:

Compiled from "BuildConfig.java"public final class com.yunos.system.empty3.BuildConfig {  public static final boolean DEBUG;  public static final java.lang.String APPLICATION_ID = "com.yunos.system.empty3";  public static final java.lang.String BUILD_TYPE = "debug";  public static final java.lang.String FLAVOR = "";  public static final int VERSION_CODE = 1;  public static final java.lang.String VERSION_NAME = "1.0";  public com.yunos.system.empty3.BuildConfig();    Code:       0: aload_0       1: invokespecial #1                  // Method java/lang/Object."<init>":()V       4: return    LineNumberTable:      line 6: 0    LocalVariableTable:      Start  Length  Slot  Name   Signature          0       5     0  this   Lcom/yunos/system/empty3/BuildConfig;  static {};    Code:       0: ldc           #2                  // String true       2: invokestatic  #3                  // Method java/lang/Boolean.parseBoolean:(Ljava/lang/String;)Z       5: putstatic     #4                  // Field DEBUG:Z       8: return    LineNumberTable:      line 7: 0}

BuildConfig构造方法

我们先看BuildConfig构造这段:

       0: aload_0       1: invokespecial #1                  // Method java/lang/Object."<init>":()V       4: return

第一条是aload_指令,这个家族共有4条指令,aload_0, aload_1, aload_2, aload_3.指令代码从0x2a到0x2d。
这条指令的含义是:从局部变量中,取第n个的值。取第0个就是aload_0,第1个是aload_1。
如果取第4个怎么办?这时候一个字节的指令不够用了,另有一个aload指令,后面接一个字节提供数字。
相当于,aload_0是aload 0指令的简写,但是aload 0占两个字节,而aload_0占一个字节。

第二条指令是invokespecial,调用对象实例的方法,尤其是调用super方法,私有方法和构造方法。因为这里是要调用父类的初始化方法,所以正好该是invokespecial.

第三条是return,不带值返回。

一共就这3条指令,还是挺好理解的吧?

BuildConfig的静态部分

学习了3条指令的,我们再学一个4条指令的:

  static {};    Code:       0: ldc           #2                  // String true       2: invokestatic  #3                  // Method java/lang/Boolean.parseBoolean:(Ljava/lang/String;)Z       5: putstatic     #4                  // Field DEBUG:Z       8: return    LineNumberTable:      line 7: 0

第一条,ldc,从常量池中将常量读出来压入栈中。JVM是基于栈的,操作数都是从栈上取,结果也压到栈里面去。
第二条,invokestatic,这是我们学到的第二条invoke类指令了,上一条是invokespecial,这个顾名思义,就是调用静态方法专用的指令。
第三条,putstatic,将栈中的值,放到静态域中。
第四条,return,无数据返回。

流程很简单,先从常量池将true读出来放到栈里,然后调用Boolean.parseBoolean,参数就是刚才放入栈的true字符串,解析好之后的值又入栈。接着,putstatic从栈里读取这个boolean的值,写到DEBUG这个域中。最后返回。

class的基本结构

因为是入门文章,暂时我们先不讲class文件各模块的细节,只是先有个感性认识:
* 常量池:class中有常量池,有很多指令是操作常量池的。将常量池中的值读出来放到栈中。
* 方法:class文件中,方法是有专门存储模块的,invoke集指令去调用的时候,从中去查找。
* 域:不管是静态域还是对象实例中的普通域,我们有很多指令是用来操作它们的。
* 栈:JVM最重要的结构就是这个栈,大部分的操作都是通过这个栈来操作。后面学习Dalvik指令的时候我们会看到,比起JVM中基本都是栈操作的这种指令,Dalvik大量使用了寄存器。

运算指令

下面我们再看另一大类的指令,运算相关的指令。
我们还是老办法,先写个例子,然后再反汇编,看它背后的故事。我们先写个最简单的加法运算:

package com.yunos.xulun.testcppjni2;public class TestART {    public static int add(int a, int b){        return a+b;    }}

反汇编之后是这样的:

Compiled from "TestART.java"public class com.yunos.xulun.testcppjni2.TestART {  public com.yunos.xulun.testcppjni2.TestART();    Code:       0: aload_0       1: invokespecial #1                  // Method java/lang/Object."<init>":()V       4: return    LineNumberTable:      line 3: 0    LocalVariableTable:      Start  Length  Slot  Name   Signature          0       5     0  this   Lcom/yunos/xulun/testcppjni2/TestART;  public static int add(int, int);    Code:       0: iload_0       1: iload_1       2: iadd       3: ireturn    LineNumberTable:      line 5: 0    LocalVariableTable:      Start  Length  Slot  Name   Signature          0       4     0     a   I          0       4     1     b   I}

默认生成的构造方法,以后我们就略过不提了。
这个加法运算,一共4条指令:
1. iload_0,从栈顶第0个位置取一个整数
2. iload_1,从栈顶第1个位置取一个整数
3. iadd,将这两个整数相加
4. ireturn,返回一个整数。

运算指令多,是因为指令级没办法做泛型,针对每种类型数据都得做一条指令,所以,加法这一个操作,就得4条指令,分别对应整型,长整型,单精度,双精度:

指令码 序号 助记符 0x60 96 iadd 0x61 97 ladd 0x62 98 fadd 0x63 99 dadd

指令中,第一个字符为i的对应整型,l是长整型,f是单精度,d是双精度。
当然不光加法是这样,减法,乘法,除法也是一样。从栈上读数,转化成什么类型,也是4种类型都要支持。

类型转换

那么一个问题来了,既然只有4种类型的计算指令,其它类型怎么办?
JVM提供了一堆类型转换的指令来满足这个需求。
有一些类型直接连转换都省了,比如short和byte,在JVM里,就是当int来处理。

我们做个试验:

    public static int sub(int a, short b){        return a-b;    }

反汇编了之后发现,一个int跟short,或者是两个short相减,跟两个int做减法就没有区别:

  public static int sub(int, short);    Code:       0: iload_0       1: iload_1       2: isub       3: ireturn    LineNumberTable:      line 9: 0    LocalVariableTable:      Start  Length  Slot  Name   Signature          0       4     0     a   I          0       4     1     b   S

所以,以后大家就用int吧,不是数组的话,short跟byte也是int。
为什么?因为栈就是以int为单位的啊!寄存器的还值得拆成两个用,栈真就不需要了。

我们再看一个带类型转换的:

    public static long mul(int a, byte b){        return a*b;    }

反汇编之后,出现一条将整型转成长整型的i2l指令。

  public static long mul(int, byte);    Code:       0: iload_0       1: iload_1       2: imul       3: i2l       4: lreturn    LineNumberTable:      line 13: 0    LocalVariableTable:      Start  Length  Slot  Name   Signature          0       5     0     a   I          0       5     1     b   B

因为返回值也是长整型了,所以返回指令变成lreturn了。

趁热打铁,我们强势切入Dalvik指令

对JVM指令有了初步的理解之后,我们绝不沾沾自喜,迅速看看Dalvik指令是什么样子的。

先从记忆中把BuildConfig那段翻出来,JVM是这样的:

       0: aload_0       1: invokespecial #1                  // Method java/lang/Object."<init>":()V       4: return

我们看看,转成Dalvik是什么样的:

00052c:                                        |[00052c] com.yunos.system.empty3.BuildConfig.<init>:()V00053c: 7010 1100 0000                         |0000: invoke-direct {v0}, Ljava/lang/Object;.<init>:()V // method@0011000542: 0e00                                   |0003: return-void

看完了之后有没有会心一笑?invokespecial指令换了个名,叫invoke-direct,带一个v0寄存器的参数,所以aload_0省了。
return换了个更贴切的名字:return-void。我们前面学过了ireturn,lreturn,这个不带值的return,确实叫return-void很合适。

再对比另一段:

       0: ldc           #2                  // String true       2: invokestatic  #3                  // Method java/lang/Boolean.parseBoolean:(Ljava/lang/String;)Z       5: putstatic     #4                  // Field DEBUG:Z       8: return

对应过来是:

000508:                                        |[000508] com.yunos.system.empty3.BuildConfig.<clinit>:()V000518: 1a00 4a00                              |0000: const-string v0, "true" // string@004a00051c: 7110 1000 0000                         |0002: invoke-static {v0}, Ljava/lang/Boolean;.parseBoolean:(Ljava/lang/String;)Z // method@0010000522: 0a00                                   |0005: move-result v0000524: 6a00 0200                              |0006: sput-boolean v0, Lcom/yunos/system/empty3/BuildConfig;.DEBUG:Z // field@0002000528: 0e00                                   |0008: return-void

ldc变成了const-string,就是换个名,多个v0寄存器。invokestatic多了个”-“,也是多了个寄存器参数。
因为invoke-static返回的值是在栈里,所以需要一条额外的move-result指令将栈顶值放入寄存器。
putstatic变成了sput,加上类型,变成sput-boolean。
最后return-void.

好,我们再看下,加,减,乘的那几个:

0f477c:                                        |[0f477c] com.yunos.xulun.testcppjni2.TestART.add:(II)I0f478c: 9000 0102                              |0000: add-int v0, v1, v20f4790: 0f00                                   |0002: return v0

iadd变成了add-int指令,带有三个寄存器参数,v1和v2是两个加数,和放在v0中。
ireturn变成return v0

减法以此类推:

0f47ac:                                        |[0f47ac] com.yunos.xulun.testcppjni2.TestART.sub:(IS)I0f47bc: 9100 0102                              |0000: sub-int v0, v1, v20f47c0: 0f00                                   |0002: return v0

乘法的增加一条类型转换:

0f4794:                                        |[0f4794] com.yunos.xulun.testcppjni2.TestART.mul:(IB)J0f47a4: 9200 0203                              |0000: mul-int v0, v2, v30f47a8: 8100                                   |0002: int-to-long v0, v00f47aa: 1000                                   |0003: return-wide v0

i2l换了个马甲叫int-to-long。
lreturn变成了return-wide。

最后收尾,ARM指令

我们最后看下几个计算函数生成的机器代码吧:

add的机器代码

    CODE: (code_offset=0x0050151c size_offset=0x00501518 size=76)...      0x0050151c: d1400bf0  sub x16, sp, #0x2000 (8192)      0x00501520: b940021f  ldr wzr, [x16]      suspend point dex PC: 0x0000      0x00501524: f81e0fe0  str x0, [sp, #-32]!      0x00501528: f9000ffe  str lr, [sp, #24]      0x0050152c: b9002be1  str w1, [sp, #40]      0x00501530: b9002fe2  str w2, [sp, #44]      0x00501534: 79400250  ldrh w16, [tr] (state_and_flags)      0x00501538: 35000130  cbnz w16, #+0x24 (addr 0x50155c)      0x0050153c: b9402be0  ldr w0, [sp, #40]      0x00501540: b9402fe1  ldr w1, [sp, #44]      0x00501544: 0b010002  add w2, w0, w1      0x00501548: b90013e2  str w2, [sp, #16]      0x0050154c: b94013e0  ldr w0, [sp, #16]      0x00501550: f9400ffe  ldr lr, [sp, #24]      0x00501554: 910083ff  add sp, sp, #0x20 (32)      0x00501558: d65f03c0  ret      0x0050155c: f9421e5e  ldr lr, [tr, #1080] (pTestSuspend)      0x00501560: d63f03c0  blr lr      suspend point dex PC: 0x0000      0x00501564: 17fffff6  b #-0x28 (addr 0x50153c)

核心就这一条add w2, w0, w1,其余都是折腾栈和寄存器。指令用的是w[n]而不是x[n],进行的是32位的加法。

减法

    CODE: (code_offset=0x0050160c size_offset=0x00501608 size=76)...      0x0050160c: d1400bf0  sub x16, sp, #0x2000 (8192)      0x00501610: b940021f  ldr wzr, [x16]      suspend point dex PC: 0x0000      0x00501614: f81e0fe0  str x0, [sp, #-32]!      0x00501618: f9000ffe  str lr, [sp, #24]      0x0050161c: b9002be1  str w1, [sp, #40]      0x00501620: b9002fe2  str w2, [sp, #44]      0x00501624: 79400250  ldrh w16, [tr] (state_and_flags)      0x00501628: 35000130  cbnz w16, #+0x24 (addr 0x50164c)      0x0050162c: b9402be0  ldr w0, [sp, #40]      0x00501630: b9402fe1  ldr w1, [sp, #44]      0x00501634: 4b010002  sub w2, w0, w1      0x00501638: b90013e2  str w2, [sp, #16]      0x0050163c: b94013e0  ldr w0, [sp, #16]      0x00501640: f9400ffe  ldr lr, [sp, #24]      0x00501644: 910083ff  add sp, sp, #0x20 (32)      0x00501648: d65f03c0  ret      0x0050164c: f9421e5e  ldr lr, [tr, #1080] (pTestSuspend)      0x00501650: d63f03c0  blr lr      suspend point dex PC: 0x0000      0x00501654: 17fffff6  b #-0x28 (addr 0x50162c)

除了加法换成了减法:sub w2, w0, w1,其余基本一样啊。

乘法

    CODE: (code_offset=0x0050158c size_offset=0x00501588 size=88)...      0x0050158c: d1400bf0  sub x16, sp, #0x2000 (8192)      0x00501590: b940021f  ldr wzr, [x16]      suspend point dex PC: 0x0000      0x00501594: f81e0fe0  str x0, [sp, #-32]!      0x00501598: f9000ffe  str lr, [sp, #24]      0x0050159c: b9002be1  str w1, [sp, #40]      0x005015a0: b9002fe2  str w2, [sp, #44]      0x005015a4: 79400250  ldrh w16, [tr] (state_and_flags)      0x005015a8: 35000190  cbnz w16, #+0x30 (addr 0x5015d8)      0x005015ac: b9402be0  ldr w0, [sp, #40]      0x005015b0: b9402fe1  ldr w1, [sp, #44]      0x005015b4: 1b017c02  mul w2, w0, w1      0x005015b8: b9000fe2  str w2, [sp, #12]      0x005015bc: b9400fe0  ldr w0, [sp, #12]      0x005015c0: 93407c01  sxtw x1, w0      0x005015c4: f800c3e1  stur x1, [sp, #12]      0x005015c8: f840c3e0  ldur x0, [sp, #12]      0x005015cc: f9400ffe  ldr lr, [sp, #24]      0x005015d0: 910083ff  add sp, sp, #0x20 (32)      0x005015d4: d65f03c0  ret      0x005015d8: f9421e5e  ldr lr, [tr, #1080] (pTestSuspend)      0x005015dc: d63f03c0  blr lr      suspend point dex PC: 0x0000      0x005015e0: 17fffff3  b #-0x34 (addr 0x5015ac)

首先,是mul指令:mul w2, w0, w1
另外,还有一条是将32位整数转成64位的长整型,请注意,32位的w寄存器之外,64位的x寄存器出来干活了。
sxtw x1, w0:是将w0中的32位值扩展成64位的值,结果放在x1 64位寄存器中。

基本概念我们先说这么多,分支,异常等高级话题,下面分别讨论。
最后我们会cover到完整的指令集。

0 0
原创粉丝点击