Java-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编译解析流程:
  • TestHelloWorld.java 源码、字节码:

二、 Java class文件格式

⚠️:用010editor能够直观看到二进制数据每个数据块的语义和值,可以对照学习。

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十六进制:

img

2.1 Magic(魔数)

魔数是class文件的标识符,固定值为0xCAFEBABE,JVM加载class文件时会先读取4字节(u4 magic;)的魔数信息校验是否是一个class文件。

2.2 Minor/Major Version(版本号)

class文件的版本号由两个u2组成(u2 minor_version; u2 major_version;),分别表示的是minor_version(副版本号)、major_version (主版本号),我们常说的JDK1.8Java9等说的就是主版本号,如上图中的TestHelloWorld.class的版本号0x34JDK1.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
Java16 60 3C 2021-03
Java17 61 3D 2021-09
Java18 62 3E 2022-03
Java19 63 3F 2022-09

2.3 constant_pool_count (常量池计数器)

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

2.4 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规范章节

2.5 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 标识枚举类型

2.6 this_class(当前类名称)

u2 this_class;表示的是当前class文件的类名所在常量池中的索引位置。

2.7 super_class(当前类的父类名称)

u2 super_class;表示的是当前class文件的父类类名所在常量池中的索引位置。java/lang/Object类的super_class的为0,其他任何类的super_class都必须是一个常量池中存在的索引位置。

2.8 interfaces_count(当前类继承或实现的接口数)

u2 interfaces_count;表示的是当前类继承或实现的接口数。

2.9 interfaces[] (接口名称数组)

u2 interfaces[interfaces_count];表示的是所有接口数组。

2.10 fields_count(当前类的成员变量数)

u2 fields_count;表示的是当前class中的成员变量个数。

2.11 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];表示的是成员变量的属性信息;

2.12 methods_count(当前类的成员方法数)

u2 methods_count;表示的是当前class中的成员方法个数。

2.13 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];表示的是成员方法的属性信息;

2.14 attributes_count (当前类的属性数)

u2 attributes_count;表示当前class文件属性表的成员个数。

2.15 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"
}

3.1 魔数/版本解析

一个合法的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。

3.2 常量池解析

解析常量池信息时需要先解析出常量池对象的数量,然后遍历常量池,解析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;
}

3.3 访问标志解析

// u2 access_flags;
this.accessFlags = dis.readUnsignedShort();

解析结果:"accessFlags": 33,

3.4 当前类名称解析

解析类名称的时候直接读取2个无符号数,获取到类名所在的常量池中的索引位置,然后根据常量池ID读取常量池中的字符串内容即可解析出类名。

// u2 this_class;
this.thisClass = (String) getConstantPoolValue(dis.readUnsignedShort());

解析结果:"thisClass": "com/anbai/sec/bytecode/TestHelloWorld"

3.5 当前类的父类名称解析

解析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",

3.6 接口解析

解析接口信息时需要先解析出接口的数量,然后就可以遍历出所有的接口名称索引值了。

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

3.7 成员变量/成员方法解析

成员变量和成员方法的数据结构是一样的,所以可以使用相同的解析逻辑。首先解析出变量/方法的总数量,然后遍历并解析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
                                }
                            ]
                        }
                    }
                }
            }
        }
    ]
}

3.8 属性解析

成员变量、成员方法、类对象这三种数据结构都需要解析属性信息,因为逻辑非常复杂,将在下一小节详解。

四、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版本
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

4.1 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
    }
  }
}

4.2 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虚拟机的指令集。

5.1 类型/方法描述符

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 普通方法,无返回值

5.2 Java虚拟机指令

栈指令是由0-255的整型表示,在JVM规范的第六章中有完整的说明:JVM规范-指令。不同的指令会有自己的数据结构,如TABLESWITCHLOOKUPSWITCH表示的是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方法是一个非静态方法,返回值是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类结构访问的所有事件;

6.1 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类图:

6.2 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)计算出本地变量存储的位置,为我们省去了许多不必要的麻烦。

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

为了学习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实现遍历一个类的基础信息。

6.4 修改类名/方法名称/方法修饰符示例

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

