一、反编译class文件,获取字节码清单
1.1 编译 .java文件,得到 .class文件
编写java文件
public class App {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
编译java文件,得到class文件
# javac -help 查看用法
# -encoding utf-8 指定编码
# -g 在反编译时,会有局部变量表
javac -encoding utf-8 App.java
查看class文件,16进制格式(使用IDEA的HEX View插件)
1.2 javap 反编译class文件, 获取字节码清单
javap 的使用帮助
javap -help
反编译, 输出到文件App.javap
javap -verbose -p App.class > App.javap
打开App.javap文件
Classfile /D:/IDEA/demo/javalearn/src/main/java/cn/hdj/App.class
Last modified 2020-10-12; size 419 bytes
MD5 checksum 61d336aa9be2ce70eff27134d076f642
Compiled from "App.java"
public class cn.hdj.App
minor version: 0
major version: 55
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello World!
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // cn/hdj/App
#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 App.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello World!
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 cn/hdj/App
#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 cn.hdj.App();
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 8: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 10: 0
line 11: 8
}
SourceFile: "App.java"
二、 字节码了解
2.1 字节码格式
在了解字节码文件信息前,我们需要先了解class文件格式有哪些组成?
一个class文件组成
ClassFile {
u4 magic; //魔数, 用于识别class文件格式
u2 minor_version;//次版本号
u2 major_version;//主版本号
u2 constant_pool_count; //常量池计数器
cp_info constant_pool[constant_pool_count-1]; //常量池
u2 access_flags;//访问标志
u2 this_class;//当前类名
u2 super_class;//父类名称
u2 interfaces_count;//接口计数器
u2 interfaces[interfaces_count];//接口信息
u2 fields_count;//字段计数器
field_info fields[fields_count];//字段表
u2 methods_count;//方法计数器
method_info methods[methods_count];//方法表
u2 attributes_count; //属性计数器
attribute_info attributes[attributes_count];附加属性表
}
访问标志 | 标识符名称 | 标识符值 | 描述 | | —- | —- | —- | | ACC_PUBLIC | 0x0001 | 声明 public ,能在外部其它包中访问 | |
ACC_FINAL
| 0x0010 | 声明为final;只用于类,标识不能创建子类 | |ACC_SUPER
| 0x0020 | 可通过invokespecial指令调用超类的方法 | |ACC_INTERFACE
| 0x0200 | 标志这是一个接口 | |ACC_ABSTRACT
| 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,
次标志值为真,其他类型为假 | |ACC_SYNTHETIC
| 0x1000 | 标志这个类并非由用户代码产生 | |ACC_ANNOTATION
| 0x2000 | 标志这是一个注解 | |ACC_ENUM
| 0x4000 | 标志这是一个枚举 |
- 字节码的类型对应
| 字段术语 | 代表类型 | 解析 |
| —- | —- | —- |
|
B
|byte
| 基本类型byte | |C
|char
| 基本类型char | |D
|double
| 基本类型double | |F
|float
| 基本类型float | |I
|int
| 基本类型int | |J
|long
| 基本类型long | |L
ClassName |reference
| 对象类型,以分号结尾,如Ljava/lang/Object; | |S
|short
| 基本类型short | |Z
|boolean
| 基本类型布尔值;true 或者 false | |[
|reference
| 表示一维数组引用 | | V | void | 特殊类型void |
- 算数操作与类型转换
- 方法调用的指令 | 指令 | 描述 | | —- | —- | | invokestatic | 用于调用某个类的静态方法 | | invokespecial | 用来调用构造函数,但也可以用于调用同一个类中的 private 方法, 以及可见的超类方法。 | | invokevirtual | 如果是具体类型的目标对象,invokevirtual 用于调用公共,受保护和package 级的私有方法。 | | invokeinterface | 当通过接口引用来调用方法时,将会编译invokeinterface 指令。 | | invokedynamic | JDK7 新增加的指令,是实现“动态类型语言”(Dynamically TypedLanguage)支持而进行的升级改进,同时也是 JDK8 以后支持 lambda 表达式的实现基础。 |
2.3 解读字节码
源码文件
public class DemoDynamic {
public static void foo() {
int a = 1;
int b = 2;
int c = (a + b) * 5;
}
}
反编译
javac -g -encoding utf-8 DemoDynamic.java
javap -verbose -c .\DemoDynamic.class > .\DemoDynamic.javap
DemoDynamic.javap 反编译文件
头部信息
Classfile /D:/IDEA/Java-Learning/src/main/java/cn/hdj/jvm/bytecode/DemoDynamic.class
Last modified 2020-10-17; size 419 bytes
MD5 checksum 0242e2d86e94eb62d302f5a034336416
Compiled from "DemoDynamic.java"
public class cn.hdj.jvm.bytecode.DemoDynamic
minor version: 0 //版本号
major version: 52
flags: ACC_PUBLIC, ACC_SUPER 访问标识符
常量池信息
//常量池, 常量池可以理解成Class文件中的资源仓库
//主要存放的是两大类常量:字面量(Literal)和符号引用(Symbolic References)
Constant pool:
// #1常量是一个方法定义,,指向了第3和第18个常量,注释为该常量的内容
// 注释的内容可以理解为构造函数的初始化,执行的是Object对象的<init>方法,没有参数,无返回值
#1 = Methodref #3.#18 // java/lang/Object."<init>":()V
#2 = Class #19 // cn/hdj/jvm/bytecode/DemoDynamic
#3 = Class #20 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Lcn/hdj/jvm/bytecode/DemoDynamic;
#11 = Utf8 foo
#12 = Utf8 a
#13 = Utf8 I //通过字节码类型可知,该常量为int类型
#14 = Utf8 b
#15 = Utf8 c
#16 = Utf8 SourceFile
#17 = Utf8 DemoDynamic.java
#18 = NameAndType #4:#5 // "<init>":()V
#19 = Utf8 cn/hdj/jvm/bytecode/DemoDynamic
#20 = Utf8 java/lang/Object
foo() 方法
public static void foo();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC //标识符,public static
Code:
stack=2, locals=3, args_size=0 //栈容量2 , 局部变量表容量3, 参数个数0
0: iconst_1 // 加载int型1到栈顶 -> 栈1=1
1: istore_0 // 把栈顶 1 值存到局部变量表0位置 -> 局部0=1
2: iconst_2 // 加载int型2到栈顶位置 -> 栈1=2
3: istore_1 // 把栈2值存到局部变量表1位置 -> 局部1=2
4: iload_0 // 从局部变量表中加载1,入栈
5: iload_1 // 从局部变量表中加载2,入栈
6: iadd //执行相加操作, 1+2 = 3, 入栈
7: iconst_5 //加载int型5,入栈
8: imul //执行相乘操作,3*5=15,入栈
9: istore_2 // 保存到局部变量表2位置,-> 局部2=15
10: return //返回
LineNumberTable: //行数表
line 9: 0
line 10: 2
line 11: 4
line 12: 10
LocalVariableTable: //局部变量表
Start Length Slot Name Signature
2 9 0 a I
4 7 1 b I
10 1 2 c I
}
- stack 最大操作数栈,JVM运行时会根据这个值来分配栈帧(Frame)中的操作栈深度,此处为1
- locals:局部变量所需的存储空间,单位为Slot, Slot是虚拟机为局部变量分配内存时所使用的最小单位,为4个字节大小。方法参数(包括实例方法中的隐藏参数this),显示异常处理器的参数(try catch中的catch块所定义的异常),方法体中定义的局部变量都需要使用局部变量表来存放。值得一提的是,locals的大小并不一定等于所有局部变量所占的Slot之和,因为局部变量中的Slot是可以重用的。
- args_size: 方法参数的个数,这里是1,因为每个实例方法都会有一个隐藏参数this
- LocalVariableTable : 该属性的作用是描述帧栈中局部变量与源码中定义的变量之间的关系。start 表示该局部变量在哪一行开始可见,length表示可见行数,Slot代表所在帧栈位置,Name是变量名称,然后是类型签名。
- foo()方法字节码执行图示
三、字节码增强
3.1 ASM
3.2 Javassist
辅助工具
IDEA插件 jclasslib Bytecode viewer
参考
轻松看懂Java字节码 https://juejin.im/post/6844903588716609543
- 字节码增强技术探索 https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html
- .class 文件格式https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html