字节码分析与操作

Posted cherrytab

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了字节码分析与操作相关的知识,希望对你有一定的参考价值。

1.1什么是字节码

https://zh.wikipedia.org/wiki/Java%E5%AD%97%E8%8A%82%E7%A0%81

Java所宣称的一次编译处处运行就是靠的字节码技术,java文件编译后会生成字节码文件.class,供jvm使用。字节码文件是由十六进制值组成,两个十六进制为一组,以一个字节为单位进行读取。

技术图片

 

编译 javac *.java

反编译javap -c -verbose *.class

 

1.2.字节码结构

public class ByteCodeDemo {
    private int a = 1;

    public int add() {
        int b = 2;
        int c = a + b;
        System.out.println(c);
        return c;
    }

    public static void main(String[] args) {
        System.out.println("sss");
    }
}

编译后生成的.class文件,这里我们用notepad++ 和 HEX-Editor插件查看这个十六进制文件

技术图片

 

 

分析文件

技术图片

 

 

(1) 魔数(Magic Number)

所有的.class文件的前四个字节都是魔数,魔数的固定值为:0xCAFEBABE。魔数放在文件开头,JVM可以根据文件的开头来判断这个文件是否可能是一个.class文件,避免不必要的操作。

  cafeebabe是java之父James Gosling制定的,Java的图标为一杯咖啡,应该是有关系的。

 

(2) 版本号

版本号为魔数之后的4个字节,前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version)。上图中版本号为“00 00 00 34”,次版本号转化为十进制为0,主版本号转化为十进制为52,在Oracle官网中查询序号52对应的主版本号为1.8,所以编译该文件的Java版本号为1.8.0。

 

(3) 常量池(Constant Pool)

常量池中存储两类常量:字面量与符号引用。字面量为代码中声明为final的常量值,符号引用如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。常量池整体上分为两部分:常量池计数器以及常量池数据区,如下图所示。

技术图片

 

常量池计数器(constant_pool_count):由于常量的数量不固定,所以需要先放置两个字节来表示常量池容量计数值。示例代码的字节码前10个字节如下图所示,将十六进制的2d转化为十进制值为46,排除掉下标“0”,也就是说,这个类文件中共有46个常量。

技术图片

 

(4) 访问标志

常量池结束之后的两个字节,描述该class为类还是接口,以及是否被public,abstract,final等修饰过。JVM并没有穷举所有的访问标志,而是使用按位或操作来进行描述的,比如某个类的修饰符为Public Final,则对应的访问修饰符的值为ACC_PUBLIC | ACC_FINAL,即0x0001 | 0x0010=0x0011。

技术图片

 

 

 

(5) 当前类名

访问标志后的两个字节,描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。

 

(6) 父类名称

当前类名后的两个字节,描述父类的全限定名,同上,保存的也是常量池中的索引值。

 

(7) 接口信息

父类名称后为两字节的接口计数器,描述了该类或父类实现的接口数量。紧接着的n个字节是所有接口名称的字符串常量的索引值。

 

(8) 字段表

字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量,但是不包含方法内部声明的局部变量。字段表也分为两部分,第一部分为两个字节,描述字段个数;第二部分是每个字段的详细信息fields_info。字段表结构如下图所示:

 技术图片

 

 

 

(9)方法表

字段表结束后为方法表,方法表分为两部分,第一部分为用两字节描述方法的个数,第二部分为每个方法的详细信息。方法的详细信息较为复杂,包括方法的访问标志、方法名、方法的描述符以及方法的属性,如下图所示:

技术图片

技术图片

 

 

 

(10)附加属性表

字节码的最后一部分,该项存放了在该文件中类或接口所定义属性的基本信息。

 

1.3查看字节码的工具

classlib,可以在idea内install这个插件

代码编译后在菜单栏”View”中选择”Show Bytecode With jclasslib”,可以很直观地看到当前字节码文件的类信息、常量池、方法区等信息

技术图片

 

 

 

 

 

2字节码操作增强

技术图片

 

 

 

 2.1 ASM

