什么是反编译?
※ 编译 Compile
将一个 *.java文件编译成 *.class 文件的过程,称为编译。
比如,HelloWorld.java 被编译后得到 HelloWorld.class
※ 反编译 Decompile
在.class文件里包含了完全的信息,包含类名、方法、属性、注解,除了注释文字之外的所有信息。所以从.class文件可以恢复得到原来的*.java文件,而且一丝不差!
从 *.class 逆向得到 *.java 的过程,称为反编译。
源代码
package com.bytecode;
public class MyTest1 {
private int a = 1;
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}
反编译的代码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.bytecode;
public class MyTest1 {
private int a = 1;
public MyTest1() {
}
public int getA() {
return this.a;
}
public void setA(int a) {
this.a = a;
}
}
F:\titled2\target\classes>javap com.bytecode.MyTest1
Compiled from "MyTest1.java"
public class com.bytecode.MyTest1 {
public com.bytecode.MyTest1();
public int getA();
public void setA(int);
}
F:\titled2\target\classes>javap -c com.bytecode.MyTest1
Compiled from "MyTest1.java"
public class com.bytecode.MyTest1 {
public com.bytecode.MyTest1();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field a:I
9: return
public int getA();
Code:
0: aload_0
1: getfield #2 // Field a:I
4: ireturn
public void setA(int);
Code:
0: aload_0
1: iload_1
2: putfield #2 // Field a:I
5: return
}
F:\titled2\target\classes>javap -verbose -p com.bytecode.MyTest1
Classfile /F:/titled2/target/classes/com/bytecode/MyTest1.class
Last modified 2020-12-12; size 471 bytes
MD5 checksum a136dc27b0f590f1bd86dde81d103bcb
Compiled from "MyTest1.java"
public class com.bytecode.MyTest1
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#21 // com/bytecode/MyTest1.a:I
#3 = Class #22 // com/bytecode/MyTest1
#4 = Class #23 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/bytecode/MyTest1;
#14 = Utf8 getA
#15 = Utf8 ()I
#16 = Utf8 setA
#17 = Utf8 (I)V
#18 = Utf8 SourceFile
#19 = Utf8 MyTest1.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = NameAndType #5:#6 // a:I
#22 = Utf8 com/bytecode/MyTest1
#23 = Utf8 java/lang/Object
{
public com.bytecode.MyTest1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field a:I
9: return
LineNumberTable:
line 3: 0
line 4: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/bytecode/MyTest1;
public int getA();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: ireturn
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/bytecode/MyTest1;
public void setA(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #2 // Field a:I
5: return
LineNumberTable:
line 11: 0
line 12: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/bytecode/MyTest1;
0 6 1 a I
}
SourceFile: "MyTest1.java"
规则
1、使用javap -verbose命令分析一个字节码文件时,将会分析该字节码文件的魔数、版本号、常量池、类信息、类的构造方法、类中的方法信息、类变量与成员变量等信息
2、魔数:所有的.class字节码文件的前4个字节都是魔数,魔数值为固定值:0xCAFEBABE。(咖啡宝贝)
3、魔数之后的4个字节为版本信息,前两个字节表示minor version(次版本号),后两上字节表示major version(主版本号),所以这里的版本号为“00 00 00 33”,换算成十进制,表示次版本号为0,主版本号为51,所以,该文件的版本号为:1.7.0
image.png
javap -verbose后有
minor version: 0
major version: 51
4、常量池(constant pool):紧接着主版本号之后的就是常量池的入口。一个Java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作是Class文件的资源仓库,比如说Java类中定义的方法与变量信息,都是存储在常量池中。常量池主要存储两类常量:字面量与符号引用。字面量如文本字符串,Java声明为final的常量等,而符号引用如类和接口的全局限定名(包名+类名),字段的名称和描述符,方法的名称和描述符。
【注意】:常量池千万不要理解成它里面只能存不变的常量值,里面也可以有变量相关的信息。
5、常量池的总体结构:Java类所对应的常量池主要由常量池数量与常量池数组这两部分共同构成。常量池数量紧跟在主版本号后面,占据2个字节;常量池数组则紧跟常量池数量之后,常量池数组与一般的数组不同的是,常量池数组中不同的元素的类型、结构都是不同的,长度当然也就不同,但是每一种元素的第一个数据都是一个u1类型,该字节是一个标志位,占据1个字节,JVM在解析常量池时,会根据这个u1类型来获取元素的具体类型。
值得注意的是,常量池数组中元素的个数 = 常量池 - 1(其中0暂时不使用),目的是满足某些常量池索引值的数据在特定情况下表达【不引用任何一个常量池】的含义;根本原因在于,索引为0也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应null值;所以,常量池的索引从1而非0开始
例如:
18的十六进制是24
image.png image.png
常量池数据类型结构表
Class字节码中有两种数据类型:
- 字节数据直接量:这是基本的数据类型。共细分为u1、u2、u3、u4四种,分别代表连续的1个字节、2个字节、4个字节、8个字节组成的整体数据。
- 表(数组):表是由多个基本数据或其它表,依照既定顺序组成的大的数据集合。表是有结构的,它的结构体现在:组成表的成分所在的位置和顺序都是已经严格定义好的。
6、在JVM规范中,每个变量/字段都有描述信息,描述信息主要的作用是描述字段的数量类型、方法的参数列表(包括数量、类型与顺序)与返回值,根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符来表示,对象类型则使用字符L加对象的全局限定名称来表示。为了压缩字节码文件的体积,对于基本数据类型,JVM都只使用一个大写字母来表示,如下表示:B - byte,C - char,D - double, F - float, I - int, J - long,S - short,Z - boolean,V - void, L - 对象类型,如Ljava/lang/String
7、对于数组类型来说,每个维度使用一共前置[
来表示,如int[]
被记录为[I
, String[][]
被记录为[[Ljava/lang/String;
8、用描述符描述方法时,按照先参数列表,后返回值的顺序来描述。参数列表按照参数的严格顺序放在一组()之内,如方法:String getRealnamebyIdAndNickname(int id, String name)的描述符为:(I, Ljava/lang/String)Ljava/lang/String;
image.png image.png9、Access_flag访问标记
访问标志信息包括该Class文件是类还是接口,是否被定义成public,是否是abstract,如果是类,是否被声明为final。而我们所定义的源代码就知道文件是类而且是public的
JVM预定义的attribute为如下表
image.png
0x0021:是0x0020和0x0001的并集,表示ACC_PUBLIC与ACC_SUPER
image.pngflags: ACC_PUBLIC, ACC_SUPER
10、字段表:fields
字段表用于描述类和接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量。
结构:
field_info{
u2 access_flags; 0002
u2 name_index; 0005
u2 descriptor_index; 0006
u2 attributes_count; 0000
attribute_info attributes[attributes_count];
}
access_flags代表访问修饰符,占2个字节;
name_index代表字段的名称索引,占2个字节;
descriptor_index代表描述符的索引,占2个字节;前三个信息就可以完整的描述一个字段的信息;
attributes_count:属性个数,可有可无;
11、方法表:
结构
method_info{
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
- access_flags:占用两个字节,表示访问标记。
- name_index:占用两个字节,名字索引,指向的是常量池。
- descriptor_index:占用两个字节,描述索引,指向的是常量池。
- attributes_count:占用两个字节,属性个数,如果为0,则下面的属性表就不显示了。
- attributes_info:属性表。
12、属性表
属性表是attribute_info类型,很显然也有它自己的结构,那长啥样呢?
attribute_info{
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
- attribute_name_index:占2个字节,表示属性名字的索引,指向常量池。(每个方法表都有一个Utf8 的 Code的属性,如下)
- attribute_length:占4个字节,表示属性的长度。
-
info[attribute_length]:占1个字节,表示具体的信息
依照上面的顺序,先数2个字节:
image.png
对应常量池:
image.png
13、Code属性
每个方法表都有一个Utf8 的 Code的属性,Code属性的结构如下
Code_attribute{
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{
u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
- attribute_length表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段。
- max_stack表示这个方法运行的任何时刻所能达到的操作数栈的最大深度。
- max_locals表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量。(当该方法是非静态方法时,会把this的变量传进方法内作为局部变量)
- code_length表法该方法所包含的字节码的字节数以及具体的指令码。
具体的字节码既是该方法被调用时,虚拟机所执行的字节码。 - exception_table:这里存放的是处理异常的信息。
第一个exception_table表项由start_pc、end_pc、handler_pc、catch_type组成。 - start_pc和end_pc表示在code数组中的从start_pc到end_pc处(包含start_pc,不包含end_pc)的指令抛出的异常会由这个表项来处理。
- handler_pc表示处理异常的代码的开始处。catch_type表示会被处理的异常类型,它指向常量池里的一个异常类。当catch_type为0时,表示处理所有的异常。
code属性中也有两个附加属性
①LineNumberTable:这个属性用来表示code数组中的字节码和Java代码行数之间的关系。这个属性可以用来再调试的时候定位代码执行的行数
结构
LineNumberTable_attribute{
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{
u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}
②LocalVariableTable:记录code数组中的局部变量表(如果是实例方法,则一定会有个this的局部遍历)
结构
LineNumberTable_attribute{
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{
u2 start_pc;
u2 length;
u2 index;
u2 name;
u2 descriptor;
} local_variable_table[local_variable_table_length];
}
14、构造方法
1、当类没有构造方法时,编译成class文件的时候,会自动创建构造方法,并且将类中的成员变量(非静态)放到构造方法中优先执行
2、当类中存在1个构造方法是,则不会自动创建构造方法,同时将将类中的成员变量(非静态)放到构造方法中优先执行
3、当类中存在多个构造方法时,则不会自动创建构造方法,同时每个构造方法中都会有成员变量(非静态)放到构造方法中优先执行
package com.bytecode;
public class MyTest2 {
String str = "Welcome";
private int x = 5;
public static Integer in = 10;
public MyTest2(){
}
public MyTest2(int i){
System.out.println("aaa");
}
public static void main(String[] args){
MyTest2 myTest2 = new MyTest2();
myTest2.setX(8);
}
private synchronized void setX(int x){
this.x = x;
}
private void test(String str){
synchronized (str){
System.out.println("hello world");
}
}
private synchronized static void test2(){
}
}
public com.bytecode.MyTest2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String Welcome
7: putfield #3 // Field str:Ljava/lang/String;
10: aload_0
11: iconst_5
12: putfield #4 // Field x:I
15: return
LineNumberTable:
line 11: 0
line 5: 4
line 7: 10
line 13: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Lcom/bytecode/MyTest2;
public com.bytecode.MyTest2(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: ldc #2 // String Welcome
7: putfield #3 // Field str:Ljava/lang/String;
10: aload_0
11: iconst_5
12: putfield #4 // Field x:I
15: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
18: ldc #6 // String aaa
20: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
23: return
LineNumberTable:
line 15: 0
line 5: 4
line 7: 10
line 16: 15
line 17: 23
LocalVariableTable:
Start Length Slot Name Signature
0 24 0 this Lcom/bytecode/MyTest2;
0 24 1 i I
反编译后可以看到两个构造方法的Code属性中前14行都是一样的,都是对成员变量的初始化
15、<clinit>方法
静态代码块的的赋值操作和执行都会放在<clinit>方法中,并且按照从上到下的顺序放在<clinit>方法中
网友评论