ASM字节码学习

文章发布时间:

最后更新时间:

Java 字节码

Java class文件格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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];
}
  • u1u2u4 分别表示1、2、4个字节的无符号数。
    x即代表字节无符号数、其余info类型是复合结构

    可使用java.io.DataInputStream类中的对应方法:readUnsignedBytereadUnsignedShortreadInt方法读取。

  • 魔数 Magic

    固定值 0xCAFEBABE

    image-20230115172830015

    JVM加载class文件时会先读取4字节(u4 magic;)的魔数信息校验是否是一个class文件

    image-20230115172915362

  • 版本号 Minor/Major Version

    class文件的版本号由两个u2组成(u2 minor_version; u2 major_version;),分别表示的是minor_version(副版本号)、major_version (主版本号),我们常说的JDK1.8Java9等说的就是主版本号。

    image-20230115173100707

    | JDK版本 | 十进制 | 十六进制 | 发布时间 |
    | ———- | ————— | —————— | ———— |
    | JDK1.1 | 45 | 2D | 1996-05 |
    | JDK1.2 | 46 | 2E | 1998-12 |
    | JDK1.3 | 47 | 2F | 2000-05 |
    | JDK1.4 | 48 | 30 | 2002-02 |
    | JDK1.5 | 49 | 31 | 2004-09 |
    | JDK1.6 | 50 | 32 | 2006-12 |
    | JDK1.7 | 51 | 33 | 2011-07 |
    | JDK1.8 | 52 | 34 | 2014-03 |
    | Java9 | 53 | 35 | 2017-09 |
    | Java10 | 54 | 36 | 2018-03 |
    | Java11 | 55 | 37 | 2018-09 |
    | Java12 | 56 | 38 | 2019-03 |
    | Java13 | 57 | 39 | 2019-09 |
    | Java14 | 58 | 3A | 2020-03 |
    | Java15 | 59 | 3B | 2020-09 |

  • constant_pool_count 常量池计数器

    表示常量池中的数量,数值上等于 常量池的数量+1

    需要特别注意的是longdouble类型的常量池对象占用两个常量位

  • constant_pool 常量池

    表结构 cp_info constant_pool[constant_pool_count-1];

    常量池对象 cp_info 数据结构:

    1
    2
    3
    4
    cp_info {
    u1 tag;
    u1 info[];
    }

    u1 tag;表示的是常量池中的存储类型,每一种tag都对应了不同的数据结构。

  • access_flags 访问标志

    表示的是某个类或者接口的访问权限及属性

    | 标志名 | 十六进制值 | 描述 |
    | ——————— | ————— | ——————————————————————————— |
    | ACC_PUBLIC | 0x0001 | 声明为public |
    | ACC_FINAL | 0x0010 | 声明为final |
    | ACC_SUPER | 0x0020 | 废弃/仅JDK1.0.2前使用 |
    | ACC_INTERFACE | 0x0200 | 声明为接口 |
    | ACC_ABSTRACT | 0x0400 | 声明为abstract |
    | ACC_SYNTHETIC | 0x1000 | 声明为synthetic,表示该class文件并非由Java源代码所生成 |
    | ACC_ANNOTATION | 0x2000 | 标识注解类型 |
    | ACC_ENUM | 0x4000 | 标识枚举类型 |

  • this_class 当前类名称

    当前class文件的类名所在常量池中的索引位置

  • super_class 当前类的父类

    当前class文件的父类类名所在常量池中的索引位置。

    java/lang/Object类的super_class的为0,其他任何类的super_class都必须是一个常量池中存在的索引位置

  • interfaces_count 当前类继承或实现的接口数

    表示的是当前类继承或实现的接口数

  • interfaces[] 接口名称数组

    表示所有接口数组

  • fields_count 当前类的成员变量数

    当前class中的成员变量个数

  • fields[] 成员变量数组

    field_info 成员变量的数据结构:

    1
    2
    3
    4
    5
    6
    7
    field_info {
    u2 access_flags;
    u2 name_index;
    u2 descriptor_index;
    u2 attributes_count;
    attribute_info attributes[attributes_count];
    }
    • access_flags 表示的是成员变量的修饰符

      | Flag Name | Value | Interpretation |
      | ———————- | ——— | —————————————————————————————— |
      | ACC_PUBLIC | 0x0001 | Declared public; may be accessed from outside its package. |
      | ACC_PRIVATE | 0x0002 | Declared private; accessible only within the defining class and other classes belonging to the same nest (§5.4.4). |
      | ACC_PROTECTED | 0x0004 | Declared protected; may be accessed within subclasses. |
      | ACC_STATIC | 0x0008 | Declared static. |
      | ACC_FINAL | 0x0010 | Declared final; never directly assigned to after object construction (JLS §17.5). |
      | ACC_VOLATILE | 0x0040 | Declared volatile; cannot be cached. |
      | ACC_TRANSIENT | 0x0080 | Declared transient; not written or read by a persistent object manager. |
      | ACC_SYNTHETIC | 0x1000 | Declared synthetic; not present in the source code. |
      | ACC_ENUM | 0x4000 | Declared as an element of an enum. |

    • name_index 表示的是成员变量的名称

    • descriptor_index 表示的是成员变量的描述符

    • attributes_count 表示的是成员变量的属性数量

    • attribute_info attributes[attributes_count] 表示的成员变量的属性信息

  • methods_count 当前类的成员方法数

    表示的是当前class中的成员方法个数

  • methods 成员方法数组

    method_info 成员方法对象数据结构

    1
    2
    3
    4
    5
    6
    7
    method_info {
    u2 access_flags;
    u2 name_index;
    u2 descriptor_index;
    u2 attributes_count;
    attribute_info attributes[attributes_count];
    }
    • access_flags 成员方法的修饰符

      | Flag Name | Value | Interpretation |
      | ————————— | ——— | —————————————————————————————— |
      | ACC_PUBLIC | 0x0001 | Declared public; may be accessed from outside its package. |
      | ACC_PRIVATE | 0x0002 | Declared private; accessible only within the defining class and other classes belonging to the same nest (§5.4.4). |
      | ACC_PROTECTED | 0x0004 | Declared protected; may be accessed within subclasses. |
      | ACC_STATIC | 0x0008 | Declared static. |
      | ACC_FINAL | 0x0010 | Declared final; must not be overridden (§5.4.5). |
      | ACC_SYNCHRONIZED | 0x0020 | Declared synchronized; invocation is wrapped by a monitor use. |
      | ACC_BRIDGE | 0x0040 | A bridge method, generated by the compiler. |
      | ACC_VARARGS | 0x0080 | Declared with variable number of arguments. |
      | ACC_NATIVE | 0x0100 | Declared native; implemented in a language other than the Java programming language. |
      | ACC_ABSTRACT | 0x0400 | Declared abstract; no implementation is provided. |
      | ACC_STRICT | 0x0800 | Declared strictfp; floating-point mode is FP-strict. |
      | ACC_SYNTHETIC | 0x1000 | Declared synthetic; not present in the source code. |

    • name_index 成员方法的名称

    • descriptor_index 成员方法的描述符

    • attributes_count 成员方法的属性数量

    • attributes 成员方法的属性信息

  • attributes_count 当前类的属性数

    当前class文件属性表的成员个数

  • attributes[] 属性数组

    attribute_info 属性信息数据结构

    1
    2
    3
    4
    5
    attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
    }
    • attribute_name_index 属性名称索引

      读取attribute_name_index值所在常量池中的名称可以得到属性名称

  • 属性对象

    属性表是动态的,新的JDK版本可能会添加新的属性值。每一种属性的数据结构都不相同,所以读取到属性名称后还需要根据属性的类型解析不同属性表中的值。比如Code Attribute中存储了类方法的异常表、字节码指令集、属性信息等重要信息。

    img

