虚拟机的实现方式

Java 官方的虚拟机 Hotspot 是基于栈的,而不是基于寄存器的。

  • 基于栈的优点是可移植性更好、指令更短、实现起来简单,但不能随机访问栈中的元素,完成相同功能所需要的指令数也比寄存器的要多,需要频繁的入栈和出栈。 绝大多数主流的JVM(Java虚拟机)实现都是基于栈的架构
  1. Oracle HotSpot JVM: HotSpot是Oracle官方的JVM实现,也是最广泛使用的JVM之一。它使用基于栈的执行引擎,通过Java栈和本地方法栈来管理方法的调用和局部变量。
  2. OpenJ9 JVM: OpenJ9是由IBM开发的JVM实现,它也采用基于栈的架构。OpenJ9注重性能和资源利用效率,适用于嵌入式和云环境。
  3. Apache Harmony JVM: Apache Harmony是一个开源的Java SE项目,虽然在2011年宣布终止,但一些社区仍在维护。它使用基于栈的执行引擎。
  • 基于寄存器的优点是速度快,有利于程序运行速度的优化,但操作数需要显式指定,指令也比较长。
  1. JRockit: JRockit是BEA公司(后被Oracle收购)开发的JVM实现,它在某些情况下使用寄存器架构来提高性能。
  2. Azul Systems的Zing JVM: Azul Systems的Zing JVM是一种用于大规模Java应用的商业JVM,采用了C4垃圾收集器和特定的执行引擎,其中可能包括一些寄存器架构的优化。

官方文档 jvm 规范文档 se8 class文件格式: Chapter 4. The class File Format 官方文档 jvm 规范文档 se8 指令集概要: 2.11. Instruction Set Summary

Class 结构

Java 类文件(.class 文件)是 Java 源代码编译后生成的二进制文件,它遵循特定的格式。每个类文件包含一个类或接口的定义。类文件的结构由一系列具有固定顺序的组件构成,这些组件包括:

  1. 魔数(Magic Number) 长度:4 字节 内容:固定的 0xCAFEBABE(十六进制),用于标识该文件是一个有效的 Java 类文件。
  2. 版本信息(Version Information) 长度:4 字节 分为两个部分: 次版本号(Minor Version):2 字节。 主版本号(Major Version):2 字节,表示 Java 版本。例如,Java 8 对应主版本号为 52(0x34),Java 11 对应 55(0x37)等。
  3. 常量池(Constant Pool) 长度:可变 常量池是类文件中的资源仓库,包含字符串常量、类和接口名、字段名、方法名等。常量池的入口是一个常量池计数(constant_pool_count),后面跟着 constant_pool_count-1 个常量项(因为常量池索引从1开始,0为保留索引)。 每个常量项的第一个字节是标签(tag),用于标识常量的类型(如:CONSTANT_Class, CONSTANT_Fieldref, CONSTANT_Methodref, CONSTANT_String 等)。
  4. 访问标志(Access Flags) 长度:2 字节 用于表示类或接口的访问权限和属性,例如:public、final、abstract 等。
  5. 当前类索引(This Class) 长度:2 字节 指向常量池中一个 CONSTANT_Class_info 结构的索引,表示当前类的全限定名。
  6. 父类索引(Super Class) 长度:2 字节 指向常量池中一个 CONSTANT_Class_info 结构的索引,表示父类的全限定名。对于 java.lang.Object,父类索引为0(因为它没有父类),但通常所有类都有父类,除了 Object 类。
  7. 接口索引集合(Interfaces) 长度:可变 包括接口计数(interfaces_count)和 interfaces_count 个接口索引(每个索引2字节),每个索引指向常量池中的 CONSTANT_Class_info。
  8. 字段表(Fields) 长度:可变 字段表包括字段计数(fields_count)和 fields_count 个字段信息。每个字段信息包括访问标志、名称索引、描述符索引和属性表。字段表用于描述类中声明的字段。
  9. 方法表(Methods) 长度:可变 方法表包括方法计数(methods_count)和 methods_count 个方法信息。每个方法信息包括访问标志、名称索引、描述符索引和属性表。方法表用于描述类中声明的方法。
  10. 属性表(Attributes) 长度:可变 属性表包括属性计数(attributes_count)和 attributes_count 个属性信息。属性可以出现在类、字段和方法中。常见的属性有:Code(方法的字节码)、LineNumberTable(行号表)、SourceFile(源文件名称)等。

