zoukankan      html  css  js  c++  java
  • 字节码分析与操作

    首先强烈安利美团技术团队官方博客

     

    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

    一个没有高级趣味的人。 email:hushui502@gmail.com
  • 相关阅读:
    第一篇:理论篇
    day 3:注释,缩进
    HTML的报告
    SAP DIALOG屏幕新增搜索帮助
    财务凭证科目替代(未写完)
    表维护生成器本地转请求包
    web安全之SQL注入
    Ubuntu16.04 下安装Sublime Text 3
    ubuntu16.04 下安装配置python3.6
    Ubuntu 16.04 下安装 PyCharm
  • 原文地址:https://www.cnblogs.com/CherryTab/p/12210060.html
Copyright © 2011-2022 走看看