您的位置:首页 > 编程语言 > Java开发

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

2016-07-21 21:46 691 查看

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进制值(相当于序号)助记符
0x000nop
0x011aconst_null
0x022iconst_m1
0x033iconst_0
0x044iconst_1
0x055iconst_2
0x066iconst_3
0x077iconst_4
0x088iconst_5
0x099lconst_0
0x0a10lconst_1
0x0b11fconst_0
0x0c12fconst_1
0x0d13fconst_2
0x0e14dconst_0
0x0f15dconst_1
0x1016bipush
0x1117sipush
0x1218ldc
0x1319ldc_w
0x1420ldc2_w
0x1521iload
0x1622lload
0x1723fload
0x1824dload
0x1925aload
0x1a26iload_0
0x1b27iload_1
0x1c28iload_2
0x1d29iload_3
0x1e30lload_0
0x1f31lload_1
0x2032lload_2
0x2133lload_3
0x2234fload_0
0x2335fload_1
0x2436fload_2
0x2537fload_3
0x2638dload_0
0x2739dload_1
0x2840dload_2
0x2941dload_3
0x2a42aload_0
0x2b43aload_1
0x2c44aload_2
0x2d45aload_3
0x2e46iaload
0x2f47laload
0x3048faload
0x3149daload
0x3250aaload
0x3351baload
0x3452caload
0x3553saload
0x3654istore
0x3755lstore
0x3856fstore
0x3957dstore
0x3a58astore
0x3b59istore_0
0x3c60istore_1
0x3d61istore_2
0x3e62istore_3
0x3f63lstore_0
0x4064lstore_1
0x4165lstore_2
0x4266lstore_3
0x4367fstore_0
0x4468fstore_1
0x4569fstore_2
0x4670fstore_3
0x4771dstore_0
0x4872dstore_1
0x4973dstore_2
0x4a74dstore_3
0x4b75astore_0
0x4c76astore_1
0x4d77astore_2
0x4e78astore_3
0x4f79iastore
0x5080lastore
0x5181fastore
0x5282dastore
0x5383aastore
0x5484bastore
0x5585castore
0x5686sastore
0x5787pop
0x5888pop2
0x5989dup
0x5a90dup_x1
0x5b91dup_x2
0x5c92dup2
0x5d93dup2_x1
0x5e94dup2_x2
0x5f95swap
0x6096iadd
0x6197ladd
0x6298fadd
0x6399dadd
0x64100isub
0x65101lsub
0x66102fsub
0x67103dsub
0x68104imul
0x69105lmul
0x6a106fmul
0x6b107dmul
0x6c108idiv
0x6d109ldiv
0x6e110fdiv
0x6f111ddiv
0x70112irem
0x71113lrem
0x72114frem
0x73115drem
0x74116ineg
0x75117lneg
0x76118fneg
0x77119dneg
0x78120ishl
0x79121lshl
0x7a122ishr
0x7b123lshr
0x7c124iushr
0x7d125lushr
0x7e126iand
0x7f127land
0x80128ior
0x81129 |lor
0x82130ixor
0x83131lxor
0x84132iinc
0x85133i2l
0x86134i2f
0x87135i2d
0x88136l2i
0x89137l2f
0x8a138l2d
0x8b139f2i
0x8c140f2l
0x8d141f2d
0x8e142d2i
0x8f143d2l
0x90144d2f
0x91145i2b
0x92146i2c
0x93147i2s
0x94148lcmp
0x95149fcmpl
0x96150fcmpg
0x97151dcmpl
0x98152dcmpg
0x99153ifeq
0x9a154ifne
0x9b155iflt
0x9c156ifge
0x9d157ifgt
0x9e158ifle
0x9f159if_icmpeq
0xa0160if_icmpne
0xa1161if_icmplt
0xa2162if_icmpge
0xa3163if_icmpgt
0xa4164if_icmple
0xa5165if_acmpeq
0xa6166if_acmpne
0xa7167goto
0xa8168jsr
0xa9169ret
0xaa170tableswitch
0xab171lookupswitch
0xac172ireturn
0xad173lreturn
0xae174freturn
0xaf175dreturn
0xb0176areturn
0xb1177return
0xb2178getstatic
0xb3179putstatic
0xb4180getfield
0xb5181putfield
0xb6182invokevirtual
0xb7183invokespecial
0xb8184invokestatic
0xb9185invokeinterface
0xba186(unused)
0xbb187new
0xbc188newarray
0xbd189anewarray
0xbe190arraylength
0xbf191athrow
0xc0192checkcast
0xc1193instanceof
0xc2194monitorenter
0xc3195monitorexit
0xc4196wide
0xc5197multianewarray
0xc6198ifnull
0xc7199ifnonnull
0xc8200goto_w
0xc9201jsr_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条指令,分别对应整型,长整型,单精度,双精度:

指令码序号助记符
0x6096iadd
0x6197ladd
0x6298fadd
0x6399dadd
指令中,第一个字符为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, (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
而不是x
,进行的是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, (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, (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到完整的指令集。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  dalvik ART