https://www.ibm.com/developerworks/cn/java/j-lo-asm30/index.html

使用ASM可以直接生产.class文件,在类被加载进jvm之前动态修改。ASM的应用场景有AOP(Cglib就是基于ASM)、热部署、修改其他jar包中的类等。主要是利用了访问者设计模式。

技术图片

 

 

2.1.1.1 ASM 核心API

ASM Core API可以类比解析XML文件中的SAX方式,不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。然而出于性能考虑,一般情况下编程都使用Core API。在Core API中有以下几个关键类:

ClassReader:用于读取已经编译好的.class文件。

ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。

 

2.1.1.2树形API

ASM Tree API可以类比解析XML文件中的DOM方式,把整个类的结构读取到内存中,缺点是消耗内存多,但是编程比较简单。TreeApi不同于CoreAPI,TreeAPI通过各种Node类来映射字节码的各个区域,类比DOM节点,就可以很好地理解这种编程方式。

 

2.1.2 直接利用ASM实现AOP

package asm;

public class Base {
    public void process() {
        System.out.println("process");
    }
}

我们的目的是在process之前和之后都进行操作。

 

为了利用ASM实现AOP,需要定义两个类:一个是MyClassVisitor类,用于对字节码的visit以及修改;另一个是Generator类,在这个类中定义ClassReader和ClassWriter,其中的逻辑是,classReader读取字节码,然后交给MyClassVisitor类处理,处理完成后由ClassWriter写字节码并将旧的字节码替换掉。Generator类较简单,我们先看一下它的实现,如下所示,然后重点解释MyClassVisitor类。

package asm;

import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.ClassWriter;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public class Generator {
    public static void main(String[] args) throws IOException {
        //读取
        ClassReader classReader = new ClassReader("asm/Base");
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        //处理
        ClassVisitor classVisitor = new MyClassVisitor(classWriter);
        classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
        byte[] data = classWriter.toByteArray();
        //输出
        File f = new File("D:\\program\\java project\\guava\\target\\classes\\asm\\Base.class");
        FileOutputStream fout = new FileOutputStream(f);
        fout.write(data);
        fout.close();
        System.out.println("now generator cc success!!!!!");

        new Base().process();
    }
}

 

MyClassVisitor继承自ClassVisitor,用于对字节码的观察。它还包含一个内部类MyMethodVisitor,继承自MethodVisitor用于对类内方法的观察,它的整体代码如下:

package asm;

import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.MethodVisitor;
import jdk.internal.org.objectweb.asm.Opcodes;

public class MyClassVisitor extends ClassVisitor implements Opcodes {
    public MyClassVisitor(ClassVisitor visitor) {
        super(ASM5, visitor);
    }

    @Override
    public void visit(int version, int access, String name, String signature,
                      String superName, String[] interfaces) {
        cv.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
                exceptions);
        //Base类中有两个方法:无参构造以及process方法,这里不增强构造方法
        if (!name.equals("<init>") && mv != null) {
            mv = new MyMethodVisitor(mv);
        }
        return mv;
    }
}

class MyMethodVisitor extends MethodVisitor implements Opcodes {
    public MyMethodVisitor(MethodVisitor mv) {
        super(Opcodes.ASM5, mv);
    }

    @Override
    public void visitCode() {
        super.visitCode();
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("start");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    }

    @Override
    public void visitInsn(int opcode) {
        if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
                || opcode == Opcodes.ATHROW) {
            //方法在返回之前,打印"end"
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("end");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
        mv.visitInsn(opcode);
    }
}

运行generator之前的Base.class

package asm;

public class Base {
    public Base() {
    }

    public void process() {
        System.out.println("process");
    }
}

运行之后

package asm;

public class Base {
    public Base() {
    }

    public void process() {
        System.out.println("start");
        System.out.println("process");
        System.out.println("end");
    }
}

 

分析:

首先通过MyClassVisitor类中的visitMethod方法,判断当前字节码读到哪一个方法了。跳过构造方法 <init> 后,将需要被增强的方法交给内部类MyMethodVisitor来进行处理。

 