6.5 修改类方法字节码

大多数使用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文件:

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

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

6.7 IDEA插件

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

6.7.1 IDEA - ASM Bytecode Outline

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

img

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

img

我们可以借助Bytecode Outline插件学习ASM,也可以直接使用Bytecode Outline生成的ASM代码来实现字节码编辑。

七、Java 类字节码编辑 - Javassist

Javassist是一个开源的分析、编辑和创建Java字节码的类库;相比ASM,Javassist提供了更加简单便捷的API,使用Javassist我们可以像写Java代码一样直接插入Java代码片段,让我们不再需要关注Java底层的字节码的和栈操作,仅需要学会如何使用Javassist的API即可实现字节码编辑。学习Javassist可以阅读官方的入门教程:Getting Started with Javassist

7.1 Javassist API和标识符

Javassist为我们提供了类似于Java反射机制的API,如:CtClassCtConstructorCtMethodCtField与Java反射的ClassConstructorMethodField非常的类似。

描述
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类型
  • 方法签名

Java 的方法签名是方法名称参数类型列表(按顺序、数量)的组合,它唯一标识该方法在类中的身份。它不包含返回值类型、访问修饰符(如 public)、异常声明或参数名称。正是基于方法签名,Java 能够实现方法重载——允许同一个类中出现多个同名但参数列表不同的方法。简单来说,它就是 方法名(参数类型1, 参数类型2, ...) 的形式。

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

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

7.3 修改类方法

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();
        }
    }

}

程序执行后结果如下:

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

}

7.5 Java class反编译

在渗透测试的时候需要审计的代码通常是class文件或者jar包,在没有源文件的情况下我们可以通过反编译class/jar的方式阅读程序逻辑。

Java源码就是未经编译的.java文件,我们可以很轻松的阅读其中的代码逻辑,而字节码.class文件则是.java文件经过编译之后产生的无法直接阅读的二进制文件,不过我们可以通过反编译工具将class文件反编译成java源文件。我们通常会使用到JD-GUIRecafIDEA Fernflower插件Bytecode-Viewer/FernflowerJADJBECafebabeJByteModjclasslib等工具来反编译/分析class文件

当然,反编译工具很多时候也不是万能的,JD-GUI经常遇到无法反编译或反编译过程中程序直接崩溃的情况,遇到这类情况我们通常可以使用IDEA反编译试试,如果IDEA也无法反编译可以使用JBE或者JDK自带的javap命令来读取class类字节码,如果连javap都无法识别该class文件,那么这个类可能存在无法编译问题,也有可能类文件被加密处理过(自定义ClassLoaderloadClass加密后的类、或者借助JNIJVMTI调用动态链接库)。

7.5.1 javap

javap是JDK自带的一个命令行反汇编工具,命令行参数如下:

用法: javap <options> <classes>
其中, 可能的选项包括:
  -help  --help  -?        输出此用法消息
  -version                 版本信息
  -v  -verbose             输出附加信息
  -l                       输出行号和本地变量表
  -public                  仅显示公共类和成员
  -protected               显示受保护的/公共类和成员
  -package                 显示程序包/受保护的/公共类
                           和成员 (默认)
  -p  -private             显示所有类和成员
  -c                       对代码进行反汇编
  -s                       输出内部类型签名
  -sysinfo                 显示正在处理的类的
                           系统信息 (路径, 大小, 日期, MD5 散列)
  -constants               显示最终常量
  -classpath <path>        指定查找用户类文件的位置
  -cp <path>               指定查找用户类文件的位置
  -bootclasspath <path>    覆盖引导类文件的位置
  • 查看类字节码

可根据javap命令控制输出信息,如:javap -c -l TestHelloWorld.class可显示类方法的字节码信息,如下图:

如果想要获取到详细的类信息可使用-v参数。

7.5.2 jd-GUI

JD-GUI使用纯Java编写,使用的反编译实现是jd-core,支持JDK 1.1.8 - Java 12,支持Lambda表达式方法引用默认方法等特性,是一款非常简单易用的反编译工具。

