ASM 概述

0x00 什么是 ASM

ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form.

ASM offers similar functionality as other Java bytecode frameworks, but is focused on performance. Because it was designed and implemented to be as small and as fast as possible.

0x10 為什么要操縱分析字節碼

  • 程序分析,發現 bug,檢測無用代碼
    • JaCoCo(Java Code Coverage Library 用于檢查單元測試覆蓋率)
  • 產生代碼
    • openJDK lambda、Groovy 編譯器、Kotlin 編譯器
  • 優化、混淆代碼,注入調試及監控代碼等
    • Aspectj
    • Proguard

可以利用 ASM 實現自己的代碼混淆工具

0x20 ASM 編程框架簡介

Core API

代理模式、訪問者模式

  • ClassReader

    • 解析字節碼文件,調用 ClassVisitor 的特定的方法訪問 Class 的字段、方法以及字節碼
  • ClassVisitor

    • 用于訪問 Java 類文件
    • 必須以下面的順序調用該類的方法
    {@code visit} [ {@code visitSource} ] [ {@code visitModule} ][ {@code visitNestHost} ][ {@code
    visitOuterClass} ] ( {@code visitAnnotation} | {@code visitTypeAnnotation} | {@code
    visitAttribute} )* ( {@code visitNestMember} | {@code visitInnerClass} | {@code visitField} |
    {@code visitMethod} )* {@code visitEnd}.
    
    • 如果需要增加或者刪除類的字段/方法,可以自定義該類實現
  • FieldVisitor

    • 用于訪問 Java 類的字段
    • 必須以下面的順序調用該類的方法
    ( {@code visitAnnotation} | {@code visitTypeAnnotation} | {@code visitAttribute} )* {@code visitEnd}.
    
    • 修改類字段的內容,通過自定義 ClassVisitor 已經可以實現,自定義該類的場景不多
  • MethodVisitor

    • 用于訪問 Java 類的方法
    • 必須以下面的順序調用該類的方法
    ( {@code visitParameter} )* [ {@code visitAnnotationDefault} ] ( {@code visitAnnotation} |
    {@code visitAnnotableParameterCount} | {@code visitParameterAnnotation} {@code
    visitTypeAnnotation} | {@code visitAttribute} )* [ {@code visitCode} ( {@code visitFrame} |
    {@code visit<i>X</i>Insn} | {@code visitLabel} | {@code visitInsnAnnotation} | {@code
    visitTryCatchBlock} | {@code visitTryCatchAnnotation} | {@code visitLocalVariable} | {@code
    visitLocalVariableAnnotation} | {@code visitLineNumber} )* {@code visitMaxs} ] {@code visitEnd}.
    
    • 如果需要修改類方法的實現,需要自定義該類
  • ClassWriter

    • 存儲類文件字節碼信息,用于生成類文件的 ClassVisitor
      • ClassWriter 內部有個 FieldWriter 鏈表用于保存類文件的所有 Field 信息
      • ClassWriter 內部有個 MethodWriter 鏈表用于保存類文件的所有 Method 信息
    • 與一個或多個 ClassReader 及 ClassVisitor 一起使用修改類文件

Core API 實現原理

下面通過一個簡單的字節碼文件拷貝的例子來分析 ASM 的原理

ASM 實現字節碼拷貝關鍵代碼

......
// inputClass 為輸入字節碼文件
InputStream inputStream = new FileInputStream(inputClass);
final ClassReader classReader = new ClassReader(inputStream);
final ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classReader.accept(classWriter, ClassReader.EXPAND_FRAMES);
final byte[] newClassFile = classWriter.toByteArray();
......

WorkFlow

  • ASM Core API 主要用到了兩種關鍵的設計模式,代理模式及訪問者模式
  • ClassReader 負責讀取要修改的字節碼文件,同時根據字節碼文件的格式規定了訪問方式(也就是 ClassVisitor 以及 FieldVisitor、MethodVisitor 的 visitXxx 方法的調用順序)
  • ClassWriter 繼承自 ClassVisitor,如果 ClassWriter 直接作為訪問者訪問 ClassReader,也就是上面的例子。字節碼最終不會修改只是保存到 ClassWriter 中
  • 如果我們需要增加、刪除 Filed 或者 Method,我們只需要自定義 ClassVisitor 代理 ClassWriter,用自定義的 ClassVisitor 訪問 ClassReader 即可,這樣在 ClassReader 使用我們自定義的 ClassVisitor 的 visitXxx 遍歷字節碼的 Field、Method 時,我們可以重寫 visitXxx 方法來實現相應的功能
  • 如果需要修改 Method 的內部實現,我們不僅需要自定義 ClassVisitor 還需要自定義 MethodVisitor 用于代理 ClassWriter 內部 MethodWriter 訪問 Method 的實現,并在訪問過程中做出修改
  • ClassWriter 的 toByteArray 方法實際只是 dump 出內部兩個鏈表中保存的字節碼信息