Java Class 文件解析
  • 解析流程

    首先是魔术头和版本,调用 DataInputStream()正常获取即可

    然后是常量池的解析:

    1. 读取常量池数量(u2 constant_pool_count;);
    2. 读取tag
    3. 根据不同的tag类型解析常量池对象;
    4. 解析常量池中的对象;
    5. 链接常量池中的索引引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 解析常量池数据
*
* @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();
}

有很多对象的数据结构中又引用了其他对象,为了能够直观的看到常量池ID为1的对象信息我们就必须要将所有使用索引方式链接的映射关系改成直接字符串引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 链接常量池中的引用
*/
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;
}

访问标志解析

u2 access_flags

直接 readUnsignedShort()

当前类名称解析:

dis.readUnsignedShort()获取到类名所在的常量池中的索引位置,然后根据常量池ID读取常量池中的字符串内容即可解析出类名

当前类的父类名称解析:

当解析java.lang.Objectsuper_class的值为0,常量池中不包含索引为0的对象,所以需要直接将父类名称设置为java/lang/Object

接口解析:

和上面一样,先获取接口数量,然后遍历获取所有接口名称的索引值

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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);
}

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

两者数据结构一致,所以按照相同的处理逻辑。先解析出变量/方法的总数,然后遍历解析对象的数据结构中的所有信息