JD-GUI在反编译的时候还会主动关联Maven仓库,如果反编译的jar在maven仓库中存在会自动下载类源码,如下图:

字符串搜索:

JD-GUI支持批量反编译,在菜单中点击File–>Save All Sources即可反编译整个jar文件,如下图:

除此之外,JD-GUI还有Eclipse和IDEA的插件:http://java-decompiler.github.io/、https://plugins.jetbrains.com/plugin/7100-java-decompiler-intellij-plugin。

官方网站:http://java-decompiler.github.io/

Github:https://github.com/java-decompiler/jd-gui

反编译jd-core:https://github.com/java-decompiler/jd-core

版本下载:https://github.com/java-decompiler/jd-gui/releases

7.5.3 Recaf

Recaf是一个使用JavaFX开发的现代化反编译工具,它不但具备编译功能,而且还可以直接直接编辑反编译后的类文件、查看字节码、Hex编辑、中文支持等丰富功能。

  • 编辑模式

Recaf默认使用的是反编译模式,可根据需求选择为Hex或者Table模式,如下图:

img

  • 反编译模式

Recaf的默认编辑视图是反编译模式,如果使用的是JDK运行的Recaf,还可以直接编辑反编译后的class文件,如下图:

image-20201019113213048

  • Hex模式编辑

image-20201019135516449

  • Table模式/字节码编辑

img

  • 字节码编辑:

img

  • Java Agent/Attach模式

Recaf支持Agent模式或者attach模式(注入)。

  • Agent Attach模式

在菜单栏中点击注入菜单,选择Running process可以看到本机所有运行的Java进程,如下图:

img

attach模式可附加Recaf到一个指定的JVM进程:

img

  • Agent模式

Agent模式需要在启动Recaf的时候指定-javaagent:参数,如下图,以Agent模式启动Recaf,启动完成后会弹出一个Recaf Instrumentation的窗体:

image-20201019142104679

  • 字符串搜索

Recaf支持很多种搜索方式,如下图:

img

字符串搜索测试:

img

详细文档:https://www.coley.software/Recaf/doc-setup-get.html

Github:https://github.com/Col-E/Recaf

版本下载:https://github.com/Col-E/Recaf/releases

7.5.4 FernFlower/IDEA

Fernflower是一个简单高效的反编译命令行工具,Fernflower已成为了JetBrainsintellij-community内置反编译工具,同时Fernflower还有一个非常好用的GUI工具: Bytecode Viewer

Fernflower反编译jar示例:

java -jar fernflower.jar jarToDecompile.jar decomp/

其中jarToDecompile.jar是需要反编译的jar文件,decomp是反编译后的class文件所存放的目录。需要注意的是Fernflower如遇无法反编译的情况可能会生成空的java文件!

  • 使用Find命令和Fernflower实现批量反编译jar

通常我们在某些特殊的场景下拿到的只是jar文件,那么我们应该如何反编译整个jar包的class文件呢?

find命令并不能支持Java反编译,但是find命令可以非常方便的搜索经过编译后的二进制文件中的内容,所以有的时候使用find命令通常是最简单实用的。例如使用find命令搜索某个关键字: find ./ -type f -name "*.class" |xargs grep XXXX

有的时候我们只有项目war包没有源码,只能在WEB-INF/lib中找程序的源码,这个时候我们可以巧妙的使用find命令加Fernflower来实现反编译所有的jar包。

这里以jcms的一个非常老版本为例,jcms最终给客户部署的war包中源码并不是在WEB-INF/classes目录下,而是将整个jcms系统按模块打包成了多个jar包放在了WEB-INF/lib目录下。我们可以通过搜索com.hanweb包名称来找出所有jar中包含了jcms的文件并通过Fernflower来反编译。

java -jar /Users/yz/Desktop/javaweb-decomplier/javaweb-decomplier.jar -dgs=1 $(find /Users/yz/Desktop/jcms/WEB-INF/lib/ -type f -name "*.jar" |xargs grep "com.hanweb" |awk '{print $3}') /Users/yz/jcms-decomplier

执行上面的命令后会在jcms-decomplier目录下看到所有的jar已经被Fernflower反编译了。

img