Tree API

  • 不介紹

0x30 使用 ASM 進行 AOP 編程 (Android studio 插件開發)

類中添加 Field

  • 例子
public class ASMAddField {
    public static final String LOG_TAG = "ASM.ASMAddField";
    // 使用 ASM 增加 addField 字段
//    public Object addField;

    private ASMAddField() {

    }
}
  • 關鍵實現代碼
......
private class AddFieldAdapter extends ClassVisitor {
    public AddFieldAdapter(int api, ClassVisitor cv) {
        super(api, cv);
    }
    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        // 檢查字段是否已經存在
        if (name.equals(fieldName)) {
            isFieldPresent = true;
        }
        return super.visitField(access, name, desc, signature, value);
    }
    @Override
    public void visitEnd() {
        if (!isFieldPresent) {
            // 添加字段
            FieldVisitor fieldVisitor =
                    cv.visitField(fieldACC, fieldName, fieldDesc, null, null);
            if (fieldVisitor != null) {
                fieldVisitor.visitEnd();
            }
        }
        super.visitEnd();
    }
}
......

類中刪除 Field

  • 例子
public class ASMDeleteField {
    public static final String LOG_TAG = "ASM.ASMDeleteField";
    // 使用 ASM 刪除該字段
    public Object deleteField = new Object();

    private ASMDeleteField() {

    }
}
  • 關鍵實現代碼
private class DeleteFieldAdapter extends ClassVisitor {
    public DeleteFieldAdapter(int api, ClassVisitor cv) {
        super(api, cv);
    }

    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        if (name.equals(fieldName)) {
            return null;
        }
        return super.visitField(access, name, desc, signature, value);
    }
}

類中增加 Method

  • 例子
public class ASMAddMethod {
    public static final String LOG_TAG = "ASM.ASMAddMethod";

    private ASMAddMethod() {
    }

    // 使用 ASM 增加下面方法
    // public static void addMethod() {
    //     Log.i(LOG_TAG, "this is add method");
    // }
}
  • 關鍵實現代碼
private class AddMethodAdapter extends ClassVisitor {
    public AddMethodAdapter(int api, ClassVisitor cv) {
        super(api, cv);
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        if (name.equals(methodName) && desc.equals(methodDesc)) {
            isMethodPresent = true;
        }
        return super.visitMethod(access, name, desc, signature, exceptions);
    }
    @Override
    public void visitEnd() {
        if (!isMethodPresent) {
            // 增加方法及方法的內部實現,可以借助 ASM Bytecode Outline 2017 插件事先獲得方法的內部實現
            MethodVisitor methodVisitor = cv.visitMethod(methodACC,
                    methodName, methodDesc, null, null);
            if (methodVisitor != null) {
                methodVisitor.visitCode();
                methodVisitor.visitLdcInsn("ASM.ASMAddMethod");
                methodVisitor.visitLdcInsn("this is add method");
                methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC,
                        "android/util/Log",
                        "i",
                        "(Ljava/lang/String;Ljava/lang/String;)I",
                        false);
                methodVisitor.visitInsn(POP);
                methodVisitor.visitInsn(Opcodes.RETURN);
                methodVisitor.visitMaxs(2, 0);
                methodVisitor.visitEnd();
            }
        }
        super.visitEnd();
    }
}

類中刪除 Method

  • 例子
public class ASMDeleteMethod {
    public static final String LOG_TAG = "ASM.ASMDeleteMethod";

    private ASMDeleteMethod() {
    }

    // 使用 ASM 刪除該方法
    public static void deleteMethod() {
        Log.i(LOG_TAG, "this is delete method");
    }
}
  • 關鍵實現代碼
private class DeleteMethodAdapter extends ClassVisitor {
    public DeleteMethodAdapter(int api, ClassVisitor cv) {
        super(api, cv);
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        if (access == methodACC
                && name.equals(methodName)
                && desc.equals(methodDesc)) {
            return null;
        }
        return super.visitMethod(access, name, desc, signature, exceptions);
    }
}

修改類中的 Method 實現

  • 例子
public class ASMModifyMethod {
    public static final String LOG_TAG = "ASM.ASMModifyMethod";

    private ASMModifyMethod() {

    }

