因为前面学习各种知识,发现很难理解,于是先跳到工具篇.
javac java文件编译为class文件
javac xxx.java
会生成xxx.class文件,在使用IDE时,这步骤会被IDE来执行.
public class Foo {
private int tryBlock;
private int catchBlock;
private int finallyBlock;
private int methodExit;
public void test() {
try {
tryBlock = 0;
} catch (Exception e) {
catchBlock = 1;
} finally {
finallyBlock = 2;
}
methodExit = 3;
}
}
javap:查阅 Java 字节码
javap xxx.class
其中.class可以省略
默认会打印非私有的方法和字段.
javap -p
默认情况下 javap 会打印所有非私有的字段和方法,当加了 -p 选项后,它还将打印私有的字段和方法。
javap -c
javap -v
基本信息
class 文件的版本号 minor version
class 文件的版本号(minor version: 0,major version: 52)
class 文件的版本号指的是编译生成该 class 文件时所用的 JRE 版本。由较新的 JRE 版本中的 javac 编译而成的 class 文件,不能在旧版本的 JRE 上跑,否则,会出现如下异常信息。(Java 8 对应的版本号为 52,Java 10 对应的版本号为 54。)
这里的版本号对应的是javac编译jdk的版本,上面的2个图,
major version
flags 访问权限
该类的访问权限(flags: (0x0021) ACC_PUBLIC, ACC_SUPER)
类的访问权限通常为 ACC_ 开头的常量。
this_class
该类(this_class: #8)的名字
super_class
父类(super_class: #2)的名字
interfaces
所实现接口(interfaces: 0)的数目.
fields
字段(fields: 4)的数目.
methods
方法(methods: 2)的数目.
attributes
属性(attributes: 1)的数目。
常量池 Constant pool
常量池中的每一项都有一个对应的索引,#1、#2这种.
常量池之间也会有引用
如果将它看成一个树结构的话,那么它的叶节点会是字符串常量
字段区域与方法区域
字段/方法的类型 descriptor
I
方法的数栈stack
代码区域一开始会声明该方法中的操作数栈(stack=xx)和局部变量数目(locals=xx)的最大值,以及该方法接收参数的个数(args_size=xx)。 这里局部变量指的是字节码中的局部变量,而非 Java 程序中的局部变量。
方法的字节码
每条字节码均标注了对应的偏移量(bytecode index,BCI),这是用来定位字节码的。比如说偏移量为 10 的跳转字节码 10: goto 35,将跳转至偏移量为 35 的字节码 35: aload_0。
异常表 Exception Table
也会使用偏移量来定位每个异常处理器所监控的范围(由 from 到 to 的代码区域),以及异常处理器的起始位置(target)。除此之外,它还会声明所捕获的异常类型(type)。其中,any 指代任意异常类型。
行数表LineNumberTable:
是 Java 源程序到字节码偏移量的映射。
如果你在编译时使用了 -g 参数(javac -g Foo.java),那么这里还将出现局部变量表(LocalVariableTable:),展示 Java 程序中每个局部变量的名字、类型以及作用域。
行数表和局部变量表均属于调试信息。Java 虚拟机并不要求 class 文件必备这些信息。
字节码操作数栈映射表 StackMapTable
amstools
OpenJDK的Code Tools项目
使用时,先把.class文件编译为jasm文件,然后再对jasm文件进行操作
$ java -cp /path/to/asmtools.jar org.openjdk.asmtools.jdis.Main Foo.class > Foo.jasm
$ java -cp /path/to/asmtools.jar org.openjdk.asmtools.jasm.Main Foo.jasm
JOL
JOL 可用于查阅 Java 虚拟机中对象的内存分布
$ java -jar /path/to/jol-cli-0.9-full.jar internals java.util.HashMap
$ java -jar /path/to/jol-cli-0.9-full.jar estimates java.util.HashMap
ASM
ASM下载地址
repository.ow2.org/nexus/conte…
ASM是一个字节码分析及修改框架。它被广泛应用于许多项目之中,例如 Groovy、Kotlin 的编译器,代码覆盖测试工具 Cobertura、JaCoCo,以及各式各样通过字节码注入实现的程序行为监控工具。甚至是 Java 8 中 Lambda 表达式的适配器类,也是借助 ASM 来动态生成的。
ASM 既可以生成新的 class 文件,也可以修改已有的 class 文件。前者相对比较简单一些。
ASM 甚至还提供了一个辅助类 ASMifier,它将接收一个 class 文件并且输出一段生成该 class 文件原始字节数组的代码。如果你想快速上手 ASM 的话,那么你可以借助 ASMifier 生成的代码来探索各个 API 的用法。
使用方法
echo '
public class Foo1 {
public static void main(String[] args) {
boolean flag = true;
if (flag) System.out.println("Hello, Java!");
if (flag == true) System.out.println("Hello, JVM!");
}
}' > Foo1.java
javac Foo.java
这里的javac,是Java8版本的,高版本的好像有问题
java -cp /PATH/TO/asm-all-6.0_BETA.jar org.objectweb.asm.util.ASMifier Foo.class | tee FooDump.java
会生成如下代码
import java.util.*;
import org.objectweb.asm.*;
public class Foo1Dump implements Opcodes {
public static byte[] dump () throws Exception {
ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;
cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "Foo1", null, "java/lang/Object", null);
{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
mv.visitCode();
mv.visitInsn(ICONST_1);
mv.visitVarInsn(ISTORE, 1);
mv.visitVarInsn(ILOAD, 1);
Label l0 = new Label();
mv.visitJumpInsn(IFEQ, l0);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello, Java!");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitLabel(l0);
mv.visitFrame(Opcodes.F_APPEND,1, new Object[] {Opcodes.INTEGER}, 0, null);
mv.visitVarInsn(ILOAD, 1);
mv.visitInsn(ICONST_1);
Label l1 = new Label();
mv.visitJumpInsn(IF_ICMPNE, l1);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello, JVM!");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitLabel(l1);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
}
可以看到,ASMifier 生成的代码中包含一个名为 FooDump 的类,其中定义了一个名为 dump 的方法。该方法将返回一个 byte 数组,其值为生成类的原始字节。
ClassReader与ClassWriter
ClassReader 将读取“Foo”类的原始字节,并且翻译成对应的访问请求。也就是说,在上面 ASMifier 生成的代码中的各个访问操作,现在都交给 ClassReader.accept 这一方法来发出了。那么,如何修改这个 class 文件的字节码呢?原理很简单,就是将 ClassReader 的访问请求发给另外一个访问者,再由这个访问者委派给 ClassWriter。 这样一来,新增操作可以通过在某一需要转发的请求后面附带新的请求来实现;删除操作可以通过不转发请求来实现;修改操作可以通过忽略原请求,新建并发出另外的请求来实现。
参考文章 :gk.link/a/11V4M