依赖的jar: javaweb-decomplierIntellij java-decompiler

Github:https://github.com/JetBrains/intellij-community/tree/master/plugins/java-decompiler/engine/src/org/jetbrains/java/decompiler

Fernflower文档:https://the.bytecode.club/fernflower.txt

7.5.5 IDEA反编译

IDEA自带的反编译工具FernFlower,在IDEA中可以直接打开class文件,默认将使用FernFlower反编译,如下图:

IDEA支持class文件名(⇧⌘F)、类方法名称(⇧⌘O)搜索。

7.5.6 Bytecode Viewer

img

Bytecode Viewer是一个基于FernFlower实现的轻量级,用户友好的Java字节码可视化工具,Bytecode Viewer具备了如下强大功能:

  1. Java 反编译GUI;
  2. Bytecode编辑器 GUI;
  3. Smali GUI ;
  4. Baksmali GUI;
  5. APK编辑器 GUI;
  6. Dex编辑器 GUI;
  7. APK反编译 GUI;
  8. DEX反编译 GUI;
  9. Procyon Java反编译 GUI;
  10. Krakatau GUI;
  11. CFR Java反编译 GUI;
  12. FernFlower Java反编译 GUI;
  13. DEX2Jar GUI;
  14. Jar2DEX GUI;
  15. Jar-Jar GUI;
  16. Hex视图;
  17. 代码搜索;
  18. 调试器;
  • 多视图

Bytecode Viewer提供了多种视图可供选择,在View菜单中可选择不同的视图或反编译引擎(默认使用的是反编译和字节码视图),当选择了Editable后可编辑反编译后的class文件,如下图:

image-20201019151037905

  • 代码搜索功能

代码搜索功能支持字符串、正则表达式、调用方法和调用字段搜索,如下图:

img

  • 反编译安卓APK

image-20201019151742200

Github:https://github.com/Konloch/bytecode-viewer

版本下载:https://github.com/Konloch/bytecode-viewer/releases

7.5.7 jad

jad是一个C++编写的跨平台的Java反编译命令行工具,可使用jad命令反编译class文件,jad最新版本是发布于2006年的1.5.8g,距今,已有14年没更新了。

反编译示例:jad TestHelloWorld.class

JAD参数如下:

Jad accepts the following options:

-a       - annotate the output with JVM bytecodes (default: off)
-af      - same as -a, but output fully qualified names when annotating
-clear   - clear all prefixes, including the default ones (can be abbreviated as -cl)
-b       - output redundant braces (e.g., if(a) { b(); }, default: off)
-d 
 - directory for output files (will be created when necessary)
  -dead    - try to decompile dead parts of code (if any) (default: off)
  -disass  - disassemble method bytecodes (no JAVA source generated)
  -f       - output fully qualified names for classes/fields/methods (default: off)
  -ff      - output class fields before methods (default: after methods)
  -i       - output default initializers for all non-final fields
  -l  - split strings into pieces of maximum  chars (default: off)
  -lnc     - annotate the output with line numbers (default: off)
  -lradix - display long integers using the specified radix (8, 10 or 16)
  -nl      - split strings on newline character (default: off)
  -nocast  - don't generate auxiliary casts
  -nocode  - don't generate the source code for methods
  -noconv  - don't convert Java identifiers (default: convert)
  -noctor  - suppress the empty constructors
  -nodos   - do not check for class files written in DOS mode (CR before NL, default: check)
  -nofd    - don't disambiguate fields with the same names by adding signatures to their names (default: do)
  -noinner - turn off the support of inner classes (default: on)
  -nolvt   - ignore Local Variable Table information
  -nonlb   - don't output a newline before opening brace (default: do)
  -o       - overwrite output files without confirmation (default: off)
  -p       - send decompiled code to STDOUT (e.g., for piping)
  -pi - pack imports into one line after  imports (default: 3)
  -pv - pack fields with identical types into one line (default: off)
  -pa - prefix for all packages in generated source files
  -pc - prefix for classes with numerical names (default: _cls)
  -pf - prefix for fields with numerical names (default: _fld)
  -pe - prefix for unused exception names (default: _ex)
  -pl - prefix for locals with numerical names (default: _lcl)
  -pm - prefix for methods with numerical names (default: _mth)
  -pp - prefix for method parms with numerical names (default: _prm)
  -r       - restore package directory structrure
  -radix - display integers using the specified radix (8, 10 or 16)
  -s  - output file extension (by default '.jad')
  -safe    - generate additional casts to disambiguate methods/fields (default: off)
  -space   - output space between keyword (if/for/while/etc) and expression (default: off)
  -stat    - display the total number of processed classes/methods/fields
  -t       - use tabs instead of spaces for indentation
  -t  - use  spaces for indentation (default: 4)
  -v       - display method names being decompiled
  -8       - convert UNICODE strings into 8-bit strings
  using the current ANSI code page (Win32 only)
  -&       - redirect STDERR to STDOUT (Win32 only)