这里还有一个 attribute_info attribute[attributes_count] 需要解析

Java class 文件属性解析

属性信息表数据结构:

1
2
3
4
5
6
7
8
u2 attributes_count;
attribute_info attributes[attributes_count];

attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}

解析前面几个的流程类似,先解析属性表长度,然后遍历对attribute_info进行解析,前面 attribute_name_index 和 attribute_length 为固定长度值,直接 DataInstream 调用即可

接下来解析出属性名称后(在常量池中查找)按照 JVM虚拟机规范-属性https://docs.oracle.com/javase/specs/jvms/se15/html/jvms-4.html#jvms-4.7 来解析

对于 ConstantValue:

用于表示field_info中的静态变量的初始值

数据结构如下: 只需要读取解析最后一个字段即可

1
2
3
4
5
ConstantValue_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;
}

对于 Code:

Code属性用于表示成员方法的代码部分,Code中包含了指令集(byte数组),JVM调用成员方法时实际上就是执行的Code中的指令,而反编译工具则是把Code中的指令翻译成了Java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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属性时code_length表示的是Code的字节长度,max_stackmax_locals是一个固定值,表示的是最大操作数栈和最大局部变量数,这两个值是在编译类方法时自动计算出来的,如果通过ASM修改了类方法可能会需要重新计算max_stackmax_locals

解析Code的指令集时需要对照指令集映射表,然后根据不同的指令实现不一样的指令处理逻辑 https://docs.oracle.com/javase/specs/jvms/se15/html/jvms-6.html#jvms-6.5

可视化:

1
javap -verbose xxx.class
Java虚拟机指令集
  • Jvm

    首先要了解栈帧的概念

    栈帧是用来存储数据和部分过程结果的数据结构,同时也用来处理动态连接、方法返回值和异常分派。 栈帧随着方法调用而创建,随着方法结束而销毁——无论方法正常完成还是异常完成都算作方法结束。 栈帧的存储空间由创建它的线程分配在Java虚拟机栈之中,每一个栈帧都有自己的本地变量表(局部变量表)、操作数栈和指向当前方法所属的类的运行时常量池的引用。

    img

    • 局部变量表

      Local Variable Table 是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。单位规范以变量槽(Slot)为最小单位,一个槽应该可以存放一个32位以内的数据类型。

      虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从0~局部变量表最大容量。如果Slot是32位的,则遇到一个64位数据类型的变量(如long或double型),则会连续使用两个连续的Slot来存储

    • 操作数栈

      Operand Stack LIFO数据结构,其最大深度在编译时写入到方法的Code属性的max_stacks数据项中

      操作数栈的每一个元素可以是任意Java数据类型,32位的数据类型占一个栈容量,64位的数据类型占2个栈容量,且在方法执行的任意时刻,操作数栈的深度都不会超过max_stacks中设置的最大值

    • 动态链接

      在class文件中,方法调用需要将目标方法的符号引用转换为其在内存地址中的直接引用,符号引用存在于方法区中的运行时常量池

      Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态连接(Dynamic Linking)

    • 方法返回

      分为正常完成和异常完成两种

  • 类型/方法描述符

    | 描述符 | Java类型 | 示例 |
    | ———— | ————————— | ———————————— |
    | B | byte | B |
    | C | char | C |
    | D | double | D |
    | F | float | F |
    | I | int | I |
    | J | long | J |
    | S | short | S |
    | Z | boolean | Z |
    | [ | 数组 | [IJ |
    | L类名; | 引用类型对象** | Ljava/lang/Object;** |

    方法描述符实例:

    | 方法示例 | 描述符 | 描述 |
    | —————————————————- | ———————————————————— | ——————————————- |
    | static{...}static int id = 1; | 方法名:<clinit> | 静态语句块/静态变量初始化 |
    | public Test (){...} | 方法名:<init>,描述符()V | 构造方法 |
    | void hello(){...} | ()V | V表示void,无返回值 |
    | Object login(String str) {...} | (Ljava/lang/String;)Ljava/lang/Object; | 普通方法,返回Object类型 |
    | void login(String str) {...} | (Ljava/lang/String;)V | 普通方法,无返回值 |

  • Java 虚拟机指令

    详情:https://docs.oracle.com/javase/specs/jvms/se15/html/jvms-6.html#jvms-6.5

    速查表:

    | 十六进制 | 助记符 | 指令说明 |
    | ———— | ———————- | —————————————————————————————— |
    | 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下一条指令地址压入栈顶 |

TODO dup的作用?

看下栈帧的变化,dup在里面的效果

image-20230118111219258

可以看到 new 完之后一定会接一个 invokespecial 指令,其会调用实例初始化方法:()V,注意这个方法是一个实例方法,所以需要从操作数栈顶弹出一个this引用,也就是说这一步会弹出一个之前入栈的对象地址。那么第二个我就可以直接拿来用了,因为他已经是实例化好的对象。如果我们不用,那编译器也会生成dup指令,在初始化方法调用完成后再从栈顶pop出来,如果要用的话需要我们手动从局部变量表里取

Java 类字节码编辑 - ASM

img

ASM提供了三个基于ClassVisitor API的核心API,用于生成和转换类

  1. ClassReader类用于解析class文件或二进制流;
  2. ClassWriter类是ClassVisitor的子类,用于生成类二进制;
  3. ClassVisitor是一个抽象类,自定义ClassVisitor重写visitXXX方法,可获取捕获ASM类结构访问的所有事件;

ClassReader和ClassVistor

  • ClassReader

    用于解析类字节码,提供三种创建方式:类名、类字节码数组、类输入流对象

    image-20230115225203475

  • ClassVisitor

    两个重要字段:api 指出了 ASM api 版本;cv 是一个 ClassVisitor 类型的数据,它的作用是将多个 ClassVisitor 串连起来

    image-20230116173506490

    其通常作为访问者模式中的访问者来根据顺序执行访问类结构中的不同部分,重点关注visit()、visitField()、visitMethod()和visitEnd()

    有一个signature参数需要注意,它与泛型密切先关,例子如下:无泛型的情况下设置为null即可

    image-20230116174308989

    常用的 Visitor 回调事件 注意参数!

    | 方法名 | 说明 |
    | —————————- | —————————————————————————————— |
    | visit | 访问class的头部信息时,version为class版本(编译版本),access为访问修饰符,name为类名称,signature为class的签名,可能是null,superName为超类名称,interfaces为接口的名称 |
    | visitAnnotation | 访问class的注解信息时,descriptor为签名描述信息,visible为是否运行时可见 |
    | visitAttribute | 访问该类的属性 |
    | visitInnerClass | 访问class中内部类的信息,而且这个内部类不一定是被访问类的成员(有可能是一段方法中的匿名内部类或者声明在一个方法中的类等等)。name为内部类的名称,outerName为内部类所在类的名称,innerName为内部类的名称 |
    | visitOuterClass | 访问该类的外部类,仅当类具有封闭类时,才必须调用此方法。owner为拥有该类的class名称,name为包含该类的方法的名称,如果该类未包含在其封闭类的方法中,则返回null,descriptor为签名描述信息 |
    | visitEnd | 结束访问class时调用 |
    | visitField | 访问class中字段的信息,返回一个FieldVisitor用于操作字段相关的信息,access为访问修饰符,name为类名称,signature为class的签名,可能是null,descriptor为描述信息 |
    | visitMethod | 访问class中方法的信息,返回一个MethodVisitor用于操作字段相关的信息,access为访问修饰符,name为方法名称,signature为方法的签名,可能是null,descriptor为描述信息,exceptions为异常 |
    | visitModule | 访问对应的模块 |
    | visitTypeAnnotation | 访问类的签名的注解 |
    | visitNestHost | 访问类的nest host;(nest 指的一个共享私有成员变量的包名相同的class集合,nest中有一个host(主类)和多个members(成员类),jdk11为了提供更大,更广泛的嵌套类型,并且为了补足访问控制检测不足,引进了两个新的class文件属性,nest host 和nest member,nest host中包含了一个nest members列表,用来确定其他静态nest members;nest member中包含了一个nest host属性用来确定它的nesthost;) |
    | visitNestMember | 访问嵌套类的nest member,只有host class被visited时才能调用该方法 |

    image-20230117203859698

    参数说明:

    • visitField (access, name, descriptor, signature, value)
    • visitMethod(access, name, descriptor, signature, exceptions)

    • access参数:表示当前字段或方法带有的访问标识(access flag)信息,例如ACC_PUBLICACC_STATICACC_FINAL等。

    • name参数:表示当前字段或方法的名字。
    • descriptor参数:表示当前字段或方法的描述符。这些描述符,与我们平时使用的Java类型是有区别的。
    • signature参数:表示当前字段或方法是否带有泛型信息。换句话说,如果不带有泛型信息,提供一个null就可以了;如果带有泛型信息,就需要给它提供某一个具体的值。
    • value参数:是visitField()方法的第5个参数。这个参数的取值,与当前字段是否为常量有关系。如果当前字段是一个常量,就需要给value参数提供某一个具体的值;如果当前字段不是常量,那么使用null就可以了。
    • exceptions参数:是visitMethod()方法的第5个参数。这个参数的取值,与当前方法头(Method Header)中是否具有throws XxxException相关。
  • FieldVisitor类

    通过 ClassVisitor类的 visitField方法返回

    1
    2
    3
    4
    5
    6
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value);
    //示例
    {
    fieldVisitor = classWriter.visitField(ACC_PUBLIC | ACC_STATIC, "a", "I", null, null);
    fieldVisitor.visitEnd();
    }

    同样也支持一系列 visitxxx的回调

    这里的方法只需要关注一个visitEnd() 即可

  • ClassWriter 类

    构造函数中的参数

    image-20230116175440576

    使用该类生成一个 Class 文件,可以分成三个步骤:

    • 第一步,创建ClassWriter对象。
    • 第二步,调用ClassWriter对象的visitXxx()方法。
    • 第三步,调用ClassWriter对象的toByteArray()方法。

MethodVisitor和AdviceAdapter

MethodVisitorClassVisitor重写MethodVisitor类方法可获取捕获到对应的visit事件

顺序如下

1
2
3
4
5
6
7
( 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

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

使用AdviceAdapter可以直接调用newLocal(type)计算出本地变量存储的位置,为我们省去了许多不必要的麻烦

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

MethodVisitor 常用API

  • visitFieldInsn

    Visits a field instruction. A field instruction is an instruction that loads or stores the value of a field of an object

    支持 GETSTATIC, PUTSTATIC, GETFIELD or PUTFIELD操作

  • visitFrame

    Visits the current state of the local variables and operand stack elements

    参数就是局部变量表和操作数栈的内容

  • visitIincInsn

    处理 IINC指令:将指定int型变量增加指定值

  • visitVarInsn

    Visits a local variable instruction. A local variable instruction is an instruction that loads or stores the value of a local variable

    就是取局部变量变的值放入操作数栈

  • visitMethodInsn

    visits a method instruction. A method instruction is an instruction that invokes a method

    支持INVOKEVIRTUAL, INVOKESPECIAL, INVOKESTATIC or INVOKEINTERFACE。也就是调用某个方法

  • visitInsn

    Visits a zero operand instruction

    参数就是操作码op

  • visitTypeInsn

    Visits a type instruction. A type instruction is an instruction that takes the internal name of a class as parameter

    将一个类的全限定名作为参数然后new一个对象压入操作数栈中

  • visitCode/End

    访问的开始/结束

  • visitLdcInsn

    the constant to be loaded on the stack

    访问常量池索引

    <init>初始化函数的构造中,this变量也需要经过一个初始化的操作,其位于局部变量表索引为0的位置

    image-20230118105514531

    这里说明一下构造函数方法的创建,就以最基本的空构造函数为例,由于前面说了刚开始的this变量是处于未初始化的状态,因此需要进行初始化,那么用什么方法呢。这里实际就是用的父类的构造函数,对于一般的对象,就调用父类 Object 类的构造函数即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // <init>
    public A() {
    super();
    }

    // asm
    MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
    mv1.visitCode();
    mv1.visitVarInsn(ALOAD, 0);
    mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
    mv1.visitInsn(RETURN);
    mv1.visitMaxs(1, 1);
    mv1.visitEnd();

Label类

  • A position in the bytecode of a method.
  • Labels are used for jump, goto, and switch instructions, and for try catch blocks.
  • A label designates the instruction that is just after. Note however that there can be other elements between a label and the instruction it designates (such as other labels, stack map frames, line numbers, etc.).

Label类中存在一个属性 bytecodeOffset 它用于计算相对偏移量。由于指令是索引值位置可变,我们想要固定一个跳转位置的话则需要 Label 的参与,当asm转换成byte[] 时,将通过Label与待跳转位置相减来计算出相对偏移量。

image-20230118114237744

如果要添加一个跳转位置的话,调用 MethodVisitor.visitLabel(Label)

可以用于实现选择、循环、try-catch语句

生成if语句

1
2
3
4
5
6
if (value == 0) {
System.out.println("0");
}
else {
System.out.println("not 0");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "test", "(I)V", null, null);
methodVisitor.visitCode();
methodVisitor.visitVarInsn(ILOAD, 1);
Label label0 = new Label();
methodVisitor.visitJumpInsn(IFNE, label0);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("0");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label label1 = new Label();
methodVisitor.visitJumpInsn(GOTO, label1);

methodVisitor.visitLabel(label0);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("not 0");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
methodVisitor.visitLabel(label1);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(2, 2);
methodVisitor.visitEnd();
}

生成switch语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
switch (val) {
case 1:
System.out.println("1");
break;
case 2:
System.out.println("2");
break;
case 3:
System.out.println("3");
break;
case 9:
System.out.println("9");
break;
default:
System.out.println("unknown");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "test", "(I)V", null, null);
methodVisitor.visitCode();
methodVisitor.visitVarInsn(ILOAD, 1);
Label label0 = new Label();
Label label1 = new Label();
Label label2 = new Label();
Label label3 = new Label();
Label label4 = new Label();
methodVisitor.visitTableSwitchInsn(1, 9, label3, new Label[] { label0, label1, label2, label3, label3, label3, label3, label3, label4 });
methodVisitor.visitLabel(label0);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("1");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label label5 = new Label();
methodVisitor.visitJumpInsn(GOTO, label5);
methodVisitor.visitLabel(label1);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("2");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
methodVisitor.visitJumpInsn(GOTO, label5);
methodVisitor.visitLabel(label2);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("3");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
methodVisitor.visitJumpInsn(GOTO, label5);
methodVisitor.visitLabel(label4);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("9");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
methodVisitor.visitJumpInsn(GOTO, label5);
methodVisitor.visitLabel(label3);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("unknown");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
methodVisitor.visitLabel(label5);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(2, 2);
methodVisitor.visitEnd();
}

