JMCR 字節(jié)碼插樁(一)

簡述

本文內(nèi)容將介紹 Java 字節(jié)碼相關知識,以及如何通過 javaagent 技術(shù)加上 ASM 框架進行插樁怔匣。

本文提綱

  • 字節(jié)碼
  • javaagent
  • ASM 框架
  • ASM + javaagent 插樁實戰(zhàn)

系列文章:
1. JMCR 簡介
2. JMCR 字節(jié)碼插樁(一)
3. JMCR 字節(jié)碼插樁(二)
4. JMCR 約束求解原理
5. JMCR 線程調(diào)度

一击胜、Java 字節(jié)碼

本節(jié)內(nèi)容中的 Java 字節(jié)碼介紹不會占用太多篇幅亏狰,若有興趣,可以參考《深入理解Java虛擬機》偶摔。

對于一個 Java 編程人員骚揍,字節(jié)碼這個概念一定不會陌生,作為 Java 語言和 機器碼之間做翻譯的中間語言啰挪,我們每一次編譯一個 Java 類文件的時候都會生成一個 .class 文件。


編譯過程

而 .class 文件會被加載到虛擬機(JVM)內(nèi)執(zhí)行嘲叔,這時我們的程序才運行起來亡呵。
首先來看一個字節(jié)碼是什么樣的,以HelloWorld編譯后的字節(jié)碼為例:
源代碼:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello ByteCode!");
    }
}

