1. 解析Class文件和二进制文件的转化规则(带案例)

带大家了解一个.class文件是如何运行的

1. 准备工作

首先我们编写一段java代码(带上main方法,因为需要编译)

public class MyTest {
    //创建两个静态变量
    public static int static_a;
    public static double static_b;
    //私有变量
    private int private_c = 1;

    public MyTest(int c){
        this.private_c = c;
    }

    public int add(int a,int b){
            int c = 0;
        try{
            c = a + b;
            return  c;
        }catch (Exception e){
            return 0;
        }
    }

    public static void main(String[] args){
        MyTest myTest = new MyTest(1);
        int return_add = myTest.add(1,4);
        System.out.println(return_add);
    }

}

运行之后我们找到他的class文件,在cmd窗口下使用 javap -v xxx.class打开文件,结果如下

   D:\workspace\TestClass\out\production\TestClass>javap -v MyTest.class
    Classfile /D:/workspace/TestClass/out/production/TestClass/MyTest.class
      Last modified 2019-6-29; size 937 bytes
      MD5 checksum 17e7ebae90a730b8b15f05df32588d11
      Compiled from "MyTest.java"
    public class MyTest
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #9.#40         // java/lang/Object."<init>":()V
       #2 = Fieldref           #4.#41         // MyTest.private_c:I
       #3 = Class              #42            // java/lang/Exception
       #4 = Class              #43            // MyTest
       #5 = Methodref          #4.#44         // MyTest."<init>":(I)V
       #6 = Methodref          #4.#45         // MyTest.add:(II)I
       #7 = Fieldref           #46.#47        // java/lang/System.out:Ljava/io/Print
    Stream;
       #8 = Methodref          #48.#49        // java/io/PrintStream.println:(I)V
       #9 = Class              #50            // java/lang/Object
      #10 = Utf8               static_a
      #11 = Utf8               I
      #12 = Utf8               static_b
      #13 = Utf8               D
      #14 = Utf8               private_c
      #15 = Utf8               <init>
      #16 = Utf8               (I)V
      #17 = Utf8               Code
      #18 = Utf8               LineNumberTable
      #19 = Utf8               LocalVariableTable
      #20 = Utf8               this
      #21 = Utf8               LMyTest;
      #22 = Utf8               c
      #23 = Utf8               add
      #24 = Utf8               (II)I
      #25 = Utf8               e
      #26 = Utf8               Ljava/lang/Exception;
      #27 = Utf8               a
      #28 = Utf8               b
      #29 = Utf8               StackMapTable
      #30 = Class              #43            // MyTest
      #31 = Class              #42            // java/lang/Exception
      #32 = Utf8               main
      #33 = Utf8               ([Ljava/lang/String;)V
      #34 = Utf8               args
      #35 = Utf8               [Ljava/lang/String;
      #36 = Utf8               myTest
      #37 = Utf8               return_add
      #38 = Utf8               SourceFile
      #39 = Utf8               MyTest.java
      #40 = NameAndType        #15:#51        // "<init>":()V
      #41 = NameAndType        #14:#11        // private_c:I
      #42 = Utf8               java/lang/Exception
      #43 = Utf8               MyTest
      #44 = NameAndType        #15:#16        // "<init>":(I)V
      #45 = NameAndType        #23:#24        // add:(II)I
      #46 = Class              #52            // java/lang/System
      #47 = NameAndType        #53:#54        // out:Ljava/io/PrintStream;
      #48 = Class              #55            // java/io/PrintStream
      #49 = NameAndType        #56:#16        // println:(I)V
      #50 = Utf8               java/lang/Object
      #51 = Utf8               ()V
      #52 = Utf8               java/lang/System
      #53 = Utf8               out
      #54 = Utf8               Ljava/io/PrintStream;
      #55 = Utf8               java/io/PrintStream
      #56 = Utf8               println
    {
      public static int static_a;
        descriptor: I
        flags: ACC_PUBLIC, ACC_STATIC
    
      public static double static_b;
        descriptor: D
        flags: ACC_PUBLIC, ACC_STATIC
    
      public MyTest(int);
        descriptor: (I)V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=2, args_size=2
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>
    ":()V
             4: aload_0
             5: iconst_1
             6: putfield      #2                  // Field private_c:I
             9: aload_0
            10: iload_1
            11: putfield      #2                  // Field private_c:I
            14: return
          LineNumberTable:
            line 8: 0
            line 6: 4
            line 9: 9
            line 10: 14
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      15     0  this   LMyTest;
                0      15     1     c   I
    
      public int add(int, int);
        descriptor: (II)I
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=5, args_size=3
             0: iconst_0
             1: istore_3
             2: iload_1
             3: iload_2
             4: iadd
             5: istore_3
             6: iload_3
             7: ireturn
             8: astore        4
            10: iconst_0
            11: ireturn
          Exception table:
             from    to  target type
                 2     7     8   Class java/lang/Exception
          LineNumberTable:
            line 13: 0
            line 15: 2
            line 16: 6
            line 17: 8
            line 18: 10
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
               10       2     4     e   Ljava/lang/Exception;
                0      12     0  this   LMyTest;
                0      12     1     a   I
                0      12     2     b   I
                2      10     3     c   I
          StackMapTable: number_of_entries = 1
            frame_type = 255 /* full_frame */
              offset_delta = 8
              locals = [ class MyTest, int, int, int ]
              stack = [ class java/lang/Exception ]
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=3, locals=3, args_size=1
             0: new           #4                  // class MyTest
             3: dup
             4: iconst_1
             5: invokespecial #5                  // Method "<init>":(I)V
             8: astore_1
             9: aload_1
            10: iconst_1
            11: iconst_4
            12: invokevirtual #6                  // Method add:(II)I
            15: istore_2
            16: getstatic     #7                  // Field java/lang/System.out:Ljav
    a/io/PrintStream;
            19: iload_2
            20: invokevirtual #8                  // Method java/io/PrintStream.prin
    tln:(I)V
            23: return
          LineNumberTable:
            line 23: 0
            line 24: 9
            line 25: 16
            line 26: 23
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      24     0  args   [Ljava/lang/String;
                9      15     1 myTest   LMyTest;
               16       8     2 return_add   I
    }
    SourceFile: "MyTest.java"

之后我们将class文件使用notpead++编译器打开,notpead++需要安装HEX-Editor插件,将2进制转为16进制。以下划红线是重点用到的数据
在这里插入图片描述

2. Class文件的规定格式

下图中是class文件格式规范,

  1. 类型- u几就代表几个字节。一个字节为8位。上图中展示数据为16进制,所以每两个数即为一个字节。如16进制的 “ca” 转为2进制为 “11001010”
  2. 名称- 即规范
  3. 数量- 如父类名占两个字节,数量只允许有一个。而接口名每个占两个字节,允许有多个。具体多少由接口个数所占的两个字节决定。

整体流程图

3. 翻译字节码的整体流程(所需要的表和方法在下面都可以找到)

名称长度字节码意义
魔数u4ca fe ba be验证是否是java文件,会在类加载器加载文件时候进行校验
次版本号+主版本号u400 00 00 345、6字节是次版本号,7、8是主版本号。

解析:34是16进制,转换为10进制为52,计算法则=44+版本号。 即为jdk1.8,同样会在类加载时候进行校验 ,低版本号不能运行高版本号代码就是因为这四个字节
常量个数u200 39代表常量池长度。

解析:10进制是57.说明有57-1条常量指令,即为class文件中的Constant pool下所有指令
常量池表不定常量池常量表常量池包括字面量和符号引用,这里字面量就是表中第一个字节代表的数字。符号引用则包括下面三类常量:
1.类和接口的全限定名(Fully Qualified Name)
2.字段的名称和描述符(Descriptor)
3.方法的名称和描述符。
常量池中数据指令存储形式是表结构,每个表结构都可能不同,占用字节数也可能不同,具体参照下图常量池表(图1,仅以一条常量池指令为例子)
u10a 解析:根据第一个字节0a到(图1)中查找,找到字面量tag为10,表示类中的一个方法。其后还需要两组两个字节的数据
u200 09根据(图1)接下来两个字节表示CONSTANT_Class_info的索引项,即为方法的类型描述 。

解析:这两个字节十进制为9,从class文件中的常量池部分找到 #9,#9又指向了#50,#50表示java/lang/Object,所以方法返回值类型就是对象
u200 28根据(图1)这两个字节表示CONSTANT_NameAndType的索引项,即为类型和名称描述符。

解析:这两个字节十进制为40,从class文件中的常量池部分找到 #15,#51。#15表示< iniit >,#51表示<>V
......(图2)此三步具体含义请见下面常量池表结构代表含义
类访问控制权限u200 21类访问控制权限。

解析:参考(图3),这两个字节是ACC_PUBLIC,ACC_SUPER的并集,即0x0001 + 0x0020 = 0x0021;所以ACC_PUBLIC,ACC_SUPER标志位为真,而ACC_FINAL、ACC_INYERFACE、ACC_ABSTRACT、ACC_ANNOTATION、ACC_ENUM等标志为假。
类索引u200 04确定类名

解析:这两个字节十进制为4,在常量池中对应 #43 对应类名称MyTest
父类索引u200 09确定父类名

解析:这两个字节十进制为9,在常量池中对应 #50 对应父类名称java/lang/Object
接口个数u200 00表示无实现接口
接口名解析:每个接口名占u2,但是此类无接口,不占字节数
域个数u200 03定义了多少域对象,这里表示有3个
字段表集合包含以下几项不定 1.字段表(field_info)用于描述类或者接口中声明的变量,换言之是对字段的定义。
2.字段(field)包括类变量以及实例级变量,但是不包括在方法内部声明的局部变量。
3.关于描述字段可以包含的信息有:字段的作用域(public、private、protect修饰符)、是实例变量还是类变量(staic)、可变性(final)、并发可见性(volatile)、可否被序列化(transient)、字段数据类型(基本类型、对象、数组)、字段名称。其中,各个修饰符都是布尔值,而字段名字、数据类型,只能引用常量池中的常量来描述
4.参考(图4),查看每个字段所需的描述字段和字节数
access_flagsu200 09字段的修饰符,占两个字节

解析:参考(图3),查询类访问权限表类型为0009 = 0001+0008 = public static
name_indexu200 0a代表字段的简单名称(指没有类型和参数修饰的方法或字段的名称)

解析:这两个字节10进制为10,参考常量池#10,表示字段名为static_a
descriptor_indexu200 0b字段的描述符。

解析:这两个字节10进制为11,在常量池中找到#11表示 I,在(图5)中可找到I的映射关系为int,所以该字段类型为int型
attributes_countu200 00表示这个字段有几个属性。
attributes表结构参考(图7)attributes 可以看成一个数组, 数组中的每一项都是一个attribute_info , 每个attribute_info 表示一个属性, 数组中一共有attributes_count个属性。可以出现在filed_info中的属性有三种, 分别是ConstantValue, Deprecated, 和 Synthetic
方法个数u200 03定义了3个方法
方法表集合包含以下几项不定方法的结构和字段结构相同,所以参考(图4)获取方法所需的描述字段和字节数
access_flagsu200 01方法的修饰符,占两个字节

解析:这两个字节10进制为1,参考(图3),表示字段的修饰符为public
name_indexu200 0f方法的名称,占两个字节

解析:这两个字节10进制为15,在常量池中#15表示初始方法
descriptor_indexu200 10 方法的描述符,作用是描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。

解析:这两个字节10进制为16,查找常量池#16为 (I)V,参考(图5)< I >V 代表参数是int,返回值为void
attributes_countu200 01表示这个方法有1个属性。
attributes表结构附加的属性内容,格式参照(图8)
attribute_name_index00 11附加的属性内容名称

解析:这两个字节10进制为17,在常量池中代表Code,参考(图7),查看Code释义。当确定attribute_name_index=Code时,其属性可以使用更为详细的(图9)
attribute_length00 00 00 4f附加的属性内容长度

这两个字节10进制为79,表示长度为79个字节
info的具体解析可参看(图6)

3.1 常量池表(图1)

3.2 常量池表结构代表含义案例(图2)

第一步:根据二进制文件找到表结构数据,找出第一条常量池指令
	根据第一个字节,匹配tag为10,两组index,每组2个字节,找到tag #9 #40
 	1. 	#9    	CONSTANT_Class_info的索引项
	2.  #40  	CONSTANT_NameAndType的索引项
第二步:找到#9号常量 Class   #50
	1. 	#50 	代表的是  java/lang/Object
第三步:找到#40对应的常量 NameAndType   #15  #51
	1. 	#15 	代表 < iniit >  
 	2. 	#51 	代表<>V
最终结果:
 1. 拼接后为:V java/lang/Object.< init >() 
 2. 此条指令 就是返回值为V类型的java/lang/Object(Object父类)的init方法 
 3. 我们可以看到我们的结果和class文件第一条常量池指令注释是一样的

3.3 类访问控制权限表(图3)

在这里插入图片描述

3.4 字段/方法属性描述表(图4)

在这里插入图片描述

  1. 字段表修饰符放在access_flags项中,他与类中的access_flags非常类似,都是一个u2的数据类型。而name_index和descriptor_index,他们都是对常量池的引用,分别代表字段的简单名称以及字段和方法的描述符。
  2. 全限定名:包名加类名。例:在常量池中:org/fenixsoft/TestClass(org.fenixsoft.TestClass),仅仅是把“.”换成了“/”。
  3. 简单名称:指没有类型和参数修饰的方法或字段的名称,如Test类中inc()方法和m字段的简单名称分别是“inc”和“m”
  4. 描述符:作用是描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。

3.5 基本数据类型映射表(图5)

在这里插入图片描述

3.6 关于方法表的查找方法(图6)

a)	找到访问控制 	access_flag  			00 01: public 
b)	找到简单名字 	name_index  			00 17: init
c)	找到描述符 	descriptor_index  		00 18: (I)v	
	- 翻译过来就是:public  void init(int i)
