好了,好久没更新了,今天来一篇关于java反编译的篇举例子。
Java编译文件是指通过Java编译器将Java源代码文件(.java文件)编译成字节码文件(.class文件)的过程。在Java中,源代码文件包含人类可读的文本,而字节码文件是计算机可以理解的二进制指令集。
Java编译器的主要任务是将源代码文件中的语法、类型检查和语义分析转换为字节码文件。这个过程包括两个主要阶段:
编译和链接。
- 编译阶段:编译器首先对源代码进行语法分析和类型检查,然后将代码转换为抽象语法树(AST)。接下来,编译器会优化AST,并将其转换为中间表示(IR)。最后,编译器将IR转换为字节码文件。
- 链接阶段:链接器负责将生成的类文件和相关的库文件合并成一个可执行的Java程序。链接器需要确保所有引用的类和方法都存在,并按照正确的顺序进行加载。
Java编译器的输出是一个或多个字节码文件,通常以.class扩展名结尾。这些字节码文件可以在Java虚拟机(JVM)上运行,或者使用Java开发工具(如Eclipse、IntelliJ IDEA等)进行调试和分析。
需要注意的是,Java源代码文件本身不能直接被Java编译器编译。用户需要先使用文本编辑器或其他开发工具编写源代码,然后将其保存为.java文件。接下来,用户可以通过命令行或集成开发环境(IDE)调用Java编译器(如javac命令)来编译源代码文件。
创建一个最简单的例子:
java
public class Demo01{
public static void main(String[] args){
String name = "LXH";
System.out.println(name);
}
}
在cmd控制台,我们进行反编译这个.java文件。
shell
# 进入这个类所在的文件夹
javac -version # 检查一下javac -version 是否配置好了环境变量
javac Demo01.java # 将我们编写的类文件转换为二进制文件
java Demo01 # Java虚拟机执行二进制文件
# 接下来,重头戏,反编译
javap -verbose Demo01
得到反编译后的代码
java
Classfile /D:/工作中的学习资料/Git Down/java_play/CMD_PLAY/DEMO1/Demo01.class
Last modified 2023-8-28; size 415 bytes
MD5 checksum 1e5cfb3099439767a8a3f40ebccce0b7
Compiled from "Demo01.java"
public class Demo01
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = String #16 // LXH
#3 = Fieldref #17.#18 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // Demo01
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Demo01.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Utf8 LXH
#17 = Class #23 // java/lang/System
#18 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 Demo01
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public Demo01();
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 static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: ldc #2 // String LXH
2: astore_1
3: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
6: aload_1
7: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
10: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 10
}
SourceFile: "Demo01.java"
我们根据上述的代码进行分析:
Classfile /D:/工作中的学习资料/Git Down/java_play/CMD_PLAY/DEMO1/Demo01.class
,代表这个类当前所在的文件夹具体位置。
Last modified 2023-8-28; size 415 bytes
表示上次修改日期,另外这个生成的文件的大小,415个字节。
重点,我们看常量池:Constant pool 存储了编译后的字节码指令和相关信息。
- #1: 表示对java/lang/Object类的""方法的引用。
- #2: 表示字符串常量"LXH"。
- #3: 表示对System.out字段的引用,它是java.io.PrintStream类的静态成员变量。
- #4: 表示对java.io.PrintStream.println方法的引用,它是java.io.PrintStream类的实例方法。
- #5: 表示Demo01类本身。
- #6: 表示java/lang/Object类本身。
- #7: 表示构造函数的签名,即无参数的构造函数。
- #8: 表示无参数构造函数的方法类型。
- #9: 表示行号表条目,用于在调试时定位源代码行。
- #10: 表示主方法的签名,即接受一个String数组参数的方法。
- #11: 表示主方法所在的源文件名。
- #12: 表示主方法的参数类型,即String数组。
- #13: 表示行号表中的主方法行号。
- #14: 表示主方法所在的源文件名。
- #15: 表示主方法的参数类型,即String数组。
- #16: 表示字符串常量"LXH"。
- #17: 表示java/lang/System类。
- #18: 表示System.out字段的名称和类型。
- #19: 表示java/io/PrintStream类。
- #20: 表示java.io.PrintStream.println方法的名称和类型。
- #21: 表示Demo01类本身。
- #22: 表示java/lang/Object类本身。
- #23: 表示java/lang/System类。
- #24: 表示System.out字段的名称和类型。
- #25: 表示java.io.PrintStream类中的out字段名称。
- #26: 表示java.io.PrintStream类。
- #27: 表示java.io.PrintStream.println方法的名称和类型。
分析:
shell
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
这是一个方法引用(Methodref),它引用了常量池中的第6个项(#6),该项是一个类引用,指向 java/lang/Object。
#15 表示常量池中的第15个项,是一个名称和类型(NameAndType)引用,它指向构造函数的名字和描述符(<init>:()V)。
这个引用表示调用 java/lang/Object 类的无参数构造函数。
#2 = String #16 // LXH
这是一个字符串常量(String),它引用了常量池中的第16个项(#16),表示字符串 "LXH"。
#3 = Fieldref #17.#18 // java/lang/System.out:Ljava/io/PrintStream;
这是一个字段引用(Fieldref),它引用了常量池中的第17个项(#17),该项是一个类引用,指向 java/lang/System。#18 表示常量池中的第18个项,是一个名称和类型引用,它指向 out 字段的名字和描述符(Ljava/io/PrintStream;)。
这个引用表示访问 java/lang/System 类中的 out 字段,该字段的类型是 java/io/PrintStream。
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
这是一个方法引用,它引用了常量池中的第19个项(#19),该项是一个类引用,指向 java/io/PrintStream。#20 表示常量池中的第20个项,是一个名称和类型引用,它指向 println 方法的名字和描述符((Ljava/lang/String;)V)。这个引用表示调用 java/io/PrintStream 类的 println 方法,该方法接受一个 java/lang/String 类型的参数,返回类型是 void。
#5 = Class #21 // Demo01
这是一个类引用,它引用了常量池中的第21个项(#21),表示类 Demo01。
#6 = Class #22 // java/lang/Object
这是一个类引用,它引用了常量池中的第22个项(#22),表示类 java/lang/Object。
#7 = Utf8 <init>
这是一个UTF-8编码的字符串,表示字符串 "<init>",通常用于表示构造函数。
#8 = Utf8 ()V
这是一个UTF-8编码的字符串,表示方法的描述符,( )V 表示该方法没有参数且返回类型是 void。
#9 = Utf8 Code
这是一个UTF-8编码的字符串,表示"Code",通常用于标识字节码中的代码部分。
#10 = Utf8 LineNumberTable
这是一个UTF-8编码的字符串,表示"LineNumberTable",通常用于存储字节码指令与源代码行号之间的映射关系。
#11 = Utf8 main
这是一个UTF-8编码的字符串,表示方法名"main"。
#12 = Utf8 ([Ljava/lang/String;)V
这是一个UTF-8编码的字符串,表示方法的描述符,([Ljava/lang/String;)V 表示方法接受一个 java/lang/String 类型的数组作为参数,返回类型是 void。
#13 = Utf8 SourceFile
这是一个UTF-8编码的字符串,表示"SourceFile",通常用于指定源代码文件的名称。
#14 = Utf8 Demo01.java
这是一个UTF-8编码的字符串,表示源代码文件的名称为"Demo01.java"。
#15 = NameAndType #7:#8 // "<init>":()V
这是一个名称和类型引用,它引用了常量池中的第7个项(#7),表示构造函数的名字。#8 表示常量池中的第8个项,是一个方法描述符,()V 表示该方法没有参数且返回类型是 void。这个引用表示构造函数的名字和描述符。
#16 = Utf8 LXH
这是一个UTF-8编码的字符串,表示字符串"LXH"。
#17 = Class #23 // java/lang/System
这是一个类引用,引用了常量池中的第23个项,表示类java/lang/System。
#18 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
这是一个名称和类型引用,它引用了常量池中的第24个项(#24),表示字段名"out"。#25 表示常量池中的第25个项,是一个字段的类型描述符,Ljava/io/PrintStream; 表示字段的类型是java/io/PrintStream。这个引用表示字段名和类型描述符。
#19 = Class #26 // java/io/PrintStream
这是一个类引用,引用了常量池中的第26个项,表示类java/io/PrintStream。
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
这是一个名称和类型引用,它引用了常量池中的第27个项(#27),表示方法名"println"。#28 表示常量池中的第28个项,是一个方法描述符,(Ljava/lang/String;)V 表示方法接受一个java/lang/String类型的参数,返回类型是 void。这个引用表示方法名和方法描述符。
#21 = Utf8 Demo01
这是一个UTF-8编码的字符串,表示类名"Demo01"。
#22 = Utf8 java/lang/Object
这是一个UTF-8编码的字符串,表示类名"java/lang/Object"。
#23 = Utf8 java/lang/System
这是一个UTF-8编码的字符串,表示类名"java/lang/System"。
#24 = Utf8 out
这是一个UTF-8编码的字符串,表示字段名"out"。
#25 = Utf8 Ljava/io/PrintStream;
这是一个UTF-8编码的字符串,表示字段类型描述符Ljava/io/PrintStream;,表示类型是java/io/PrintStream。
#26 = Utf8 java/io/PrintStream
这是一个UTF-8编码的字符串,表示类名"java/io/PrintStream"。
#27 = Utf8 println
这是一个UTF-8编码的字符串,表示方法名"println"。
#28 = Utf8 (Ljava/lang/String;)V
这是一个UTF-8编码的字符串,表示方法的描述符,(Ljava/lang/String;)V 表示方法接受一个java/lang/String类型的参数,返回类型是 void。
类定义部分:
- 类名:Demo01
- 访问标志:ACC_PUBLIC,表示类是公共的,可以从其他类访问。
- 超类:java/lang/Object,该类继承自 java.lang.Object。
- 常量池(Constant Pool): 常量池包含了一系列的常量项,例如方法引用、字段引用、字符串字面值等。
构造函数(Constructor):
- 公共构造函数:public Demo01(),无参数,无返回值。
- 构造函数访问标志:ACC_PUBLIC,表示构造函数是公共的。
构造函数的字节码指令(Code)部分:
- 栈大小(stack):1,表示最大堆栈深度为1。
- 本地变量表大小(locals):1,表示本地变量表中有1个元素。
- 参数大小(args_size):1,表示方法的参数个数为1。
构造函数内容:
- 指令1:aload_0,将当前对象引用(this)加载到操作数栈。
- 指令2:invokespecial #1,调用父类 Object 的构造函数(""),完成对象的初始化。
- 指令4:return,从构造函数返回。
- 构造函数的行号表(LineNumberTable):展示了字节码指令和源代码行号的映射。在这里只有一行,行号为3。
主方法(main):
- 公共静态主方法:public static void main(String[] args)。
- 方法访问标志:ACC_PUBLIC、ACC_STATIC,表示方法是公共的和静态的。
主方法的字节码指令部分:
- 指令0:将字符串 "LXH" 加载到操作数栈。
- 指令2:将操作数栈的值存储到局部变量表索引为1的位置(astore_1)。
- 指令3:获取静态字段 System.out。
- 指令6:加载局部变量1(存储的是 "LXH" 字符串)到操作数栈。
- 指令7:调用 System.out 的 println 方法,将字符串打印到控制台。
- 指令10:返回主方法。
主方法的行号表部分:
- 源代码行号5对应指令0。
- 源代码行号6对应指令2。
- 源代码行号7对应指令10。
- 源文件名(SourceFile):表示源文件名为 "Demo01.java"。
实际上,我在拿到这段编译后的代码也是吓了一跳,我没想到写的就4行代码,结果反编译出来就这么长,但是认真去看了一下,还是很长,但是认真去分析,发现还是有规律可循的,认真看,还是有收货的。