从一个基本的类的反编译说起让你读懂Java字节码文件

1,410 阅读9分钟

从一个基本的类的反编译说起:

javap是一个能够将class文件反汇编成人类可读的格式的工具。可以方便的查阅Java的字节码。 例如下面的例子:

public class Coo{

	private int tryBlock;
	
	private int catchBlock;
	
	private int finallyBlock;
	
	private int methodExit;
	
	public void test(){
		
		try{
			tryBlock = 0;
		}catch(Exception e){
			catchBlock = 1;
		}finally{
			finallyBlock = 2;
		}
		
		methodExit = 3;
	}

}

使用以下两条命令:

javac Coo.java

javap -p -v Coo

得到:

Classfile /C:/Users/dell/Desktop/bob/Coo.class
  Last modified ×××; size 540 bytes
  MD5 checksum d94c55f366ae593c8bb83fceda66f50a
  Compiled from "Coo.java"
public class Coo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #8.#24         // java/lang/Object."<init>":()V
   #2 = Fieldref           #7.#25         // Coo.tryBlock:I
   #3 = Fieldref           #7.#26         // Coo.finallyBlock:I
   #4 = Class              #27            // java/lang/Exception
   #5 = Fieldref           #7.#28         // Coo.catchBlock:I
   #6 = Fieldref           #7.#29         // Coo.methodExit:I
   #7 = Class              #30            // Coo
   #8 = Class              #31            // java/lang/Object
   #9 = Utf8               tryBlock
  #10 = Utf8               I
  #11 = Utf8               catchBlock
  #12 = Utf8               finallyBlock
  #13 = Utf8               methodExit
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               test
  #19 = Utf8               StackMapTable
  #20 = Class              #27            // java/lang/Exception
  #21 = Class              #32            // java/lang/Throwable
  #22 = Utf8               SourceFile
  #23 = Utf8               Coo.java
  #24 = NameAndType        #14:#15        // "<init>":()V
  #25 = NameAndType        #9:#10         // tryBlock:I
  #26 = NameAndType        #12:#10        // finallyBlock:I
  #27 = Utf8               java/lang/Exception
  #28 = NameAndType        #11:#10        // catchBlock:I
  #29 = NameAndType        #13:#10        // methodExit:I
  #30 = Utf8               Coo
  #31 = Utf8               java/lang/Object
  #32 = Utf8               java/lang/Throwable
{
  private int tryBlock;
    descriptor: I
    flags: ACC_PRIVATE

  private int catchBlock;
    descriptor: I
    flags: ACC_PRIVATE

  private int finallyBlock;
    descriptor: I
    flags: ACC_PRIVATE

  private int methodExit;
    descriptor: I
    flags: ACC_PRIVATE

  public Coo();
    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 1: 0

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: iconst_0
         2: putfield      #2                  // Field tryBlock:I
         5: aload_0
         6: iconst_2
         7: putfield      #3                  // Field finallyBlock:I
        10: goto          35
        13: astore_1
        14: aload_0
        15: iconst_1
        16: putfield      #5                  // Field catchBlock:I
        19: aload_0
        20: iconst_2
        21: putfield      #3                  // Field finallyBlock:I
        24: goto          35
        27: astore_2
        28: aload_0
        29: iconst_2
        30: putfield      #3                  // Field finallyBlock:I
        33: aload_2
        34: athrow
        35: aload_0
        36: iconst_3
        37: putfield      #6                  // Field methodExit:I
        40: return
      Exception table:
         from    to  target type
             0     5    13   Class java/lang/Exception
             0     5    27   any
            13    19    27   any
      LineNumberTable:
        line 14: 0
        line 18: 5
        line 19: 10
        line 15: 13
        line 16: 14
        line 18: 19
        line 19: 24
        line 18: 27
        line 21: 35
        line 22: 40
      StackMapTable: number_of_entries = 3
        frame_type = 77 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
        frame_type = 77 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 7 /* same */
}
SourceFile: "Coo.java"

默认情况下反编译会打印所有的非私有的字段和方法,当加入的-p参数,会打印出私有的字段和方法。加入的-v参数,目的是为了尽可能的打印所有的信息。如果你只需要查阅某个类中方法对应的字节码可以使用-c而不使用-v参数。