字節(jié)碼本身就是一串字節(jié)流硫戈,如果使用 16進制編碼的話锰什,它看起來會是這樣的 ——

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   
00000000: CA FE BA BE 00 00 00 34 00 1D 0A 00 06 00 0F 09    J~:>...4........
00000010: 00 10 00 11 08 00 12 0A 00 13 00 14 07 00 15 07    ................
00000020: 00 16 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29    .....<init>...()
00000030: 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E    V...Code...LineN
00000040: 75 6D 62 65 72 54 61 62 6C 65 01 00 04 6D 61 69    umberTable...mai
00000050: 6E 01 00 16 28 5B 4C 6A 61 76 61 2F 6C 61 6E 67    n...([Ljava/lang
//... 

更人性化一點的手段是使用 javap -v HelloWorld.class 命令進行查看,它將字節(jié)碼按照其結(jié)構(gòu)翻譯成了英文:

javap

字節(jié)碼中主要包含下面幾個部分

  • 元信息丁逝,包括魔數(shù)(magic number)汁胆、Java版本號信息、類修飾符等
  • 常量池霜幼,用于存放各種字符串常量信息
  • 函數(shù)嫩码,包括函數(shù)的修飾符、函數(shù)操作棧大小罪既、局部變量表大小铸题、字節(jié)碼指令、LineNumberTable等信息琢感。


    字節(jié)碼結(jié)構(gòu)

首先需要了解丢间,Java的函數(shù)傳參是通過棧實現(xiàn)的,然后我們主要關注一下 HelloWorld.class 中 main 函數(shù)的字節(jié)碼指令——

    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello ByteCode!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return

第一條 getStatic #2驹针,意味著把常量池中 2 號字符串(System.out)指向的變量值壓入棧中
第二條 ldc #3烘挫,將常量池中3號字符串 Hello ByteCode! 的指針壓入棧中
第三條 invokevirtual #4,意味著執(zhí)行常量池中 4 號字符串(java/io/PrintStream.println:(Ljava/lang/String;)V)所指向的函數(shù)柬甥。println有一個參數(shù)饮六,因此會從棧頂中取出字符串Hello ByteCode!的指針其垄,然后在棧頂取出函數(shù)執(zhí)行者 Syste.out

閱讀字節(jié)碼可以幫我們打開新的大門喜滨,可以探尋一些編譯器對于 Java 源碼做的手腳捉捅,例如原始類型的拆箱裝箱——


Source code

Byte code

字節(jié)碼文件是一個描述類的數(shù)據(jù)結(jié)構(gòu),有著嚴謹?shù)慕Y(jié)構(gòu)虽风,一定以 0x CAFEBABE 開頭棒口,然后是主版本號、次版本號辜膝,然后是類的描述等等无牵,每一個類型都有著固定的大小,其偏移量可以通過確定的規(guī)則計算出來厂抖。 JVM 中的解釋器通過順序讀取字節(jié)碼指令茎毁,逐條進行解釋,最終生成可執(zhí)行的二進制文件忱辅。

字節(jié)碼結(jié)構(gòu)

思考一下七蜘,如果,我們在字節(jié)碼被 JVM 解釋之前墙懂,對其進行修改...

二橡卤、javaagent 技術(shù)

  1. 什么是javaagent
    在 JDK 1.5 中,Java 引入了 java.lang.Instrument 包损搬,該包提供了一些工具幫助開發(fā)人員在 Java 程序運行時碧库,動態(tài)修改系統(tǒng)中的 Class 類型,而 javaagent 是其中的一個關鍵組件巧勤。這項技術(shù)多中 agentmain 多用于熱部署嵌灰,提供一個在運行時修改字節(jié)碼并重新加載入虛擬機的功能;而 premain 則是在字節(jié)碼在執(zhí)行之前攔截并修改颅悉,再讀入虛擬機中執(zhí)行沽瞭。
javaagent premain
  1. javaagent premain 的使用

首先新建一個類,類中聲明一個 premain 方法如下:

class Instrumentor{
    public static void premain(String agentArgs, Instrumentation inst) {
         inst.addTransformer(new ClassFileTransformer() {
            public byte[] transform(ClassLoader loader,
                                    String className,
                                    Class<?> classBeingRedefined,
                                    ProtectionDomain protectionDomain,
                                    byte[] classfileBuffer) throws IllegalClassFormatException {
                return classfileBuffer;
            }
        });
    }
}

這個類里面我們定義了premain函數(shù)剩瓶,為 Inst 加上了一個字節(jié)碼轉(zhuǎn)換器秕脓,在一個字節(jié)碼被加載如虛擬機之前,會執(zhí)行一邊 trasform 方法儒搭,這個方法中的 classFileBuffer 參數(shù)就是變化之前的字節(jié)碼吠架,而我們可以再函數(shù)里面進行一頓操作,返回的是轉(zhuǎn)換后的搂鲫,載入虛擬機的字節(jié)碼傍药,這里我們原封不動的返回,并沒有做任何操作。

創(chuàng)建一個 MANIFEST.MF 文件拐辽,指定剛剛寫好的Premain-Class

Manifest-Version: 1.0
Premain-Class: Instrumentor
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Created-By: 1.8.0_101 (Oracle Corporation)

使用 IDEA 將這個 MANIFEST 打包成agent.jar
然后通過運行時加上 -javaagent agent.jar 參數(shù)即可拣挪。

三、ASM

現(xiàn)在我們已經(jīng)有了字節(jié)碼俱诸,可以使用 ASM框架 來操作和修改字節(jié)碼菠劝。

ASM 庫是一款基于 Java 字節(jié)碼層面的代碼分析和修改工具。ASM 可以直接生產(chǎn)二進制的 class 文件睁搭,也可以在類被加載入 JVM 之前動態(tài)修改類行為赶诊。Java class 被存儲在嚴格格式定義的 .class 文件里,這些類文件擁有足夠的元數(shù)據(jù)來解析類中的所有元素:類名稱园骆、方法舔痪、屬性以及 Java 字節(jié)碼(指令)。ASM 從類文件中讀入信息后锌唾,能夠改變類行為锄码,分析類信息,甚至能夠根據(jù)用戶要求生成新類晌涕。

ASM 通過樹這種數(shù)據(jù)結(jié)構(gòu)來表示復雜的字節(jié)碼結(jié)構(gòu)滋捶,并利用 Push 模型來對樹進行遍歷,在遍歷過程中對字節(jié)碼進行修改余黎。

在 ASM 中重窟,提供了一個 ClassReader 類,這個類可以直接由字節(jié)數(shù)組或由 class 文件間接的獲得字節(jié)碼數(shù)據(jù)驯耻,它能正確的分析字節(jié)碼,構(gòu)建出抽象的樹在內(nèi)存中表示字節(jié)碼炒考。它會調(diào)用 accept 方法可缚,這個方法接受一個實現(xiàn)了 ClassVisitor 接口的對象實例作為參數(shù),然后依次調(diào)用 ClassVisitor 接口的各個方法斋枢。字節(jié)碼空間上的偏移被轉(zhuǎn)換成 visit 事件時間上調(diào)用的先后帘靡,所謂 visit 事件是指對各種不同 visit 函數(shù)的調(diào)用,ClassReader 知道如何調(diào)用各種 visit 函數(shù)瓤帚。在這個過程中用戶無法對操作進行干涉描姚,所以遍歷的算法是確定的,用戶可以做的是提供不同的 Visitor 來對字節(jié)碼樹進行不同的修改戈次。ClassVisitor 會產(chǎn)生一些子過程轩勘,比如 visitMethod 會返回一個實現(xiàn) MethordVisitor 接口的實例,visitField 會返回一個實現(xiàn) FieldVisitor 接口的實例怯邪,完成子過程后控制返回到父過程绊寻,繼續(xù)訪問下一節(jié)點。

流程

下面來一個使用 ASM 生成字節(jié)碼文件,并修改類的一些屬性的例子:

import org.apache.xpath.compiler.OpCodes;
import org.objectweb.asm.*;

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

public class HelloWorld implements Opcodes {

    public static void main(String[] args) throws IOException {


        ClassWriter cw = new ClassWriter(0);
        MethodVisitor mv;

        cw.visit(V1_7, ACC_PUBLIC + ACC_SUPER, "HelloWorld", null, "java/lang/Object", null);

        cw.visitSource("HelloWorld.java", null);

        //默認初始化構(gòu)造器
        {
            mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(9, l0);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
            mv.visitInsn(RETURN);
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLocalVariable("this", "LHelloWorld;", null, l0, l1, 0);
            mv.visitMaxs(1, 1);
            mv.visitEnd();
        }

        //public static void main方法
        {
            mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(12, l0);
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("Hello Bytecode!");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLineNumber(13, l1);
            mv.visitInsn(RETURN);
            Label l2 = new Label();
            mv.visitLabel(l2);
            mv.visitLocalVariable("args", "[Ljava/lang/String;", null, l0, l2, 0);
            mv.visitMaxs(2, 1);
            mv.visitEnd();
        }
        cw.visitEnd();

        byte[] code = cw.toByteArray();
        File file = new File("HelloWorld.class");
        FileOutputStream output = new FileOutputStream(file);
        output.write(code);
        output.close();
    }
}

上述代碼會生成一個 HelloWorld 字節(jié)碼文件澄步。內(nèi)容與文章開頭的例子一致冰蘑。然后我們對這個字節(jié)碼文件使用 ASM 再進行修改,在打印“Hello World”之前和之后村缸,分別記錄當前的時間祠肥,計算時間差并打印。

public class HelloWorld implements Opcodes {

    public static void main(String[] args) throws IOException {
        File file = new File("HelloWorld.class");
        InputStream inputStream = new FileInputStream(file);
        byte[] source = new byte[inputStream.available()];
        int a = inputStream.read(source);
        if (a == -1) {
            System.err.println("文件讀取問題");
            System.exit(1);
        }
        ClassReader cr = new ClassReader(source);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
        cr.accept(new ClassVisitor(ASM5, cw) {
            @Override
            public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
                super.visit(version, access, "HelloWorldInstrumented", signature, superName, interfaces);//更改類的名稱為 HelloWorldInstrumented
            }

            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
                MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
                mv = new AdviceAdapter(ASM5, mv, access, name, desc) {
                    @Overri![
](https://upload-images.jianshu.io/upload_images/8081626-b7627e062e0d0f11.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
de
                    public void visitMethodInsn(int opcode, String owner, String name, String desc) {
                        if (opcode == INVOKEVIRTUAL) {
                            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");
                            mv.visitVarInsn(LSTORE, 1);
                            super.visitMethodInsn(opcode, owner, name, desc);
                            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");
                            mv.visitVarInsn(LLOAD, 1);
                            mv.visitInsn(LSUB);
                            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V");
                        }
                    }
                };
                return mv;
            }

        }, ClassReader.EXPAND_FRAMES);

        byte[] code = cw.toByteArray();
        file = new File("HelloWorldInstrumented.class");
        FileOutputStream output = new FileOutputStream(file);
        output.write(code);
        output.close();
    }
}