Java 大部分字节码指令

2.6.1. Local Variables 2.6.2. Operand Stacks 2.6.3. Dynamic Linking 2.6.4. Normal Method Invocation Completion 2.6.5. Abrupt Method Invocation Completion Java字节码指令大全

指令 常量入栈

指令码操作码(助记符)操作数描述(栈指操作数栈)
0x01aconst_nullnull值入栈;
0x02iconst_m1(int)值 -1入栈;
0x03iconst_0(int)值 0入栈;
0x09lconst_0(long)值 0入栈;
0x0alconst_11(long)值入栈;
0x0bfconst_00(float)值入栈;
0x0cfconst_11(float)值入栈;
0x0dfconst_22(float)值入栈;
0x0edconst_00(double)值入栈;
0x0fdconst_11(double)值入栈;
0x10bipushvaluebytevaluebyte值带符号扩展成int值入栈;
0x11sipushvaluebyte1(valuebyte1 << 8)
valuebyte2valuebyte2 值带符号扩展成int值入栈;
0x12ldcindexbyte1常量池中的常量值(int, float, string reference, object reference)入栈;
0x13ldc_windexbyte1常量池中常量(int, float, string reference, object reference)入栈;
indexbyte2
0x14ldc2_windexbyte1常量池中常量(long, double)入栈;
indexbyte2

push 系列,主要包括 bipush 和 sipush,前者接收 8 位整数作为参数,后者接收 16 位整数。 Idc 指令,当 const 和 push 不能满足的时候,万能的 Idc 指令就上场了,它接收一个 8 位的参数,指向常量池中的索引。

指令 局部变量入栈

指令码操作码(助记符)操作数描述(栈指操作数栈)
0x19(wide)aloadindexbyte从局部变量indexbyte中装载引用类型值入栈;
0x2aaload_0从局部变量0中装载引用类型值入栈;
0x2baload_1从局部变量1中装载引用类型值入栈;
0x2caload_2从局部变量2中装载引用类型值入栈;
0x2daload_3从局部变量3中装载引用类型值入栈;
0x15(wide)iloadindexbyte从局部变量indexbyte中装载int类型值入栈;
0x1aiload_0从局部变量0中装载int类型值入栈;
0x16(wide)lloadindexbyte从局部变量indexbyte中装载long类型值入栈;
0x1elload_0从局部变量0中装载int类型值入栈;
0x17(wide)floadindexbyte从局部变量indexbyte中装载float类型值入栈;
0x22fload_0从局部变量0中装载float类型值入栈;
0x18(wide)dloadindexbyte从局部变量indexbyte中装载double类型值入栈;
0x26dload_0从局部变量0中装载double类型值入栈;
0x32aaload从引用类型数组中装载指定项的值;
0x2eiaload从int类型数组中装载指定项的值;
0x2flaload从long类型数组中装载指定项的值;
0x30faload从float类型数组中装载指定项的值;
0x31daload从double类型数组中装载指定项的值;
0x33baload从boolean类型数组或byte类型数组中装载指定项的值(先转换为int类型值, 后压栈);
0x34caload从char类型数组中装载指定项的值(先转换为int类型值, 后压栈);
0x35saload从short类型数组中装载指定项的值(先转换为int类型值, 后压栈);

{X}load_{N}(X 为 i(int)、l(long)、f(float)、d(double)、a(引用类型); N 默认为 0 到 3),表示将第 n 个局部变量压入操作数栈中。