d)	找到			attribute_count  		00 01: 代表有一个属性表
e)	对照属性表 	attribute_info(u2,u4,u1*length)
f)	找到域名		attribute_name_index	00 11: Code
g)	找到域长度 	attribute_length  		00 00 00 4f:79个u1
h)	找到最大栈深	max_stacks  			00 02:2 max_stacks
i)	找到最大变量数max_locals, 			00 02:2 max_locals
	- args_size方法的参数有多少个(默认this,如果方法是static,那么就是0)
j)	找到代码行数	code_length  			00 00 00 0f:15
k)	对应的字节码	code
l)	异常表长度 	exception_table_length	00 00
	- 如果没有异常表,可能是:1.debug断点的问题	2.错误日志没有行号
m) 异常表(图10)exception_table
......
n) 行号表(LineNumberTable)
o) 本地变量表(LocalVariableTable)		00 02 :2
	1.	Start+length :一个本地变量的作用域
	2.	Start : 几个槽来存储
	3.	Name : 简单名字
	4.	Signature伪泛型,泛型擦除的标志

3.7 JVM 规范中预定义的属性(图7)

属性名称

使用位置

含义

Code

方法表中

Java代码编译成的字节码指令(即:具体的方法逻辑字节码指令)

ConstantValue

字段表中