生成for语句

1
2
3
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "test", "()V", null, null);
methodVisitor.visitCode();
methodVisitor.visitInsn(ICONST_0);
methodVisitor.visitVarInsn(ISTORE, 1);

Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitVarInsn(ILOAD, 1);
methodVisitor.visitIntInsn(BIPUSH, 10);

Label label1 = new Label();
methodVisitor.visitJumpInsn(IF_ICMPGE, label1);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitVarInsn(ILOAD, 1);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
methodVisitor.visitIincInsn(1, 1);
methodVisitor.visitJumpInsn(GOTO, label0);
methodVisitor.visitLabel(label1);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(2, 2);
methodVisitor.visitEnd();
}

生成try catch语句

1
2
3
4
5
try {
System.out.println("try");
} catch (Exception e) {
System.out.println("catch");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "test", "(I)V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
Label label1 = new Label();
Label label2 = new Label();
methodVisitor.visitTryCatchBlock(label0, label1, label2, "java/lang/Exception");
methodVisitor.visitLabel(label0);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("try");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
methodVisitor.visitLabel(label1);
Label label3 = new Label();
methodVisitor.visitJumpInsn(GOTO, label3);
methodVisitor.visitLabel(label2);
methodVisitor.visitVarInsn(ASTORE, 2);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("catch");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
methodVisitor.visitLabel(label3);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(2, 3);
methodVisitor.visitEnd();
}