大部分的指令都不支持 byte、short 和 char,甚至没有任何指令支持 boolean 类型。编译器会将 byte 和 short 类型的数据带符号扩展(Sign-Extend)为 int 类型,将 boolean 和 char 零位扩展(Zero-Extend)为 int 类型。

指令 将栈顶值保存到局部变量

指令码操作码(助记符)操作数描述(栈指操作数栈)
0x3a(wide)astoreindexbyte将栈顶引用类型值保存到局部变量indexbyte中;
0x4bastroe_0将栈顶引用类型值保存到局部变量0中;
0x4castore_1将栈顶引用类型值保存到局部变量1中;
0x4dastore_2将栈顶引用类型值保存到局部变量2中;
0x4eastore_3将栈顶引用类型值保存到局部变量3中;
0x36(wide)istoreindexbyte将栈顶int类型值保存到局部变量indexbyte中;
0x3bistore_0将栈顶int类型值保存到局部变量0中;
0x3cistore_1将栈顶int类型值保存到局部变量1中;
0x3distore_2将栈顶int类型值保存到局部变量2中;
0x3eistore_3将栈顶int类型值保存到局部变量3中;
0x37(wide)lstoreindexbyte将栈顶long类型值保存到局部变量indexbyte中;
0x3flstore_0将栈顶long类型值保存到局部变量0中;
0x40lstore_1将栈顶long类型值保存到局部变量1中;
0x41lstore_2将栈顶long类型值保存到局部变量2中;
0x42lstroe_3将栈顶long类型值保存到局部变量3中;
0x38(wide)fstoreindexbyte将栈顶float类型值保存到局部变量indexbyte中;
0x43fstore_0将栈顶float类型值保存到局部变量0中;
0x44fstore_1将栈顶float类型值保存到局部变量1中;
0x45fstore_2将栈顶float类型值保存到局部变量2中;
0x46fstore_3将栈顶float类型值保存到局部变量3中;
0x39(wide)dstoreindexbyte将栈顶double类型值保存到局部变量indexbyte中;
0x47dstore_0将栈顶double类型值保存到局部变量0中;
0x48dstore_1将栈顶double类型值保存到局部变量1中;
0x49dstore_2将栈顶double类型值保存到局部变量2中;
0x4adstore_3将栈顶double类型值保存到局部变量3中;
0x53aastore将栈顶引用类型值保存到指定引用类型数组的指定项;
0x4fiastore将栈顶int类型值保存到指定int类型数组的指定项;
0x50lastore将栈顶long类型值保存到指定long类型数组的指定项;
0x51fastore将栈顶float类型值保存到指定float类型数组的指定项;
0x52dastore将栈顶double类型值保存到指定double类型数组的指定项;
0x54bastroe将栈顶boolean类型值或byte类型值保存到指定boolean类型数组或byte类型数组的指定项;
0x55castore将栈顶char类型值保存到指定char类型数组的指定项;
0x56sastore将栈顶short类型值保存到指定short类型数组的指定项;

指令 通用(无类型)栈操作

指令码操作码(助记符)操作数描述(栈指操作数栈)
0x00nop空操作;
0x57pop从栈顶弹出一个字长的数据;
0x58pop2从栈顶弹出两个字长的数据;
0x59dup复制栈顶一个字长的数据, 将复制后的数据压栈;
0x5adup_x1复制栈顶一个字长的数据, 弹出栈顶两个字长数据, 先将复制后的数据压栈, 再将弹出的两个字长数据压栈;
0x5bdup_x2复制栈顶一个字长的数据, 弹出栈顶三个字长的数据, 将复制后的数据压栈, 再将弹出的三个字长的数据压栈;
0x5cdup2复制栈顶两个字长的数据, 将复制后的两个字长的数据压栈;
0x5ddup2_x1复制栈顶两个字长的数据, 弹出栈顶三个字长的数据, 将复制后的两个字长的数据压栈, 再将弹出的三个字长的数据压栈;
0x5edup2_x2复制栈顶两个字长的数据, 弹出栈顶四个字长的数据, 将复制后的两个字长的数据压栈, 再将弹出的四个字长的数据压栈;
0x5fswap交换栈顶两个字长的数据的位置; Java指令中没有提供以两个字长为单位的交换指令;

