目录导航
Java源文件(*.java
)通过编译后会变成class文件
,class文件
有固定的二进制格式,class文件
的结构在JVM虚拟机规范第四章:The class File Format中有详细的说明。本章节将学习class文件结构
、class文件解析
、class文件反编译
以及ASM字节码库
。
Java语言和JVM虚拟机规范:《Java15语言规范》、《Java15虚拟机实现规范》
示例代码TestHelloWorld:
package com.anbai.sec.classloader;
/**
* Creator: yz
* Date: 2019/12/17
*/
public class TestHelloWorld {
public String hello() {
return "Hello World~";
}
}
TestHelloWorld.java编译解析流程:

TestHelloWorld.java 源码、字节码:

Java class文件格式
在JVM虚拟机规范第四章中规定了class文件必须是一个固定的结构,如下所示:
ClassFile {
u4 magic;
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];
}
在JVM规范中u1
、u2
、u4
分别表示的是1、2、4个字节的无符号数,可使用java.io.DataInputStream
类中的对应方法:readUnsignedByte
、readUnsignedShort
、readInt
方法读取。除此之外,表结构(table
)由任意数量的可变长度的项组成,用于表示class中的复杂结构,如上述的:cp_info
、field_info
、method_info
、attribute_info
。
TestHelloWorld.class十六进制:

Magic(魔数)
魔数是class文件的标识符,固定值为0xCAFEBABE
,JVM加载class文件时会先读取4字节(u4 magic;
)的魔数信息校验是否是一个class文件。
Minor/Major Version(版本号)
class文件的版本号由两个u2
组成(u2 minor_version; u2 major_version;
),分别表示的是minor_version
(副版本号)、major_version
(主版本号),我们常说的JDK1.8
、Java9
等说的就是主版本号,如上图中的TestHelloWorld.class
的版本号0x34
即JDK1.8
。
Java版本对应表:
JDK版本 | 十进制 | 十六进制 | 发布时间 |
---|---|---|---|
JDK1.1 | 45 | 2D | 1996-05 |
JDK1.2 | 46 | 2E | 1998-12 |
JDK1.3 | 47 | 2F | 2000-05 |
JDK1.4 | 48 | 30 | 2002-02 |
JDK1.5 | 49 | 31 | 2004-09 |
JDK1.6 | 50 | 32 | 2006-12 |
JDK1.7 | 51 | 33 | 2011-07 |
JDK1.8 | 52 | 34 | 2014-03 |
Java9 | 53 | 35 | 2017-09 |
Java10 | 54 | 36 | 2018-03 |
Java11 | 55 | 37 | 2018-09 |
Java12 | 56 | 38 | 2019-03 |
Java13 | 57 | 39 | 2019-09 |
Java14 | 58 | 3A | 2020-03 |
Java15 | 59 | 3B | 2020-09 |
constant_pool_count (常量池计数器)
u2 constant_pool_count;
表示的是常量池中的数量,constant_pool_count
的值等于常量池中的数量加1,需要特别注意的是long
和double
类型的常量池对象占用两个常量位。
constant_pool(常量池)
cp_info constant_pool[constant_pool_count-1];
是一种表结构,cp_info
表示的是常量池对象。
cp_info
数据结构:
cp_info {
u1 tag;
u1 info[];
}
u1 tag;
表示的是常量池中的存储类型,常量池中的tag
说明:
常量池类型 | Tag | 章节 |
---|---|---|
CONSTANT_Utf8 | 1 | §4.4.7 |
CONSTANT_Integer | 3 | §4.4.4 |
CONSTANT_Float | 4 | §4.4.4 |
CONSTANT_Long | 5 | §4.4.5 |
CONSTANT_Double | 6 | §4.4.5 |
CONSTANT_Class | 7 | §4.4.1 |
CONSTANT_String | 8 | §4.4.3 |
CONSTANT_Fieldref | 9 | §4.4.2 |
CONSTANT_Methodref | 10 | §4.4.2 |
CONSTANT_InterfaceMethodref | 11 | §4.4.2 |
CONSTANT_NameAndType | 12 | §4.4.6 |
CONSTANT_MethodHandle | 15 | §4.4.8 |
CONSTANT_MethodType | 16 | §4.4.9 |
CONSTANT_Dynamic | 17 | §4.4.10 |
CONSTANT_InvokeDynamic | 18 | §4.4.10 |
CONSTANT_Module | 19 | §4.4.11 |
CONSTANT_Package | 20 | §4.4.12 |
每一种tag
都对应了不同的数据结构,上述表格中标记了不同类型的tag值以及对应的JVM规范章节。
access_flags (访问标志)
u2 access_flags;
,表示的是某个类或者接口的访问权限及属性。
标志名 | 十六进制值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | 声明为public |
ACC_FINAL | 0x0010 | 声明为final |
ACC_SUPER | 0x0020 | 废弃/仅JDK1.0.2前使用 |
ACC_INTERFACE | 0x0200 | 声明为接口 |
ACC_ABSTRACT | 0x0400 | 声明为abstract |
ACC_SYNTHETIC | 0x1000 | 声明为synthetic,表示该class文件并非由Java源代码所生成 |
ACC_ANNOTATION | 0x2000 | 标识注解类型 |
ACC_ENUM | 0x4000 | 标识枚举类型 |
this_class(当前类名称)
u2 this_class;
表示的是当前class文件的类名所在常量池中的索引位置。
super_class(当前类的父类名称)
u2 super_class;
表示的是当前class文件的父类类名所在常量池中的索引位置。java/lang/Object
类的super_class
的为0,其他任何类的super_class
都必须是一个常量池中存在的索引位置。
interfaces_count(当前类继承或实现的接口数)
u2 interfaces_count;
表示的是当前类继承或实现的接口数。
interfaces[] (接口名称数组)
u2 interfaces[interfaces_count];
表示的是所有接口数组。
fields_count(当前类的成员变量数)
u2 fields_count;
表示的是当前class中的成员变量个数。
fields[](成员变量数组)
field_info fields[fields_count];
表示的是当前类的所有成员变量,field_info
表示的是成员变量对象。
field_info数据结构:
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
属性结构:
u2 access_flags;
表示的是成员变量的修饰符;u2 name_index;
表示的是成员变量的名称;u2 descriptor_index;
表示的是成员变量的描述符;u2 attributes_count;
表示的是成员变量的属性数量;attribute_info attributes[attributes_count];
表示的是成员变量的属性信息;
methods_count(当前类的成员方法数)
u2 methods_count;
表示的是当前class中的成员方法个数。
methods[](成员方法数组)
method_info methods[methods_count];
表示的是当前class中的所有成员方法,method_info
表示的是成员方法对象。
method_info数据结构:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
属性结构:
u2 access_flags;
表示的是成员方法的修饰符;u2 name_index;
表示的是成员方法的名称;u2 descriptor_index;
表示的是成员方法的描述符;u2 attributes_count;
表示的是成员方法的属性数量;attribute_info attributes[attributes_count];
表示的是成员方法的属性信息;
attributes_count (当前类的属性数)
u2 attributes_count;
表示当前class文件属性表的成员个数。
attributes[](属性数组)
attribute_info attributes[attributes_count];
表示的是当前class文件的所有属性,attribute_info
是一个非常复杂的数据结构,存储着各种属性信息。
attribute_info
数据结构:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
u2 attribute_name_index;
表示的是属性名称索引,读取attribute_name_index
值所在常量池中的名称可以得到属性名称。
Java15属性表:
属性名称 | 章节 |
---|---|
ConstantValue Attribute | §4.7.2 |
Code Attribute | §4.7.3 |
StackMapTable Attribute | §4.7.4 |
Exceptions Attribute | §4.7.5 |
InnerClasses Attribute | §4.7.6 |
EnclosingMethod Attribute | §4.7.7 |
Synthetic Attribute | §4.7.8 |
Signature Attribute | §4.7.9 |
SourceFile Attribute | §4.7.10 |
SourceDebugExtension Attribute | §4.7.11 |
LineNumberTable Attribute | §4.7.12 |
LocalVariableTable Attribute | §4.7.13 |
LocalVariableTypeTable Attribute | §4.7.14 |
Deprecated Attribute | §4.7.15 |
RuntimeVisibleAnnotations Attribute | §4.7.16 |
RuntimeInvisibleAnnotations Attribute | §4.7.17 |
RuntimeVisibleParameterAnnotations Attribute | §4.7.18 |
RuntimeInvisibleParameterAnnotations Attribute | §4.7.19 |
RuntimeVisibleTypeAnnotations Attribute | §4.7.20 |
RuntimeInvisibleTypeAnnotations Attribute | §4.7.21 |
AnnotationDefault Attribute | §4.7.22 |
BootstrapMethods Attribute | §4.7.23 |
MethodParameters Attribute | §4.7.24 |
Module Attribute | §4.7.25 |
ModulePackages Attribute | §4.7.26 |
ModuleMainClass Attribute | §4.7.27 |
NestHost Attribute | §4.7.28 |
NestMembers Attribute | §4.7.29 |
属性对象
属性表是动态的,新的JDK版本可能会添加新的属性值。每一种属性的数据结构都不相同,所以读取到属性名称后还需要根据属性的类型解析不同属性表中的值。比如Code Attribute
中存储了类方法的异常表、字节码指令集、属性信息等重要信息。
Java class文件解析
为了能够更加深入的学习class结构,本章节将写一个ClassByteCodeParser类(有极小部分数据结构较复杂没解析)来实现简单的class文件解析。
首先我们创建一个用于测试的TestHelloWorld.java
文件,源码如下:
package com.anbai.sec.bytecode;
import java.io.Serializable;
/**
* Creator: yz
* Date: 2019/12/17
*/
@Deprecated
public class TestHelloWorld implements Serializable {
private static final long serialVersionUID = -7366591802115333975L;
private long id = 1l;
private String username;
private String password;
public String hello(String content) {
String str = "Hello:";
return str + content;
}
public static void main(String[] args) {
TestHelloWorld test = new TestHelloWorld();
String str = test.hello("Hello World~");
System.out.println(str);
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "TestHelloWorld{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
然后使用javac
将TestHelloWorld.java
编译成TestHelloWorld.class
文件,或者使用maven构建javaweb-sec/javaweb-sec-source/javase/
项目,构建成功后在javaweb-sec/javaweb-sec-source/javase/target/classes/com/anbai/sec/bytecode/
目录下可以找到TestHelloWorld.class
文件。
最后编写一个ClassByteCodeParser类
,严格按照JVM规范中的类文件格式文档规定,依次解析class文件的各种数据类型就可以实现字节码解析了。
ClassByteCodeParser代码片段(省略了getter/setter和解析逻辑):
package com.anbai.sec.bytecode;
/**
* Java类字节码解析,参考:https://docs.oracle.com/javase/specs/jvms/se15/jvms15.pdf和https://github.com/ingokegel/jclasslib
*/
public class ClassByteCodeParser {
/**
* 转换为数据输入流
*/
private DataInputStream dis;
/**
* Class文件魔数
*/
private int magic;
/**
* Class小版本号
*/
private int minor;
/**
* Class大版本号
*/
private int major;
/**
* 常量池中的对象数量
*/
private int poolCount;
/**
* 创建常量池Map
*/
private final Map<Integer, Map<String, Object>> constantPoolMap = new LinkedHashMap<>();
/**
* 类访问修饰符
*/
private int accessFlags;
/**
* thisClass
*/
private String thisClass;
/**
* superClass
*/
private String superClass;
/**
* 接口数
*/
private int interfacesCount;
/**
* 接口Index数组
*/
private String[] interfaces;
/**
* 成员变量数量
*/
private int fieldsCount;
/**
* 成员变量数组
*/
private final Set<Map<String, Object>> fieldList = new HashSet<>();
/**
* 方法数
*/
private int methodsCount;
/**
* 方法数组
*/
private final Set<Map<String, Object>> methodList = new HashSet<>();
/**
* 属性数
*/
private int attributesCount;
/**
* 属性
*/
private Map<String, Object> attributes;
/**
* 解析Class字节码
*
* @param in 类字节码输入流
* @throws IOException 解析IO异常
*/
private void parseByteCode(InputStream in) throws IOException {
// 将输入流转换成DataInputStream
this.dis = new DataInputStream(in);
// 解析字节码逻辑代码
}
public static void main(String[] args) throws IOException {
// 解析单个class文件
File classFile = new File(System.getProperty("user.dir"), "javaweb-sec-source/javase/target/classes/com/anbai/sec/bytecode/TestHelloWorld.class");
ClassByteCodeParser codeParser = new ClassByteCodeParser();
codeParser.parseByteCode(new FileInputStream(classFile));
System.out.println(JSON.toJSONString(codeParser));
}
}
解析完TestHelloWorld.class
后将会生成一个json字符串,省略掉复杂的constantPoolMap
、fieldList
、methodList
、attributes
属性后格式如下:
{
"accessFlags": 33,
"attributes": {},
"attributesCount": 3,
"constantPoolMap": {},
"fieldList": [],
"fieldsCount": 4,
"interfaces": [
"java/io/Serializable"
],
"interfacesCount": 1,
"magic": -889275714,
"major": 51,
"methodList": [],
"methodsCount": 10,
"minor": 0,
"poolCount": 95,
"superClass": "java/lang/Object",
"thisClass": "com/anbai/sec/bytecode/TestHelloWorld"
}
魔数/版本解析
一个合法的class文件以固定的0xCAFEBABE
格式开始,所以需要先读取4个字节,判断文件二进制格式是否是合法。
u4 magic;
u2 minor_version;
u2 major_version;
魔数和版本号解析代码片段:
// u4 magic;
int magic = dis.readInt();
// 校验文件魔数
if (0xCAFEBABE == magic) {
this.magic = magic;
// u2 minor_version
this.minor = dis.readUnsignedShort();
// u2 major_version;
this.major = dis.readUnsignedShort();
}
解析结果:
{
"magic": -889275714,
"minor": 0,
"major": 51
}
其中"major": 51
对应的JDK版本是JDK1.7。
常量池解析
解析常量池信息时需要先解析出常量池对象的数量,然后遍历常量池,解析cp_info
对象。
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
为了便于理解解析过程,特意将常量池解析流程单独拆开成如下几步:
- 读取常量池数量(
u2 constant_pool_count;
); - 读取
tag
; - 根据不同的
tag
类型解析常量池对象; - 解析常量池中的对象;
- 链接常量池中的索引引用;
常量池解析片段:
/**
* 解析常量池数据
*
* @throws IOException 数据读取异常
*/
private void parseConstantPool() throws IOException {
// u2 constant_pool_count;
this.poolCount = dis.readUnsignedShort();
// cp_info constant_pool[constant_pool_count-1];
for (int i = 1; i <= poolCount - 1; i++) {
// cp_info {
// u1 tag;
// u1 info[];
// }
int tag = dis.readUnsignedByte();
Constant constant = Constant.getConstant(tag);
if (constant == null) {
throw new RuntimeException("解析常量池异常,无法识别的常量池类型:" + tag);
}
// 解析常量池对象
parseConstantItems(constant, i);
// Long和Double是宽类型,占两位
if (CONSTANT_LONG == constant || CONSTANT_DOUBLE == constant) {
i++;
}
}
// 链接常量池中的引用
linkConstantPool();
}
解析常量池对象代码片段:
/**
* 解析常量池中的对象
*
* @param constant 常量池
* @param index 常量池中的索引位置
* @throws IOException 数据读取异常
*/
private void parseConstantItems(Constant constant, int index) throws IOException {
Map<String, Object> map = new LinkedHashMap<>();
switch (constant) {
case CONSTANT_UTF8:
// CONSTANT_Utf8_info {
// u1 tag;
// u2 length;
// u1 bytes[length];
// }
int length = dis.readUnsignedShort();
byte[] bytes = new byte[length];
dis.read(bytes);
map.put("tag", CONSTANT_UTF8);
map.put("value", new String(bytes, UTF_8));
break;
case CONSTANT_INTEGER:
// CONSTANT_Integer_info {
// u1 tag;
// u4 bytes;
// }
map.put("tag", CONSTANT_INTEGER);
map.put("value", dis.readInt());
break;
case CONSTANT_FLOAT:
// CONSTANT_Float_info {
// u1 tag;
// u4 bytes;
// }
map.put("tag", CONSTANT_FLOAT);
map.put("value", dis.readFloat());
break;
case CONSTANT_LONG:
// CONSTANT_Long_info {
// u1 tag;
// u4 high_bytes;
// u4 low_bytes;
// }
map.put("tag", CONSTANT_LONG);
map.put("value", dis.readLong());
break;
case CONSTANT_DOUBLE:
// CONSTANT_Double_info {
// u1 tag;
// u4 high_bytes;
// u4 low_bytes;
// }
map.put("tag", CONSTANT_DOUBLE);
map.put("value", dis.readDouble());
break;
case CONSTANT_CLASS:
// CONSTANT_Class_info {
// u1 tag;
// u2 name_index;
// }
map.put("tag", CONSTANT_CLASS);
map.put("nameIndex", dis.readUnsignedShort());
break;
case CONSTANT_STRING:
// CONSTANT_String_info {
// u1 tag;
// u2 string_index;
// }
map.put("tag", CONSTANT_STRING);
map.put("stringIndex", dis.readUnsignedShort());
break;
case CONSTANT_FIELD_REF:
// CONSTANT_Fieldref_info {
// u1 tag;
// u2 class_index;
// u2 name_and_type_index;
// }
map.put("tag", CONSTANT_FIELD_REF);
map.put("classIndex", dis.readUnsignedShort());
map.put("nameAndTypeIndex", dis.readUnsignedShort());
break;
case CONSTANT_METHOD_REF:
// CONSTANT_Methodref_info {
// u1 tag;
// u2 class_index;
// u2 name_and_type_index;
// }
map.put("tag", CONSTANT_METHOD_REF);
map.put("classIndex", dis.readUnsignedShort());
map.put("nameAndTypeIndex", dis.readUnsignedShort());
break;
case CONSTANT_INTERFACE_METHOD_REF:
// CONSTANT_InterfaceMethodref_info {
// u1 tag;
// u2 class_index;
// u2 name_and_type_index;
// }
map.put("tag", CONSTANT_INTERFACE_METHOD_REF);
map.put("classIndex", dis.readUnsignedShort());
map.put("nameAndTypeIndex", dis.readUnsignedShort());
break;
case CONSTANT_NAME_AND_TYPE:
// CONSTANT_NameAndType_info {
// u1 tag;
// u2 name_index;
// u2 descriptor_index;
// }
map.put("tag", CONSTANT_NAME_AND_TYPE);
map.put("nameIndex", dis.readUnsignedShort());
map.put("descriptorIndex", dis.readUnsignedShort());
break;
case CONSTANT_METHOD_HANDLE:
// CONSTANT_MethodHandle_info {
// u1 tag;
// u1 reference_kind;
// u2 reference_index;
// }
map.put("tag", CONSTANT_METHOD_HANDLE);
map.put("referenceKind", dis.readUnsignedByte());
map.put("referenceIndex", dis.readUnsignedShort());
break;
case CONSTANT_METHOD_TYPE:
// CONSTANT_MethodType_info {
// u1 tag;
// u2 descriptor_index;
// }
map.put("tag", CONSTANT_METHOD_TYPE);
map.put("descriptorIndex", dis.readUnsignedShort());
break;
case CONSTANT_DYNAMIC:
// CONSTANT_Dynamic_info {
// u1 tag;
// u2 bootstrap_method_attr_index;
// u2 name_and_type_index;
// }
map.put("tag", CONSTANT_DYNAMIC);
map.put("bootstrapMethodAttrIdx", dis.readUnsignedShort());
map.put("nameAndTypeIndex", dis.readUnsignedShort());
break;
case CONSTANT_INVOKE_DYNAMIC:
// CONSTANT_InvokeDynamic_info {
// u1 tag;
// u2 bootstrap_method_attr_index;
// u2 name_and_type_index;
// }
map.put("tag", CONSTANT_INVOKE_DYNAMIC);
map.put("bootstrapMethodAttrIdx", dis.readUnsignedShort());
map.put("nameAndTypeIndex", dis.readUnsignedShort());
break;
case CONSTANT_MODULE:
// CONSTANT_Module_info {
// u1 tag;
// u2 name_index;
// }
map.put("tag", CONSTANT_MODULE);
map.put("nameIndex", dis.readUnsignedShort());
break;
case CONSTANT_PACKAGE:
// CONSTANT_Package_info {
// u1 tag;
// u2 name_index;
// }
map.put("tag", CONSTANT_PACKAGE);
map.put("nameIndex", dis.readUnsignedShort());
break;
}
constantPoolMap.put(index, map);
}
解析完常量池的对象后会发现很多数据结构中都引用了其他对象,比如ID(索引位置)为1的常量池对象CONSTANT_METHOD_REF
引用了ID为21的CONSTANT_CLASS
对象和ID为64的CONSTANT_NAME_AND_TYPE
对象,而CONSTANT_CLASS
对象又引用了CONSTANT_UTF8
(java/lang/Object
)、CONSTANT_NAME_AND_TYPE
同时引用了CONSTANT_UTF8
(<init>
)和CONSTANT_UTF8
(()V
),为了能够直观的看到常量池ID为1的对象信息我们就必须要将所有使用索引方式链接的映射关系改成直接字符串引用,最终得到如下结果:
{
"constantPoolMap": {
"1": {
"tag": "CONSTANT_METHOD_REF",
"classIndex": 21,
"nameAndTypeIndex": 64,
"classValue": "java/lang/Object",
"nameAndTypeValue": "<init>"
}
.... 省略其他对象
}
}
常量池对象链接代码片段:
/**
* 链接常量池中的引用
*/
private void linkConstantPool() {
for (Integer id : constantPoolMap.keySet()) {
Map<String, Object> valueMap = constantPoolMap.get(id);
if (!valueMap.containsKey("value")) {
Map<String, Object> newMap = new LinkedHashMap<>();
for (String key : valueMap.keySet()) {
if (key.endsWith("Index")) {
Object value = recursionValue((Integer) valueMap.get(key));
if (value != null) {
String newKey = key.substring(0, key.indexOf("Index"));
newMap.put(newKey + "Value", value);
}
}
}
valueMap.putAll(newMap);
}
}
}
/**
* 递归查找ID对应的常量池中的值
*
* @param id 常量池ID
* @return 常量池中存储的值
*/
private Object recursionValue(Integer id) {
Map<String, Object> map = constantPoolMap.get(id);
if (map.containsKey("value")) {
return map.get("value");
}
for (String key : map.keySet()) {
if (key.endsWith("Index")) {
Integer value = (Integer) map.get(key);
return recursionValue(value);
}
}
return null;
}
为了方便通过ID(常量池索引)访问常量池中的对象值,封装了一个getConstantPoolValue
方法:
/**
* 通过常量池中的索引ID和名称获取常量池中的值
*
* @param index 索引ID
* @return 常量池对象值
*/
private Object getConstantPoolValue(int index) {
if (constantPoolMap.containsKey(index)) {
Map<String, Object> dataMap = constantPoolMap.get(index);
Constant constant = (Constant) dataMap.get("tag");
switch (constant) {
case CONSTANT_UTF8:
case CONSTANT_INTEGER:
case CONSTANT_FLOAT:
case CONSTANT_LONG:
case CONSTANT_DOUBLE:
return dataMap.get("value");
case CONSTANT_CLASS:
case CONSTANT_MODULE:
case CONSTANT_PACKAGE:
return dataMap.get("nameValue");
case CONSTANT_STRING:
return dataMap.get("stringValue");
case CONSTANT_FIELD_REF:
case CONSTANT_METHOD_REF:
case CONSTANT_INTERFACE_METHOD_REF:
return dataMap.get("classValue") + "." + dataMap.get("nameAndTypeValue");
case CONSTANT_NAME_AND_TYPE:
case CONSTANT_METHOD_TYPE:
return dataMap.get("descriptorValue");
case CONSTANT_METHOD_HANDLE:
return dataMap.get("referenceValue");
case CONSTANT_DYNAMIC:
case CONSTANT_INVOKE_DYNAMIC:
return dataMap.get("bootstrapMethodAttrValue") + "." + dataMap.get("nameAndTypeValue");
default:
break;
}
}
return null;
}
访问标志解析
// u2 access_flags;
this.accessFlags = dis.readUnsignedShort();
解析结果:"accessFlags": 33,
。
当前类名称解析
解析类名称的时候直接读取2个无符号数,获取到类名所在的常量池中的索引位置,然后根据常量池ID读取常量池中的字符串内容即可解析出类名。
// u2 this_class;
this.thisClass = (String) getConstantPoolValue(dis.readUnsignedShort());
解析结果:"thisClass": "com/anbai/sec/bytecode/TestHelloWorld"
。
当前类的父类名称解析
解析super_class
的时候也是需要特别注意,当解析java.lang.Object
时super_class
的值为0,常量池中不包含索引为0的对象,所以需要直接将父类名称设置为java/lang/Object
。
// u2 super_class;
int superClassIndex = dis.readUnsignedShort();
// 当解析Object类的时候super_class为0
if (superClassIndex != 0) {
this.superClass = (String) getConstantPoolValue(superClassIndex);
} else {
this.superClass = "java/lang/Object";
}
解析结果:"superClass": "java/lang/Object",
。
接口解析
解析接口信息时需要先解析出接口的数量,然后就可以遍历出所有的接口名称索引值了。
u2 interfaces_count;
u2 interfaces[interfaces_count];
接口解析代码片段:
// u2 interfaces_count;
this.interfacesCount = dis.readUnsignedShort();
// 创建接口Index数组
this.interfaces = new String[interfacesCount];
// u2 interfaces[interfaces_count];
for (int i = 0; i < interfacesCount; i++) {
int index = dis.readUnsignedShort();
// 设置接口名称
this.interfaces[i] = (String) getConstantPoolValue(index);
}
解析结果:
{
"interfacesCount": 1,
"interfaces": [
"java/io/Serializable"
]
}
成员变量/成员方法解析
成员变量和成员方法的数据结构是一样的,所以可以使用相同的解析逻辑。首先解析出变量/方法的总数量,然后遍历并解析field_info
或method_info
对象的所有信息。
成员变量/成员方法解析代码片段:
// u2 fields_count;
this.fieldsCount = dis.readUnsignedShort();
// field_info fields[fields_count];
for (int i = 0; i < this.fieldsCount; i++) {
// field_info {
// u2 access_flags;
// u2 name_index;
// u2 descriptor_index;
// u2 attributes_count;
// attribute_info attributes[attributes_count];
// }
this.fieldList.add(readFieldOrMethod());
}
/**
* 读取成员变量或者方法的公用属性
*
* @return 成员变量或方法属性信息
* @throws IOException 读取异常
*/
private Map<String, Object> readFieldOrMethod() throws IOException {
Map<String, Object> dataMap = new LinkedHashMap<>();
// u2 access_flags;
dataMap.put("access", dis.readUnsignedShort());
// u2 name_index;
dataMap.put("name", getConstantPoolValue(dis.readUnsignedShort()));
// u2 descriptor_index;
dataMap.put("desc", getConstantPoolValue(dis.readUnsignedShort()));
// u2 attributes_count;
int attributesCount = dis.readUnsignedShort();
dataMap.put("attributesCount", attributesCount);
// 读取成员变量属性信息
dataMap.put("attributes", readAttributes(attributesCount));
return dataMap;
}
成员变量解析结果:
{
"fieldsCount": 4,
"fieldList": [
{
"access": 2,
"name": "password",
"desc": "Ljava/lang/String;",
"attributesCount": 0,
"attributes": { }
},
{
"access": 2,
"name": "id",
"desc": "J",
"attributesCount": 0,
"attributes": { }
},
{
"access": 26,
"name": "serialVersionUID",
"desc": "J",
"attributesCount": 1,
"attributes": {
"attributeName": "ConstantValue",
"attributeLength": 2,
"ConstantValue": {
"constantValue": -7366591802115334000
}
}
},
{
"access": 2,
"name": "username",
"desc": "Ljava/lang/String;",
"attributesCount": 0,
"attributes": { }
}
]
}
成员方法解析结果(因结果过大,仅保留了一个getPassword
方法):
{
"methodsCount": 10,
"methodList": [
{
"access": 1,
"name": "getPassword",
"desc": "()Ljava/lang/String;",
"attributesCount": 1,
"attributes": {
"attributeName": "Code",
"attributeLength": 47,
"Code": {
"maxStack": 1,
"maxLocals": 1,
"codeLength": 5,
"opcodes": [
"aload_0",
"getfield #15 <com/anbai/sec/bytecode/TestHelloWorld.password>",
"areturn"
],
"exceptionTable": {
"exceptionTableLength": 0,
"exceptionTableList": [ ]
},
"attributeLength": 47,
"attributes": {
"attributeName": "LocalVariableTable",
"attributeLength": 12,
"LineNumberTable": {
"lineNumberTableLength": 1,
"lineNumberTableList": [
{
"startPc": 0,
"lineNumber": 49
}
]
},
"LocalVariableTable": {
"localVariableTableLength": 1,
"localVariableTableList": [
{
"startPc": 0,
"length": 5,
"name": "this",
"desc": "Lcom/anbai/sec/bytecode/TestHelloWorld;",
"index": 0
}
]
}
}
}
}
}
]
}
属性解析
成员变量、成员方法、类对象这三种数据结构都需要解析属性信息,因为逻辑非常复杂,将在下一小节详解。
Java class文件属性解析
class文件的属性解析是非常复杂的,因为属性表由非常多的类型组成,几乎每一个数据类型都不一样,而且属性表是动态的,它还会随着JDK的版本升级而新增属性对象。在class文件中:成员变量
、成员方法
、类
都拥有属性信息,解析的时候可以使用同样的方法。因为属性表中的属性类型过多,本节仅以解析ConstantValue
、Code
为例,完整的解析代码请参考ClassByteCodeParser类。
属性信息表数据结构:
u2 attributes_count;
attribute_info attributes[attributes_count];
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
u2 attributes_count;
表示的是属性表的长度,循环所有属性对象可得到attribute_info
对象。attribute_info
对象有两个固定的属性值:u2 attribute_name_index;
(属性名称)和u4 attribute_length;
(属性的字节长度),我们可以先解析出这两个属性:
// u2 attribute_name_index;
String attributeName = (String) getConstantPoolValue(dis.readUnsignedShort());
// u4 attribute_length;
int attributeLength = dis.readInt();
解析出属性名称后就需要参考JVM虚拟机规范第4.7章-属性来解析各类属性信息了。
预定义属性表
属性名称 | 属性位置 | 章节 | Java版本 |
---|---|---|---|
ConstantValue | field_info | §4.7.2 | 1.0.2 |
Code | method_info | §4.7.3 | 1.0.2 |
StackMapTable | Code | §4.7.4 | 6 |
Exceptions | method_info | §4.7.5 | 1.0.2 |
InnerClasses | ClassFile | §4.7.6 | 1.1 |
EnclosingMethod | ClassFile | §4.7.7 | 5.0 |
Synthetic | ClassFile , field_info , method_info | §4.7.8 | 1.1 |
Signature | ClassFile , field_info , method_info | §4.7.9 | 5.0 |
SourceFile | ClassFile | §4.7.10 | 1.0.2 |
SourceDebugExtension | ClassFile | §4.7.11 | 5.0 |
LineNumberTable | Code | §4.7.12 | 1.0.2 |
LocalVariableTable | Code | §4.7.13 | 1.0.2 |
LocalVariableTypeTable | Code | §4.7.14 | 5.0 |
Deprecated | ClassFile , field_info , method_info | §4.7.15 | 1.1 |
RuntimeVisibleAnnotations | ClassFile , field_info , method_info | §4.7.16 | 5.0 |
RuntimeInvisibleAnnotations | ClassFile , field_info , method_info | §4.7.17 | 5.0 |
RuntimeVisibleParameterAnnotations | method_info | §4.7.18 | 5.0 |
RuntimeInvisibleParameterAnnotations | method_info | §4.7.19 | 5.0 |
RuntimeVisibleTypeAnnotations | ClassFile , field_info , method_info , Code | §4.7.20 | 8 |
RuntimeInvisibleTypeAnnotations | ClassFile , field_info , method_info , Code | §4.7.21 | 8 |
AnnotationDefault | method_info | §4.7.22 | 5.0 |
BootstrapMethods | ClassFile | §4.7.23 | 7 |
MethodParameters | method_info | §4.7.24 | 8 |
Module | ClassFile | §4.7.25 | 9 |
ModulePackages | ClassFile | §4.7.26 | 9 |
ModuleMainClass | ClassFile | §4.7.27 | 9 |
NestHost | ClassFile | §4.7.28 | 11 |
NestMembers | ClassFile | §4.7.29 | 11 |
ConstantValue
ConstantValue
属性用于表示field_info
中的静态变量的初始值,结构如下:
ConstantValue_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;
}
ConstantValue解析代码片段:
// 创建属性Map
Map<String, Object> attrMap = new LinkedHashMap<>();
// u2 constantvalue_index;
attrMap.put("constantValue", getConstantPoolValue(dis.readUnsignedShort()));
attributeMap.put("ConstantValue", attrMap);
解析后的结果如下:
{
"access": 26,
"name": "serialVersionUID",
"desc": "J",
"attributesCount": 1,
"attributes": {
"attributeName": "ConstantValue",
"attributeLength": 2,
"ConstantValue": {
"constantValue": -7366591802115334000
}
}
}
Code
Code
属性用于表示成员方法的代码部分,Code
中包含了指令集(byte数组
),JVM调用成员方法时实际上就是执行的Code
中的指令,而反编译工具则是把Code
中的指令翻译成了Java代码。
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];
}
Code解析代码片段:
int maxStack = dis.readUnsignedShort();
int maxLocals = dis.readUnsignedShort();
int codeLength = dis.readInt();
List<String> opcodeList = new ArrayList<>();
byte[] bytes = new byte[codeLength];
// 读取所有的code字节
dis.read(bytes);
// 创建Code输入流
DataInputStream bis = new DataInputStream(new ByteArrayInputStream(bytes));
// 创建属性Map
Map<String, Object> attrMap = new LinkedHashMap<>();
attrMap.put("maxStack", maxStack);
attrMap.put("maxLocals", maxLocals);
attrMap.put("codeLength", codeLength);
// 是否是宽类型
boolean wide = false;
for (int offset = 0; offset < codeLength; offset++) {
int branchOffset = -1;
int defaultOffset = -1;
int switchNumberofPairs = -1;
int switchNumberOfOffsets = -1;
int immediateByte = -1;
int immediateShort = -1;
int arrayDimensions = 0;
int incrementConst = -1;
int incrementConst2 = -1;
int switchMatch = -1;
int switchOffset = -1;
int[] switchJumpOffsets = null;
int bytesToRead = 0;
int code = bis.readUnsignedByte();
Opcodes opcode = Opcodes.getOpcodes(code);
if (opcode == null) {
continue;
}
switch (opcode) {
case BIPUSH:
case LDC:
case ILOAD:
case LLOAD:
case FLOAD:
case DLOAD:
case ALOAD:
case ISTORE:
case LSTORE:
case FSTORE:
case DSTORE:
case ASTORE:
case RET:
case NEWARRAY:
if (wide) {
immediateByte = bis.readUnsignedShort();
} else {
immediateByte = bis.readUnsignedByte();
}
addOpcodes(opcodeList, opcode, immediateByte);
// 因为读取了byte,所以需要重新计算bis偏移量
offset += wide ? 2 : 1;
break;
case LDC_W:
case LDC2_W:
case GETSTATIC:
case PUTSTATIC:
case GETFIELD:
case PUTFIELD:
case INVOKEVIRTUAL:
case INVOKESPECIAL:
case INVOKESTATIC:
case NEW:
case ANEWARRAY:
case CHECKCAST:
case INSTANCEOF:
case SIPUSH:
addOpcodes(opcodeList, opcode, bis.readUnsignedShort());
offset += 2;
break;
case IFEQ:
case IFNE:
case IFLT:
case IFGE:
case IFGT:
case IFLE:
case IF_ICMPEQ:
case IF_ICMPNE:
case IF_ICMPLT:
case IF_ICMPGE:
case IF_ICMPGT:
case IF_ICMPLE:
case IF_ACMPEQ:
case IF_ACMPNE:
case GOTO:
case JSR:
case IFNULL:
case IFNONNULL:
branchOffset = bis.readShort();
opcodeList.add(opcode.getDesc() + " " + branchOffset);
offset += 2;
break;
case GOTO_W:
case JSR_W:
branchOffset = bis.readInt();
opcodeList.add(opcode.getDesc() + " " + branchOffset);
offset += 4;
break;
case IINC:
if (wide) {
incrementConst = bis.readUnsignedShort();
} else {
incrementConst = bis.readUnsignedByte();
}
if (wide) {
incrementConst2 = bis.readUnsignedShort();
} else {
incrementConst2 = bis.readUnsignedByte();
}
opcodeList.add(opcode.getDesc() + " " + incrementConst + " by " + incrementConst2);
offset += wide ? 4 : 2;
break;
case TABLESWITCH:
bytesToRead = readPaddingBytes(bytes, bis);
defaultOffset = bis.readInt();
int lowByte = bis.readInt();
int highByte = bis.readInt();
switchNumberOfOffsets = highByte - lowByte + 1;
switchJumpOffsets = new int[switchNumberOfOffsets];
for (int k = 0; k < switchNumberOfOffsets; k++) {
switchJumpOffsets[k] = bis.readInt();
}
opcodeList.add(opcode.getDesc());
offset += bytesToRead + 12 + 4 * switchNumberOfOffsets;
break;
case LOOKUPSWITCH:
bytesToRead = readPaddingBytes(bytes, bis);
defaultOffset = bis.readInt();
switchNumberofPairs = bis.readInt();
for (int k = 0; k < switchNumberofPairs; k++) {
switchMatch = bis.readInt();
switchOffset = bis.readInt();
}
opcodeList.add(opcode.getDesc());
offset += bytesToRead + 8 + 8 * switchNumberofPairs;
break;
case INVOKEINTERFACE:
immediateShort = bis.readUnsignedShort();
offset += 2;
int count = bis.readUnsignedByte();
// 下1个byte永远为0,所以直接丢弃
bis.readByte();
addOpcodes(opcodeList, opcode, immediateShort);
offset += 2;
break;
case INVOKEDYNAMIC:
immediateShort = bis.readUnsignedShort();
offset += 2;
// 下2个byte永远为0,所以直接丢弃
bis.readUnsignedShort();
addOpcodes(opcodeList, opcode, immediateShort);
offset += 2;
break;
case MULTIANEWARRAY:
immediateShort = bis.readUnsignedShort();
offset += 2;
arrayDimensions = bis.readUnsignedByte();
addOpcodes(opcodeList, opcode, immediateShort);
offset += 1;
break;
default:
opcodeList.add(opcode.getDesc());
}
wide = (WIDE == opcode);
}
attrMap.put("opcodes", opcodeList);
// 读取异常表
attrMap.put("exceptionTable", readExceptionTable());
// u2 attributes_count;
int attributesCount = dis.readShort();
attrMap.put("attributeLength", attributeLength);
attrMap.put("attributes", readAttributes(attributesCount));
// 递归读取属性信息
attributeMap.put("Code", attrMap);
在解析Code
属性时code_length
表示的是Code
的字节长度,max_stack
和max_locals
是一个固定值,表示的是最大操作数栈和最大局部变量数,这两个值是在编译类方法时自动计算出来的,如果通过ASM
修改了类方法可能会需要重新计算max_stack
和max_locals
。
示例 – TestHelloWorld类Hello方法解析结果:
{
"access": 1,
"name": "hello",
"desc": "(Ljava/lang/String;)Ljava/lang/String;",
"attributesCount": 1,
"attributes": {
"attributeName": "Code",
"attributeLength": 88,
"Code": {
"maxStack": 2,
"maxLocals": 3,
"codeLength": 22,
"opcodes": [
"ldc #3 <Hello:>",
"astore_2",
"new #4 <java/lang/StringBuilder>",
"dup",
"invokespecial #5 <java/lang/StringBuilder.<init>>",
"aload_2",
"invokevirtual #6 <java/lang/StringBuilder.append>",
"aload_1",
"invokevirtual #6 <java/lang/StringBuilder.append>",
"invokevirtual #7 <java/lang/StringBuilder.toString>",
"areturn"
],
"exceptionTable": {
"exceptionTableLength": 0,
"exceptionTableList": [ ]
},
"attributeLength": 88,
"attributes": {
"attributeName": "LocalVariableTable",
"attributeLength": 32,
"LineNumberTable": {
"lineNumberTableLength": 2,
"lineNumberTableList": [
{
"startPc": 0,
"lineNumber": 21
},
{
"startPc": 3,
"lineNumber": 22
}
]
},
"LocalVariableTable": {
"localVariableTableLength": 3,
"localVariableTableList": [
{
"startPc": 0,
"length": 22,
"name": "this",
"desc": "Lcom/anbai/sec/bytecode/TestHelloWorld;",
"index": 0
},
{
"startPc": 0,
"length": 22,
"name": "content",
"desc": "Ljava/lang/String;",
"index": 1
},
{
"startPc": 3,
"length": 19,
"name": "str",
"desc": "Ljava/lang/String;",
"index": 2
}
]
}
}
}
}
}
解析Code
的指令集时需要对照指令集映射表,然后根据不同的指令实现不一样的指令处理逻辑,指令列表和详细的描述请参考:JVM规范-指令。
Java虚拟机指令集
在上一章节我们解析了Code
属性,并从中解析出了一些虚拟机指令,本章节我们将深入学习Java虚拟机的指令集。
类型/方法描述符
Java虚拟机中描述类型和方法有固定的描述符和Java语法中所所用的完全不一样,比如int
应当表示为i
,表示一个java类名,如:java.lang.Object
类在虚拟机中应该使用java/lang/Object
,表示引用对象应当使L类名;
如:Object obj
应当使用Ljava/lang/Object;
表示,表示成员方法时候应当使用(参数类型描述符)返回值,如:void main(String[] args)
应当使用([Ljava/lang/String;)V
表示,表示数组使用[类型描述符
,如int[]
应当使用[i
表示,表示构造方法名称应当使用<init>
表示。
类型描述符表
描述符 | Java类型 | 示例 |
---|---|---|
B | byte | B |
C | char | C |
D | double | D |
F | float | F |
I | int | I |
J | long | J |
S | short | S |
Z | boolean | Z |
[ | 数组 | [IJ |
L类名; | 引用类型对象 | Ljava/lang/Object; |
方法描述符示例
方法示例 | 描述符 | 描述 |
---|---|---|
static{...} ,static int id = 1; | 方法名:<clinit> | 静态语句块/静态变量初始化 |
public Test (){...} | 方法名:<init> ,描述符()V | 构造方法 |
void hello(){...} | ()V | V 表示void ,无返回值 |
Object login(String str) {...} | (Ljava/lang/String;)Ljava/lang/Object; | 普通方法,返回Object类型 |
void login(String str) {...} | (Ljava/lang/String;)V | 普通方法,无返回值 |
Java虚拟机指令
栈指令是由0-255的整型表示,在JVM规范的第六章中有完整的说明:JVM规范-指令。不同的指令会有自己的数据结构,如TABLESWITCH
和LOOKUPSWITCH
表示的是switch
语句,当匹配到该指令时需要按照它特有的二进制格式解析。除此之外,新版本的JDK可能会新增指令,Java15
所有的指令大概有205个。
Java虚拟机指令表
十六进制 | 助记符 | 指令说明 |
---|---|---|
0x00 | nop | 什么都不做 |
0x01 | aconst_null | 将null推送至栈顶 |
0x02 | iconst_m1 | 将int型-1推送至栈顶 |
0x03 | iconst_0 | 将int型0推送至栈顶 |
0x04 | iconst_1 | 将int型1推送至栈顶 |
0x05 | iconst_2 | 将int型2推送至栈顶 |
0x06 | iconst_3 | 将int型3推送至栈顶 |
0x07 | iconst_4 | 将int型4推送至栈顶 |
0x08 | iconst_5 | 将int型5推送至栈顶 |
0x09 | lconst_0 | 将long型0推送至栈顶 |
0x0a | lconst_1 | 将long型1推送至栈顶 |
0x0b | fconst_0 | 将float型0推送至栈顶 |
0x0c | fconst_1 | 将float型1推送至栈顶 |
0x0d | fconst_2 | 将float型2推送至栈顶 |
0x0e | dconst_0 | 将double型0推送至栈顶 |
0x0f | dconst_1 | 将double型1推送至栈顶 |
0x10 | bipush | 将单字节的常量值(-128~127)推送至栈顶 |
0x11 | sipush | 将一个短整型常量值(-32768~32767)推送至栈顶 |
0x12 | ldc | 将int, float或String型常量值从常量池中推送至栈顶 |
0x13 | ldc_w | 将int, float或String型常量值从常量池中推送至栈顶(宽索引) |
0x14 | ldc2_w | 将long或double型常量值从常量池中推送至栈顶(宽索引) |
0x15 | iload | 将指定的int型本地变量推送至栈顶 |
0x16 | lload | 将指定的long型本地变量推送至栈顶 |
0x17 | fload | 将指定的float型本地变量推送至栈顶 |
0x18 | dload | 将指定的double型本地变量推送至栈顶 |
0x19 | aload | 将指定的引用类型本地变量推送至栈顶 |
0x1a | iload_0 | 将第一个int型本地变量推送至栈顶 |
0x1b | iload_1 | 将第二个int型本地变量推送至栈顶 |
0x1c | iload_2 | 将第三个int型本地变量推送至栈顶 |
0x1d | iload_3 | 将第四个int型本地变量推送至栈顶 |
0x1e | lload_0 | 将第一个long型本地变量推送至栈顶 |
0x1f | lload_1 | 将第二个long型本地变量推送至栈顶 |
0x20 | lload_2 | 将第三个long型本地变量推送至栈顶 |
0x21 | lload_3 | 将第四个long型本地变量推送至栈顶 |
0x22 | fload_0 | 将第一个float型本地变量推送至栈顶 |
0x23 | fload_1 | 将第二个float型本地变量推送至栈顶 |
0x24 | fload_2 | 将第三个float型本地变量推送至栈顶 |
0x25 | fload_3 | 将第四个float型本地变量推送至栈顶 |
0x26 | dload_0 | 将第一个double型本地变量推送至栈顶 |
0x27 | dload_1 | 将第二个double型本地变量推送至栈顶 |
0x28 | dload_2 | 将第三个double型本地变量推送至栈顶 |
0x29 | dload_3 | 将第四个double型本地变量推送至栈顶 |
0x2a | aload_0 | 将第一个引用类型本地变量推送至栈顶 |
0x2b | aload_1 | 将第二个引用类型本地变量推送至栈顶 |
0x2c | aload_2 | 将第三个引用类型本地变量推送至栈顶 |
0x2d | aload_3 | 将第四个引用类型本地变量推送至栈顶 |
0x2e | iaload | 将int型数组指定索引的值推送至栈顶 |
0x2f | laload | 将long型数组指定索引的值推送至栈顶 |
0x30 | faload | 将float型数组指定索引的值推送至栈顶 |
0x31 | daload | 将double型数组指定索引的值推送至栈顶 |
0x32 | aaload | 将引用型数组指定索引的值推送至栈顶 |
0x33 | baload | 将boolean或byte型数组指定索引的值推送至栈顶 |
0x34 | caload | 将char型数组指定索引的值推送至栈顶 |
0x35 | saload | 将short型数组指定索引的值推送至栈顶 |
0x36 | istore | 将栈顶int型数值存入指定本地变量 |
0x37 | lstore | 将栈顶long型数值存入指定本地变量 |
0x38 | fstore | 将栈顶float型数值存入指定本地变量 |
0x39 | dstore | 将栈顶double型数值存入指定本地变量 |
0x3a | astore | 将栈顶引用型数值存入指定本地变量 |
0x3b | istore_0 | 将栈顶int型数值存入第一个本地变量 |
0x3c | istore_1 | 将栈顶int型数值存入第二个本地变量 |
0x3d | istore_2 | 将栈顶int型数值存入第三个本地变量 |
0x3e | istore_3 | 将栈顶int型数值存入第四个本地变量 |
0x3f | lstore_0 | 将栈顶long型数值存入第一个本地变量 |
0x40 | lstore_1 | 将栈顶long型数值存入第二个本地变量 |
0x41 | lstore_2 | 将栈顶long型数值存入第三个本地变量 |
0x42 | lstore_3 | 将栈顶long型数值存入第四个本地变量 |
0x43 | fstore_0 | 将栈顶float型数值存入第一个本地变量 |
0x44 | fstore_1 | 将栈顶float型数值存入第二个本地变量 |
0x45 | fstore_2 | 将栈顶float型数值存入第三个本地变量 |
0x46 | fstore_3 | 将栈顶float型数值存入第四个本地变量 |
0x47 | dstore_0 | 将栈顶double型数值存入第一个本地变量 |
0x48 | dstore_1 | 将栈顶double型数值存入第二个本地变量 |
0x49 | dstore_2 | 将栈顶double型数值存入第三个本地变量 |
0x4a | dstore_3 | 将栈顶double型数值存入第四个本地变量 |
0x4b | astore_0 | 将栈顶引用型数值存入第一个本地变量 |
0x4c | astore_1 | 将栈顶引用型数值存入第二个本地变量 |
0x4d | astore_2 | 将栈顶引用型数值存入第三个本地变量 |
0x4e | astore_3 | 将栈顶引用型数值存入第四个本地变量 |
0x4f | iastore | 将栈顶int型数值存入指定数组的指定索引位置 |
0x50 | lastore | 将栈顶long型数值存入指定数组的指定索引位置 |
0x51 | fastore | 将栈顶float型数值存入指定数组的指定索引位置 |
0x52 | dastore | 将栈顶double型数值存入指定数组的指定索引位置 |
0x53 | aastore | 将栈顶引用型数值存入指定数组的指定索引位置 |
0x54 | bastore | 将栈顶boolean或byte型数值存入指定数组的指定索引位置 |
0x55 | castore | 将栈顶char型数值存入指定数组的指定索引位置 |
0x56 | sastore | 将栈顶short型数值存入指定数组的指定索引位置 |
0x57 | pop | 将栈顶数值弹出 (数值不能是long或double类型的) |
0x58 | pop2 | 将栈顶的一个(long或double类型的)或两个数值弹出(其它) |
0x59 | dup | 复制栈顶数值并将复制值压入栈顶 |
0x5a | dup_x1 | 复制栈顶数值并将两个复制值压入栈顶 |
0x5b | dup_x2 | 复制栈顶数值并将三个(或两个)复制值压入栈顶 |
0x5c | dup2 | 复制栈顶一个(long或double类型的)或两个(其它)数值并将复制值压入栈顶 |
0x5d | dup2_x1 | <待补充> |
0x5e | dup2_x2 | <待补充> |
0x5f | swap | 将栈最顶端的两个数值互换(数值不能是long或double类型的) |
0x60 | iadd | 将栈顶两int型数值相加并将结果压入栈顶 |
0x61 | ladd | 将栈顶两long型数值相加并将结果压入栈顶 |
0x62 | fadd | 将栈顶两float型数值相加并将结果压入栈顶 |
0x63 | dadd | 将栈顶两double型数值相加并将结果压入栈顶 |
0x64 | isub | 将栈顶两int型数值相减并将结果压入栈顶 |
0x65 | lsub | 将栈顶两long型数值相减并将结果压入栈顶 |
0x66 | fsub | 将栈顶两float型数值相减并将结果压入栈顶 |
0x67 | dsub | 将栈顶两double型数值相减并将结果压入栈顶 |
0x68 | imul | 将栈顶两int型数值相乘并将结果压入栈顶 |
0x69 | lmul | 将栈顶两long型数值相乘并将结果压入栈顶 |
0x6a | fmul | 将栈顶两float型数值相乘并将结果压入栈顶 |
0x6b | dmul | 将栈顶两double型数值相乘并将结果压入栈顶 |
0x6c | idiv | 将栈顶两int型数值相除并将结果压入栈顶 |
0x6d | ldiv | 将栈顶两long型数值相除并将结果压入栈顶 |
0x6e | fdiv | 将栈顶两float型数值相除并将结果压入栈顶 |
0x6f | ddiv | 将栈顶两double型数值相除并将结果压入栈顶 |
0x70 | irem | 将栈顶两int型数值作取模运算并将结果压入栈顶 |
0x71 | lrem | 将栈顶两long型数值作取模运算并将结果压入栈顶 |
0x72 | frem | 将栈顶两float型数值作取模运算并将结果压入栈顶 |
0x73 | drem | 将栈顶两double型数值作取模运算并将结果压入栈顶 |
0x74 | ineg | 将栈顶int型数值取负并将结果压入栈顶 |
0x75 | lneg | 将栈顶long型数值取负并将结果压入栈顶 |
0x76 | fneg | 将栈顶float型数值取负并将结果压入栈顶 |
0x77 | dneg | 将栈顶double型数值取负并将结果压入栈顶 |
0x78 | ishl | 将int型数值左移位指定位数并将结果压入栈顶 |
0x79 | lshl | 将long型数值左移位指定位数并将结果压入栈顶 |
0x7a | ishr | 将int型数值右(符号)移位指定位数并将结果压入栈顶 |
0x7b | lshr | 将long型数值右(符号)移位指定位数并将结果压入栈顶 |
0x7c | iushr | 将int型数值右(无符号)移位指定位数并将结果压入栈顶 |
0x7d | lushr | 将long型数值右(无符号)移位指定位数并将结果压入栈顶 |
0x7e | iand | 将栈顶两int型数值作“按位与”并将结果压入栈顶 |
0x7f | land | 将栈顶两long型数值作“按位与”并将结果压入栈顶 |
0x80 | ior | 将栈顶两int型数值作“按位或”并将结果压入栈顶 |
0x81 | lor | 将栈顶两long型数值作“按位或”并将结果压入栈顶 |
0x82 | ixor | 将栈顶两int型数值作“按位异或”并将结果压入栈顶 |
0x83 | lxor | 将栈顶两long型数值作“按位异或”并将结果压入栈顶 |
0x84 | iinc | 将指定int型变量增加指定值(i++, i–, i+=2) |
0x85 | i2l | 将栈顶int型数值强制转换成long型数值并将结果压入栈顶 |
0x86 | i2f | 将栈顶int型数值强制转换成float型数值并将结果压入栈顶 |
0x87 | i2d | 将栈顶int型数值强制转换成double型数值并将结果压入栈顶 |
0x88 | l2i | 将栈顶long型数值强制转换成int型数值并将结果压入栈顶 |
0x89 | l2f | 将栈顶long型数值强制转换成float型数值并将结果压入栈顶 |
0x8a | l2d | 将栈顶long型数值强制转换成double型数值并将结果压入栈顶 |
0x8b | f2i | 将栈顶float型数值强制转换成int型数值并将结果压入栈顶 |
0x8c | f2l | 将栈顶float型数值强制转换成long型数值并将结果压入栈顶 |
0x8d | f2d | 将栈顶float型数值强制转换成double型数值并将结果压入栈顶 |
0x8e | d2i | 将栈顶double型数值强制转换成int型数值并将结果压入栈顶 |
0x8f | d2l | 将栈顶double型数值强制转换成long型数值并将结果压入栈顶 |
0x90 | d2f | 将栈顶double型数值强制转换成float型数值并将结果压入栈顶 |
0x91 | i2b | 将栈顶int型数值强制转换成byte型数值并将结果压入栈顶 |
0x92 | i2c | 将栈顶int型数值强制转换成char型数值并将结果压入栈顶 |
0x93 | i2s | 将栈顶int型数值强制转换成short型数值并将结果压入栈顶 |
0x94 | lcmp | 比较栈顶两long型数值大小,并将结果(1,0,-1)压入栈顶 |
0x95 | fcmpl | 比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶 |
0x96 | fcmpg | 比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶 |
0x97 | dcmpl | 比较栈顶两double型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶 |
0x98 | dcmpg | 比较栈顶两double型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶 |
0x99 | ifeq | 当栈顶int型数值等于0时跳转 |
0x9a | ifne | 当栈顶int型数值不等于0时跳转 |
0x9b | iflt | 当栈顶int型数值小于0时跳转 |
0x9c | ifge | 当栈顶int型数值大于等于0时跳转 |
0x9d | ifgt | 当栈顶int型数值大于0时跳转 |
0x9e | ifle | 当栈顶int型数值小于等于0时跳转 |
0x9f | if_icmpeq | 比较栈顶两int型数值大小,当结果等于0时跳转 |
0xa0 | if_icmpne | 比较栈顶两int型数值大小,当结果不等于0时跳转 |
0xa1 | if_icmplt | 比较栈顶两int型数值大小,当结果小于0时跳转 |
0xa2 | if_icmpge | 比较栈顶两int型数值大小,当结果大于等于0时跳转 |
0xa3 | if_icmpgt | 比较栈顶两int型数值大小,当结果大于0时跳转 |
0xa4 | if_icmple | 比较栈顶两int型数值大小,当结果小于等于0时跳转 |
0xa5 | if_acmpeq | 比较栈顶两引用型数值,当结果相等时跳转 |
0xa6 | if_acmpne | 比较栈顶两引用型数值,当结果不相等时跳转 |
0xa7 | goto | 无条件跳转 |
0xa8 | jsr | 跳转至指定16位offset位置,并将jsr下一条指令地址压入栈顶 |
0xa9 | ret | 返回至本地变量指定的index的指令位置(一般与jsr, jsr_w联合使用) |
0xaa | tableswitch | 用于switch条件跳转,case值连续(可变长度指令) |
0xab | lookupswitch | 用于switch条件跳转,case值不连续(可变长度指令) |
0xac | ireturn | 从当前方法返回int |
0xad | lreturn | 从当前方法返回long |
0xae | freturn | 从当前方法返回float |
0xaf | dreturn | 从当前方法返回double |
0xb0 | areturn | 从当前方法返回对象引用 |
0xb1 | return | 从当前方法返回void |
0xb2 | getstatic | 获取指定类的静态域,并将其值压入栈顶 |
0xb3 | putstatic | 为指定的类的静态域赋值 |
0xb4 | getfield | 获取指定类的实例域,并将其值压入栈顶 |
0xb5 | putfield | 为指定的类的实例域赋值 |
0xb6 | invokevirtual | 调用实例方法 |
0xb7 | invokespecial | 调用超类构造方法,实例初始化方法,私有方法 |
0xb8 | invokestatic | 调用静态方法 |
0xb9 | invokeinterface | 调用接口方法 |
0xba | — | |
0xbb | new | 创建一个对象,并将其引用值压入栈顶 |
0xbc | newarray | 创建一个指定原始类型(如int, float, char…)的数组,并将其引用值压入栈顶 |
0xbd | anewarray | 创建一个引用型(如类,接口,数组)的数组,并将其引用值压入栈顶 |
0xbe | arraylength | 获得数组的长度值并压入栈顶 |
0xbf | athrow | 将栈顶的异常抛出 |
0xc0 | checkcast | 检验类型转换,检验未通过将抛出ClassCastException |
0xc1 | instanceof | 检验对象是否是指定的类的实例,如果是将1压入栈顶,否则将0压入栈顶 |
0xc2 | monitorenter | 获得对象的锁,用于同步方法或同步块 |
0xc3 | monitorexit | 释放对象的锁,用于同步方法或同步块 |
0xc4 | wide | <待补充> |
0xc5 | multianewarray | 创建指定类型和指定维度的多维数组(执行该指令时,操作栈中必须包含各维度的长度值),并将其引用值压入栈顶 |
0xc6 | ifnull | 为null时跳转 |
0xc7 | ifnonnull | 不为null时跳转 |
0xc8 | goto_w | 无条件跳转(宽索引) |
0xc9 | jsr_w | 跳转至指定32位offset位置,并将jsr_w下一条指令地址压入栈顶 |
指令解析
为了便于理解,以TestHelloWorld
类中有一个hello
方法为例,学习字节码和源代码之间的关联性。
TestHelloWorld类的hello方法代码如下:
public String hello(String content) {
String str = "Hello:";
return str + content;
}
hello
方法是一个非静态方法,返回值是String
,hello
方法有一个String
类型的参数。
编译后的栈指令如下:
{
"opcodes": [
"ldc #3 <Hello:>",
"astore_2",
"new #4 <java/lang/StringBuilder>",
"dup",
"invokespecial #5 <java/lang/StringBuilder.<init>>",
"aload_2",
"invokevirtual #6 <java/lang/StringBuilder.append>",
"aload_1",
"invokevirtual #6 <java/lang/StringBuilder.append>",
"invokevirtual #7 <java/lang/StringBuilder.toString>",
"areturn"
]
}
hello方法字节码解析
虽然hello
方法的代码非常简单,但是翻译成指令后就会变得比较难以理解了,有很多细节是隐藏在编译细节中的,比如return str + content;
是一个简单的两个字符串相加的操作,但实际上javac
编译时会创建一个StringBuilder
对象,然后调用append
方法来实现str
字符串和content
字符串相加的。
hello方法字节码解析:
ldc
表示的是将int, float或String型常量值从常量池中推送至栈顶,而ldc #3
表示的是将常量池中的第三个索引位置压入栈顶,也就是Hello:
;astore_2
表示的是将栈顶的值存入到局部变量表
的第二个位置,局部变量表
的索引位置是从0开始的,因为hello
方法是一个非静态方法,所以索引0表示的是this
对象(如果是static
方法那么就意味着没有this
对象,索引0就应该表示第一个参数)。索引1表示的是hello
方法的第一个参数,也就是String content
。如果在方法体中想创建一个新的对象,那么就必须计算这个变量在局部变量表中的索引位置,否则无法存储对象。还有一个需要特别注意的点是long
和double
是宽类型(wide type
)需要占用两个索引位置。astore_2
实际上表达的是将栈顶的对象压入到局部变量表中,等价于String arg2 = new String("Hello:")
;new #4
表示的是创建java/lang/StringBuilder
类实例;dup
表示复制栈顶数值并将复制值压入栈顶,即StringBuilder
对象;invokespecial #5
,invokespecial
表示的是调用超类构造方法,实例初始化方法,私有方法,即调用StringBuilder
类的构造方法(<init>
),#5
在常量池中是一个CONSTANT_METHOD_REF
类型的对象,用于表示一个类方法引用,invokespecial #5
实际上是在调用的StringBuilder
的构造方法,等价于:new StringBuilder()
;aload_2
表示的是加载局部变量表中的第二个变量,也就是读取astore_2
存入的值,即Hello:
。invokevirtual #6
表示的是调用StringBuilder
类的append
方法,等价于:sb.append("Hello:")
;aload_1
表示的是将局部变量表中的第一个变量压入栈顶,也就是将hello
方法的第一个参数content
的值压入栈顶;invokevirtual #6
,再次调用StringBuilder
类的append
方法,等价于:sb.append(content)
;invokevirtual #7
,调用StringBuilder
类的toString
方法,等价于:sb.toString()
;areturn
表示的是返回一个引用类型对象,需要注意的是如果不同的数据类型需要使用正确的return指令;
hello
方法的逻辑非常简单,如果只是看源代码的情况下我们可以秒懂该方法的执行流程和逻辑,但是如果我们从字节码层来看就会显得非常复杂不便于阅读;从第3步到第10步实际上只是在做源代码中的str + content
字符串相加操作而已。正是因为直接阅读虚拟机的指令对我们是一种非常不好的体验,所以才会有根据字节码逆向生成Java源代码的需求,通过反编译工具我们能够非常好的阅读程序逻辑,从而省去阅读字节码和指令的压力。但是反编译工具不是万能的,某些时候在解析指令的时候可能会报错,甚至是崩溃,所以为了更好的分析类业务逻辑以及学习ASM
字节码库,我们需要尽可能的掌握字节码解析和虚拟机指令解析的原理。
Java 类字节码编辑 – ASM
Java字节码库允许我们通过字节码库的API动态创建或修改Java类、方法、变量等操作而被广泛使用,本节将讲解ASM库的使用。
ASM是一种通用Java字节码操作和分析框架,它可以直接以二进制形式修改一个现有的类或动态生成类文件。ASM的版本更新快(ASM 9.0
已经支持JDK 16
)、性能高、功能全,学习成本也相对较高,ASM官方用户手册:ASM 4.0 A Java bytecode engineering library。
ASM提供了三个基于ClassVisitor API
的核心API,用于生成和转换类:
ClassReader
类用于解析class文件或二进制流;ClassWriter
类是ClassVisitor
的子类,用于生成类二进制;ClassVisitor
是一个抽象类,自定义ClassVisitor
重写visitXXX
方法,可获取捕获ASM类结构访问的所有事件;
ClassReader和ClassVisitor
ClassReader
类用于解析类字节码,创建ClassReader
对象可传入类名、类字节码数组或者类输入流对象。
创建完ClassReader
对象就会触发字节码解析(解析class基础信息,如常量池、接口信息等),所以可以直接通过ClassReader
对象获取类的基础信息,如下:
// 创建ClassReader对象,用于解析类对象,可以根据类名、二进制、输入流的方式创建
final ClassReader cr = new ClassReader(className);
System.out.println(
"解析类名:" + cr.getClassName() + ",父类:" + cr.getSuperName() +
",实现接口:" + Arrays.toString(cr.getInterfaces())
);
调用ClassReader
类的accpet
方法需要传入自定义的ClassVisitor
对象,ClassReader
会按照如下顺序,依次调用该ClassVisitor
的类方法。
visit
[ visitSource ] [ visitModule ][ visitNestHost ][ visitPermittedclass ][ visitOuterClass ]
( visitAnnotation | visitTypeAnnotation | visitAttribute )*
( visitNestMember | visitInnerClass | visitRecordComponent | visitField | visitMethod )*
visitEnd
ClassVisitor类图:

MethodVisitor和AdviceAdapter
MethodVisitor
同ClassVisitor
,重写MethodVisitor
类方法可获取捕获到对应的visit
事件,MethodVisitor
会依次按照如下顺序调用visit
方法:
( visitParameter )* [ visitAnnotationDefault ]
( visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation visitTypeAnnotation | visitAttribute )*
[ visitCode
( visitFrame | visit<i>X</i>Insn | visitLabel | visitInsnAnnotation | visitTryCatchBlock | visitTryCatchAnnotation | visitLocalVariable | visitLocalVariableAnnotation | visitLineNumber )*
visitMaxs
]
visitEnd
AdviceAdapter
的父类是GeneratorAdapter
和LocalVariablesSorter
,在MethodVisitor
类的基础上封装了非常多的便捷方法,同时还为我们做了非常有必要的计算,所以我们应该尽可能的使用AdviceAdapter
来修改字节码。
AdviceAdapter
类实现了一些非常有价值的方法,如:onMethodEnter
(方法进入时回调方法)、onMethodExit
(方法退出时回调方法),如果我们自己实现很容易掉进坑里面,因为这两个方法都是根据条件推算出来的。比如我们如果在构造方法的第一行直接插入了我们自己的字节码就可能会发现程序一运行就会崩溃,因为Java语法中限制我们第一行代码必须是super(xxx)
。
GeneratorAdapter
封装了一些栈指令操作的方法,如loadArgArray
方法可以直接获取方法所有参数数组、invokeStatic
方法可以直接调用类方法、push
方法可压入各种类型的对象等。
比如LocalVariablesSorter
类实现了计算本地变量索引位置的方法,如果要在方法中插入新的局部变量就必须计算变量的索引位置,我们必须先判断是否是非静态方法、是否是long/double
类型的参数(宽类型占两个位),否则计算出的索引位置还是错的。使用AdviceAdapter
可以直接调用mv.newLocal(type)
计算出本地变量存储的位置,为我们省去了许多不必要的麻烦。
读取类/成员变量/方法信息
为了学习ClassVisitor
,我们写一个简单的读取类、成员变量、方法信息的一个示例,需要重写ClassVisitor
类的visit
、visitField
和visitMethod
方法。
ASM读取类信息示例代码:
package com.anbai.sec.bytecode.asm;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import java.io.IOException;
import java.util.Arrays;
import static org.objectweb.asm.ClassReader.EXPAND_FRAMES;
import static org.objectweb.asm.Opcodes.ASM9;
public class ASMClassVisitorTest {
public static void main(String[] args) {
// 定义需要解析的类名称
String className = "com.anbai.sec.bytecode.TestHelloWorld";
try {
// 创建ClassReader对象,用于解析类对象,可以根据类名、二进制、输入流的方式创建
final ClassReader cr = new ClassReader(className);
System.out.println(
"解析类名:" + cr.getClassName() + ",父类:" + cr.getSuperName() +
",实现接口:" + Arrays.toString(cr.getInterfaces())
);
System.out.println("-----------------------------------------------------------------------------");
// 使用自定义的ClassVisitor访问者对象,访问该类文件的结构
cr.accept(new ClassVisitor(ASM9) {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
System.out.println(
"变量修饰符:" + access + "\t 类名:" + name + "\t 父类名:" + superName +
"\t 实现的接口:" + Arrays.toString(interfaces)
);
System.out.println("-----------------------------------------------------------------------------");
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
System.out.println(
"变量修饰符:" + access + "\t 变量名称:" + name + "\t 描述符:" + desc + "\t 默认值:" + value
);
return super.visitField(access, name, desc, signature, value);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println(
"方法修饰符:" + access + "\t 方法名称:" + name + "\t 描述符:" + desc +
"\t 抛出的异常:" + Arrays.toString(exceptions)
);
return super.visitMethod(access, name, desc, signature, exceptions);
}
}, EXPAND_FRAMES);
} catch (IOException e) {
e.printStackTrace();
}
}
}
程序执行后输出:
解析类名:com/anbai/sec/bytecode/TestHelloWorld,父类:java/lang/Object,实现接口:[java/io/Serializable]
-----------------------------------------------------------------------------
变量修饰符:131105 类名:com/anbai/sec/bytecode/TestHelloWorld 父类名:java/lang/Object 实现的接口:[java/io/Serializable]
-----------------------------------------------------------------------------
变量修饰符:26 变量名称:serialVersionUID 描述符:J 默认值:-7366591802115333975
变量修饰符:2 变量名称:id 描述符:J 默认值:null
变量修饰符:2 变量名称:username 描述符:Ljava/lang/String; 默认值:null
变量修饰符:2 变量名称:password 描述符:Ljava/lang/String; 默认值:null
方法修饰符:1 方法名称:<init> 描述符:()V 抛出的异常:null
方法修饰符:1 方法名称:hello 描述符:(Ljava/lang/String;)Ljava/lang/String; 抛出的异常:null
方法修饰符:9 方法名称:main 描述符:([Ljava/lang/String;)V 抛出的异常:null
方法修饰符:1 方法名称:getId 描述符:()J 抛出的异常:null
方法修饰符:1 方法名称:setId 描述符:(J)V 抛出的异常:null
方法修饰符:1 方法名称:getUsername 描述符:()Ljava/lang/String; 抛出的异常:null
方法修饰符:1 方法名称:setUsername 描述符:(Ljava/lang/String;)V 抛出的异常:null
方法修饰符:1 方法名称:getPassword 描述符:()Ljava/lang/String; 抛出的异常:null
方法修饰符:1 方法名称:setPassword 描述符:(Ljava/lang/String;)V 抛出的异常:null
方法修饰符:1 方法名称:toString 描述符:()Ljava/lang/String; 抛出的异常:null
通过这个简单的示例,我们可以通过ASM实现遍历一个类的基础信息。
修改类名/方法名称/方法修饰符示例
使用ClassWriter
可以实现类修改功能,使用ASM修改类字节码时如果插入了新的局部变量、字节码,需要重新计算max_stack
和max_locals
,否则会导致修改后的类文件无法通过JVM校验。手动计算max_stack
和max_locals
是一件比较麻烦的事情,ASM为我们提供了内置的自动计算方式,只需在创建ClassWriter
的时候传入COMPUTE_FRAMES
即可:new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
ASM修改类字节码示例代码:
package com.anbai.sec.bytecode.asm;
import org.javaweb.utils.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import java.io.File;
import java.io.IOException;
import static org.objectweb.asm.ClassReader.EXPAND_FRAMES;
import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES;
import static org.objectweb.asm.Opcodes.*;
public class ASMClassWriterTest {
public static void main(String[] args) {
// 定义需要解析的类名称
String className = "com.anbai.sec.bytecode.TestHelloWorld";
// 定义修改后的类名
final String newClassName = "JavaSecTestHelloWorld";
try {
// 创建ClassReader对象,用于解析类对象,可以根据类名、二进制、输入流的方式创建
final ClassReader cr = new ClassReader(className);
// 创建ClassWriter对象,COMPUTE_FRAMES会自动计算max_stack和max_locals
final ClassWriter cw = new ClassWriter(cr, COMPUTE_FRAMES);
// 使用自定义的ClassVisitor访问者对象,访问该类文件的结构
cr.accept(new ClassVisitor(ASM9, cw) {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, newClassName, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
// 将"hello"方法名字修改为"hi"
if (name.equals("hello")) {
// 修改方法访问修饰符,移除public属性,修改为private
access = access & ~ACC_PUBLIC | ACC_PRIVATE;
return super.visitMethod(access, "hi", desc, signature, exceptions);
}
return super.visitMethod(access, name, desc, signature, exceptions);
}
}, EXPAND_FRAMES);
File classFilePath = new File(new File(System.getProperty("user.dir"), "javaweb-sec-source/javase/src/main/java/com/anbai/sec/bytecode/asm/"), newClassName + ".class");
// 修改后的类字节码
byte[] classBytes = cw.toByteArray();
// 写入修改后的字节码到class文件
FileUtils.writeByteArrayToFile(classFilePath, classBytes);
} catch (IOException e) {
e.printStackTrace();
}
}
}
修改成功后将会生成一个名为JavaSecTestHelloWorld.class
的新的class文件,反编译JavaSecTestHelloWorld
类会发现该类的hello
方法也已被修改为了hi
,修饰符已被改为private
,如下图:

修改类方法字节码
大多数使用ASM库的目的其实是修改类方法的字节码,在原方法执行的前后动态插入新的Java代码,从而实现类似于AOP的功能。修改类方法字节码的典型应用场景如:APM和RASP;APM需要统计和分析每个类方法的执行时间,而RASP需要在Java底层API方法执行之前插入自身的检测代码,从而实现动态拦截恶意攻击。
假设我们需要修改com.anbai.sec.bytecode.TestHelloWorld
类的hello方法,实现以下两个需求:
- 在原业务逻辑执行前打印出该方法的参数值;
- 修改该方法的返回值;
原业务逻辑:
public String hello(String content) {
String str = "Hello:";
return str + content;
}
修改之后的业务逻辑代码:
public String hello(String content) {
System.out.println(content);
String var2 = "javasec.org";
String str = "Hello:";
String var4 = str + content;
System.out.println(var4);
return var2;
}
借助ASM我们可以实现类方法的字节码编辑。
修改类方法字节码实现代码:
package com.anbai.sec.bytecode.asm;
import org.javaweb.utils.FileUtils;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.AdviceAdapter;
import java.io.File;
import java.io.IOException;
import static org.objectweb.asm.ClassReader.EXPAND_FRAMES;
import static org.objectweb.asm.Opcodes.ASM9;
public class ASMMethodVisitorTest {
public static void main(String[] args) {
// 定义需要解析的类名称
String className = "com.anbai.sec.bytecode.TestHelloWorld";
try {
// 创建ClassReader对象,用于解析类对象,可以根据类名、二进制、输入流的方式创建
final ClassReader cr = new ClassReader(className);
// 创建ClassWriter对象,COMPUTE_FRAMES会自动计算max_stack和max_locals
final ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
// 使用自定义的ClassVisitor访问者对象,访问该类文件的结构
cr.accept(new ClassVisitor(ASM9, cw) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if (name.equals("hello")) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
// 创建自定义的MethodVisitor,修改原方法的字节码
return new AdviceAdapter(api, mv, access, name, desc) {
int newArgIndex;
// 获取String的ASM Type对象
private final Type stringType = Type.getType(String.class);
@Override
protected void onMethodEnter() {
// 输出hello方法的第一个参数,因为hello是非static方法,所以0是this,第一个参数的下标应该是1
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// 创建一个新的局部变量,newLocal会计算出这个新局部对象的索引位置
newArgIndex = newLocal(stringType);
// 压入字符串到栈顶
mv.visitLdcInsn("javasec.org");
// 将"javasec.org"字符串压入到新生成的局部变量中,String var2 = "javasec.org";
storeLocal(newArgIndex, stringType);
}
@Override
protected void onMethodExit(int opcode) {
dup(); // 复制栈顶的返回值
// 创建一个新的局部变量,并获取索引位置
int returnValueIndex = newLocal(stringType);
// 将栈顶的返回值压入新生成的局部变量中
storeLocal(returnValueIndex, stringType);
// 输出hello方法的返回值
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitVarInsn(ALOAD, returnValueIndex);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// 压入方法进入(onMethodEnter)时存入到局部变量的var2值到栈顶
loadLocal(newArgIndex);
// 返回一个引用类型,即栈顶的var2字符串,return var2;
// 需要特别注意的是不同数据类型应当使用不同的RETURN指令
mv.visitInsn(ARETURN);
}
};
}
return super.visitMethod(access, name, desc, signature, exceptions);
}
}, EXPAND_FRAMES);
File classFilePath = new File(new File(System.getProperty("user.dir"), "javaweb-sec-source/javase/src/main/java/com/anbai/sec/bytecode/"), "TestHelloWorld.class");
// 修改后的类字节码
byte[] classBytes = cw.toByteArray();
// 写入修改后的字节码到class文件
FileUtils.writeByteArrayToFile(classFilePath, classBytes);
} catch (IOException e) {
e.printStackTrace();
}
}
}
程序执行后会在com.anbai.sec.bytecode
包下创建一个TestHelloWorld.class
文件:

命令行运行TestHelloWorld
类,可以看到程序执行的逻辑已经被成功修改,输出结果如下:

动态创建Java类二进制
在某些业务场景下我们需要动态一个类来实现一些业务,这个时候就可以使用ClassWriter
来动态创建出一个Java类的二进制文件,然后通过自定义的类加载器就可以将我们动态生成的类加载到JVM中。假设我们需要生成一个TestASMHelloWorld
类,代码如下:
示例TestASMHelloWorld类:
package com.anbai.sec.classloader;
public class TestASMHelloWorld {
public static String hello() {
return "Hello World~";
}
}
使用ClassWriter生成类字节码示例:
package com.anbai.sec.bytecode.asm;
import org.javaweb.utils.HexUtils;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class TestASMHelloWorldDump implements Opcodes {
private static final String CLASS_NAME = "com.anbai.sec.classloader.TestASMHelloWorld";
private static final String CLASS_NAME_ASM = "com/anbai/sec/classloader/TestASMHelloWorld";
public static byte[] dump() throws Exception {
// 创建ClassWriter,用于生成类字节码
ClassWriter cw = new ClassWriter(0);
// 创建MethodVisitor
MethodVisitor mv;
// 创建一个字节码版本为JDK1.7的com.anbai.sec.classloader.TestASMHelloWorld类
cw.visit(V1_7, ACC_PUBLIC + ACC_SUPER, CLASS_NAME_ASM, null, "java/lang/Object", null);
// 设置源码文件名
cw.visitSource("TestHelloWorld.java", null);
// 创建一个空的构造方法,
// public TestASMHelloWorld() {
// }
{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(5, l0);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLocalVariable("this", "L" + CLASS_NAME_ASM + ";", null, l0, l1, 0);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
// 创建一个hello方法,
// public static String hello() {
// return "Hello World~";
// }
{
mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "hello", "()Ljava/lang/String;", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(8, l0);
mv.visitLdcInsn("Hello World~");
mv.visitInsn(ARETURN);
mv.visitMaxs(1, 0);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
public static void main(String[] args) throws Exception {
final byte[] classBytes = dump();
// 输出ASM生成的TestASMHelloWorld类HEX
System.out.println(new String(HexUtils.hexDump(classBytes)));
// 创建自定义类加载器,加载ASM创建的类字节码到JVM
ClassLoader classLoader = new ClassLoader(TestASMHelloWorldDump.class.getClassLoader()) {
@Override
protected Class<?> findClass(String name) {
try {
return super.findClass(name);
} catch (ClassNotFoundException e) {
return defineClass(CLASS_NAME, classBytes, 0, classBytes.length);
}
}
};
System.out.println("-----------------------------------------------------------------------------");
// 反射调用通过ASM生成的TestASMHelloWorld类的hello方法,输出返回值
System.out.println("hello方法执行结果:" + classLoader.loadClass(CLASS_NAME).getMethod("hello").invoke(null));
}
}
程序执行结果如下:
0000019F CA FE BA BE 00 00 00 33 00 14 01 00 2B 63 6F 6D .......3....+com
000001AF 2F 61 6E 62 61 69 2F 73 65 63 2F 63 6C 61 73 73 /anbai/sec/class
000001BF 6C 6F 61 64 65 72 2F 54 65 73 74 41 53 4D 48 65 loader/TestASMHe
000001CF 6C 6C 6F 57 6F 72 6C 64 07 00 01 01 00 10 6A 61 lloWorld......ja
000001DF 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 07 00 va/lang/Object..
000001EF 03 01 00 13 54 65 73 74 48 65 6C 6C 6F 57 6F 72 ....TestHelloWor
000001FF 6C 64 2E 6A 61 76 61 01 00 06 3C 69 6E 69 74 3E ld.java...<init>
0000020F 01 00 03 28 29 56 0C 00 06 00 07 0A 00 04 00 08 ...()V..........
0000021F 01 00 04 74 68 69 73 01 00 2D 4C 63 6F 6D 2F 61 ...this..-Lcom/a
0000022F 6E 62 61 69 2F 73 65 63 2F 63 6C 61 73 73 6C 6F nbai/sec/classlo
0000023F 61 64 65 72 2F 54 65 73 74 41 53 4D 48 65 6C 6C ader/TestASMHell
0000024F 6F 57 6F 72 6C 64 3B 01 00 05 68 65 6C 6C 6F 01 oWorld;...hello.
0000025F 00 14 28 29 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 ..()Ljava/lang/S
0000026F 74 72 69 6E 67 3B 01 00 0C 48 65 6C 6C 6F 20 57 tring;...Hello W
0000027F 6F 72 6C 64 7E 08 00 0E 01 00 04 43 6F 64 65 01 orld~......Code.
0000028F 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C ..LineNumberTabl
0000029F 65 01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C e...LocalVariabl
000002AF 65 54 61 62 6C 65 01 00 0A 53 6F 75 72 63 65 46 eTable...SourceF
000002BF 69 6C 65 00 21 00 02 00 04 00 00 00 00 00 02 00 ile.!...........
000002CF 01 00 06 00 07 00 01 00 10 00 00 00 2F 00 01 00 ............/...
000002DF 01 00 00 00 05 2A B7 00 09 B1 00 00 00 02 00 11 .....*..........
000002EF 00 00 00 06 00 01 00 00 00 05 00 12 00 00 00 0C ................
000002FF 00 01 00 00 00 05 00 0A 00 0B 00 00 00 09 00 0C ................
0000030F 00 0D 00 01 00 10 00 00 00 1B 00 01 00 00 00 00 ................
0000031F 00 03 12 0F B0 00 00 00 01 00 11 00 00 00 06 00 ................
0000032F 01 00 00 00 08 00 01 00 13 00 00 00 02 00 05 ...............
-----------------------------------------------------------------------------
hello方法执行结果:Hello World~
程序执行后会在TestASMHelloWorldDump
类同级的包下生成一个TestASMHelloWorld
类,如下图:

IDEA/Eclipse插件
初学ASM,读写ASM字节码对我们来说是非常困难的,但是我们可以借助开发工具的ASM插件,可以极大程度的帮助我们学习ASM。
IDEA – ASM Bytecode Outline
在IDEA中插件中心搜索:ASM Bytecode Outline
,就可以找到ASM字节码插件,如下图:

安装完ASM Bytecode Outline
后选择任意Java类,右键菜单中会出现Show Bytecode outline
选项,点击之后就可以看到该类对应的ASM和Bytecode代码,如下图:

Eclipse – Bytecode Outline
Eclipse同IDEA,在插件中心搜索bytecode就可以找到Bytecode Outline
插件,值得一提的是Eclipse的Bytecode Outline
相比IDEA
而言更加的方便,打开任意Java类会在Bytecode
窗体中生成对应的ASM代码,点击任意行代码还能自动对应到高亮对应的ASM代码。
安装Bytecode Outline
如果您使用的Eclipse版本相对较低(低版本的Eclipse自带了ASM依赖,如Eclipse Photon Release (4.8.0)
)可直接在插件中心安装Bytecode Outline
,否则需要先安装ASM依赖,点击Help
->Eclipse Marketplace...
,如下图:

然后搜索bytecode
,找到Bytecode Outline
,如下图:

点击Instal
->I accept the terms of the license agreement
->Finish
:

提示安全警告,直接点击Install anyway
:

安装完成后重启Eclipse即可。
安装Eclipse ASM依赖库
如果您是使用的Eclipse版本较新可能会无法安装,提示:Cannot complete the install because one or more required items could not be...
,这是因为新版本的Eclipse不自带ASM依赖库,需要我们先安装好ASM依赖然后才能安装Bytecode Outline
插件。
点击Help
->Install New Software...

然后在https://download.eclipse.org/tools/orbit/downloads/drops/选择对应的Eclipse版本:

复制仓库地址:

然后在Work with
输入框中输入:https://download.eclipse.org/tools/orbit/downloads/drops/I20200904215518/repository
,点击Add..
,填入仓库名字,如下图:

选择All Bundles
或者找到ASM
相关依赖,并按照提示完成依赖安装,如下图:

Bytecode Outline配置
安装好Bytecode Outline
插件以后默认没有Bytecode
窗体,需要再视图中添加Bytecode
,点击Window
->Show View
->Other
,如下图:

然后在弹出的视图窗体中输入bytecode
后点击open
,如下图:

随便写一个测试类,在Bytecode
窗体中可以看到对应的Bytecode
,如果需要看ASM代码,点击右侧菜单的ASM图标
即可,如下图:

如果想对照查看Java和ASM代码,只需点击对应的Java代码就会自动高亮ASM部分的代码,如下图:

我们可以借助Bytecode Outline
插件学习ASM,也可以直接使用Bytecode Outline
生成的ASM代码来实现字节码编辑。
Java 类字节码编辑 – Javassist
Javassist
是一个开源的分析、编辑和创建Java字节码的类库;相比ASM,Javassist
提供了更加简单便捷的API,使用Javassist
我们可以像写Java代码一样直接插入Java代码片段,让我们不再需要关注Java底层的字节码的和栈操作,仅需要学会如何使用Javassist
的API即可实现字节码编辑。学习Javassist
可以阅读官方的入门教程:Getting Started with Javassist。
Javassist API和标识符
Javassist
为我们提供了类似于Java反射机制的API,如:CtClass,CtConstructor、CtMethod、CtField与Java反射的Class
、Constructor
、Method
、Field
非常的类似。
类 | 描述 |
---|---|
ClassPool | ClassPool是一个存储CtClass的容器,如果调用get 方法会搜索并创建一个表示该类的CtClass对象 |
CtClass | CtClass表示的是从ClassPool获取的类对象,可对该类就行读写编辑等操作 |
CtMethod | 可读写的类方法对象 |
CtConstructor | 可读写的类构造方法对象 |
CtField | 可读写的类成员变量对象 |
Javassist
使用了内置的标识符来表示一些特定的含义,如:$_
表示返回值。我们可以在动态插入类代码的时候使用这些特殊的标识符来表示对应的对象。
表达式 | 描述 |
---|---|
$0, $1, $2, ... | this 和方法参数 |
$args | Object[] 类型的参数数组 |
$$ | 所有的参数,如m($$) 等价于m($1,$2,...) |
$cflow(...) | cflow变量 |
$r | 返回类型,用于类型转换 |
$w | 包装类型,用于类型转换 |
$_ | 方法返回值 |
$sig | 方法签名,返回java.lang.Class[] 数组类型 |
$type | 返回值类型,java.lang.Class 类型 |
$class | 当前类,java.lang.Class 类型 |
读取类/成员变量/方法信息
Javassist
读取类信息非常简单,使用ClassPool
对象获取到CtClass
对象后就可以像使用Java反射API一样去读取类信息了。
Javassist读取类信息示例代码:
package com.anbai.sec.bytecode.javassist;
import javassist.*;
import java.util.Arrays;
public class JavassistClassAccessTest {
public static void main(String[] args) {
// 创建ClassPool对象
ClassPool classPool = ClassPool.getDefault();
try {
CtClass ctClass = classPool.get("com.anbai.sec.bytecode.TestHelloWorld");
System.out.println(
"解析类名:" + ctClass.getName() + ",父类:" + ctClass.getSuperclass().getName() +
",实现接口:" + Arrays.toString(ctClass.getInterfaces())
);
System.out.println("-----------------------------------------------------------------------------");
// 获取所有的构造方法
CtConstructor[] ctConstructors = ctClass.getDeclaredConstructors();
// 获取所有的成员变量
CtField[] ctFields = ctClass.getDeclaredFields();
// 获取所有的成员方法
CtMethod[] ctMethods = ctClass.getDeclaredMethods();
// 输出所有的构造方法
for (CtConstructor ctConstructor : ctConstructors) {
System.out.println(ctConstructor.getMethodInfo());
}
System.out.println("-----------------------------------------------------------------------------");
// 输出所有成员变量
for (CtField ctField : ctFields) {
System.out.println(ctField);
}
System.out.println("-----------------------------------------------------------------------------");
// 输出所有的成员方法
for (CtMethod ctMethod : ctMethods) {
System.out.println(ctMethod);
}
} catch (NotFoundException e) {
e.printStackTrace();
}
}
}
程序执行结果:
解析类名:com.anbai.sec.bytecode.TestHelloWorld,父类:java.lang.Object,实现接口:[javassist.CtClassType@60addb54[public abstract interface class java.io.Serializable fields= constructors= methods=]]
-----------------------------------------------------------------------------
<init> ()V
-----------------------------------------------------------------------------
com.anbai.sec.bytecode.TestHelloWorld.serialVersionUID:J
com.anbai.sec.bytecode.TestHelloWorld.id:J
com.anbai.sec.bytecode.TestHelloWorld.username:Ljava/lang/String;
com.anbai.sec.bytecode.TestHelloWorld.password:Ljava/lang/String;
-----------------------------------------------------------------------------
javassist.CtMethod@ca717109[public hello (Ljava/lang/String;)Ljava/lang/String;]
javassist.CtMethod@44a4fe33[public static main ([Ljava/lang/String;)V]
javassist.CtMethod@fb809fd2[public getId ()J]
javassist.CtMethod@5321790a[public setId (J)V]
javassist.CtMethod@7a2b684d[public getUsername ()Ljava/lang/String;]
javassist.CtMethod@7942008f[public setUsername (Ljava/lang/String;)V]
javassist.CtMethod@3b463cd2[public getPassword ()Ljava/lang/String;]
javassist.CtMethod@da549dd4[public setPassword (Ljava/lang/String;)V]
javassist.CtMethod@69cb6c6d[public toString ()Ljava/lang/String;]
修改类方法
Javassist
实现类方法修改比ASM简单多了,我们只需要调用CtMethod
类的对应的API就可以了。CtMethod
提供了类方法修改的API,如:setModifiers
可修改类的访问修饰符,insertBefore
和insertAfter
能够实现在类方法执行的前后插入任意的Java代码片段,setBody
可以修改整个方法的代码等。
Javassist修改类方法示例代码:
package com.anbai.sec.bytecode.javassist;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;
import org.javaweb.utils.FileUtils;
import java.io.File;
public class JavassistClassModifyTest {
public static void main(String[] args) {
// 创建ClassPool对象
ClassPool classPool = ClassPool.getDefault();
try {
CtClass ctClass = classPool.get("com.anbai.sec.bytecode.TestHelloWorld");
// 获取hello方法
CtMethod helloMethod = ctClass.getDeclaredMethod("hello", new CtClass[]{classPool.get("java.lang.String")});
// 修改方法的访问权限为private
helloMethod.setModifiers(Modifier.PRIVATE);
// 输出hello方法的content参数值
helloMethod.insertBefore("System.out.println($1);");
// 输出hello方法的返回值
helloMethod.insertAfter("System.out.println($_); return \"Return:\" + $_;");
File classFilePath = new File(new File(System.getProperty("user.dir"), "javaweb-sec-source/javase/src/main/java/com/anbai/sec/bytecode/"), "TestHelloWorld.class");
// 使用类CtClass,生成类二进制
byte[] bytes = ctClass.toBytecode();
// 将class二进制内容写入到类文件
FileUtils.writeByteArrayToFile(classFilePath, bytes);
} catch (Exception e) {
e.printStackTrace();
}
}
}
程序执行后结果如下:

动态创建Java类二进制
Javassist
可以像ASM一样动态的创建出一个类的二进制,不过使用Javassist
可比ASM简单了不少,假设我们需要生成一个JavassistHelloWorld
类,代码如下:
package com.anbai.sec.bytecode.javassist;
public class JavassistHelloWorld {
private static String content = "Hello world~";
public static void main(String[] args) {
System.out.println(content);
}
}
使用Javassist生成类字节码示例:
package com.anbai.sec.bytecode.javassist;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import org.javaweb.utils.FileUtils;
import java.io.File;
public class JavassistTest {
public static void main(String[] args) {
// 创建ClassPool对象
ClassPool classPool = ClassPool.getDefault();
// 使用ClassPool创建一个JavassistHelloWorld类
CtClass ctClass = classPool.makeClass("com.anbai.sec.bytecode.javassist.JavassistHelloWorld");
try {
// 创建类成员变量content
CtField ctField = CtField.make("private static String content = \"Hello world~\";", ctClass);
// 将成员变量添加到ctClass对象中
ctClass.addField(ctField);
// 创建一个主方法并输出content对象值
CtMethod ctMethod = CtMethod.make(
"public static void main(String[] args) {System.out.println(content);}", ctClass
);
// 将成员方法添加到ctClass对象中
ctClass.addMethod(ctMethod);
File classFilePath = new File(new File(System.getProperty("user.dir"), "javaweb-sec-source/javase/src/main/java/com/anbai/sec/bytecode/javassist/"), "JavassistHelloWorld.class");
// 使用类CtClass,生成类二进制
byte[] bytes = ctClass.toBytecode();
// 将class二进制内容写入到类文件
FileUtils.writeByteArrayToFile(classFilePath, bytes);
} catch (Exception e) {
e.printStackTrace();
}
}
}
转载请注明出处及链接