final关键字定义的常量值

Deprecated

类中、方法表中、字段表中

被声明为deprecated的方法和字段

Exceptions

方法表中

方法声明的异常

LocalVariableTable

Code属性中

方法的局部变量描述

LocalVariableTypeTable

类中

JDK1.5中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加

InnerClasses

类中

内部类列表

EnclosingMethod

类中

仅当一个类为局部类或者匿名类时,才能拥有这个属性,这个属性用于表示这个类所在的外围方法

LineNumberTable

Code属性中

Java源码的行号与字节码指令的对应关系

StackMapTable

Code属性中

JDK1.6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配

Signature

类中、方法表中、字段表中

JDK1.5新增的属性,这个属性用于支持泛型情况下的方法签名,在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息

SourceFile

类中

记录源文件名称

SourceDebugExtension

类中

JDK1.6中新增的属性,SourceDebugExtension用于存储额外的调试信息。如在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号,JSR-45规范为这些非Java语言编写,却需要编译成字节码运行在Java虚拟机汇中的程序提供了一个进行调试的标准机制,使用SourceDebugExtension就可以存储这些调试信息。

Synthetic

类中、方法表中、字段表中

标识方法或字段为编译器自动产生的

RuntimeVisibleAnnotations

类中、方法表中、字段表中

JDK1.5中新增的属性,为动态注解提供支持。RuntimeVisibleAnnotations属性,用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的。

RuntimeInvisibleAnnotations

类中、方法表中、字段表中

JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations相反用于指明哪些注解是运行时不可见的。

RuntimeVisibleParameterAnnotations

方法表中

JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations类似,只不过作用对象为方法的参数。

RuntimeInvisibleParameterAnnotations

方法表中

JDK1.5中新增的属性,作用与RuntimeInvisibleAnnotations类似,只不过作用对象为方法的参数。

AnnotationDefault

方法表中

JDK1.5中新增的属性,用于记录注解类元素的默认值

BootstrapMethods

类中

JDK1.7新增的属性,用于保存invokedynamic指令引用的引导方法限定符

3.8 字段/方法表的attributes属性(图8)

在这里插入图片描述

3.9 字段/方法表的attributes的详细属性(图9)

当确定attribute_name_index=Code时,此表相当于(图8的补充版)
在这里插入图片描述

3.10 ExceptionTable异常表(图10)

在这里插入图片描述

3.11 LineNumberTable行号和源代码关系表(图11)

在这里插入图片描述

3.12 LocalVariableTable局部变量表(图12)

在这里插入图片描述

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鱼鱼大头鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值