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指令的时候篇幅搞不好要长一点。
反汇编,学指令
如果一个一个指令地讲下来,估计大家都睡着了。所以我们都过反汇编我们写的代码的方式来学习,学得差不多了,我们再把指令串一下。一切以实用为先,我们尝试一下吧。
首先我们还是以上一讲的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条指令,分别对应整型,长整型,单精度,双精度:
指令中,第一个字符为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到完整的指令集。
- ART世界探险(2) - 从java byte code说起
- ART世界探险(7) - 数组
- ART世界探险(18) InlineMethod
- ART探险(1) - oatdump看到的世界
- ART世界探险(5) - 计算指令
- ART世界探险(6) - 流程控制指令
- ART世界探险(8) - 面向对象编程
- ART世界探险(9) - 同步锁
- ART世界探险(10) - 异常处理
- ART世界探险(11) - OAT文件格式分析
- ART世界探险(13) - 初入dex2oat
- ART世界探险(12) - OAT文件分析(2) - ELF文件头分析(中)
- ART世界探险(14) - 快速编译器和优化编译器
- ART世界探险(15) - CompilerDriver,ClassLinker,Runtime三大组件
- ART世界探险(16) - 快速编译器下的方法编译
- ART世界探险(17) - 中层中间代码MIR
- ART世界探险(19) - 优化编译器的编译流程
- ART世界探险(20) - Android N上的编译流程
- LeetCode - 284. Peeking Iterator
- 下面重载乘法运算符的函数原型声明中正确的是:
- 树形DP总结
- hdoj-2092-整数解
- HDD is Outdated Technology
- ART世界探险(2) - 从java byte code说起
- 服务器后台开发中的一点经验
- 指针的指针的理解以及应用
- linux 信号的使用
- mq基础知识
- Java学习-循环(for)
- hdoj-1722-Cake
- hdu5734
- LINUX-内核-中断分析-中断向量表(3)-arm