从一个基本的类的反编译说起:
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 类型 |
| 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将其展示。