Java Web安全之java基础-Java字节码

Java Web安全之java基础-Java字节码

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编译解析流程:

Java Web安全之java基础-Java字节码

TestHelloWorld.java 源码、字节码:

Java Web安全之java基础-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规范中u1u2u4分别表示的是1、2、4个字节的无符号数,可使用java.io.DataInputStream类中的对应方法:readUnsignedBytereadUnsignedShortreadInt方法读取。除此之外,表结构(table)由任意数量的可变长度的项组成,用于表示class中的复杂结构,如上述的:cp_infofield_infomethod_infoattribute_info

TestHelloWorld.class十六进制:

Java Web安全之java基础-Java字节码

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.8Java9等说的就是主版本号,如上图中的TestHelloWorld.class的版本号0x34JDK1.8

Java版本对应表:

JDK版本十进制十六进制发布时间
JDK1.1452D1996-05
JDK1.2462E1998-12
JDK1.3472F2000-05
JDK1.448302002-02
JDK1.549312004-09
JDK1.650322006-12
JDK1.751332011-07
JDK1.852342014-03
Java953352017-09
Java1054362018-03
Java1155372018-09
Java1256382019-03
Java1357392019-09
Java14583A2020-03
Java15593B2020-09

constant_pool_count (常量池计数器)

u2 constant_pool_count;表示的是常量池中的数量,constant_pool_count的值等于常量池中的数量加1,需要特别注意的是longdouble类型的常量池对象占用两个常量位。

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_Utf81§4.4.7
CONSTANT_Integer3§4.4.4
CONSTANT_Float4§4.4.4
CONSTANT_Long5§4.4.5
CONSTANT_Double6§4.4.5
CONSTANT_Class7§4.4.1
CONSTANT_String8§4.4.3
CONSTANT_Fieldref9§4.4.2
CONSTANT_Methodref10§4.4.2
CONSTANT_InterfaceMethodref11§4.4.2
CONSTANT_NameAndType12§4.4.6
CONSTANT_MethodHandle15§4.4.8
CONSTANT_MethodType16§4.4.9
CONSTANT_Dynamic17§4.4.10
CONSTANT_InvokeDynamic18§4.4.10
CONSTANT_Module19§4.4.11
CONSTANT_Package20§4.4.12

每一种tag都对应了不同的数据结构,上述表格中标记了不同类型的tag值以及对应的JVM规范章节

access_flags (访问标志)

u2 access_flags;,表示的是某个类或者接口的访问权限及属性。

标志名十六进制值描述
ACC_PUBLIC0x0001声明为public
ACC_FINAL0x0010声明为final
ACC_SUPER0x0020废弃/仅JDK1.0.2前使用
ACC_INTERFACE0x0200声明为接口
ACC_ABSTRACT0x0400声明为abstract
ACC_SYNTHETIC0x1000声明为synthetic,表示该class文件并非由Java源代码所生成
ACC_ANNOTATION0x2000标识注解类型
ACC_ENUM0x4000标识枚举类型

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];
}

属性结构:

  1. u2 access_flags;表示的是成员变量的修饰符;
  2. u2 name_index;表示的是成员变量的名称;
  3. u2 descriptor_index;表示的是成员变量的描述符;
  4. u2 attributes_count;表示的是成员变量的属性数量;
  5. 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];
}

属性结构:

  1. u2 access_flags;表示的是成员方法的修饰符;
  2. u2 name_index;表示的是成员方法的名称;
  3. u2 descriptor_index;表示的是成员方法的描述符;
  4. u2 attributes_count;表示的是成员方法的属性数量;
  5. 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 + '\'' +
                '}';
    }

}

然后使用javacTestHelloWorld.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字符串,省略掉复杂的constantPoolMapfieldListmethodListattributes属性后格式如下:

{
    "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];

为了便于理解解析过程,特意将常量池解析流程单独拆开成如下几步:

  1. 读取常量池数量(u2 constant_pool_count;);
  2. 读取tag
  3. 根据不同的tag类型解析常量池对象;
  4. 解析常量池中的对象;
  5. 链接常量池中的索引引用;

常量池解析片段:

/**
 * 解析常量池数据
 *
 * @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_UTF8java/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.Objectsuper_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_infomethod_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文件中:成员变量成员方法都拥有属性信息,解析的时候可以使用同样的方法。因为属性表中的属性类型过多,本节仅以解析ConstantValueCode为例,完整的解析代码请参考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版本
ConstantValuefield_info§4.7.21.0.2
Codemethod_info§4.7.31.0.2
StackMapTableCode§4.7.46
Exceptionsmethod_info§4.7.51.0.2
InnerClassesClassFile§4.7.61.1
EnclosingMethodClassFile§4.7.75.0
SyntheticClassFilefield_infomethod_info§4.7.81.1
SignatureClassFilefield_infomethod_info§4.7.95.0
SourceFileClassFile§4.7.101.0.2
SourceDebugExtensionClassFile§4.7.115.0
LineNumberTableCode§4.7.121.0.2
LocalVariableTableCode§4.7.131.0.2
LocalVariableTypeTableCode§4.7.145.0
DeprecatedClassFilefield_infomethod_info§4.7.151.1
RuntimeVisibleAnnotationsClassFilefield_infomethod_info§4.7.165.0
RuntimeInvisibleAnnotationsClassFilefield_infomethod_info§4.7.175.0
RuntimeVisibleParameterAnnotationsmethod_info§4.7.185.0
RuntimeInvisibleParameterAnnotationsmethod_info§4.7.195.0
RuntimeVisibleTypeAnnotationsClassFilefield_infomethod_infoCode§4.7.208
RuntimeInvisibleTypeAnnotationsClassFilefield_infomethod_infoCode§4.7.218
AnnotationDefaultmethod_info§4.7.225.0
BootstrapMethodsClassFile§4.7.237
MethodParametersmethod_info§4.7.248
ModuleClassFile§4.7.259
ModulePackagesClassFile§4.7.269
ModuleMainClassClassFile§4.7.279
NestHostClassFile§4.7.2811
NestMembersClassFile§4.7.2911

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_stackmax_locals是一个固定值,表示的是最大操作数栈和最大局部变量数,这两个值是在编译类方法时自动计算出来的,如果通过ASM修改了类方法可能会需要重新计算max_stackmax_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类型示例
BbyteB
CcharC
DdoubleD
FfloatF
IintI
JlongJ
SshortS
ZbooleanZ
[数组[IJ
L类名;引用类型对象Ljava/lang/Object;

方法描述符示例

方法示例描述符描述
static{...}static int id = 1;方法名:<clinit>静态语句块/静态变量初始化
public Test (){...}方法名:<init>,描述符()V构造方法
void hello(){...}()VV表示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规范-指令。不同的指令会有自己的数据结构,如TABLESWITCHLOOKUPSWITCH表示的是switch语句,当匹配到该指令时需要按照它特有的二进制格式解析。除此之外,新版本的JDK可能会新增指令,Java15所有的指令大概有205个。

Java虚拟机指令表

十六进制助记符指令说明
0x00nop什么都不做
0x01aconst_null将null推送至栈顶
0x02iconst_m1将int型-1推送至栈顶
0x03iconst_0将int型0推送至栈顶
0x04iconst_1将int型1推送至栈顶
0x05iconst_2将int型2推送至栈顶
0x06iconst_3将int型3推送至栈顶
0x07iconst_4将int型4推送至栈顶
0x08iconst_5将int型5推送至栈顶
0x09lconst_0将long型0推送至栈顶
0x0alconst_1将long型1推送至栈顶
0x0bfconst_0将float型0推送至栈顶
0x0cfconst_1将float型1推送至栈顶
0x0dfconst_2将float型2推送至栈顶
0x0edconst_0将double型0推送至栈顶
0x0fdconst_1将double型1推送至栈顶
0x10bipush将单字节的常量值(-128~127)推送至栈顶
0x11sipush将一个短整型常量值(-32768~32767)推送至栈顶
0x12ldc将int, float或String型常量值从常量池中推送至栈顶
0x13ldc_w将int, float或String型常量值从常量池中推送至栈顶(宽索引)
0x14ldc2_w将long或double型常量值从常量池中推送至栈顶(宽索引)
0x15iload将指定的int型本地变量推送至栈顶
0x16lload将指定的long型本地变量推送至栈顶
0x17fload将指定的float型本地变量推送至栈顶
0x18dload将指定的double型本地变量推送至栈顶
0x19aload将指定的引用类型本地变量推送至栈顶
0x1aiload_0将第一个int型本地变量推送至栈顶
0x1biload_1将第二个int型本地变量推送至栈顶
0x1ciload_2将第三个int型本地变量推送至栈顶
0x1diload_3将第四个int型本地变量推送至栈顶
0x1elload_0将第一个long型本地变量推送至栈顶
0x1flload_1将第二个long型本地变量推送至栈顶
0x20lload_2将第三个long型本地变量推送至栈顶
0x21lload_3将第四个long型本地变量推送至栈顶
0x22fload_0将第一个float型本地变量推送至栈顶
0x23fload_1将第二个float型本地变量推送至栈顶
0x24fload_2将第三个float型本地变量推送至栈顶
0x25fload_3将第四个float型本地变量推送至栈顶
0x26dload_0将第一个double型本地变量推送至栈顶
0x27dload_1将第二个double型本地变量推送至栈顶
0x28dload_2将第三个double型本地变量推送至栈顶
0x29dload_3将第四个double型本地变量推送至栈顶
0x2aaload_0将第一个引用类型本地变量推送至栈顶
0x2baload_1将第二个引用类型本地变量推送至栈顶
0x2caload_2将第三个引用类型本地变量推送至栈顶
0x2daload_3将第四个引用类型本地变量推送至栈顶
0x2eiaload将int型数组指定索引的值推送至栈顶
0x2flaload将long型数组指定索引的值推送至栈顶
0x30faload将float型数组指定索引的值推送至栈顶
0x31daload将double型数组指定索引的值推送至栈顶
0x32aaload将引用型数组指定索引的值推送至栈顶
0x33baload将boolean或byte型数组指定索引的值推送至栈顶
0x34caload将char型数组指定索引的值推送至栈顶
0x35saload将short型数组指定索引的值推送至栈顶
0x36istore将栈顶int型数值存入指定本地变量
0x37lstore将栈顶long型数值存入指定本地变量
0x38fstore将栈顶float型数值存入指定本地变量
0x39dstore将栈顶double型数值存入指定本地变量
0x3aastore将栈顶引用型数值存入指定本地变量
0x3bistore_0将栈顶int型数值存入第一个本地变量
0x3cistore_1将栈顶int型数值存入第二个本地变量
0x3distore_2将栈顶int型数值存入第三个本地变量
0x3eistore_3将栈顶int型数值存入第四个本地变量
0x3flstore_0将栈顶long型数值存入第一个本地变量
0x40lstore_1将栈顶long型数值存入第二个本地变量
0x41lstore_2将栈顶long型数值存入第三个本地变量
0x42lstore_3将栈顶long型数值存入第四个本地变量
0x43fstore_0将栈顶float型数值存入第一个本地变量
0x44fstore_1将栈顶float型数值存入第二个本地变量
0x45fstore_2将栈顶float型数值存入第三个本地变量
0x46fstore_3将栈顶float型数值存入第四个本地变量
0x47dstore_0将栈顶double型数值存入第一个本地变量
0x48dstore_1将栈顶double型数值存入第二个本地变量
0x49dstore_2将栈顶double型数值存入第三个本地变量
0x4adstore_3将栈顶double型数值存入第四个本地变量
0x4bastore_0将栈顶引用型数值存入第一个本地变量
0x4castore_1将栈顶引用型数值存入第二个本地变量
0x4dastore_2将栈顶引用型数值存入第三个本地变量
0x4eastore_3将栈顶引用型数值存入第四个本地变量
0x4fiastore将栈顶int型数值存入指定数组的指定索引位置
0x50lastore将栈顶long型数值存入指定数组的指定索引位置
0x51fastore将栈顶float型数值存入指定数组的指定索引位置
0x52dastore将栈顶double型数值存入指定数组的指定索引位置
0x53aastore将栈顶引用型数值存入指定数组的指定索引位置
0x54bastore将栈顶boolean或byte型数值存入指定数组的指定索引位置
0x55castore将栈顶char型数值存入指定数组的指定索引位置
0x56sastore将栈顶short型数值存入指定数组的指定索引位置
0x57pop将栈顶数值弹出 (数值不能是long或double类型的)
0x58pop2将栈顶的一个(long或double类型的)或两个数值弹出(其它)
0x59dup复制栈顶数值并将复制值压入栈顶
0x5adup_x1复制栈顶数值并将两个复制值压入栈顶
0x5bdup_x2复制栈顶数值并将三个(或两个)复制值压入栈顶
0x5cdup2复制栈顶一个(long或double类型的)或两个(其它)数值并将复制值压入栈顶
0x5ddup2_x1<待补充>
0x5edup2_x2<待补充>
0x5fswap将栈最顶端的两个数值互换(数值不能是long或double类型的)
0x60iadd将栈顶两int型数值相加并将结果压入栈顶
0x61ladd将栈顶两long型数值相加并将结果压入栈顶
0x62fadd将栈顶两float型数值相加并将结果压入栈顶
0x63dadd将栈顶两double型数值相加并将结果压入栈顶
0x64isub将栈顶两int型数值相减并将结果压入栈顶
0x65lsub将栈顶两long型数值相减并将结果压入栈顶
0x66fsub将栈顶两float型数值相减并将结果压入栈顶
0x67dsub将栈顶两double型数值相减并将结果压入栈顶
0x68imul将栈顶两int型数值相乘并将结果压入栈顶
0x69lmul将栈顶两long型数值相乘并将结果压入栈顶
0x6afmul将栈顶两float型数值相乘并将结果压入栈顶
0x6bdmul将栈顶两double型数值相乘并将结果压入栈顶
0x6cidiv将栈顶两int型数值相除并将结果压入栈顶
0x6dldiv将栈顶两long型数值相除并将结果压入栈顶
0x6efdiv将栈顶两float型数值相除并将结果压入栈顶
0x6fddiv将栈顶两double型数值相除并将结果压入栈顶
0x70irem将栈顶两int型数值作取模运算并将结果压入栈顶
0x71lrem将栈顶两long型数值作取模运算并将结果压入栈顶
0x72frem将栈顶两float型数值作取模运算并将结果压入栈顶
0x73drem将栈顶两double型数值作取模运算并将结果压入栈顶
0x74ineg将栈顶int型数值取负并将结果压入栈顶
0x75lneg将栈顶long型数值取负并将结果压入栈顶
0x76fneg将栈顶float型数值取负并将结果压入栈顶
0x77dneg将栈顶double型数值取负并将结果压入栈顶
0x78ishl将int型数值左移位指定位数并将结果压入栈顶
0x79lshl将long型数值左移位指定位数并将结果压入栈顶
0x7aishr将int型数值右(符号)移位指定位数并将结果压入栈顶
0x7blshr将long型数值右(符号)移位指定位数并将结果压入栈顶
0x7ciushr将int型数值右(无符号)移位指定位数并将结果压入栈顶
0x7dlushr将long型数值右(无符号)移位指定位数并将结果压入栈顶
0x7eiand将栈顶两int型数值作“按位与”并将结果压入栈顶
0x7fland将栈顶两long型数值作“按位与”并将结果压入栈顶
0x80ior将栈顶两int型数值作“按位或”并将结果压入栈顶
0x81lor将栈顶两long型数值作“按位或”并将结果压入栈顶
0x82ixor将栈顶两int型数值作“按位异或”并将结果压入栈顶
0x83lxor将栈顶两long型数值作“按位异或”并将结果压入栈顶
0x84iinc将指定int型变量增加指定值(i++, i–, i+=2)
0x85i2l将栈顶int型数值强制转换成long型数值并将结果压入栈顶
0x86i2f将栈顶int型数值强制转换成float型数值并将结果压入栈顶
0x87i2d将栈顶int型数值强制转换成double型数值并将结果压入栈顶
0x88l2i将栈顶long型数值强制转换成int型数值并将结果压入栈顶
0x89l2f将栈顶long型数值强制转换成float型数值并将结果压入栈顶
0x8al2d将栈顶long型数值强制转换成double型数值并将结果压入栈顶
0x8bf2i将栈顶float型数值强制转换成int型数值并将结果压入栈顶
0x8cf2l将栈顶float型数值强制转换成long型数值并将结果压入栈顶
0x8df2d将栈顶float型数值强制转换成double型数值并将结果压入栈顶
0x8ed2i将栈顶double型数值强制转换成int型数值并将结果压入栈顶
0x8fd2l将栈顶double型数值强制转换成long型数值并将结果压入栈顶
0x90d2f将栈顶double型数值强制转换成float型数值并将结果压入栈顶
0x91i2b将栈顶int型数值强制转换成byte型数值并将结果压入栈顶
0x92i2c将栈顶int型数值强制转换成char型数值并将结果压入栈顶
0x93i2s将栈顶int型数值强制转换成short型数值并将结果压入栈顶
0x94lcmp比较栈顶两long型数值大小,并将结果(1,0,-1)压入栈顶
0x95fcmpl比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶
0x96fcmpg比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶
0x97dcmpl比较栈顶两double型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶
0x98dcmpg比较栈顶两double型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶
0x99ifeq当栈顶int型数值等于0时跳转
0x9aifne当栈顶int型数值不等于0时跳转
0x9biflt当栈顶int型数值小于0时跳转
0x9cifge当栈顶int型数值大于等于0时跳转
0x9difgt当栈顶int型数值大于0时跳转
0x9eifle当栈顶int型数值小于等于0时跳转
0x9fif_icmpeq比较栈顶两int型数值大小,当结果等于0时跳转
0xa0if_icmpne比较栈顶两int型数值大小,当结果不等于0时跳转
0xa1if_icmplt比较栈顶两int型数值大小,当结果小于0时跳转
0xa2if_icmpge比较栈顶两int型数值大小,当结果大于等于0时跳转
0xa3if_icmpgt比较栈顶两int型数值大小,当结果大于0时跳转
0xa4if_icmple比较栈顶两int型数值大小,当结果小于等于0时跳转
0xa5if_acmpeq比较栈顶两引用型数值,当结果相等时跳转
0xa6if_acmpne比较栈顶两引用型数值,当结果不相等时跳转
0xa7goto无条件跳转
0xa8jsr跳转至指定16位offset位置,并将jsr下一条指令地址压入栈顶
0xa9ret返回至本地变量指定的index的指令位置(一般与jsr, jsr_w联合使用)
0xaatableswitch用于switch条件跳转,case值连续(可变长度指令)
0xablookupswitch用于switch条件跳转,case值不连续(可变长度指令)
0xacireturn从当前方法返回int
0xadlreturn从当前方法返回long
0xaefreturn从当前方法返回float
0xafdreturn从当前方法返回double
0xb0areturn从当前方法返回对象引用
0xb1return从当前方法返回void
0xb2getstatic获取指定类的静态域,并将其值压入栈顶
0xb3putstatic为指定的类的静态域赋值
0xb4getfield获取指定类的实例域,并将其值压入栈顶
0xb5putfield为指定的类的实例域赋值
0xb6invokevirtual调用实例方法
0xb7invokespecial调用超类构造方法,实例初始化方法,私有方法
0xb8invokestatic调用静态方法
0xb9invokeinterface调用接口方法
0xba
0xbbnew创建一个对象,并将其引用值压入栈顶
0xbcnewarray创建一个指定原始类型(如int, float, char…)的数组,并将其引用值压入栈顶
0xbdanewarray创建一个引用型(如类,接口,数组)的数组,并将其引用值压入栈顶
0xbearraylength获得数组的长度值并压入栈顶
0xbfathrow将栈顶的异常抛出
0xc0checkcast检验类型转换,检验未通过将抛出ClassCastException
0xc1instanceof检验对象是否是指定的类的实例,如果是将1压入栈顶,否则将0压入栈顶
0xc2monitorenter获得对象的锁,用于同步方法或同步块
0xc3monitorexit释放对象的锁,用于同步方法或同步块
0xc4wide<待补充>
0xc5multianewarray创建指定类型和指定维度的多维数组(执行该指令时,操作栈中必须包含各维度的长度值),并将其引用值压入栈顶
0xc6ifnull为null时跳转
0xc7ifnonnull不为null时跳转
0xc8goto_w无条件跳转(宽索引)
0xc9jsr_w跳转至指定32位offset位置,并将jsr_w下一条指令地址压入栈顶

指令解析

为了便于理解,以TestHelloWorld类中有一个hello方法为例,学习字节码和源代码之间的关联性。

TestHelloWorld类的hello方法代码如下:

public String hello(String content) {
    String str = "Hello:";
    return str + content;
}

hello方法是一个非静态方法,返回值是Stringhello方法有一个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方法字节码解析:

  1. ldc表示的是将int, float或String型常量值从常量池中推送至栈顶,而ldc #3表示的是将常量池中的第三个索引位置压入栈顶,也就是Hello:
  2. astore_2表示的是将栈顶的值存入到局部变量表的第二个位置,局部变量表的索引位置是从0开始的,因为hello方法是一个非静态方法,所以索引0表示的是this对象(如果是static方法那么就意味着没有this对象,索引0就应该表示第一个参数)。索引1表示的是hello方法的第一个参数,也就是String content。如果在方法体中想创建一个新的对象,那么就必须计算这个变量在局部变量表中的索引位置,否则无法存储对象。还有一个需要特别注意的点是longdouble是宽类型(wide type)需要占用两个索引位置。astore_2实际上表达的是将栈顶的对象压入到局部变量表中,等价于String arg2 = new String("Hello:")
  3. new #4表示的是创建java/lang/StringBuilder类实例;
  4. dup表示复制栈顶数值并将复制值压入栈顶,即StringBuilder对象;
  5. invokespecial #5invokespecial表示的是调用超类构造方法,实例初始化方法,私有方法,即调用StringBuilder类的构造方法(<init>),#5在常量池中是一个CONSTANT_METHOD_REF类型的对象,用于表示一个类方法引用,invokespecial #5实际上是在调用的StringBuilder的构造方法,等价于:new StringBuilder()
  6. aload_2表示的是加载局部变量表中的第二个变量,也就是读取astore_2存入的值,即Hello:
  7. invokevirtual #6表示的是调用StringBuilder类的append方法,等价于:sb.append("Hello:")
  8. aload_1表示的是将局部变量表中的第一个变量压入栈顶,也就是将hello方法的第一个参数content的值压入栈顶;
  9. invokevirtual #6,再次调用StringBuilder类的append方法,等价于:sb.append(content)
  10. invokevirtual #7,调用StringBuilder类的toString方法,等价于:sb.toString()
  11. 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,用于生成和转换类:

  1. ClassReader类用于解析class文件或二进制流;
  2. ClassWriter类是ClassVisitor的子类,用于生成类二进制;
  3. 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类图:

Java Web安全之java基础-Java字节码

MethodVisitor和AdviceAdapter

MethodVisitorClassVisitor,重写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的父类是GeneratorAdapterLocalVariablesSorter,在MethodVisitor类的基础上封装了非常多的便捷方法,同时还为我们做了非常有必要的计算,所以我们应该尽可能的使用AdviceAdapter来修改字节码。

AdviceAdapter类实现了一些非常有价值的方法,如:onMethodEnter(方法进入时回调方法)、onMethodExit(方法退出时回调方法),如果我们自己实现很容易掉进坑里面,因为这两个方法都是根据条件推算出来的。比如我们如果在构造方法的第一行直接插入了我们自己的字节码就可能会发现程序一运行就会崩溃,因为Java语法中限制我们第一行代码必须是super(xxx)

GeneratorAdapter封装了一些栈指令操作的方法,如loadArgArray方法可以直接获取方法所有参数数组、invokeStatic方法可以直接调用类方法、push方法可压入各种类型的对象等。

比如LocalVariablesSorter类实现了计算本地变量索引位置的方法,如果要在方法中插入新的局部变量就必须计算变量的索引位置,我们必须先判断是否是非静态方法、是否是long/double类型的参数(宽类型占两个位),否则计算出的索引位置还是错的。使用AdviceAdapter可以直接调用mv.newLocal(type)计算出本地变量存储的位置,为我们省去了许多不必要的麻烦。

读取类/成员变量/方法信息

为了学习ClassVisitor,我们写一个简单的读取类、成员变量、方法信息的一个示例,需要重写ClassVisitor类的visitvisitFieldvisitMethod方法。

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_stackmax_locals,否则会导致修改后的类文件无法通过JVM校验。手动计算max_stackmax_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,如下图:

Java Web安全之java基础-Java字节码

修改类方法字节码

大多数使用ASM库的目的其实是修改类方法的字节码,在原方法执行的前后动态插入新的Java代码,从而实现类似于AOP的功能。修改类方法字节码的典型应用场景如:APM和RASP;APM需要统计和分析每个类方法的执行时间,而RASP需要在Java底层API方法执行之前插入自身的检测代码,从而实现动态拦截恶意攻击。

假设我们需要修改com.anbai.sec.bytecode.TestHelloWorld类的hello方法,实现以下两个需求:

  1. 在原业务逻辑执行前打印出该方法的参数值;
  2. 修改该方法的返回值;

原业务逻辑:

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文件:

Java Web安全之java基础-Java字节码

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

Java Web安全之java基础-Java字节码

动态创建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类,如下图:

Java Web安全之java基础-Java字节码

IDEA/Eclipse插件

初学ASM,读写ASM字节码对我们来说是非常困难的,但是我们可以借助开发工具的ASM插件,可以极大程度的帮助我们学习ASM。

IDEA – ASM Bytecode Outline

在IDEA中插件中心搜索:ASM Bytecode Outline,就可以找到ASM字节码插件,如下图:

Java Web安全之java基础-Java字节码

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

Java Web安全之java基础-Java字节码

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...,如下图:

Java Web安全之java基础-Java字节码

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

Java Web安全之java基础-Java字节码

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

Java Web安全之java基础-Java字节码

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

Java Web安全之java基础-Java字节码

安装完成后重启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...

Java Web安全之java基础-Java字节码

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

Java Web安全之java基础-Java字节码

复制仓库地址:

Java Web安全之java基础-Java字节码

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

Java Web安全之java基础-Java字节码

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

Java Web安全之java基础-Java字节码

Bytecode Outline配置

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

Java Web安全之java基础-Java字节码

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

Java Web安全之java基础-Java字节码

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

Java Web安全之java基础-Java字节码

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

Java Web安全之java基础-Java字节码

我们可以借助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,如:CtClassCtConstructorCtMethodCtField与Java反射的ClassConstructorMethodField非常的类似。

描述
ClassPoolClassPool是一个存储CtClass的容器,如果调用get方法会搜索并创建一个表示该类的CtClass对象
CtClassCtClass表示的是从ClassPool获取的类对象,可对该类就行读写编辑等操作
CtMethod可读写的类方法对象
CtConstructor可读写的类构造方法对象
CtField可读写的类成员变量对象

Javassist使用了内置的标识符来表示一些特定的含义,如:$_表示返回值。我们可以在动态插入类代码的时候使用这些特殊的标识符来表示对应的对象。

表达式描述
$0, $1, $2, ...this和方法参数
$argsObject[]类型的参数数组
$$所有的参数,如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,实现接口:[[email protected][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;
-----------------------------------------------------------------------------
[email protected][public hello (Ljava/lang/String;)Ljava/lang/String;]
[email protected][public static main ([Ljava/lang/String;)V]
[email protected][public getId ()J]
[email protected][public setId (J)V]
[email protected][public getUsername ()Ljava/lang/String;]
[email protected][public setUsername (Ljava/lang/String;)V]
[email protected][public getPassword ()Ljava/lang/String;]
[email protected][public setPassword (Ljava/lang/String;)V]
[email protected][public toString ()Ljava/lang/String;]

修改类方法

Javassist实现类方法修改比ASM简单多了,我们只需要调用CtMethod类的对应的API就可以了。CtMethod提供了类方法修改的API,如:setModifiers可修改类的访问修饰符,insertBeforeinsertAfter能够实现在类方法执行的前后插入任意的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 Web安全之java基础-Java字节码

动态创建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();
        }
    }

}

from

转载请注明出处及链接

Leave a Reply

您的电子邮箱地址不会被公开。