轉(zhuǎn)換后的字節(jié)碼反編譯結(jié)果如下:


具體詳細的 ASM 的 Api 和使用的方法建議參考官網(wǎng)梯皿,而具體的細節(jié)仇箱,例如字節(jié)碼指令的名稱、方法的名稱如果記不住的話索烹,推薦一款 IDEA 的插件 —— ASM Bytecode Outline工碾,安裝后,可以右鍵查看類的字節(jié)碼百姓、以及如何通過ASM的方式構(gòu)造這個類出來渊额。

四、ASM + javaagent 插樁實戰(zhàn)

其實在前面兩個小節(jié)垒拢,我們已經(jīng)分別講述了兩個工具的使用旬迹,接下來就是怎么結(jié)合的問題了。其實也很簡單求类,在第二小節(jié)的javaagent示例中奔垦,在transform方法中直接將攔截到的字節(jié)流返回了,而第三小節(jié)中尸疆,ASM 框架的輸入輸出都是文件椿猎。不難想到,只需將 ASM 操作寫到transform方法中寿弱,以參數(shù)為輸入犯眠,輸出到返回之中就行了。

public static void premain(String agentArgs, Instrumentation inst) {
    inst.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {
    ClassReader classReader = new ClassReader(classfileBuffer);
    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES);
    ClassVisitor myClassTransformer = new MyClassTransformer(classWriter); //自定義ClassVisitor
    classReader.accept(myClassTransformer,ClassReader.EXPAND_FRAMES);
    classfileBuffer = classWriter.toByteArray();
 
    return classfileBuffer;
});