指令 控制流

条件跳转

指令码操作码(助记符)操作数描述(栈指操作数栈)
0x99ifeqbranchbyte1若栈顶int类型值为0则跳转;
branchbyte2
0x9aifnebranchbyte1若栈顶int类型值不为0则跳转;
branchbyte2
0x9bifltbranchbyte1若栈顶int类型值小于0则跳转;
branchbyte2
0x9eiflebranchbyte1若栈顶int类型值小于等于0则跳转;
branchbyte2
0x9difgtbranchbyte1若栈顶int类型值大于0则跳转;
branchbyte2
0x9cifgebranchbyte1若栈顶int类型值大于等于0则跳转;
branchbyte2
0x9fif_icmpeqbranchbyte1若栈顶两int类型值相等则跳转;
branchbyte2
0xa0if_icmpnebranchbyte1若栈顶两int类型值不相等则跳转;
branchbyte2
0xa1if_icmpltbranchbyte1若栈顶两int类型值前小于后则跳转;
branchbyte2
0xa4if_icmplebranchbyte1若栈顶两int类型值前小于等于后则跳转;
branchbyte2
0xa3if_icmpgtbranchbyte1若栈顶两int类型值前大于后则跳转;
branchbyte2
0xa2if_icmpgebranchbyte1若栈顶两int类型值前大于等于后则跳转;
branchbyte2
0xc6ifnullbranchbyte1若栈顶引用值为null则跳转;
branchbyte2
0xc7ifnonnullbranchbyte1若栈顶引用值不为null则跳转;
branchbyte2
0xa5if_acmpeqbranchbyte1若栈顶两引用类型值相等则跳转;
branchbyte2
0xa6if_acmpnebranchbyte1若栈顶两引用类型值不相等则跳转;
branchbyte2

栈顶比较

指令码操作码(助记符)操作数描述(栈指操作数栈)
0x94lcmp比较栈顶两long类型值, 前者大, 1入栈; 相等, 0入栈; 后者大, -1入栈;
0x95fcmpl比较栈顶两float类型值, 前者大, 1入栈; 相等, 0入栈; 后者大, -1入栈; 有NaN存在, -1入栈;
0x96fcmpg比较栈顶两float类型值, 前者大, 1入栈; 相等, 0入栈; 后者大, -1入栈; 有NaN存在, -1入栈;
0x97dcmpl比较栈顶两double类型值, 前者大, 1入栈; 相等, 0入栈; 后者大, -1入栈; 有NaN存在, -1入栈;
0x98dcmpg比较栈顶两double类型值, 前者大, 1入栈; 相等, 0入栈; 后者大, -1入栈; 有NaN存在, -1入栈;

无条件跳转

指令码操作码(助记符)操作数描述(栈指操作数栈)
0xa7gotobranchbyte1无条件跳转到指定位置;
branchbyte2
0xc8goto_wbranchbyte1无条件跳转到指定位置(宽索引);
branchbyte2
branchbyte3
branchbyte4

指令 对象操作

指令码操作码(助记符)操作数描述(栈指操作数栈)
0xbbnewindexbyte1创建新的对象实例;
indexbyte2
0xc0checkcastindexbyte1类型强转;
indexbyte
0xc1instanceofindexbyte1判断类型;
indexbyte2
0xb4getfieldindexbyte1获取对象字段的值;
indexbyte2
0xb5putfieldindexbyte1给对象字段赋值;
indexbyte2
0xb2getstaticindexbyte1获取静态字段的值;
indexbyte2
0xb3putstaticindexbyte1给静态字段赋值;
indexbyte2

指令 方法调用

