Java字節碼介紹及動態修改類

前言

對於Java字節碼,它是在Java類的編譯過程產生的,即由.java源文件到.class二進制字節碼文件的過程。而Java類的加載又是經過類的名字獲取二進制字節流,而後在內存中將字節流生成類對象。因此動態修改類的時機在於修改.class文件,只要經過修改.class文件的字節碼,便可達到修改類的目的。修改字節碼能夠經過ASM這個開源框架實現,ASM是一個Java字節碼引擎的庫,具體能夠查看官網,它能夠經過操做字節碼實現修改類或者生成類。html

介紹

Java字節碼的執行操做主要是在虛擬機的棧執行,這個棧主要有局部變量表,操做數棧等幾個部分。java

(一)局部變量表

主要用來保存方法中的局部變量,基本的存儲單位爲slot(32位的存儲空間),因此long double的數據類型須要兩個slot, 當方法被調用時,參數會傳遞從0開始的局部變量表的索引位置上,因此局部變量最大的大小是在編譯期就決定的,特別須要注意的是若是調用的是實例方法,局部變量第0個位置是實例對象的引用。 ###(二)操做數棧 主要用來看成字節碼指令操做的出棧入棧的容器,例如變量的出棧入棧都是在操做數棧裏面進行的。 ###(三)指令 指令主要是由操做碼+操做數組成的,指令包括加載和存儲指令,運算指令和類型轉換指令,方法調用指令等等。指令所須要的操做,調用方法,賦值等,都是在操做數棧進行的。git

過程

首先是導包,包的版本關係能夠查看發佈版本,這裏我導入的是implementation "org.ow2.asm:asm:6.2"。修改字節碼主要須要如下這幾個類:ClassReader, ClassWriter, ClassVisitor, MethodVisitor。各個類的做用以下:github

  1. **ClassReader: 讀取類文件 **
  2. ClassWriter: 繼承ClassVisitor 主要用來生成修改類以後的字節
  3. ClassVisitor: 用於訪問修改類
  4. MethodVisitor: 用於訪問修改類的方法

通常用法以下:數組

try {
      String classPath = "asmdemo/ModifyInstanceClass";
      ClassReader classReader = new ClassReader(classPath);
      ClassWriter classWriter = new ClassWriter(classReader, 0);
      ClassVisitor classVisitor = new ClassVisitorDemo(classWriter);
      classReader.accept(classVisitor, 0);
      
      File file = new File(ROOT_SUFFIX + "ClassDynamicLoader/ASMProject/build/classes/java/main/asmdemo/ModifyInstanceClass.class");
      FileOutputStream output = new FileOutputStream(file);
      output.write(classWriter.toByteArray());
      output.close();
    } catch (IOException e) {
      e.printStackTrace();
    }



  private static class ClassVisitorDemo extends ClassVisitor {
    ClassVisitorDemo(ClassVisitor classVisitor) {
      super(Opcodes.ASM5, classVisitor);
    }

    @Override
    public void visitEnd() {

      cv.visitField(Opcodes.ACC_PRIVATE, "timer", Type.getDescriptor(long.class), null, null);

      MethodVisitor methodVisitor = cv.visitMethod(Opcodes.ACC_PUBLIC, "newFunc","()V", null,null);
      methodVisitor.visitInsn(Opcodes.RETURN);
      methodVisitor.visitMaxs(0,1);

      super.visitEnd();
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
      MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);

      if (name.equals("print") && desc.equals("()V")) {
        methodVisitor = new MethodVisitorHub.FirstMethodVisitor(methodVisitor);
      } else if(name.equals("print") && desc.equals("(Ljava/lang/String;)V")) {
        methodVisitor = new MethodVisitorHub.SecondMethodVisitor(methodVisitor);
      } else if (name.equals("connectStr")) {
        methodVisitor = new MethodVisitorHub.ThirdMethodVisitor(methodVisitor);
      }
      return methodVisitor;
    }


  }
複製代碼

先利用ClassReader讀取待修改的類文件,而後基於Reader建立了對應的ClassWriter,再基於ClassWriter建立了對應的ClassVisitor, 再接着ClassReader委託ClassVisitor去讀取修改類,最後,建立文件輸出流,利用ClassWriter生成的字節,將從新生成的字節碼寫回build目錄生成的class文件,替換編譯生成的class文件,這樣就能夠達到修改類的目的。bash

flowchat
st=>start: 開始
e=>end: 文件輸出流將字節碼寫回類生成的build路徑,替換
op1=>operation: Reader讀取類
op2=>operation: MethodVisitor修改類
op3=>operation: 獲取Writer修改以後的字節碼
st->op1->op2->op3->e

複製代碼