生成while语句

1
2
3
while (true){
System.out.println("23333");
}
1
2
3
4
5
6
7
8
9
10
11
12
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "test", "(I)V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("23333");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
methodVisitor.visitJumpInsn(GOTO, label0);
methodVisitor.visitMaxs(2, 2);
methodVisitor.visitEnd();
}

TODO 待解决:visitMaxs() 参数如何确定?

实际上并不需要注意计算,只需要当成一个占位符即可。通常情况下在创建ClassWriter 都会传入 flag COMPUTE_FRAMES 来自动帮我们计算 max_stack 和 max_local

demo:遍历一个类的基本信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public static void main(String[] args) throws Exception {
String className = "Test";

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

}

image-20230116001201930

demo:修改类名/方法名/方法修饰符

利用ClassWriter可以实现类修改功能,如果插入了新的局部变量、字节码,需要重新计算max_stackmax_localsASM为我们提供了内置的自动计算方式,只需在创建ClassWriter的时候传入COMPUTE_FRAMES即可:new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static void main(String[] args) throws Exception {
String className = "asm.HelloWorld";

// 定义修改后的类名
// final String newClassName = "JavaSecTestHelloWorld";

try {
final ClassReader cr = new ClassReader(className);

// 创建ClassWriter对象,COMPUTE_FRAMES会自动计算 max_stack 和 max_locals
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);

System.out.println("-----------------------------------------------------------------------------");
// 使用自定义的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, name, signature, superName, interfaces);
}

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
// 将 hello 方法名修改为 hi
if (name.equals("setTest")) {
// 修改方法访问修饰符,移除 public 属性,修改为 private
access = access & ~ACC_PUBLIC | ACC_PRIVATE;
}