指令码操作码(助记符)操作数描述(栈指操作数栈)
0xb7invokespecialindexbyte1编译时方法绑定调用方法;
indexbyte2
0xb6invokevirtualindexbyte1运行时方法绑定调用方法;
indexbyte2
0xb8invokestaticindexbyte1调用静态方法;
indexbyte2
0xb9invokeinterfaceindexbyte1调用接口方法;
indexbyte2
count
0

指令 方法返回

指令码操作码(助记符)操作数描述(栈指操作数栈)
0xacireturn返回int类型值;
0xadlreturn返回long类型值;
0xaefreturn返回float类型值;
0xafdreturn返回double类型值;
0xb0areturn返回引用类型值;
0xb1returnvoid函数返回;

骚操作 - 无源码如何修改class?

javassist 修改class 文件

javassist 官网 javassist API

Javassist是一个开源的分析, 编辑和创建Java字节码的类库; 是由东京技术学院的数学和计算机科学系的 Shigeru Chiba 所创建的; 它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架;

  1. 构建 ClassPool 对象, 它表示一个class

ClassPool pool = ClassPool.getDefault();

  1. 创建一个 class 返回 CtClass CtClass ctClass = pool.makeClass("top.ss007.GenerateClass");//创建

从已有的 class 文件 构建 CtClass //必须将class文件放在这个工程编译后的class文件中, 路径也对应起来, 或者使用 insertClassPath 指定

pool.insertClassPath("F:\\test\\饲料_jhw\\classes\\");
CtClass ctClass = pool.get("com.rickiyang.learn.javassist.PersonService");
  1. //写入到文件 ctClass.writeFile("D:\\test");

ClassPool需要关注的方法: getDefault: 返回默认的ClassPool 是单例模式的, 一般通过该方法创建我们的ClassPool; appendClassPath, insertClassPath: 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置; 通常通过该方法写入额外的类搜索路径, 以解决多个类加载器环境中找不到类的尴尬; toClass: 将修改后的CtClass加载至当前线程的上下文类加载器中, CtClass的toClass方法是通过调用本方法实现; 需要注意的是一旦调用该方法, 则无法继续修改已经被加载的class; get , getCtClass: 根据类路径名获取该类的CtClass对象, 用于后续的编辑;

CtClass 需要关注的方法: freeze: 冻结一个类, 使其不可修改; isFrozen: 判断一个类是否已被冻结; prune: 删除类不必要的属性, 以减少内存占用; 调用该方法后, 许多方法无法将无法正常使用, 慎用; defrost: 解冻一个类, 使其可以被修改; 如果事先知道一个类会被defrost, 则禁止调用 prune 方法; detach: 将该class从ClassPool中删除; writeFile: 根据CtClass生成 .class 文件; toClass: 通过类加载器加载该CtClass; 上面我们创建一个新的方法使用了CtMethod类; CtMthod代表类中的某个方法, 可以通过CtClass提供的API获取或者CtNewMethod新建, 通过CtMethod对象可以实现对方法的修改;

CtMethod中的一些重要方法: insertBefore: 在方法的起始位置插入代码; insterAfter: 在方法的所有 return 语句前插入代码以确保语句能够被执行, 除非遇到exception; insertAt: 在指定的位置插入代码; setBody: 将方法的内容设置为要写入的代码, 当方法被 abstract修饰时, 该修饰符被移除; make: 创建一个新的方法;

另外需要注意的是: 上面的insertBefore()setBody()中的语句, 如果你是单行语句可以直接用双引号, 但是有多行语句的情况下, 你需要将多行语句用{}括起来; javassist 只接受单个语句或用大括号括起来的语句块; javassist使用全解析

上下文环境参数

Javassist 使用指南(二)

传递给方法 insertBefore() , insertAfter() , addCatch()insertAt() 的 String 对象是由Javassist 的编译器编译的;
由于编译器支持语言扩展, 以 $ 开头的几个标识符有特殊的含义:

符号	含义
$0, $1, $2, ...	this and 方法的参数
$args	方法参数数组.它的类型为 Object[]
$$	所有实参; 例如, m($$) 等价于 m($1,$2,...)
$cflow(...)	cflow 变量
$r	返回结果的类型, 用于强制类型转换
$w	包装器类型, 用于强制类型转换
$_	返回值
$sig	类型为 java.lang.Class 的参数类型数组
$type	一个 java.lang.Class 对象, 表示返回值类型
$class	一个 java.lang.Class 对象, 表示当前正在修改的类

这种方法有限制:

  1. 需要环境上下文
  2. 只能在特定位置插入代码

javassist 和 CGLIB

  1. javassit 直接使用java编码的形式, 而不需要了解虚拟机指令, 就能动态改变类的结构, 或者动态生成类; 它是直接修改class 本质是静态

  2. cglib 动态代理类的模式是:

  3. 查找目标类上的所有非final 的public类型的方法定义

  4. 将这些方法的定义转换成字节码;

  5. 将组成的字节码转换成相应的代理的class对象

  6. 实现 MethodInterceptor接口, 用来处理 对代理类上所有方法的请求(这个接口和JDK动态代理InvocationHandler的功能和角色是一样的)

Recaf

Recaf Git项目地址

真正牛逼的字节码指令修改器, 直接改指令, (2.0 是JDK9+的..)

简单分析一则

//源码
public class Test {
 
	public void Test001() {
		String string = new String("acc");
		System.out.println(string);
	}
}
 
// Test001 被编译后分析的指令
0: LABEL A
1: LINE 18:LABEL A
2: NEW String//创建新的String对象实例
3: DUP//复制栈顶一个字长的数据, 将复制后的数据压栈; 见下 [NEW 指令之后为什么需要 DUP?]
4: LDC "acc"//常量acc入栈
5: INVOKESPECIAL String.<init>(String)void//编译时方法绑定调用, 调用String(String)的构造函数
6: ASTORE 1 [string:String]//将栈顶引用类型值保存到局部 变量1 中; 
7: LABEL B
8: LINE 19:LABEL B
9: GETSTATIC System.out PrintStream//获取 System.out的PrintStream 值
10: ALOAD 1 [string:String]//装载局部 变量1 中的引用类型值入栈
11: INVOKEVIRTUAL PrintStream.println(String)void//运行时方法绑定调用, 调用println方法
12: LABEL C
13: LINE 20:LABEL C
14: RETURN //void函数返回
15: LABEL D

关于 LABEL *LINE 18:LABEL* 应该是recaf 自动生成的 操作分隔符, 便于分析?

在11行 11: INVOKEVIRTUAL PrintStream.println(String)void 右键 show stack/locals 可以看到栈局部变量表

indexLocal source opcode
0PARAM 0 (this:Test)
12: NEW String
6:ASTORE 1 [string:String]

栈索引0 是 this 作用域 栈索引1 一个string:String 变量, 在2行创建 在6行入栈

NEW 指令之后为什么需要 DUP指令?

因为new指令之后, 紧跟着就会调用指令 invokespecial 进行初始化(会消耗掉操作数栈顶的引用作为传给构造器的”this”参数)

invokespecial的指令格式; 看一下操作数栈, 需要一个objectref引用(对象的地址), 后面是可选的参数; 由于初始化没有返回值

in short (invokespecial)构造函数没有返回值; 那么怎么获取构造完毕的对象呢?只能在调用构造函数前把对象句柄复制一份

so.. 简单理解为:

  1. NEW String //创建对象 ‘this’ 引用入栈
  2. DUP //再复制一份 ‘this’ 入栈
  3. LDC "acc"
  4. INVOKESPECIAL String.<init>(String)void // 调用构造 操作数栈顶 ‘DUP’的那份
  5. ASTORE 1 [string:String] //保存, 消耗掉NEW的那份 栈顶

保存修改/导出

File -> Export

jclasslib 查看

jclasslib bytecode viewer is a tool that visualizes all aspects of compiled Java class files and the contained bytecode. In addition, it contains a library that enables developers to read and write Java class files and bytecode.

只能看不能编辑指令; 但可以编辑常量池信息;

jclasslib Git项目地址