用法

   對於類的修改,主要關注ClassVisitor和MethodVisitor這兩個類便可,ClassVistor能夠實現成員變量和方法的增長,MethodVisitor用於修改類方法的實現。在修改類方法的時候,我是先經過把原先的方法修改成預期的方法,而後經過javap命令對預期的方法產生的類文件進行反編譯,查看編譯器產生的字節碼。命令以下:javap -v .class文件路徑。 經過反編譯以後能夠獲得修改後的類的操做數棧和局部變量表的最大大小,還有具體的字節碼指令。下面開始看具體的使用。框架

   MethodVIsitor通常經過實現visitCode visitInsan visitMaxs方法來實現類的修改。visitCode是方法的訪問開始;visitInsn能夠訪問方法的操做指令,通常應用於在return指令以前插入代碼;vistiMax則用於複寫操做數棧和局部變量表的大小,由於類被修改,因此所需的棧和變量表大小可能會增長。下面是幾個具體的例子:ide

1. 在print()空方法中插入一行輸出 System.out.print("Hello World");ui

首先利用javap -v 編譯修改前的print方法,以下
![這裏寫圖片描述](https://img-blog.csdn.net/20180906151907501?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3Rhb3N6dQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
接着在print()方法增長 `    System.out.print("Hello World");`再執行javap -c反編譯
複製代碼

這裏寫圖片描述
能夠發現多了三個指令,而且stack即操做數棧增長了2。因此代碼以下:

public static class FirstMethodVisitor extends MethodVisitor {
    public FirstMethodVisitor(MethodVisitor mv) {
      super(Opcodes.ASM5, mv);
    }

    /**
     * 進入方法 插入System.out.print("hello world")這行代碼
     */
    @Override
    public void visitCode() {
      super.visitCode();
      mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
      mv.visitLdcInsn("hello world");
      mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "print", "(Ljava/lang/String;)V", false);
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
      mv.visitMaxs(2,1);
    }
  }
複製代碼

  上面的代碼主要覆寫了visitMaxs,stack local數值是經過反編譯獲得的,visitCode則是添加了三個指令。分析System.out.print可知,實際上是經過System這個類獲取out這個變量,而後經過out調用print這個方法輸出「hello world」這個變量。   因此首先要獲取out,out是一個靜態變量,第一個指令是visitFieldInsn,顧名思義就是訪問成員的指令,第一個參數是操做碼,第二個參數是調用成員的類,第三個參數是成員的名稱,第四個參數是成員的類型,對號入座第一個指令,操做碼是獲取靜態變量,調用類是「java/lang/System」, 成員名是「out」, 類型經過反編譯可知是「Ljava/io/PrintStream;」。因此得出結論,第一個指令是經過System這個類獲取out這個靜態變量而且把變量入棧。   接着第二個指令visitLdcInsn是把常量推到操做數棧,這裏是把「hello world」入棧,   最後就是第三個指令visitMethodInsn,仍是顧名思義是訪問方法的指令,第一個參數是操做碼,第二個參數是調用方法等的類,第三個參數是方法名,第四個參數是方法的返回類型和參數類型,第五個參數是調用方法的類是不是接口,對號入座,Opcodes.INVOKEVIRTUAL指的是調用的是實例方法,調用的類是out即「java/io/PrintStream」這個類,方法名是print,返回值是void對應「V」,參數是String對應「Ljava/lang/String; 」, 這些參數的對應類型均可以從反編譯獲得。第三個指令須要兩個操做數,一個是執行方法的主體即out,第二個是參數即「hello world」,使用visitMethodInsn指令的時候,out 「」hello world「依次從操做數棧出棧,剛恰好對應指令調用的參數順序。 攔截方法的入口在ClassVisitor,以下:this

private static class ClassVisitorDemo extends ClassVisitor {
    ClassVisitorDemo(ClassVisitor classVisitor) {
      super(Opcodes.ASM5, classVisitor);
    }

    @Override
    public void visitEnd() {

      cv.visitField(Opcodes.ACC_PRIVATE, "timer", Type.getDescriptor(long.class), null, null);

      MethodVisitor methodVisitor = cv.visitMethod(Opcodes.ACC_PUBLIC, "newFunc","()V", null,null);
      methodVisitor.visitInsn(Opcodes.RETURN);
      methodVisitor.visitMaxs(0,1);

      super.visitEnd();
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
      MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);

      if (name.equals("print") && desc.equals("()V")) {
        methodVisitor = new MethodVisitorHub.FirstMethodVisitor(methodVisitor);
      } else if(name.equals("print") && desc.equals("(Ljava/lang/String;)V")) {
        methodVisitor = new MethodVisitorHub.SecondMethodVisitor(methodVisitor);
      } else if (name.equals("connectStr")) {
        methodVisitor = new MethodVisitorHub.ThirdMethodVisitor(methodVisitor);
      }
      return methodVisitor;
    }


  }
複製代碼