不妨嘗試一下使用這個技術(shù)對于現(xiàn)有的程序進行插樁症革,達到每一次對于變量訪問時筐咧,都會打印相關信息。
代碼參考:https://github.com/tjuwhy/MyJMCR

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末噪矛,一起剝皮案震驚了整個濱河市量蕊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌艇挨,老刑警劉巖残炮,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異缩滨,居然都是意外死亡吉殃,警方通過查閱死者的電腦和手機辞居,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蛋勺,“玉大人瓦灶,你說我怎么就攤上這事”辏” “怎么了贼陶?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長巧娱。 經(jīng)常有香客問我碉怔,道長,這世上最難降的妖魔是什么禁添? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任撮胧,我火速辦了婚禮,結(jié)果婚禮上老翘,老公的妹妹穿的比我還像新娘芹啥。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著陋气,像睡著了一般。 火紅的嫁衣襯著肌膚如雪猿诸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機與錄音,去河邊找鬼钓账。 笑死,一個胖子當著我的面吹牛絮宁,可吹牛的內(nèi)容都是我干的梆暮。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼羞福,長吁一口氣:“原來是場噩夢啊……” “哼惕蹄!你這毒婦竟也來了蚯涮?” 一聲冷哼從身側(cè)響起治专,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎遭顶,沒想到半個月后张峰,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡棒旗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年喘批,在試婚紗的時候發(fā)現(xiàn)自己被綠了撩荣。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡饶深,死狀恐怖餐曹,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情敌厘,我是刑警寧澤台猴,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站俱两,受9級特大地震影響饱狂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜宪彩,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一休讳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧尿孔,春花似錦俊柔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至芜辕,卻和暖如春尚骄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背侵续。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工倔丈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人状蜗。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓需五,卻偏偏與公主長得像,于是被迫代替她去往敵國和親轧坎。 傳聞我的和親對象是個殘疾皇子宏邮,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344