探索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条指令:
- iload_0,从栈顶第0个位置取一个整数
- iload_1,从栈顶第1个位置取一个整数
- iadd,将这两个整数相加
- 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>:()V
00053c: 7010 1100 0000 |0000: invoke-direct {v0}, Ljava/lang/Object;.<init>:()V // method@0011
000542: 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>:()V
000518: 1a00 4a00 |0000: const-string v0, "true" // string@004a
00051c: 7110 1000 0000 |0002: invoke-static {v0}, Ljava/lang/Boolean;.parseBoolean:(Ljava/lang/String;)Z // method@0010
000522: 0a00 |0005: move-result v0
000524: 6a00 0200 |0006: sput-boolean v0, Lcom/yunos/system/empty3/BuildConfig;.DEBUG:Z // field@0002
000528: 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)I
0f478c: 9000 0102 |0000: add-int v0, v1, v2
0f4790: 0f00 |0002: return v0
iadd变成了add-int指令,带有三个寄存器参数,v1和v2是两个加数,和放在v0中。
ireturn变成return v0
减法以此类推:
0f47ac: |[0f47ac] com.yunos.xulun.testcppjni2.TestART.sub:(IS)I
0f47bc: 9100 0102 |0000: sub-int v0, v1, v2
0f47c0: 0f00 |0002: return v0
乘法的增加一条类型转换:
0f4794: |[0f4794] com.yunos.xulun.testcppjni2.TestART.mul:(IB)J
0f47a4: 9200 0203 |0000: mul-int v0, v2, v3
0f47a8: 8100 |0002: int-to-long v0, v0
0f47aa: 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到完整的指令集。