return super.visitMethod(access, "hi", descriptor, signature, exceptions);
}
}, EXPAND_FRAMES);

String filePath = "D:\\ctf\\JNDI\\tool\\JNDIEXP\\src\\test\\java\\asm\\HelloWorld.class";
byte[] bytes = cw.toByteArray();
FileOutputStream outputStream = new FileOutputStream(filePath);
outputStream.write(bytes);
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}

}

demo3 修改类方法字节码:

HelloWorld#hello()方法为例,在原业务逻辑执行前打印出该方法的参数值;修改该方法的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public static void main(String[] args) throws Exception {
String className = "asm.HelloWorld";

// 定义修改后的类名
// final String newClassName = "JavaSecTestHelloWorld";

try {
final ClassReader cr = new ClassReader(className);

// 创建ClassWriter对象,COMPUTE_FRAMES会自动计算 max_stack 和 max_locals
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);

System.out.println("-----------------------------------------------------------------------------");
// 使用自定义的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, name, signature, superName, interfaces);
}

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if (name.equals("hello")) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);

// 创建自定义的MethodVisitor,修改原方法的字节码
return new AdviceAdapter(api, mv, access, name, descriptor) {

// 获取 String的ASM Type对象
private final Type stringType = Type.getType(String.class);

int newArgIndex;

@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");

// 创建一个新的局部变量,newLocal会计算出这个新局部对象的索引位置
newArgIndex = newLocal(stringType);
// 将int, float或String型常量值从常量池中推送至栈顶
mv.visitLdcInsn("fuck you!");
// Generates the instruction to store the top stack value in the given local variable
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");

// 压入方法进入 (onMethodEnter) 存入到局部变量的 var2值 到栈顶
loadLocal(newArgIndex);

// 返回一个引用类型,即栈顶的var2字符串,return var2;
// 需要特别注意的是不同数据类型应当使用不同的RETURN指令
mv.visitInsn(ARETURN);
}
};
}

return super.visitMethod(access, "hi", descriptor, signature, exceptions);
}
}, EXPAND_FRAMES);

String filePath = "D:\\ctf\\JNDI\\tool\\JNDIEXP\\src\\test\\java\\asm\\HelloWorld.class";
byte[] bytes = cw.toByteArray();
FileOutputStream outputStream = new FileOutputStream(filePath);
outputStream.write(bytes);
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}

demo4:动态创建Java类二进制