    // 使用 ASM 增加方法耗時打印
    public static void modifyMethod() {
//        long startTime = System.currentTimeMillis();
        Log.i(LOG_TAG, "this is modify method");
//        long endTime = System.currentTimeMillis();
//        Log.i(LOG_TAG, "method consume time: " + (endTime - startTime) + "ms");
    }
}
  • 關鍵實現代碼
private class ModifyMethodAdapter extends ClassVisitor {
    public ModifyMethodAdapter(int api, ClassVisitor cv) {
        super(api, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
        if (mv != null
                && access == methodACC
                && name.equals(methodName)
                && desc.equals(methodDesc)) {
            mv = new MethodVisitorImpl(ASM5, methodACC, methodDesc, mv);
        }
        return mv;
    }
}
private class MethodVisitorImpl extends LocalVariablesSorter {
    private int start;
    private int end;
    protected MethodVisitorImpl(int api, int access, String desc, MethodVisitor mv) {
        super(api, access, desc, mv);
    }

    @Override
    public void visitCode() {
        super.visitCode();
        mv.visitMethodInsn(INVOKESTATIC,
                "java/lang/System",
                "currentTimeMillis",
                "()J",
                false);
        start = newLocal(Type.LONG_TYPE);
        mv.visitVarInsn(LSTORE, start);
    }
    @Override
    public void visitInsn(int opcode) {
        if (opcode == RETURN || opcode == ATHROW) {
            mv.visitMethodInsn(INVOKESTATIC,
                    "java/lang/System",
                    "currentTimeMillis",
                    "()J",
                    false);
            end = newLocal(Type.LONG_TYPE);
            mv.visitVarInsn(LSTORE, end);
            mv.visitLdcInsn("ASM.ASMModifyMethod");
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL,
                    "java/lang/StringBuilder",
                    "<init>",
                    "()V",
                    false);
            mv.visitLdcInsn("method consume time: ");
            mv.visitMethodInsn(INVOKEVIRTUAL,
                    "java/lang/StringBuilder",
                    "append",
                    "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
                    false);
            mv.visitVarInsn(LLOAD, end);
            mv.visitVarInsn(LLOAD, start);
            mv.visitInsn(LSUB);
            mv.visitMethodInsn(INVOKEVIRTUAL,
                    "java/lang/StringBuilder",
                    "append",
                    "(J)Ljava/lang/StringBuilder;",
                    false);
            mv.visitLdcInsn("ms");
            mv.visitMethodInsn(INVOKEVIRTUAL,
                    "java/lang/StringBuilder",
                    "append",
                    "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
                    false);
            mv.visitMethodInsn(INVOKEVIRTUAL,
                    "java/lang/StringBuilder",
                    "toString",
                    "()Ljava/lang/String;",
                    false);
            mv.visitMethodInsn(INVOKESTATIC,
                    "android/util/Log",
                    "i",
                    "(Ljava/lang/String;Ljava/lang/String;)I",
                    false);
            mv.visitInsn(POP);
        }
        super.visitInsn(opcode);
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        super.visitMaxs(maxStack + 4, maxLocals + 4);
    }
}

文章中的樣例 Demo 完成實現已放在 Github

Android Studio 字節碼插件:ASM Bytecode Outline 2017

0x40 ASM 在 Android 中的應用

  • R 內聯
  • D8 desugar
  • Instant Run

參考

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,156評論 6 529
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,866評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 174,880評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,398評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,202評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,743評論 1 320
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,822評論 3 438
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 41,962評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,476評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,444評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,579評論 1 365
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,129評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,840評論 3 344
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,231評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,487評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,177評論 3 388
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,568評論 2 370

推薦閱讀更多精彩內容

  • 前言 很早之前就寫過面向切面的編程思想,主要學習了AOP的思想(參考:AOP簡介)以及使用 AspectJ 實現簡...
    Whyn閱讀 10,812評論 4 40
  • 引言 什么是 ASM ? ASM 是一個 Java 字節碼操控框架。它能被用來動態生成類或者增強既有類的功能。AS...
    Chauncey_Chen閱讀 1,497評論 0 6
  • 前言 之前一直使用greys及其內部升級二次開發版來排查問題。最近周末剛好事情不多,作為一名程序員本能地想要弄懂這...
    LNAmp閱讀 8,345評論 1 11
  • https://blog.csdn.net/luanlouis/article/details/24589193 ...
    小陳阿飛閱讀 871評論 1 1
  • 反射,它就像是一種魔法,引入運行時自省能力,賦予了 Java 語言令人意外的活力,通過運行時操作元數據或對象,Ja...
    小刀愛編程閱讀 819評論 0 4