-v参数主要输入分为以下几点:

1. 基本的信息:
1)包括原class文件的相关信息

2)class文件的版本号(minor version:0, major version:52)-> 指的是编译生成该 class 文件时所用的JRE版本

3) 该类的访问权限(flag:(0X0021) ACC_PUBLIC,ACC_SUPER)

4)该类(this_class:#7)以及名字

5)父类(super_class:#8)以及名字

6)所实现的接口(interfaces:0)

7)字段(feilds:4)

8)方法(methods:2)

9)属性(attributes:1)的数目 -> 该属性指class文件所携带的辅助的信息,比如该class文件的源文件的名称。本信息经常用于JVM的验证和运行,以及程序的调试。

注意:从JdK9之后从4)到9)中不在含有这些信息,如果该类实现一个接口,那么会显式的声明在前面 这样做的目的为了减少JVM的验证工作,提高JVM加载class文件时的效率。

Classfile /C:/Users/dell/Desktop/bob/Coo.class
  Last modified ×××; size 568 bytes
  MD5 checksum 156df5eb19ac7b2f6afb29df6cc3376b
  Compiled from "Coo.java"
public class Coo implements java.io.Serializable
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER

此时的本地的环境是java versionn10.0.1

重现运行

对应的2)中的minor_version 和major_version项的值是此class文件的次要版本号和主要版本号。主要版本号和次要版本号一起确定class文件格式的版本 。如果class文件具有主版本号M和次版本号m,则我们将其class文件格式的版本表示为M.m。因此,class文件格式版本可以按字典顺序排序,例如,1.5 <2.0 <2.1。Java虚拟机实现可以支持一个class文件的版本v当且仅当v在一些连续范围Mi.0 ≤ v ≤ Mj.m,该范围基于实现符合的Java SE平台版本。符合给定Java SE Platform版本的实现必须支持如下表中指定的范围。(对于历史案例,将显示JDK版本而不是Java SE平台版本)。当我们将高版本的JRE上javac编译而成的class文件,不能在旧版本中运行,反之亦然。否则,会报如下错误:

Exception in thread "main" java.lang.UnsupportedClassVersionError:Coo has been compiled by a ...

Java SE 版本 class文件中的版本范围
1.0.2 45.0 v 45.3
1.1 45.0 v 45.65535
1.2 45.0 v 46.0
1.3 45.0 v 47.0
1.4 45.0 v 48.0
5.0 45.0 v 49.0
6 45.0 v 50.0
7 45.0 v 51.0
8 45.0 v 52.0
9 45.0 v 53.0
10 45.0 v 54.0

对应的3)类的访问权限通常为 ACC_ 开头的常量。具体的每个常量的意义可以查阅Java虚拟机规范4.1小节

标志名称 解释
ACC_PUBLIC 0×0001 声明public; 可以从其包的外部访问
ACC_FINAL 0×0010 声明final; 不允许的有子类
ACC_SUPER 0×0020 invokespecial指令调用时特殊处理超类方法
ACC_INTERFACE 0×0200 是一个接口,而不是一个类
ACC_ABSTRACT 0x0400 声明abstract; 不得被直接实例化
ACC_SYNTHETIC 0x1000 合成标志(宣布为其他类进行合成,一般在运行时生成的类,比如动态代理中,生成的类会是该标志); 不在源代码中进行设置
ACC_ANNOTATION 0x2000 声明为注解类型的类
ACC_ENUM 0x4000 声明为enum类型

ACC_MODULE

0x8000 该标志表示此class文件被定义为模块(即在JDK9及其以上加入的专门用于定义模块的类),而不是类或接口,在该标识下会有一些特殊的规则

如果ACC___MODULE在access_flags项目中设置了标志,则access_flags可以不设置项目中的其他标志,并且要满足以下规则适用于Class文件结构的其余部分 :

  • major version,minor version: ≥ 53.0(即JDK 9以上)

  • this_class: module-info

  • super_class, interfaces_count, fields_count, methods_count:0

  • attributes:Module必须存在一个属性。除Module,ModulePackages,ModuleMainClass,InnerClasses,SourceFile, SourceDebugExtension,RuntimeVisibleAnnotations和 RuntimeInvisibleAnnotations,没有预先定义的属性。