接下来,进入内部类MyMethodVisitor中的visitCode方法,它会在ASM开始访问某一个方法的Code区时被调用,重写visitCode方法,将AOP中的前置逻辑就放在这里。

 

MyMethodVisitor继续读取字节码指令,每当ASM访问到无参数指令时,都会调用MyMethodVisitor中的visitInsn方法。我们判断了当前指令是否为无参数的“return”指令,如果是就在它的前面添加一些指令,也就是将AOP的后置逻辑放在该方法中。

 

综上,重写MyMethodVisitor中的两个方法,就可以实现AOP了,而重写方法时就需要用ASM的写法,手动写入或者修改字节码。通过调用methodVisitor的visitXXXXInsn()方法就可以实现字节码的插入,XXXX对应相应的操作码助记符类型,比如mv.visitLdcInsn(“end”)对应的操作码就是ldc “end”,即将字符串“end”压入栈。

 

2.1.3 ASM工具

idea install 插件ASM Bytecide Outline

使用方法是对需要操作的java文件右键show bytecode outline,然后在弹出的标签页中选ASMified

技术图片

 

直接复制ok

 

2.2Javassist

强调源代码层次操作字节码的框架Javassist。

利用Javassist实现字节码增强时,可以无须关注字节码刻板的结构,其优点就在于编程简单。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构或者动态生成类。其中最重要的是ClassPool、CtClass、CtMethod、CtField这四个类:

CtClass(compile-time class):编译时类信息,它是一个class文件在代码中的抽象表现形式,可以通过一个类的全限定名来获取一个CtClass对象,用来表示这个类文件。

ClassPool:从开发视角来看,ClassPool是一张保存CtClass信息的HashTable,key为类名,value为类名对应的CtClass对象。当我们需要对某个类进行修改时,就是通过pool.getCtClass(“className”)方法从pool中获取到相应的CtClass。

CtMethod、CtField:这两个比较好理解,对应的是类中的方法和属性。

 

示例

package asm;

import javassist.*;

import java.io.IOException;

public class JavassistTest {
    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, IllegalAccessException, InstantiationException {
        ClassPool cp = ClassPool.getDefault();
        CtClass cc = cp.get("asm.Base");
        CtMethod m = cc.getDeclaredMethod("process");
        m.insertBefore("{ System.out.println("start"); }");
        m.insertAfter("{ System.out.println("end"); }");
        Class c = cc.toClass();
        cc.writeFile("D:\\program\\java project\\guava\\target\\classes");
        Base base = (Base) c.newInstance();
        base.process();
    }
}

 

改造后的class文件

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package asm;

public class Base {
    public Base() {
    }

    public void process() {
        System.out.println("start");
        System.out.println("start");
        System.out.println("process");
        Object var2 = null;
        System.out.println("end");
        Object var4 = null;
        System.out.println("end");
    }

    public void test() {
        System.out.println("test");
    }
}

 

3.4使用场景

热部署:不部署服务而对线上服务做修改,可以做打点、增加日志等操作。

 

Mock:测试时候对某些服务做Mock。

 

性能诊断工具:比如bTrace就是利用Instrument,实现无侵入地跟踪一个正在运行的JVM,监控到类和方法级别的状态信息。

 

参考引用

https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html

https://blog.csdn.net/u011810352/article/details/80316870

 

 end

以上是关于字节码分析与操作的主要内容,如果未能解决你的问题,请参考以下文章

GroovyMOP 元对象协议与元编程 ( Groovy 类内部和外部分别获取 metaClass | 分析获取 metaClass 操作的字节码 | HandleMetaClass 注入方法 )

Day350.字节码指令集与解析举例 -JVM

java内存分析

JVM技术专题 字节码指令集调用执行流程分析「语法分析篇」

JVM原理探索字节码指令集调用执行流程分析(语法分析篇)

Java 虚拟机原理Class 字节码二进制文件分析 一 ( 字节码文件附加信息 | 魔数 | 次版本号 | 主版本号 | 常量池个数 )