Java——手撕字节码

    xiaoxiao2021-03-25  199

    这两天在看《深入理解Java虚拟机》,其中字节码那部分看得我极其烦躁,最后决定亲子拆解一遍它的例子,拆完后觉得印象深刻多了。

    字节码拆解实验

    对书上的例子进行了实现和分析,几个部分需要对照,相互印证,同时结合下面的理论部分才能明白。

    Java代码

    例子使用的Java代码和书上基本一样,只不过我写的时候包名不一样,代码如下:

    package com.way.clazz; public class TestClass { private int m; public int inc(){ return m+1; } }

    拆解图示

    不同的颜色的划线区域代表不同的字节码组成部分。下面的知识部分和图是一一对应的。

    .class字节码

    为了清洗,把原生的字节码放在这里

    javap命令解析出来的结构:

    Javap命令解析出的文件结构有助于对照验证。

    Classfile /root/TestClass.class Last modified Mar 8, 2017; size 289 bytes MD5 checksum 796893667dc8e7d29bbece83875f670d Compiled from "TestClass.java" public class com.way.clazz.TestClass minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#15 // java/lang/Object."<init>":()V #2 = Fieldref #3.#16 // com/way/clazz/TestClass.m:I #3 = Class #17 // com/way/clazz/TestClass #4 = Class #18 // java/lang/Object #5 = Utf8 m #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 inc #12 = Utf8 ()I #13 = Utf8 SourceFile #14 = Utf8 TestClass.java #15 = NameAndType #7:#8 // "<init>":()V #16 = NameAndType #5:#6 // m:I #17 = Utf8 com/way/clazz/TestClass #18 = Utf8 java/lang/Object { public com.way.clazz.TestClass(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 public int inc(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field m:I 4: iconst_1 5: iadd 6: ireturn LineNumberTable: line 8: 0 } SourceFile: "TestClass.java"

    字节码理论

    基本结构

    Class文件的结构不像XML等描述语言那样松散自由。由于它没有任何分隔符号,所以,以上数据项无论是顺序还是数量都是被严格限定的。哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。

    仔细观察上面的Class文件格式,可以看出Class文件格式采用一种类似于C语言结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表。无符号数就是u1、u2、u4、u8来分别代表1个、2个、4个、8个字节。表是由多个无符号数或其他表构成的复合数据类型,以“_info”结尾。在表开始位置,通常会使用一个前置的容量计数器,因为表通常要描述数量不定的多个数据。

    下图表示的就是Class文件格式的基本结构,所有_info就是表结构,它们的长度是不确定的。每个_info表的前两个字节会表示_info表的元素个数,根据元素个数,和每个元素的类型,可以确定最终长度。

    Header头部

    对应上面拆解图的灰色部分,长度固定,共8个字节: 1,MagicNum:4个字节,内容0xCAFEBABE,固定值 2,minor_version:2字节,JDK小版本号 3,major_version:2字节,JDK大版本号,决定着JVM能否处理该class文件,向下兼容。JDK1.8的主版本号是52

    Constant Pool

    常量池是一个表结构,表中元素可以是两种东西:字面量和符号引用

    字面量:类似于Java语言层面所说的常量,如本文字符串,被声明为final的常量值。 符号引用:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。

    表中元素即一个常量的结构如下图:

    以一个变量为例解释该表格的意义:

    上图是常量池区域的开头部分,作为一个表结构,前两的字节必然是表元素数目,即0x08,0x09,存储的是0x0013,十进制是19,这里是从1开始计数,即存储了18个元素。接下来就是元素的具体内容。每一个元素的第一个字节都是类型的标识数字,如上表,标识数字是1~12,没有2,11种。第一个元素的头(0x10)值是0x0A,即10,查表,是CONSTANT_Methodref_info, 存储的是两个index,index即指针,索引,指向的已一定是常量池中的其他元素位置,这也就是所谓符号索引。CONSTANT_Methodref_info存储的是:指向声明方法接口描述符CONSTANT_class_info的索引;指向名称及类型描述符CONSTANT_NameAndType_info的索引。

    看字节码,两个位置分别存储的是0x0004,0x000F,让我们顺藤摸瓜,看看到底是什么意思?0x0004指向常量表中第四号元素,0x000F指向第十五号元素。看解析内容,四号元素是CONSTANT_class_info类型,存储了指向十八号元素的索引,十八号元素是常量,存储的是“java/lang/Object”。再看十五号元素,十五号元素是CONSTANT_NameAndType_info,存储了一个名称索引和一个描述符索引,分别指向7号和8号,7号是,这是一个特殊标记,表示这个方法是构造器,8号是V,标识void,即空。

    至此整个脉络搞清楚了,1号位存储的是一个方法的说明,这个方法返回值的Class存在了4号位,四号位指向了真是存储的常量字符串“java.lang.Object”,这个方法的名称和传入参数存在了15号位,15号位又分别指向了7号,和8号位,标识这个方法是构造器,传入参数为空。到这里我们就明白,这是类的共有构造函数的编译结果,即public com.way.clazz.TestClass(){},这个方法我们没有写,但是隐式存在的。

    遵从以上方法,可以查询所有元素的含义,javap命令也就是做了这样的事情,并将结果以可理解的形式展示给我们。

    常量池字节码的解析内容:

    Constant pool: #1 = Methodref #4.#15 // java/lang/Object."<init>":()V #2 = Fieldref #3.#16 // com/way/clazz/TestClass.m:I #3 = Class #17 // com/way/clazz/TestClass #4 = Class #18 // java/lang/Object #5 = Utf8 m #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 inc #12 = Utf8 ()I #13 = Utf8 SourceFile #14 = Utf8 TestClass.java #15 = NameAndType #7:#8 // "<init>":()V #16 = NameAndType #5:#6 // m:I #17 = Utf8 com/way/clazz/TestClass #18 = Utf8 java/lang/Object

    后面的访问标志,域,方法,类属性部分都可以根据以上方法一一拆解。将每个区域的数据结构列在下面

    访问标签Access Right

    字节码长度为u2,数值是上表中所有为真的标志取“或”运算。例如我们这里是ACC_PUBLIC,ACC_SUPER的,那么就是(0x0001|0x0020)得到0x0021。

    索引 Inplement Interfaces

    包含类索引,父索引和其它接口索引集合。类索引即对本类的索引,父索引即对继承父类的索引,如果没有继承类,就是java.lang.Object类,所有类都继承自它,它们都最终指向常量池中的值。接口索引是一个表,前两个字节是长度,后面的是对应个数的接口索引,本例没有其他接口。

    字段区Field

    字段区是表,前两字节是元素个数,之后的每一个元素遵从上表数据结构,attributes_info会连接在元素后面,其他是对常量池的索引。

    其中Access_flag就是变量访问修饰符

    Descriptor_index指的是数据的类型

    方法区Method

    方法区和字段区几乎一样

    方法访问标识略有不同

    属性表

    上面的几个部分勾勒了一个类的框架,但一个字段和方法的具体内容则是在属性表里的。因此属性表是灵活和扩展性很强的。

    虚拟机规定的属性类别如下:

    本例中方法有code属性:

    Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0

    对应的字节码是:

    根据Code属性数据结构,可以一一对应解读。

    Java字节码是JVM运行的的运行依据,JVM在字节码上采取了开放的策略,这也就是为什么有这么多的建立在JVM的语言,如Jython,Scala等。因为只要有相应的编译器将它们的语言文件编译成.class字节码,JVM都可以运行。而这些字节码的结构为什么如此与编译原理以及JVM类加载机制有关。而类加载机制是《深入理解JVM》的下一章节。

    http://www.cnblogs.com/ITtangtang/p/3978102.html

    转载请注明原文地址: https://ju.6miu.com/read-4330.html

    最新回复(0)