2.常量池

用来存放各种常量以及符号引用。常量池中的每一项都有一个对应的索引(如#1),并且可能引用其他的常量池项(#1 = Methodref #8.#25)。

Constant pool:
   #1 = Methodref          #8.#25         // java/lang/Object."<init>":()V
      ...
   #8 = Class              #32            // java/lang/Object
	  ...
  #15 = Utf8               <init>
  #16 = Utf8               ()V
      ...
  #25 = NameAndType        #15:#16        // "<init>":()V
	  ...
  #32 = Utf8               java/lang/Object
  #33 = Utf8               java/io/Serializable
  #34 = Utf8               java/lang/Throwable

如上 1 号常量池项是一个指向 Object 类构造器的符号引用。它是由另外两个常 量池项所构成。如果将它看成一个树结构的话,那么它的叶节点会是字符串常量,如下图所示:

常量池中的引用关系图

3. 字段区域

用来列举该类中的各个字段。这里最主要的信息便是该字段的类型(descriptor: I)以及访问权限(flags: (0x0002) ACC_PRIVATE)。对于声明为 final 的静态字段而言, 如果它是基本类型或者字符串类型,那么字段区域还将包括它的常量值。

private int tryBlock;
    descriptor: I
    flags: ACC_PRIVATE

Java 虚拟机同样使用了“描述符”(descriptor)来描述字段的类型。具体的对照如下 表所示。其中比较特殊的,高亮显示。

描述符

4.方法区域

用来列举该类中的各个方法。除了方法描述符以及访问权限之外,每个方法还包 括最为重要的代码区域。

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: iconst_0
         ...
    	10: goto          35
         ...
        34: athrow
        35: aload_0
        36: iconst_3
        37: putfield      #6                  // Field methodExit:I
        40: return
      Exception table:
         from    to  target type
             0     5    13   Class java/lang/Exception
             0     5    27   any
            13    19    27   any
      LineNumberTable:
        line 16: 0
        line 20: 5
           ...
        line 23: 35
        line 24: 40
      StackMapTable: number_of_entries = 3
        frame_type = 77 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
           ...

代码区域一开始会声明该方法中的操作数栈(stack=2)和局部变量数目(locals=3)的最大值,以及该方法接收参数的个(args_size=1)。(这里局部变量指的是字节码中的局部变量,而非 Java 程序中的局部变量)。

紧接着是该方法的字节码。每条字节码均标注了对应的偏移量(bytecode index,BCI),这 是用来定位字节码的。比如说偏移量为 10 的跳转字节码 10: goto 35,将跳转至偏移量为 35 的字节码 35: aload_0

紧跟着的异常表(Exception table:)也会使用偏移量来定位每个异常处理器所监控的范围(由 from 到 to 的代码区域),以及异常处理器的起始位置(from到target)。除此之外,它还会声明所 捕获的异常类型(type)。其中,any 指代任意异常类型。

再接下来的行数表(LineNumberTable:)则是 Java 源程序到字节码偏移量的映射。如果你在 编译时使用了 -g 参数(javac -g Foo.java),那么这里还将出现局部变量表 (LocalVariableTable:),展示 Java 程序中每个局部变量的名字、类型以及作用域。 行数表和局部变量表均属于调试信息。Java 虚拟机并不要求 class 文件必备这些信息。

  LocalVariableTable:
    Start  Length  Slot  Name   Signature
       14       5     1     e   Ljava/lang/Exception;
        0      41     0  this   LCoo;

最后则是字节码操作数栈的映射表(StackMapTable: number_of_entries = 3)。该表描述的 是字节码跳转后操作数栈的分布情况,一般被 Java 虚拟机用于验证所加载的类,以及即时编译 相关的一些操作。

我们再利用jclasslib bytecode viwer打开该字节码文件:

基本信息如下:

基本信息
基本信息

常量区如下:

常量池

字段区域如下:

字段区域

方法区域如下:

方法区域

从上图中可以看出和我们分析的一致,并且可以推断出jclasslib bytecode viwer底层也是使用javap命令来获取对应的数据,,并利用Swing将其展示。