可以使用ClassWriter来动态创建出一个Java类的二进制文件,然后通过自定义的类加载器就可以将我们动态生成的类加载到JVM中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class ASM2 implements Opcodes {

private static final String CLASS_NAME = "asm.TestASMHelloWorld";

private static final String CLASS_NAME_ASM = "asm/TestASMHelloWorld";

public static byte[] dump() throws Exception {
// 创建 ClassWriter,用于生成类字节码
ClassWriter cw = new ClassWriter(0);

// 创建 MethodVisitor
MethodVisitor mv;

// 创建一个字节码版本为 JDK1.7 的目标类
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 创建的类字节码到 JVM
ClassLoader classLoader = new ClassLoader(ASM2.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("hello方法执行结果:" + classLoader.loadClass(CLASS_NAME).getMethod("hello").invoke(null));
}
}

AdviceAdapter 用法:

  • onMethodEnter()方法:在“方法进入”的时候,添加一些代码逻辑。
  • onMethodExit()方法:在“方法退出”的时候,添加一些代码逻辑。

注意事项:

  • 第一点,对于onMethodEnter()onMethodExit()这两个方法,都要注意Subclasses can use or change all the local variables, but should not change state of the stack。也就是说,要保持 operand stack 在修改前和修改后是一致的。
  • 第二点,对于onMethodExit()方法,要注意The top element on the stack contains the return value or the exception instance。也就是说,“方法退出”的时候,operand stack上有返回值或异常对象,不要忘记处理,不要弄丢了它们。

demo:打印方法参数和返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Analysis {
private String name;
private int age;

public Analysis(String name, int age) {
this.name = name;
this.age = age;
}

public void test(long idCard, Object obj) {
int hashCode = 0;
hashCode += name.hashCode();
hashCode += age;
hashCode += (int) (idCard % Integer.MAX_VALUE);
hashCode += obj.hashCode();
hashCode = Math.abs(hashCode);
System.out.println("Hash Code is " + hashCode);
if (hashCode % 2 == 1) {
throw new RuntimeException("illegal");
}
}
}

工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ParameterUtils {
private static final DateFormat fm = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

public static void printValueOnStack(Object value) {
if (value == null) {
System.out.println(" " + value);
} else if (value instanceof String) {
System.out.println(" " + value);
} else if (value instanceof Date) {
System.out.println(" " + fm.format(value));
} else if (value instanceof char[]) {
System.out.println(" " + Arrays.toString((char[]) value));
} else {
System.out.println(" " + value.getClass() + ": " + value.toString());
}
}

public static void printText(String text) {
System.out.println(text);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class ClassPrintParameterVisitor extends ClassVisitor {
public ClassPrintParameterVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if (mv != null) {
boolean isAbstractMethod = (access & Opcodes.ACC_ABSTRACT) != 0;
boolean isNativeMethod = (access & Opcodes.ACC_NATIVE) != 0;
if (!isAbstractMethod && !isNativeMethod) {

}
}

return mv;
}

public static class MethodPrintParameterAdapter extends AdviceAdapter {
public MethodPrintParameterAdapter(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
// AdviceAdapter 为 protected 类型的构造方法,因此只能子类访问
super(api, methodVisitor, access, name, descriptor);
}

@Override
protected void onMethodEnter() {
printMessage("Method Enter: " + getName() + methodDesc);

Type[] argumentTypes = getArgumentTypes();
for (int i = 0; i < argumentTypes.length; i++) {
Type t = argumentTypes[i];
loadArg(i);
box(t);
printValueOnStack("(Ljava/lang/Object;)V");
}
}

@Override
protected void onMethodExit(int opcode) {
printMessage("Method Exit: " + getName() + methodDesc);

if (opcode == ATHROW) {
super.visitLdcInsn("abnormal return");
} else if (opcode == RETURN) {
super.visitLdcInsn("return void");
} else if (opcode == ARETURN) {
dup();
} else {
if (opcode == LRETURN || opcode == DRETURN) {
dup2();
} else {
dup();
}
box(getReturnType());
}
printValueOnStack("(Ljava/lang/Object;)V");
}

private void printMessage(String str) {
super.visitLdcInsn(str);
super.visitMethodInsn(INVOKESTATIC, "asm/ParameterUtils", "printText", "(Ljava/lang/String;)V", false);
}

private void printValueOnStack(String descriptor) {
super.visitMethodInsn(INVOKESTATIC, "asm/ParameterUtils", "printValueOnStack", descriptor, false);
}
}
}

实现转换

参考链接

https://github.com/Y4tacker/JavaSec/blob/main/1.%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/ASM%E5%AD%A6%E4%B9%A0/index.md

https://javasec.org/javase/JavaByteCode/

https://lsieun.github.io/java-asm-01/advice-adapter-intro.html