在visitMethod中判斷方法名爲print,則進行攔截注入本身建立的MethodVisitor便可。   到這裏已經分析完成,能夠自信滿滿地運行代碼了,可是要切記,不能在修改以前使用該類,若是使用了以後,類已經被加載,那麼修改以後的類不會被再次加載,也就沒法發揮做用了。

2. 在print(String s )空方法中插入一行輸出 System.out.print(s)    分析的方法和上面的同樣,這裏的關鍵是讀取參數的值,反編譯以後能夠發現使用了ALOAD這個指令,這個指令的做用是從局部變量表讀取變量入棧,指令代碼以下:

mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
 mv.visitVarInsn(Opcodes.ALOAD, 1);
 mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "print", "(Ljava/lang/String;)V", false);
複製代碼

   visitVarInsn是讀取參數的指令,操做碼是ALOAD,後面的參數是指變量表的索引,上面也提到,若是是實例方法,局部變量表的0索引是實例對象,因此這裏取了索引1。

**3. 在connectStr()空方法打印執行消耗時間 ** 修改前代碼以下:

public void connectStr() {
    String s = "";
    for (int i = 0; i < 10000; i ++) {
      s += i;
    }
  }
複製代碼

修改後代碼以下:

public void connectStr() {
    this.timer = -System.currentTimeMillis();
    String s = "";

    for(int i = 0; i < 10000; ++i) {
      s = s + i;
    }

    this.timer += System.currentTimeMillis();
    System.out.println(this.timer);
  }
複製代碼

   這裏的關鍵是在return前插入代碼, 還有增長變量timer。具體的反編譯過程就不展現了,直接上代碼:

public static class ThirdMethodVisitor extends MethodVisitor {
    public ThirdMethodVisitor(MethodVisitor mv) {
      super(Opcodes.ASM5, mv);
    }

    /**
     * 進入方法
     */
    @Override
    public void visitCode() {
      super.visitCode();
      mv.visitVarInsn(Opcodes.ALOAD, 0);
      mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
      mv.visitInsn(Opcodes.LNEG);
      mv.visitFieldInsn(Opcodes.PUTFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");
    }

    /**
     * return前插入代碼
     */
    @Override
    public void visitInsn(int opcode) {
      if (opcode == Opcodes.RETURN) {
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitInsn(Opcodes.DUP);

        mv.visitFieldInsn(Opcodes.GETFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        mv.visitInsn(Opcodes.LADD);
        mv.visitFieldInsn(Opcodes.PUTFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");

        mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitFieldInsn(Opcodes.GETFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false);
      }
      super.visitInsn(opcode);
    }


    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        mv.visitMaxs(5, 3);
    }
  }

複製代碼

   首先看visitCode方法, 作的事情就是this.timer = System.currentTimeMillis(),對這行代碼進行拆分,就是獲取時間戳賦值給timer,對應底下的指令mv.visitVarInsn(Opcodes.ALOAD, 0) 先將實例對象入棧即咱們用的變量this,接着訪問方法獲取系統時間戳而後執行LNEG取反入棧,最後在執行訪問方法的指令PUTFIELD把值賦給timer,須要的參數是時間戳和this變量,this變量用於訪問timer,時間戳則是賦值的變量。    接着看visitInsn方法,visitInsn能夠攔截方法執行的指令作一些插入操做,在這裏咱們須要作的事在return以前插入時間戳的計算和打印, 代碼比較長以下:

if (opcode == Opcodes.RETURN) {
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitInsn(Opcodes.DUP);

        mv.visitFieldInsn(Opcodes.GETFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        mv.visitInsn(Opcodes.LADD);
        mv.visitFieldInsn(Opcodes.PUTFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");

        mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitFieldInsn(Opcodes.GETFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false);
      }
複製代碼

   老規矩,拆解代碼 this.timer += System.currentTimeMillis(), 須要取出timer的值,獲取時間戳,進行加法操做,而後結果賦值到timer,這裏須要用到兩個this變量,由於要訪問timer兩次,因此能夠看到一個新的指令,DUP,DUP的意思就是複製棧頂變量而後入棧,也就是說拷貝多一份this變量,底下的指令已經分析過了,就再也不贅述。    到這裏,還沒完成,由於timer變量還沒生成呢,類變量的生成就要依賴ClassVisitor了, 攔截ClassVisitor的visitEnd方法,動態增長變量,以下:

@Override
    public void visitEnd() {
      cv.visitField(Opcodes.ACC_PRIVATE, "timer", Type.getDescriptor(long.class), null, null);
      super.visitEnd();
    }
複製代碼

代碼已經上傳 AsmDemo

總結

   這裏介紹的只是動態修改類的冰山一角,動態生成類的應用場景不少,像市面上的路由框架,熱修復框架,不少都是利用了動態修改類的方式進行代碼的注入,因此路還很長,還需更加努力。

參考連接

動態生成類的一些錯誤 字節碼指令介紹 字節碼原理

相關文章
相關標籤/搜索