官方网站:http://www.kpdus.com/jad.html#general

版本下载:http://www.javadecompilers.com/jad

7.5.8 JBE

JBE(Java Bytecode Editor)是一个使用BCELjclasslib bytecode viewer实现的编辑class字节码编辑的工具,JBE只能编辑字节码,不能反编译,JBE也有近10年未更新了,不支持高版本的Java class解析。

添加常量池对象:

img

  • 删除常量池对象

img

  • 删除类成员变量

img

  • 查看字节码

img

  • 编辑字节码

image-20201019163143075

  • 删除类方法

img

官方地址:https://set.ee/jbe/

7.5.9 jclasslib bytecode viewer

jclasslib是一个有20年历史的java字节码浏览工具,可以非常方便的查看class的常量池、字节码等信息,非常适合学习解析class字节码。新版本的jclasslib支持中文,改用了Kotlin编写。

image-20201019163424897

Github:https://github.com/ingokegel/jclasslib

版本下载:https://github.com/ingokegel/jclasslib/releases

7.5.10 Cafebabe Lite

image-20201019171226961

Cafebabe是一个用户友好的Java字节码编辑器。

  • 反编译

image-20201019170742769

  • 字节码编辑

image-20201019171439589

  • 字节码可视化

img

Cafebabe / JByteMod / other比较:

reJ JBytedit JBE Recaf JByteMod Cafebabe
Edit Instructions 部分 部分 部分 部分 Yes 部分
Edit Fields Yes Yes Yes Yes Yes No
Edit Attributes 部分 部分 部分 Yes Yes No
Edit Try Catch Blocks Yes No Yes Yes Yes No
Decompiler(s) No No No Yes Yes Yes
Analytical Decompiler No No No No Yes Yes
Colored Syntax No Yes 部分 Yes Yes Yes
Labels Yes Yes No Yes Yes Yes
Multilingual support No No No Yes Yes Yes
LDC Search 部分 Yes No Yes Yes No
Instruction Search 部分 No No Yes Yes No
Regex Search No No No Yes Yes No
Class Search No No No Yes No No
In-Editor Search No No No No Yes No
In-Editor Help Yes No No unknown Yes Yes
Frame Regeneration No No No Yes Yes Yes
Automatic Frame Regeneration (no libraries) No No No No No Yes
Control Flow Graph No No No No Yes Yes
Java 8 Support No 部分 No No Yes Yes
Java 11 Support No No No Yes Yes Yes
Java 12 Support No No No Yes Yes Yes
Obfuscation Analysis No No No No Yes No
Live Code Manipulation No No No Yes Yes No

Github:https://github.com/GraxCode/Cafebabe/

版本下载:https://github.com/GraxCode/Cafebabe/releases

7.5.11 JByteMod

JByteModCafebabe都是同一个作者开发的,JByteModCafebabe基础上做了非常多的改进,支持反编译、字节码编辑、可视化分析、Agent注入等。

  • 反编译

img

  • 字节码编辑

img

  • 字节码分析

img

  • 方法信息编辑

img

  • 方法添加/编辑

img

Github:https://github.com/GraxCode/JByteMod-Beta

版本下载:https://github.com/GraxCode/JByteMod-Beta/releases

参考


文章作者: KajimiYa
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 KajimiYa !
  目录