在java世界裏,字節碼改寫 + 反射可以讓你變成“上帝”,你可以完成任何你想做的事情,而字節碼改寫中asm是當之無愧的老大哥,對字節碼認識不深的小夥伴可以看看我這篇文章
https://www.zhihu.com/question/7501915796/answer/61918375697
本文的目的是現有互聯網上asm的資料不夠體系和細緻,其和傳統java編程也非常的不一樣,使用時有很多需要注意的地方,比如棧的平衡以及如何分析棧內操作數的狀態。
接下來以我最近對隨機數方法的實際字節碼改寫需求為例帶大家熟悉整個字節碼改寫過程.
先描述下我的技術需求
攔截所有生成隨機數的方法,記錄入參、調用類名、調用方法名、當前行號、方法內行號和出參
-
橋接(代理)方法
在字節碼改寫中非常重要的一個部分就是橋接(代理)方法,原來調用A方法,當你想做一些事情那麼你可能需要將原有的調用修改為調用橋接方法,然後在橋接方法中實現你的處理邏輯
public Object method() { // 業務處理邏輯 invokeMethod(params); // 業務處理邏輯 }public Object method() { // 業務處理邏輯 invokeBrigeMethod(params...); // 業務處理邏輯 }橋接方法是否是必須的呢?一般來説如果你的處理邏輯比較簡單也是可以不需要橋接方法的,直接將相應的調用指令替換成處理指令,但一般來説如果處理沒那麼簡單使用橋接方法可以使編程更簡單,而且很多時候還有一些隱含的好處,比如可以維持原有業務代碼的行號避免干擾業務排查問題,對業務類的字節碼變更較小,可複用性更高等等。
以上面的需求為例隨機數的橋接方法被設計成這樣
// 一般來説橋接方法會設計成靜態方法,這樣在字節碼修改時調用更加方便。 // 第1個參數是Random實例,保證隨機數結果依舊從原有的Random實例生成 // 第2個參數是一個數組,因為隨機數方法入參的個數是不一致,這裏統一使用數組接收,這樣就不用每個方法都對應一個橋接方法了 // 第3個參數標識當前是哪個隨機數方法,用來生成對應的結果 // 第3個參數又是一個數組,裏面是當前隨機數方法的一些身份信息,這裏主要是考慮到後續可能會新增新的參數,為了避免簽名的修改這裏使用數組 public static Object invoke(Random random, Object[] request, RandomMethod randomMethod, Object[] randomIdentity); -
字節碼改寫
標準使用姿勢
// classFileBuffer為原始字節碼byte數組 ClassReader classReader = new ClassReader(classFileBuffer); ClassWriter writer = new EasyClassWriter(COMPUTE_MAXS | COMPUTE_FRAMES, loader); ClassVisitor visitor = new InvokeRandomTransformClassVisitor(); classReader.accept(visitor, EXPAND_FRAMES);核心邏輯在
InvokeRandomTransformClassVisitor中public class InvokeRandomTransformClassVisitor { @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { // 跳過特殊方法、Object的內部方法、抽象方法、native方法 return new InvokeRandomTransformMethodVisitor(api, visitor, access, name, desc); } }在做一系列的過濾之後引出
InvokeRandomTransformMethodVisitorprivate class InvokeRandomTransformMethodVisitor extends AdviceAdapter { private final String methodName; private int methodStartLineNumber = -1; private int currentLineNumber = -1; // 記錄方法開始行數,用來計算方法內行數 @Override public void visitLineNumber(int line, Label start) { if (this.methodStartLineNumber == -1) { this.methodStartLineNumber = line; } this.currentLineNumber = line; super.visitLineNumber(line, start); } @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { if (INVOKEVIRTUAL == opcode) { if (根據owner、name和descriptor判斷是否為隨機數方法) { // 將Random方法的入參合併成一個數組壓入操作數棧 // 壓入隨機方法名 // 創建一個大小為4的數組填入身份標識 // 調用橋接方法 invokeStatic(TYPE, INVOKE_METHOD); // 提取響應類型描述符做相應拆箱操作保證留在棧上的結果類型一致 AsmClassUtil.unbox(this, Type.getReturnType(descriptor)); return; } } super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } } }- 首先判斷是否是隨機數調用,通過指令和調用類、方法及其描述符來做判斷,這裏先判斷是否是
INVOKEVIRTUAL,這會過濾掉大多數無關指令,然後再根據調用類、方法和描述符判斷是否是隨機數方法,一般來説先判斷指令可以篩選掉很多無關的信息性能會更好。 -
接下來將Random方法的入參合併成一個數組壓入操作數棧,這個是整個改寫中最難的部分
public static void visitArgumentsAsArray(MethodVisitor methodVisitor, String descriptor) { // 解析方法描述符以獲得參數類型 // Parse the method descriptor to get the argument type Type[] argumentTypes = Type.getArgumentTypes(descriptor); int argumentCount = argumentTypes.length; // 創建一個指定大小的數組並壓入棧 // Create an array of a specified size and push it onto the stack if (argumentCount <= Byte.MAX_VALUE) { methodVisitor.visitIntInsn(BIPUSH, argumentCount); } else { methodVisitor.visitIntInsn(SIPUSH, argumentCount); } methodVisitor.visitTypeInsn(ANEWARRAY, OBJECT_JVM_CLASS_NAME); // 倒序遍歷參數類型,將每個參數裝箱並加入數組 // Traverse the argument type in reverse order, box each argument and add it to the array for (int i = argumentCount - 1; i >= 0; i--) { Type type = argumentTypes[i]; if (LONG_TYPE.equals(type) || DOUBLE_TYPE.equals(type)) { methodVisitor.visitInsn(DUP_X2); } else { methodVisitor.visitInsn(DUP_X1); } swap(methodVisitor, type); // 指定數組索引 // Specify the array index if (i <= Byte.MAX_VALUE) { methodVisitor.visitIntInsn(BIPUSH, i); } else { methodVisitor.visitIntInsn(SIPUSH, i); } swap(methodVisitor, type); box(methodVisitor, type); methodVisitor.visitInsn(AASTORE); } } public static void swap(MethodVisitor methodVisitor, Type type) { if (LONG_TYPE.equals(type) || DOUBLE_TYPE.equals(type)) { methodVisitor.visitInsn(DUP_X2); methodVisitor.visitInsn(POP); } else { methodVisitor.visitInsn(SWAP); } }首先根據入參個數創建一個Object數組,數組的長度應該是原方法的參數個數,這裏還有一個細節就是
BIPUSH用於將一個字節(8位)的常量推送到操作數棧上,它的常量範圍是從 -128 到 127,但理論上一個方法的入參最多可以可以有256個(隨機數參數最多是3個),超過127時應該使用SIPUSH,對於當前場景BIPUSH就夠用了但為了後續可能會用於其它方法所以這裏做了這個判斷。然後是倒序遍歷參數類型,將每個參數裝箱並加入數組,和傳統編程不一樣的是大家一定要理解棧的特質——先入先出,所以此時留在棧頂的是原方法的最後一個參數,所以為了將參數按照原有順序打包成數組應該是倒序地將參數存入數組,上面的字節碼改寫可能會讓人眼花繚亂,我將會描述每個指令執行之後棧的狀態大家會有更深刻的體會
以最常用的
public int nextInt(int bound)為例當執行到nextInt的
INVOKEVIRTUAL指令時棧的狀態是這樣的(可以看到棧頂是一個int值)
在執行
ANEWARRAY後會新增一個數組(在棧頂)變成了這樣
對於
int由於它只佔用一個Slot執行DUP_X1讓棧變成下面這樣
ps: 這裏解釋一下為什麼這麼做,因為後續將元素放入數組是要消耗三個操作數包括array本身的,所以這裏使用
DUP_X1對數組做一個備份使用swap因為
AASTORE對操作數的順序要求是數組,下標,元素使用
BIPUSH或SIPUSH設置下標(從後向前),這個因為只有一個參數所以是0使用swap調整操作數順序
使用
AASTORE設置數組,這會消耗最近的三個操作數
這樣就將橋接方法的前兩個參數準備好了,這裏面還有一些細節:
- 第一個是
box(methodVisitor, type);,因為在java中數組只能存放引用類型無法存放基本類型,所以對於int這種需要裝箱成包裝類型,比如對於int可以調用Integer.valueOf()
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);- 第二個細節是對於
int和reference類型要交換它們只需要使用SWAP指令即可,但如果入參是long/double這種2個Slot的類型則需要使用DUP_X2+POP來完成 - 第三個細節是多個參數第一次循環結束時棧的狀態為
[..., random, 操作數, array],按照上述的處理繼續把它們放入數組即可,保證最後留在棧頂的是array
- 第一個是
-
然後是壓入
RandomMethod,這是一個枚舉值所以使用GETSTATIC即可visitFieldInsn(GETSTATIC, "RandomMethod的JVM描述符比如xxx/xxx/RandomMethod", randomMethod.name(), "RandomMethod的JVM返回描述符比如Lxxx/xxx/RandomMethod;"); -
接下來是創建一個大小為4的數組填入身份標識,這個部分要簡單一點
visitLdcInsn(4); visitTypeInsn(ANEWARRAY, "java/lang/Object"); // 填入類名 visitInsn(DUP); visitLdcInsn(0); visitLdcInsn(className); visitInsn(AASTORE); // 填入方法名 visitInsn(DUP); visitLdcInsn(1); visitLdcInsn(methodName); visitInsn(AASTORE); // 填入行號 visitInsn(DUP); visitLdcInsn(2); visitLdcInsn(currentLineNumber); // 基本類型int轉換為包裝類型Integer,因為數組只能存放包裝類型 AsmClassUtil.box(this, INT_TYPE); visitInsn(AASTORE); // 填入方法內行號 (方法的第幾行) visitInsn(DUP); visitLdcInsn(3); visitLdcInsn(currentLineNumber - methodStartLineNumber); // 基本類型int轉換為包裝類型Integer,因為數組只能存放包裝類型 AsmClassUtil.box(this, INT_TYPE); visitInsn(AASTORE);值得注意的還是基本類型要轉換為包裝類型,同時除了傳統的行號以外,這裏面額外記錄了方法內行號(方法的第幾行),作為一個相對座標它的魯棒性相比於文件行號要好很多。
- 最後調用橋接方法並做相應拆箱操作保證留在棧上的結果類型一致,基本類型和包裝類型的拆裝箱也是我們作字節碼改寫時要時刻注意的地方
- 首先判斷是否是隨機數調用,通過指令和調用類、方法及其描述符來做判斷,這裏先判斷是否是
-
代碼驗證
由於字節碼改寫的複雜性以及無法debug的特殊性,驗證測試變成了重中之重,畢竟很難有人能一次性就完成考慮各種場景的代碼還沒有問題,下面就是一個典型的驗證測試
public class InvokeRandomTransformClassVisitorTest extends ClassLoader { public Class<?> loadClassFromBytes(String className, byte[] classBytes) { return defineClass(className, classBytes, 0, classBytes.length); } public static void main(String[] args) throws Exception { String classPath = "原始class文件路徑"; String outputClassPath = "改寫後class文件保存路徑"; executeByteCodeRewrite(classPath, outputClassPath); } private static void executeByteCodeRewrite(String classpath, String outputClassPath) throws Exception { // 從class文件中讀取字節數組 byte[] classFileBuffer = Files.readAllBytes(Paths.get(classpath)); byte[] modifiedClassBytes = 調用InvokeRandomTransformClassVisitor做字節碼改寫; // 將修改後的字節數組寫入到指定路徑的文件中 try (FileOutputStream fos = new FileOutputStream(outputClassPath)) { fos.write(modifiedClassBytes); } System.out.println("Class transformed and written to file: " + outputClassPath); // 加載修改後的類並使用反射調用方法驗證結果 Class<?> clazz = new InvokeRandomTransformClassVisitorTest().loadClassFromBytes("類名", modifiedClassBytes); Object instance = clazz.getDeclaredConstructor().newInstance(); Method method = clazz.getMethod("testRandom", String.class); System.out.println(method.invoke(instance, "")); } }其實無非就是將改寫後的字節碼輸出到新的文件這樣可以再通過IDE反編譯獲取改寫後的java文件
再將修改後的字節碼加載到JVM中通過反射調用驗證其邏輯
一個是看一個是驗證執行結果,看通常可以讓你直接明白哪裏的操作有問題,操作數的位置不對,但即使看上去沒問題執行也不一定就ok就比如上面提到的拆裝箱問題僅僅從看是很難發現的
分析報錯也是非常重要的一環
Exception in thread "main" java.lang.VerifyError: Bad type on operand stack in aastore Exception Details: Location: xxx/xxx/InvokeRandomTransformClassVisitorEntity.testRandom(Ljava/lang/String;)Ljava/util/List; @54: aastore Reason: Type 'xxx/xxx/RandomMethod' (current frame, stack[3]) is not assignable to reference type Current Frame: bci: @54 flags: { } locals: { 'xxx/xxx/InvokeRandomTransformClassVisitorEntity', 'java/lang/String', 'java/util/Random', 'java/util/ArrayList' } stack: { 'java/util/Random', '[Ljava/lang/Object;', 'xxx/xxx/RandomMethod', 'xxx/xxx/RandomMethod', integer, 'java/lang/String' } Bytecode: 0x0000000: bb00 1559 b700 164d bb00 1859 b700 194e 0x0000010: 2c14 001a 1001 bd00 045b 5b57 1000 5b57 0x0000020: b800 2153 b200 2712 28bd 0004 1229 122b 0x0000030: 5359 122c 122d 5359 122e 122f b800 3453 0x0000040: 5912 3512 2eb8 0034 53b8 003b c000 3d2d 0x0000050: 59b6 0041 57ba 0055 0000 b900 5902 002d 0x0000060: 2c11 03e8 1001 bd00 045a 5f10 005f b800 0x0000070: 3453 b200 5c12 28bd 0004 1229 122b 5359 0x0000080: 122c 122d 5359 122e 125d b800 3453 5912 0x0000090: 3512 35b8 0034 53b8 003b c000 31b6 0061 0x00000a0: b800 34b9 0049 0200 572d b800 6711 03e8 0x00000b0: 1001 bd00 045a 5f10 005f b800 3453 b200 0x00000c0: 6a12 28bd 0004 1229 122b 5359 122c 122d 0x00000d0: 5359 122e 126b b800 3453 5912 3512 28b8 0x00000e0: 0034 53b8 003b c000 31b6 0061 b800 34b9 0x00000f0: 0049 0200 572d b800 6710 00bd 0004 b200 0x0000100: 6e12 28bd 0004 1229 122b 5359 122c 122d 0x0000110: 5359 122e 126f b800 3453 5912 3512 70b8 0x0000120: 0034 53b8 003b c000 31b6 0061 b800 34b9 0x0000130: 0049 0200 572d b800 6704 100a 1002 bd00 0x0000140: 045a 5f10 015f b800 3453 5a5f 1000 5fb8 0x0000150: 0034 53b2 0073 1228 bd00 0412 2912 2b53 0x0000160: 5912 2c12 2d53 5912 2e12 74b8 0034 5359 0x0000170: 1235 1275 b800 3453 b800 3bc0 0031 b600 0x0000180: 61b8 0034 b900 4902 0057 2db8 0067 1000 0x0000190: bd00 04b2 0078 1228 bd00 0412 2912 2b53 0x00001a0: 5912 2c12 2d53 5912 2e12 79b8 0034 5359 0x00001b0: 1235 127a b800 3453 b800 3bc0 007c b600 0x00001c0: 80b8 0083 b900 4902 0057 2db8 0067 1000 0x00001d0: bd00 04b2 0086 1228 bd00 0412 2912 2b53 0x00001e0: 5912 2c12 2d53 5912 2e12 87b8 0034 5359 0x00001f0: 1235 1288 b800 3453 b800 3bc0 001d b600 0x0000200: 8cb8 0021 b900 4902 0057 1010 bc08 3a04 0x0000210: bb00 8e59 b700 8f19 0410 01bd 0004 5a5f 0x0000220: 1000 5f53 b200 9212 28bd 0004 1229 122b 0x0000230: 5359 122c 122d 5359 122e 1293 b800 3453 0x0000240: 5912 3512 94b8 0034 53b8 003b 2d19 04b9 0x0000250: 0049 0200 572d b0 at java.lang.Class.getDeclaredConstructors0(Native Method) at java.lang.Class.privateGetDeclaredConstructors(Class.java:2671) at java.lang.Class.getConstructor0(Class.java:3075) at java.lang.Class.getDeclaredConstructor(Class.java:2178) at xxx.xxx.InvokeRandomTransformClassVisitorTest.executeByteCodeRewrite(InvokeRandomTransformClassVisitorTest.java:46) at xxx.xxx.InvokeRandomTransformClassVisitorTest.main(InvokeRandomTransformClassVisitorTest.java:27)這個就是非常典型的報錯,它的含義是在執行aastore時操作數的類型不對,Reason裏面是更詳細的信息:棧中下標為3的元素不是一個引用類型,這個時候你就需要檢查棧的操作數,確保aastore指令執行時棧上前3個操作數是數組、下標、元素。
在排查問題時像我上面教的那樣分析棧的狀態,確定操作數的位置,這可以讓你少走很多彎路。
好了,到這裏如果你真的有在細心閲讀且跟着我的例子實踐了一遍,那麼恭喜你你在asm使用上已經入門了,很多字節碼改寫對你而言將沒有秘密,從此你將自由穿梭在java世界之中而沒有什麼可以阻攔你。
但修行在個人,jvm的龐大世界才剛剛向你展開,享受且繼續努力吧。
原文地址:https://pebble-skateboard-d46.notion.site/Java-asm-19734675225b80e3b763ea53693f3395?pvs=73
附件(完整代碼)
EasyClassWriter
public class EasyClassWriter extends ClassWriter {
private final ClassLoader classLoader;
public EasyClassWriter(int flags, ClassLoader classLoader) {
super(flags);
this.classLoader = classLoader;
}
@Override
public String getCommonSuperClass(String type1, String type2) {
try {
String className1 = type1.replace("/", ".");
String className2 = type2.replace("/", ".");
if (classLoader != null) {
// 從字節流中讀讀取公共superClass
String[] type1SupperArr = AsmClassUtil.readSuperClass(type1, classLoader);
String[] type2SupperArr = AsmClassUtil.readSuperClass(type2, classLoader);
for (int i = type1SupperArr.length; i >= 0; i--) {
for (int j = type2SupperArr.length; j >= 0; j--) {
String type1Idx = i == type1SupperArr.length ? className1 : type1SupperArr[i];
String type2Idx = j == type2SupperArr.length ? className2 : type2SupperArr[j];
if (type1Idx.equals(type2Idx)) {
return type1Idx.replace('.', '/');
}
}
}
}
} catch (Throwable ignored) {
}
return OBJECT_JVM_CLASS_NAME;
}
}
InvokeRandomTransformClassVisitor
public class InvokeRandomTransformClassVisitor extends TransformClassVisitor {
private static final Type TYPE = Type.getType(RandomMethodBridge.class);
private static final String RANDOM_METHOD_NAME = RandomMethod.class.getName();
private static final Method INVOKE_METHOD = Method.getMethod(
String.format("Object invoke(java.util.Random,Object[],%s,Object[])", RANDOM_METHOD_NAME));
private static final String RANDOM_METHOD_JVM_NAME = RANDOM_METHOD_NAME.replace(".", "/");
private static final String RANDOM_METHOD_JVM_RETURN_NAME = "L" + RANDOM_METHOD_JVM_NAME + ";";
public InvokeRandomTransformClassVisitor(ClassVisitor classVisitor) {
super(AsmCompatible.apiVersion(), classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature,
String[] exceptions) {
MethodVisitor visitor = super.visitMethod(access, name, desc, signature, exceptions);
// 跳過特殊方法、Object的內部方法、抽象方法、native方法
if (ExtensionUtil.ignoreMethod(name) || AsmClassUtil.isObjectMethod(name, desc)
|| Modifier.isAbstract(access) || Modifier.isNative(access)) {
return visitor;
}
// 當前方法需要增強
if (RandomUtil.shouldTransformMethod(className, name)) {
return new InvokeRandomTransformMethodVisitor(api, visitor, access, name, desc);
}
return visitor;
}
private class InvokeRandomTransformMethodVisitor extends AdviceAdapter {
private final String methodName;
private int methodStartLineNumber = -1;
private int currentLineNumber = -1;
protected InvokeRandomTransformMethodVisitor(int api, MethodVisitor methodVisitor, int access,
String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
this.methodName = name;
}
@Override
public void visitLineNumber(int line, Label start) {
if (this.methodStartLineNumber == -1) {
this.methodStartLineNumber = line;
}
this.currentLineNumber = line;
super.visitLineNumber(line, start);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor,
boolean isInterface) {
if (INVOKEVIRTUAL == opcode) {
RandomMethod randomMethod = RandomMethod.getByMethodIdentity(
MethodSignatureParser.toNormalClass(owner), name, descriptor);
if (null != randomMethod) {
result.setEnhanced(true);
// 將Random方法的入參合併成一個數組壓入操作數棧
AsmClassUtil.visitArgumentsAsArray(this, descriptor);
// 壓入隨機方法名
visitFieldInsn(GETSTATIC,
RANDOM_METHOD_JVM_NAME,
randomMethod.name(),
RANDOM_METHOD_JVM_RETURN_NAME);
// 創建一個大小為4的數組
visitLdcInsn(4);
visitTypeInsn(ANEWARRAY, "java/lang/Object");
// 填入類名
visitInsn(DUP);
visitLdcInsn(0);
visitLdcInsn(className);
visitInsn(AASTORE);
// 填入方法名
visitInsn(DUP);
visitLdcInsn(1);
visitLdcInsn(methodName);
visitInsn(AASTORE);
// 填入行號
visitInsn(DUP);
visitLdcInsn(2);
visitLdcInsn(currentLineNumber);
// 基本類型int轉換為包裝類型Integer,因為數組只能存放包裝類型
AsmClassUtil.box(this, INT_TYPE);
visitInsn(AASTORE);
// 填入方法內行號 (方法的第幾行)
visitInsn(DUP);
visitLdcInsn(3);
visitLdcInsn(currentLineNumber - methodStartLineNumber);
// 基本類型int轉換為包裝類型Integer,因為數組只能存放包裝類型
AsmClassUtil.box(this, INT_TYPE);
visitInsn(AASTORE);
invokeStatic(TYPE, INVOKE_METHOD);
// 提取響應類型描述符
AsmClassUtil.unbox(this, Type.getReturnType(descriptor));
return;
}
}
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
}
}
TransformClassVisitor
public class TransformClassVisitor extends ClassVisitor {
protected final int api;
protected final TransformResult result;
protected String className;
public TransformClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
this.api = api;
this.result = TransformResult.notEnhanced();
}
@Override
public void visit(int apiVersion, int access, String name, String signature, String superName,
String[] interfaces) {
this.className = name.replace("/", ".");
super.visit(apiVersion, access, name, signature, superName, interfaces);
}
public TransformResult getTransformResult() {
return result;
}
}
TransformResult
public class TransformResult {
private boolean enhanced = false;
private byte[] classBuffer = null;
public static TransformResult notEnhanced() {
TransformResult transformResult = new TransformResult();
transformResult.enhanced = false;
return transformResult;
}
public static TransformResult enhanced(byte[] classBuffer) {
TransformResult transformResult = new TransformResult();
transformResult.enhanced = true;
transformResult.classBuffer = classBuffer;
return transformResult;
}
public boolean isEnhanced() {
return enhanced;
}
public byte[] getClassBuffer() {
return classBuffer;
}
public void setEnhanced(boolean enhanced) {
this.enhanced = enhanced;
}
public void setClassBuffer(byte[] classBuffer) {
this.classBuffer = classBuffer;
}
}
AsmClassUtil
public class AsmClassUtil {
private static final Map<Object, Map<String, String[]>> LOADER_SUPER_CLASS_CACHE_MAP = new IdentityHashMap<>();
private static final Lock SUPPER_CLASS_CACHE_INIT_LOCK = new ReentrantLock();
private static final Lock SUPER_CLASS_ARR_INIT_LOCK = new ReentrantLock();
public static final String OBJECT_JVM_CLASS_NAME = "java/lang/Object";
/**
* @author: Ares
* @description: 包裝類型拆箱
* @description: box type unboxing
* @time: 2025-02-11 17:38:30
* @params: [methodVisitor, descriptor] 方法訪問者,描述符
*/
public static void unboxing(MethodVisitor methodVisitor, String descriptor) {
unboxing(methodVisitor, Type.getType(descriptor));
}
/**
* @author: Ares
* @description: 包裝類型拆箱
* @description: box type unboxing
* @time: 2025-01-23 22:10:15
* @params: [methodVisitor, descriptor, isMethod] 方法訪問者,描述符, 是否是方法
*/
public static void unboxing(MethodVisitor methodVisitor, Type type) {
switch (type.getSort()) {
case Type.BOOLEAN:
methodVisitor.visitTypeInsn(CHECKCAST, "java/lang/Boolean");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z",
false);
break;
case Type.BYTE:
methodVisitor.visitTypeInsn(CHECKCAST, "java/lang/Byte");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Byte", "byteValue", "()B", false);
break;
case Type.CHAR:
methodVisitor.visitTypeInsn(CHECKCAST, "java/lang/Character");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Character", "charValue", "()C",
false);
break;
case Type.SHORT:
methodVisitor.visitTypeInsn(CHECKCAST, "java/lang/Short");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Short", "shortValue", "()S", false);
break;
case Type.INT:
methodVisitor.visitTypeInsn(CHECKCAST, "java/lang/Integer");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I", false);
break;
case Type.FLOAT:
methodVisitor.visitTypeInsn(CHECKCAST, "java/lang/Float");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Float", "floatValue", "()F", false);
break;
case Type.LONG:
methodVisitor.visitTypeInsn(CHECKCAST, "java/lang/Long");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false);
break;
case Type.DOUBLE:
methodVisitor.visitTypeInsn(CHECKCAST, "java/lang/Double");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Double", "doubleValue", "()D",
false);
break;
case Type.VOID:
break;
default:
String descriptor = type.getDescriptor();
char c = descriptor.charAt(0);
if (c == '[') {
methodVisitor.visitTypeInsn(CHECKCAST, descriptor);
} else {
methodVisitor.visitTypeInsn(CHECKCAST, descriptor.substring(1, descriptor.length() - 1));
}
}
}
/**
* @author: Ares
* @description: 基本類型裝箱
* @description: base type boxing
* @time: 2025-01-23 22:10:15
* @params: [methodVisitor, descriptor] 方法訪問者,描述符
*/
public static void boxing(MethodVisitor methodVisitor, String descriptor) {
Type type = Type.getType(descriptor);
boxing(methodVisitor, type);
}
public static void boxing(MethodVisitor methodVisitor, Type type) {
// 根據參數類型進行加載和裝箱
// load and box according to the parameter type
switch (type.getSort()) {
case Type.INT:
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf",
"(I)Ljava/lang/Integer;", false);
break;
case Type.BOOLEAN:
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf",
"(Z)Ljava/lang/Boolean;", false);
break;
case Type.CHAR:
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Character", "valueOf",
"(C)Ljava/lang/Character;", false);
break;
case Type.BYTE:
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf",
"(B)Ljava/lang/Byte;", false);
break;
case Type.SHORT:
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf",
"(S)Ljava/lang/Short;", false);
break;
case Type.LONG:
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf",
"(J)Ljava/lang/Long;", false);
break;
case Type.FLOAT:
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Float", "valueOf",
"(F)Ljava/lang/Float;", false);
break;
case Type.DOUBLE:
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf",
"(D)Ljava/lang/Double;", false);
break;
case Type.OBJECT:
case Type.ARRAY:
case Type.VOID:
break;
default:
throw new IllegalArgumentException("Unsupported argument type: " + type);
}
}
/**
* @author: Ares
* @description: 將原方法的入參合併成一個數組傳給橋接方法
* @description: Merge the original method's arguments into an array and pass them to the bridge
* method
* @time: 2025-02-11 17:55:36
* @params: [methodVisitor, descriptor] 方法訪問者,描述符
*/
public static void visitArgumentsAsArray(MethodVisitor methodVisitor, String descriptor) {
/**
[..., random, int]
ANEWARRAY [..., random, int, array]
DUP_X1 [..., random, array, int, array]
SWAP [..., random, array, array, int]
*PUSH [..., random, array, array, int, 0]
SWAP [..., random, array, array, 0, int]
AASTORE [..., random, array]
[..., random, int, int]
ANEWARRAY [..., random, int, int, array]
DUP_X1 [..., random, int, array, int, array]
SWAP [..., random, int, array, array, int]
*PUSH [..., random, int, array, array, int, 1]
SWAP [..., random, int, array, array, 1, int]
AASTORE [..., random, int, array]
DUP_X1 [..., random, array, int, array]
SWAP [..., random, array, array, int]
*PUSH [..., random, array, array, int, 0]
SWAP [..., random, array, array, 0, int]
AASTORE [..., random, array]
[..., random, long]
ANEWARRAY [..., random, long, array]
DUP_X2 [..., random, array, long, array]
DUP_X2 [..., random, array, array, long, array]
POP [..., random, array, array, long]
*PUSH [..., random, array, array, long, 0]
DUP_X2 [..., random, array, array, 0, long, 0]
POP [..., random, array, array, 0, long]
AASTORE [..., random, array]
*/
// 解析方法描述符以獲得參數類型
// Parse the method descriptor to get the argument type
Type[] argumentTypes = Type.getArgumentTypes(descriptor);
int argumentCount = argumentTypes.length;
// 創建一個指定大小的數組並壓入棧
// Create an array of a specified size and push it onto the stack
if (argumentCount <= Byte.MAX_VALUE) {
methodVisitor.visitIntInsn(BIPUSH, argumentCount);
} else {
methodVisitor.visitIntInsn(SIPUSH, argumentCount);
}
methodVisitor.visitTypeInsn(ANEWARRAY, OBJECT_JVM_CLASS_NAME);
// 倒序遍歷參數類型,將每個參數裝箱並加入數組
// Traverse the argument type in reverse order, box each argument and add it to the array
for (int i = argumentCount - 1; i >= 0; i--) {
Type type = argumentTypes[i];
if (LONG_TYPE.equals(type) || DOUBLE_TYPE.equals(type)) {
methodVisitor.visitInsn(DUP_X2);
} else {
methodVisitor.visitInsn(DUP_X1);
}
swap(methodVisitor, type);
// 指定數組索引
// Specify the array index
if (i <= Byte.MAX_VALUE) {
methodVisitor.visitIntInsn(BIPUSH, i);
} else {
methodVisitor.visitIntInsn(SIPUSH, i);
}
swap(methodVisitor, type);
box(methodVisitor, type);
methodVisitor.visitInsn(AASTORE);
}
}
/**
* @author: Ares
* @description: 根據類型交換棧頂元素
* @description: Exchange the top element of the stack according to the type
* @time: 2025-02-12 18:46:54
* @params: [methodVisitor, type] 方法訪問者,類型
*/
public static void swap(MethodVisitor methodVisitor, Type type) {
if (LONG_TYPE.equals(type) || DOUBLE_TYPE.equals(type)) {
methodVisitor.visitInsn(DUP_X2);
methodVisitor.visitInsn(POP);
} else {
methodVisitor.visitInsn(SWAP);
}
}
/**
* @author: Ares
* @description: 讀取父類數組
* @description: read super class array
* @time: 2025-02-11 17:36:58
* @params: [className, loader] 類名,類加載器
* @return: java.lang.String[] 父類數組
*/
public static String[] readSuperClass(String className, ClassLoader loader) throws IOException {
return readSuperClass(className, loader, null);
}
/**
* @author: Ares
* @description: 讀取父類數組
* @description: read super class array
* @time: 2025-02-11 17:36:58
* @params: [className, loader, classBytes] 類名,類加載器,類字節碼
* @return: java.lang.String[] 父類數組
*/
public static String[] readSuperClass(String className, ClassLoader loader, byte[] classBytes)
throws IOException {
if (loader == null) {
return null;
}
Map<String, String[]> supperClassCacheMap = LOADER_SUPER_CLASS_CACHE_MAP.get(loader);
if (supperClassCacheMap == null) {
SUPPER_CLASS_CACHE_INIT_LOCK.lock();
try {
// 類加載器的父類加載器一般不會超過4層(根加載器是BootStrap加載器)
// The parent class loader of the class loader generally does not exceed 4 layers (the root loader is the BootStrap loader)
supperClassCacheMap = MapUtil.newConcurrentMap(4);
LOADER_SUPER_CLASS_CACHE_MAP.put(loader, supperClassCacheMap);
} finally {
SUPPER_CLASS_CACHE_INIT_LOCK.unlock();
}
}
String[] superClassArr = supperClassCacheMap.get(className);
if (superClassArr == null) {
SUPER_CLASS_ARR_INIT_LOCK.lock();
try {
superClassArr = supperClassCacheMap.get(className);
if (superClassArr == null) {
superClassArr = innerReadSuperClass(className, loader, classBytes);
if (superClassArr == null) {
superClassArr = new String[0];
}
supperClassCacheMap.put(className, superClassArr);
}
} finally {
SUPER_CLASS_ARR_INIT_LOCK.unlock();
}
}
return superClassArr;
}
private static String[] innerReadSuperClass(String className, ClassLoader loader,
byte[] classBytes) throws IOException {
InputStream inputStream;
if (null != classBytes) {
inputStream = new ByteArrayInputStream(classBytes);
} else {
inputStream = loader.getResourceAsStream(className.replace(".", "/") + ".class");
}
if (null == inputStream) {
return null;
}
ClassReader classReader = new ClassReader(inputStream);
String superClassName = classReader.getSuperName();
if (null != superClassName && !OBJECT_JVM_CLASS_NAME.equals(superClassName)) {
String superClassNameReal = superClassName.replace("/", ".");
String[] superClasses = readSuperClass(superClassName, loader);
if (null != superClasses && superClasses.length > 0) {
String[] mergedSuperClasses = new String[superClasses.length + 1];
System.arraycopy(superClasses, 0, mergedSuperClasses, 0, superClasses.length);
mergedSuperClasses[superClasses.length] = superClassNameReal;
return mergedSuperClasses;
} else {
return new String[]{superClassNameReal};
}
}
return null;
}
public static String forPath(String className) {
return className.replaceAll("\\.", "/").concat(".class");
}
public static boolean isObjectMethod(String methodName, String methodDesc) {
if ("hashCode".equals(methodName) && "()I".equals(methodDesc)) {
return true;
}
if ("equals".equals(methodName) && "(Ljava/lang/Object;)Z".equals(methodDesc)) {
return true;
}
if ("clone".equals(methodName) && "()Ljava/lang/Object;".equals(methodDesc)) {
return true;
}
if ("finalize".equals(methodName) && "()V".equals(methodDesc)) {
return true;
}
if ("toString".equals(methodName) && "()Ljava/lang/String;".equals(methodDesc)) {
return true;
}
if ("getClass".equals(methodName) && "()Ljava/lang/Class;".equals(methodDesc)) {
return true;
}
if ("notify".equals(methodName) && "()V".equals(methodDesc)) {
return true;
}
if ("notifyAll".equals(methodName) && "()V".equals(methodDesc)) {
return true;
}
if ("wait".equals(methodName) && "(J)V".equals(methodDesc)) {
return true;
}
return false;
}
}
ExtensionUtil
public class ExtensionUtil {
private static final Set<String> IGNORE_METHOD_SET = CollectionUtil.asSet(JACOCO_INIT_METHOD_NAME,
CLASS_INIT_METHOD_NAME, CONSTRUCTOR_METHOD_NAME);
public static boolean ignoreMethod(String methodName) {
return IGNORE_METHOD_SET.contains(methodName);
}
}
RandomMethod
public enum RandomMethod {
/**
* Random
*/
RANDOM_NEXT_BOOLEAN(0, Random.class.getName() + "/nextBoolean~()Z"),
RANDOM_NEXT_DOUBLE(1, Random.class.getName() + "/nextDouble~()D"),
RANDOM_NEXT_FLOAT(2, Random.class.getName() + "/nextFloat~()F"),
RANDOM_NEXT_GAUSSIAN(3, Random.class.getName() + "/nextGaussian~()D"),
RANDOM_NEXT_INT(4, Random.class.getName() + "/nextInt~()I"),
Random_NEXT_INT_WITH_INT(5, Random.class.getName() + "/nextInt~(I)I", true),
Random_NEXT_LONG(6, Random.class.getName() + "/nextLong~()J"),
RANDOM_NEXT_BYTES(7, Random.class.getName() + "/nextBytes~([B)V", true, true),
RANDOM_DOUBLES(8, Random.class.getName() + "/doubles~()Ljava/util/stream/DoubleStream;"),
RANDOM_DOUBLES_WITH_TWO_DOUBLES(9, Random.class.getName() + "/doubles~(DD)Ljava/util/stream/DoubleStream;", true),
RANDOM_DOUBLES_WITH_LONG(10, Random.class.getName() + "/doubles~(J)Ljava/util/stream/DoubleStream;", true),
RANDOM_DOUBLES_WITH_LONG_AND_TWO_DOUBLES(11, Random.class.getName() + "/doubles~(JDD)Ljava/util/stream/DoubleStream;"),
RANDOM_INTS(12, Random.class.getName() + "/ints~()Ljava/util/stream/IntStream;"),
RANDOM_INTS_WITH_TWO_INT(13, Random.class.getName() + "/ints~(II)Ljava/util/stream/IntStream;", true),
RANDOM_INTS_WITH_LONG(14, Random.class.getName() + "/ints~(J)Ljava/util/stream/IntStream;", true),
RANDOM_INTS_WITH_LONG_AND_TWO_INTS(15, Random.class.getName() + "/ints~(JII)Ljava/util/stream/IntStream;", true),
RANDOM_LONGS(16, Random.class.getName() + "/longs~()Ljava/util/stream/LongStream;"),
RANDOM_LONGS_WITH_LONG(17, Random.class.getName() + "/longs~()Ljava/util/stream/LongStream;", true),
RANDOM_LONGS_WITH_TWO_LONGS(18, Random.class.getName() + "/longs~(JJ)Ljava/util/stream/LongStream;", true),
RANDOM_LONGS_WITH_THIRD_LONGS(19, Random.class.getName() + "/longs~(JJJ)Ljava/util/stream/LongStream;", true),
/**
* ThreadLocalRandom
*/
THREAD_LOCAL_RANDOM_BOOLEAN(20, ThreadLocalRandom.class.getName() + "/nextBoolean~()Z"),
THREAD_LOCAL_RANDOM_NEXT_DOUBLE(21, ThreadLocalRandom.class.getName() + "/nextDouble~()D"),
THREAD_LOCAL_RANDOM_NEXT_DOUBLE_WITH_DOUBLE(22, ThreadLocalRandom.class.getName() + "/nextDouble~(D)D", true),
THREAD_LOCAL_RANDOM_NEXT_DOUBLE_WITH_TWO_DOUBLE(23, ThreadLocalRandom.class.getName() + "/nextDouble~(DD)D", true),
THREAD_LOCAL_RANDOM_NEXT_FLOAT(24, ThreadLocalRandom.class.getName() + "/nextFloat~()F"),
THREAD_LOCAL_RANDOM_NEXT_GAUSSIAN(25, ThreadLocalRandom.class.getName() + "/nextGaussian~()D"),
THREAD_LOCAL_RANDOM_NEXT_INT(26, ThreadLocalRandom.class.getName() + "/nextInt~()I"),
THREAD_LOCAL_RANDOM_NEXT_INT_WITH_INT(27, ThreadLocalRandom.class.getName() + "/nextInt~(I)I", true),
THREAD_LOCAL_RANDOM_NEXT_INT_WITH_TWO_INT(28, ThreadLocalRandom.class.getName() + "/nextInt~(II)I", true),
THREAD_LOCAL_RANDOM_NEXT_LONG(29, ThreadLocalRandom.class.getName() + "/nextLong~()J"),
THREAD_LOCAL_RANDOM_NEXT_LONG_WITH_LONG(30, ThreadLocalRandom.class.getName() + "/nextLong~(J)J", true),
THREAD_LOCAL_RANDOM_NEXT_LONG_WITH_TWO_LONG(31, ThreadLocalRandom.class.getName() + "/nextLong~(JJ)J", true),
THREAD_LOCAL_RANDOM_DOUBLES(32, ThreadLocalRandom.class.getName() + "/doubles~()Ljava/util/stream/DoubleStream;"),
THREAD_LOCAL_RANDOM_DOUBLES_WITH_TWO_DOUBLES(33, ThreadLocalRandom.class.getName() + "/doubles~(DD)Ljava/util/stream/DoubleStream;", true),
THREAD_LOCAL_RANDOM_DOUBLES_WITH_LONG(34, ThreadLocalRandom.class.getName() + "/doubles~(J)Ljava/util/stream/DoubleStream;", true),
THREAD_LOCAL_RANDOM_DOUBLES_WITH_LONG_AND_TWO_DOUBLES(35, ThreadLocalRandom.class.getName() + "/doubles~(JDD)Ljava/util/stream/DoubleStream;"),
THREAD_LOCAL_RANDOM_INTS(36, ThreadLocalRandom.class.getName() + "/ints~()Ljava/util/stream/IntStream;"),
THREAD_LOCAL_RANDOM_INTS_WITH_TWO_INT(37, ThreadLocalRandom.class.getName() + "/ints~(II)Ljava/util/stream/IntStream;", true),
THREAD_LOCAL_RANDOM_INTS_WITH_LONG(38, ThreadLocalRandom.class.getName() + "/ints~(J)Ljava/util/stream/IntStream;", true),
THREAD_LOCAL_RANDOM_INTS_WITH_LONG_AND_TWO_INTS(39, ThreadLocalRandom.class.getName() + "/ints~(JII)Ljava/util/stream/IntStream;", true),
THREAD_LOCAL_RANDOM_LONGS(40, ThreadLocalRandom.class.getName() + "/longs~()Ljava/util/stream/LongStream;"),
THREAD_LOCAL_RANDOM_LONGS_WITH_LONG(41, ThreadLocalRandom.class.getName() + "/longs~()Ljava/util/stream/LongStream;", true),
THREAD_LOCAL_RANDOM_LONGS_WITH_TWO_LONGS(42, ThreadLocalRandom.class.getName() + "/longs~(JJ)Ljava/util/stream/LongStream;", true),
THREAD_LOCAL_RANDOM_LONGS_WITH_THIRD_LONGS(43, ThreadLocalRandom.class.getName() + "/longs~(JJJ)Ljava/util/stream/LongStream;", true),
/**
* SecureRandom
*/
SECURE_RANDOM_NEXT_BYTES(44, SecureRandom.class.getName() + "/nextBytes~([B)V", true, true),
;
private static final Map<Integer, RandomMethod> CODE_CACHED = new HashMap<>(values().length);
private static final Map<String, RandomMethod> METHOD_IDENTITY_CACHED = new HashMap<>(values().length);
static {
for (RandomMethod randomMethod : values()) {
if (null != randomMethod) {
METHOD_IDENTITY_CACHED.put(randomMethod.getMethodDesc(), randomMethod);
CODE_CACHED.put(randomMethod.getCode(), randomMethod);
}
}
}
public static RandomMethod getByMethodIdentity(String className, String methodName,
String methodDesc) {
return METHOD_IDENTITY_CACHED.get(String.format("%s/%s~%s", className, methodName, methodDesc));
}
public static RandomMethod getByCode(Integer code) {
return CODE_CACHED.get(code);
}
private final Integer code;
private final String methodDesc;
private final boolean hasParam;
private final boolean nextBytes;
RandomMethod(Integer code, String methodDesc) {
this(code, methodDesc, false, false);
}
RandomMethod(boolean nextBytes, Integer code, String methodDesc) {
this(code, methodDesc, false, nextBytes);
}
RandomMethod(Integer code, String methodDesc, boolean hasParam) {
this(code, methodDesc, hasParam, false);
}
RandomMethod(Integer code, String methodDesc, boolean hasParam, boolean nextBytes) {
this.code = code;
this.methodDesc = methodDesc;
this.hasParam = hasParam;
this.nextBytes = nextBytes;
}
public Integer getCode() {
return code;
}
public String getMethodDesc() {
return methodDesc;
}
public boolean isHasParam() {
return hasParam;
}
public boolean isNextBytes() {
return nextBytes;
}
}
InvokeRandomTransformClassVisitorTest
public class InvokeRandomTransformClassVisitorTest extends ClassLoader {
public Class<?> loadClassFromBytes(String className, byte[] classBytes) {
// Define class from byte array
return defineClass(className, classBytes, 0, classBytes.length);
}
public static void main(String[] args) throws Exception {
String classpath = "原始class文件";
String outputClassPath = "改寫後class文件保存路徑";
executeByteCodeRewrite(classPath, outputClassPath);
}
private static void executeByteCodeRewrite(String classPath, String outputClassPath)
throws Exception {
// 從class文件中讀取字節數組
byte[] classFileBuffer = Files.readAllBytes(Paths.get(classPath));
TransformResult transformResult = ExtensionUtil.transformMethod(null, classFileBuffer,
InvokeRandomTransformClassVisitor::new);
byte[] modifiedClassBytes = transformResult.getClassBuffer();
// 將修改後的字節數組寫入到指定路徑的文件中
try (FileOutputStream fos = new FileOutputStream(outputClassPath)) {
fos.write(modifiedClassBytes);
}
System.out.println("Class transformed and written to file: " + outputClassPath);
// 加載修改後的類並使用反射調用方法驗證結果
Class<?> clazz = new InvokeRandomTransformClassVisitorTest().loadClassFromBytes(
InvokeRandomTransformClassVisitorEntity.class.getName(), modifiedClassBytes);
Object instance = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getMethod("testRandom", String.class);
System.out.println(method.invoke(instance, ""));
}
}
InvokeRandomTransformClassVisitorEntity
public class InvokeRandomTransformClassVisitorEntity {
public List<Object> testRandom(String noUse) {
Random random = new Random();
List<Object> list = new ArrayList<>();
random.ints(4).forEach(list::add);
list.add(random.nextInt(1_000));
list.add(ThreadLocalRandom.current().nextInt(1_000));
list.add(ThreadLocalRandom.current().nextInt());
list.add(ThreadLocalRandom.current().nextInt(1, 10));
list.add(ThreadLocalRandom.current().nextDouble());
list.add(ThreadLocalRandom.current().nextLong());
// list.add(ThreadLocalRandom.current().nextDouble(1.0));
// list.add(ThreadLocalRandom.current().nextDouble(1.0, 100.0));
byte[] bytes = new byte[16];
new SecureRandom().nextBytes(bytes);
list.add(bytes);
return list;
}
}