簡述
本文內(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)翻譯成了英文:
字節(jié)碼中主要包含下面幾個部分
- 元信息丁逝,包括魔數(shù)(magic number)汁胆、Java版本號信息、類修飾符等
- 常量池霜幼,用于存放各種字符串常量信息
-
函數(shù)嫩码,包括函數(shù)的修飾符、函數(shù)操作棧大小罪既、局部變量表大小铸题、字節(jié)碼指令、LineNumberTable等信息琢感。
首先需要了解丢间,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 源碼做的手腳捉捅,例如原始類型的拆箱裝箱——
字節(jié)碼文件是一個描述類的數(shù)據(jù)結(jié)構(gòu),有著嚴謹?shù)慕Y(jié)構(gòu)虽风,一定以 0x CAFEBABE 開頭棒口,然后是主版本號、次版本號辜膝,然后是類的描述等等无牵,每一個類型都有著固定的大小,其偏移量可以通過確定的規(guī)則計算出來厂抖。 JVM 中的解釋器通過順序讀取字節(jié)碼指令茎毁,逐條進行解釋,最終生成可執(zhí)行的二進制文件忱辅。
思考一下七蜘,如果,我們在字節(jié)碼被 JVM 解釋之前墙懂,對其進行修改...
二橡卤、javaagent 技術(shù)
- 什么是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 的使用
首先新建一個類,類中聲明一個 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