ASM在安卓開發中的應用十分廣泛,本文重點探討通過ASM對匿名內部類、Lambda表達式及方法引用的Hook。
安卓的編譯流程中Java文件會被編譯成.class
,.class
會被編譯成.dex
。而ASM的執行時機就是在.class
編譯成.dex
的過程中發生的。因此要想通過ASM修改自己碼就需要知道我們的Java文件編譯成的.class
是怎樣的。
PS:本文假設你對ASM有一定了解。
一,匿名內部類方式
我們在面試時經常會說起handler的內存泄漏問題,原因是匿名內部類默認會持有外部類的引用,因此巴拉巴拉。。。
那么匿名內部類為什么會持有外部類的引用,編譯后又是什么樣子呢?我們擼代碼看下。
寫一段簡單的啟動線程的代碼:
public class FuncActivity {
private void test1(){
new Thread(new Runnable() {
@Override
public void run() {
Log.e("FuncActivity", "thread - new Runnable");
}
}).start();
}
}
下面來看下其編譯后的產物。
1,匿名內部類編譯后的.class文件
匿名內部類會生成一個新的.class
文件,命名格式為:外部類類名$序號.class
。
首先查看一下生成的匿名內部類的.class代碼:
class FuncActivity$1 implements Runnable {
FuncActivity$1(final FuncActivity this$0) {
this.this$0 = this$0;
}
public void run() {
Log.e("FuncActivity", "thread - new Runnable");
}
}
自動生成的FuncActivity$1
實現了Runnable
接口,其構造方法傳入了外部類FuncActivity
的對象,因此匿名內部類持有了外部類的引用。
2,外部類ASM代碼
通過Android Studio查看編譯后的.class文件,發現編輯器對其做了反編譯:
public class FuncActivity {
private void test1() {
(new Thread(new Runnable() {
public void run() {
Log.e("FuncActivity", "thread - new Runnable");
}
})).start();
}
}
通過Byte Code Analyzer插件可以查看對應ASM代碼。
{
methodVisitor = classWriter.visitMethod(ACC_PRIVATE, "test1", "()V", null, null);
methodVisitor.visitCode();
...
// new Thread
methodVisitor.visitTypeInsn(NEW, "java/lang/Thread");
// 在操作數棧中復制上面new Thread的對象
methodVisitor.visitInsn(DUP);
// new 自動生成的匿名內部類(FuncActivity$1)
methodVisitor.visitTypeInsn(NEW, "me/wsj/performance/ui/FuncActivity$1");
// 復制上面的匿名內部類對象
methodVisitor.visitInsn(DUP);
// 加載當前class對象(FuncActivity.this)
methodVisitor.visitVarInsn(ALOAD, 0);
// FuncActivity$1初始化(執行init),傳入FuncActivity.this對象
methodVisitor.visitMethodInsn(INVOKESPECIAL, "me/wsj/performance/ui/FuncActivity$1", "<init>", "(Lme/wsj/performance/ui/FuncActivity;)V", false);
// new Thread對象初始化,傳入FuncActivity$1對象
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Thread", "<init>", "(Ljava/lang/Runnable;)V", false);
...
// 執行線程的start方法
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "start", "()V", false);
...
methodVisitor.visitInsn(RETURN);
...
methodVisitor.visitEnd();
}
每行代碼的含義已經大概注釋出,關鍵點就是創建了一個FuncActivity$1
對象,并將其作為參數創建一個Thread
,最后執行了Thread
對象的start()
方法。
3,植入代碼
明白了代碼編譯后的大概執行邏輯就可以對其進行hook植入代碼。
通過重寫ClassVisitor
的visitMethod()
方法,在其中根據方法的name
及descriptor
做過濾即可找到需要hook的Method:
class TrackerClassAdapter(api: Int, cv: ClassVisitor?) : BaseClassVisitor(api, cv) {
override fun visitMethod(
access: Int,
name: String,
descriptor: String?,
signature: String?,
exceptions: Array<out String>?,
mv: MethodVisitor
): MethodVisitor {
// 找到需要hook的Method
if (isRunMethod(name, descriptor)) {
// 自定義MethodVisitor
return TrackerMethodAdapter(descriptor, api, access, mv)
}
return mv
}
// 根據方法的name及descriptor做判斷
fun isRunMethod(name: String, descriptor: String?): Boolean {
return name == "run" && descriptor == "()V"
}
}
通過在自定義的MethodVisitor
中重寫visitInSn()
即可植入自定義代碼:
class TrackerMethodAdapter(
private val descriptor: String?,
api: Int,
access: Int,
mv: MethodVisitor?
) : LocalVariablesSorter(api, access, descriptor, mv) {
override fun visitInsn(opcode: Int) {
// 植入代碼
weaveTrackCode()
super.visitInsn(opcode)
}
private fun weaveTrackCode() {
mv.visitMethodInsn(
INVOKESTATIC,
"me/wsj/apm/thread/ThreadTracker",
"trackOnce",
"()V",
false
)
}
}
植入代碼后編譯出的.class代碼如下:
private void test1() {
(new Thread(new Runnable() {
public void run() {
Log.e("FuncActivity", "thread - new Runnable");
ThreadTracker.trackOnce("me.wsj.performance.ui.FuncActivity$1 line num: 33");
}
})).start();
}
詳細代碼可以參考:OutSiderAPM/ThreadTrackerClassAdapter.kt at master · Jinxqq/OutSiderAPM · GitHub
4,小結
1,自定義ClassVisitor
重寫visitMethod()
方法
2,在其中根據方法的name
及descriptor
做過濾,找到需要hook的Method
3,自定義MethodVisitor
重寫visitInSn()
方法
4,在其中插入自定義代碼的ASM代碼
二,Lambda方式
通過Lambda表達式啟動線程:
public class FuncActivity {
private void test2(){
new Thread(() -> {
Log.e("FuncActivity", "thread - lambda");
}).start();
}
}
1,編譯后的.class文件
通過Android Studio查看編譯后的.class文件,發現編輯器對其做了反編譯,看到的還是Lambda的形式:
public class FuncActivity {
private void test2() {
(new Thread(() -> {
Log.e("FuncActivity", "thread - lambda");
})).start();
}
}
實際上Lambda表達式在編譯時會生成一個方法,此方法默認是隱藏的,如果想查看,可以使用 java 的 javap -p -v xxx.class
命令查看這個方法。也可以通過編譯后的ASM代碼查看生成的方法。
2,編譯后的ASM代碼
通過Byte Code Analyzer插件查看對應ASM代碼如下:
// test2方法部分
{
methodVisitor = classWriter.visitMethod(ACC_PRIVATE, "test2", "()V", null, null);
methodVisitor.visitCode();
...
methodVisitor.visitTypeInsn(NEW, "java/lang/Thread");
methodVisitor.visitInsn(DUP);
// 通過visitInvokeDynamicInsn訪問lambda方法
methodVisitor.visitInvokeDynamicInsn("run", "()Ljava/lang/Runnable;", new Handle(Opcodes.H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory", "metafactory", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", false), new Object[]{Type.getType("()V"), new Handle(Opcodes.H_INVOKESTATIC, "me/wsj/performance/ui/FuncActivity", "lambda$test2$0", "()V", false), Type.getType("()V")});
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Thread", "<init>", "(Ljava/lang/Runnable;)V", false);
...
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "start", "()V", false);
...
methodVisitor.visitInsn(RETURN);
...
methodVisitor.visitEnd();
}
// Lambda方法部分
{
methodVisitor = classWriter.visitMethod(ACC_PRIVATE | ACC_STATIC | ACC_SYNTHETIC, "lambda$test2$0", "()V", null, null);
methodVisitor.visitCode();
...
methodVisitor.visitLdcInsn("FuncActivity");
methodVisitor.visitLdcInsn("thread - lambda");
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
methodVisitor.visitInsn(POP);
...
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(2, 0);
methodVisitor.visitEnd();
}
代碼包含兩個方法:
第一個方法就是我們寫的test2()
,其大致邏輯如下:
- 通過
NEW
指令創建一個Thread
對象 - 通過
INVOKEDYNAMIC
指令創建一個Runnable
對象 - 使用這個
Runnable
對象初始化Thread
對象 - 執行了
Thread
對象的start()
方法
第二個方法是自動生成的方法,使用了ACC_SYNTHETIC
來修飾,方法名為lambda$test2$0
。其中包含了我們寫的Lambda表達式中的代碼邏輯。
第一個方法通過InvokeDynamic指令調用了第二個方法,InvokeDynamic指令是在 JDK 7 引入的,用來實現動態類型語言功能,簡單來說就是能夠在運行時去調用實際的代碼。接著重點看一下methodVisitor.visitInvokeDynamicInsn()
:
// 這行代碼很長,格式化如下
methodVisitor.visitInvokeDynamicInsn(
"run",
"()Ljava/lang/Runnable;",
new Handle(
Opcodes.H_INVOKESTATIC,
"java/lang/invoke/LambdaMetafactory",
"metafactory",
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;",
false
),
new Object[]{
Type.getType("()V"),
new Handle(
Opcodes.H_INVOKESTATIC,
"me/wsj/performance/ui/FuncActivity",
// 第二個方法的name
"lambda$test2$0",
"()V",
false
),
Type.getType("()V")
}
);
MethodType 描述了方法的參數和返回值,MethodHandle 則是根據類名、方法名并且配合 MethodType 來找到特定方法然后執行它;MethodType 和 MethodHandle 配合起來完整表達了一個方法的構成。
其中第四個參數new Object[]
的第二個參數是一個MethodHandle
對象,MethodHandle
對象的第二個參數是Lambda方法所在的類名,三個參數lambda$test2$0
是Lambda方法的方法名。
那么我們簡單總結一下Lambda編譯后的邏輯就是:根據Lambda表達式的代碼生成一個方法,在調用Lambda表達式的地方調用這個生成的方法。事實上InvokeDynamic指令會創建一個對象,對象內部調用了生成的方法,這里不做深究。
下面分兩種方式實現對Lambda表達式hook:
3,對Lambda表達式hook(一)
和匿名內部類的方法一樣只需要找到Lambda表達式在編譯時代碼生成方法(如lambda$test2$0
),在其中插入代碼即可。
class LambdaNodeAdapter(api: Int, val classVisitor: ClassVisitor) : ClassNode(api) {
init {
this.cv = classVisitor
}
override fun visitEnd() {
super.visitEnd()
if (TypeUtil.isNeedWeaveMethod(this.name, access)) {
val shouldHookMethodList = mutableSetOf<String>()
for (methodNode in this.methods) {
// 判斷方法內部是否有需要處理的 lambda 表達式
val invokeDynamicInsnNodes = methodNode.findHookPointLambda()
invokeDynamicInsnNodes.forEach {
val handle = it.bsmArgs[1] as? Handle
if (handle != null) {
shouldHookMethodList.add(handle.name + handle.desc)
}
}
}
if (shouldHookMethodList.isNotEmpty()) {
for (methodNode in methods) {
val methodNameWithDesc = methodNode.nameWithDesc
if (shouldHookMethodList.contains(methodNameWithDesc)) {
// 獲取當前方法的指令集
val instructions = methodNode.instructions
if (instructions != null && instructions.size() > 0) {
// 植入代碼
val list = InsnList()
list.add(
MethodInsnNode(
Opcodes.INVOKESTATIC,
"me/wsj/apm/thread/ThreadTracker",
"trackOnce",
"()V",
)
)
// 將要植入的指令集插入當前方法的指令集
instructions.insert(list)
}
}
}
}
}
accept(cv)
}
}
1,繼承ClassNode,可以獲取當前類中的所有方法,遍歷每個方法中的指令集找到符合條件的Lambda表達式。
2,在Lambda表達式生成的方法中的指令集中插入Hook代碼的指令集即可實現hook。
該方法存在一定缺陷:1,難以獲取插入位置的行號(可以實現,遍歷方法中的指令集合找到LineNumberNode
即可獲取)。2,只能針對Lambda表達式進行Hook,無法對方法引用進行Hook。
詳細代碼可以參考:OutSiderAPM/LambdaNodeAdapter.kt at master · Jinxqq/OutSiderAPM · GitHub
4,對Lambda表達式hook(二)
方案一存在著兩個缺陷,那么能否克服著兩個缺陷呢?
可以通過新建一個方法來代理原方法,然后在代理方法中調用原方法的同時也可以植入我們Hook的代碼。
具體做法是:生成一個新的方法,新的方法中實現 InvokeDynamic指令中描述的代碼邏輯。然后創建新的 MethodHandle
,將這個 MethodHandle
替換原本的 MethodHandle
。代碼如下:
class LambdaMethodReferAdapter(api: Int, val classVisitor: ClassVisitor) : ClassNode(api) {
init {
this.cv = classVisitor
}
private val hookSignature = "run()V"
private val syntheticMethodList = ArrayList<MethodNode>()
private val counter = AtomicInteger(0)
override fun visitEnd() {
super.visitEnd()
this.methods.forEach { methodNode ->
val iterator = methodNode.instructions.iterator()
while (iterator.hasNext()) {
val node = iterator.next()
if (node is InvokeDynamicInsnNode) {
val desc = node.desc
val descType = Type.getType(desc)
val samBaseType = descType.returnType
// sam 接口名
val samBase = samBaseType.descriptor
// sam 方法名
val samMethodName: String = node.name
val bsmArgs: Array<Any> = node.bsmArgs
// sam 方法描述符
val samMethodType = bsmArgs[0] as Type
// sam 實現方法實際參數描述符
val implMethodType = bsmArgs[2] as Type
// sam name + desc,可以用來辨別是否是需要 Hook 的 lambda 表達式
val bsmMethodNameAndDescriptor = samMethodName + samMethodType.descriptor
// 判斷是否需要hook
if (hookSignature != bsmMethodNameAndDescriptor) {
continue
}
// 中間方法的名稱
val middleMethodName =
"lambda$" + samMethodName + "\$wsj" + counter.incrementAndGet()
// 中間方法的描述符
var middleMethodDesc = ""
val descArgTypes: Array<Type> = descType.argumentTypes
if (descArgTypes.isEmpty()) {
middleMethodDesc = implMethodType.descriptor
} else {
middleMethodDesc = "("
for (tmpType in descArgTypes) {
middleMethodDesc += tmpType.descriptor
}
middleMethodDesc += implMethodType.descriptor.replace("(", "")
}
// INDY原本的handle,將此handle替換成新的handle
val oldHandle = bsmArgs[1] as Handle
val newHandle = Handle(
Opcodes.H_INVOKESTATIC,
name, middleMethodName, middleMethodDesc, false
)
val newDynamicNode = InvokeDynamicInsnNode(
node.name,
node.desc,
node.bsm,
samMethodType,
newHandle,
implMethodType
)
iterator.remove()
iterator.add(newDynamicNode)
generateMiddleMethod(oldHandle, middleMethodName, middleMethodDesc)
}
}
}
methods.addAll(syntheticMethodList)
accept(cv)
}
private fun generateMiddleMethod(
oldHandle: Handle,
middleMethodName: String,
middleMethodDesc: String
) {
val methodNode = LambdaMiddleMethodAdapter(this.name,oldHandle, middleMethodName, middleMethodDesc)
methodNode.visitCode()
// 添加到中間方法列表
syntheticMethodList.add(methodNode)
}
}
在中間方法中執行hook邏輯:
class LambdaMiddleMethodAdapter(
private val className: String,
val oldHandle: Handle,
methodName: String,
val methodDesc: String?,
) : MethodNode( /* latest api = */Opcodes.ASM8,Opcodes.ACC_PRIVATE or Opcodes.ACC_STATIC /*| Opcodes.ACC_SYNTHETIC*/,
methodName, methodDesc, null, null) {
override fun visitCode() {
super.visitCode()
// 此處執行hook邏輯
weaveHookCode(this)
// 此塊 tag 具體可以參考: [https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokedynamic](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokedynamic)
var accResult = oldHandle.tag
when (accResult) {
Opcodes.H_INVOKEINTERFACE -> accResult = Opcodes.INVOKEINTERFACE
Opcodes.H_INVOKESPECIAL -> accResult = Opcodes.INVOKESPECIAL // private, this, super 等會調用
Opcodes.H_NEWINVOKESPECIAL -> {
// constructors
accResult = Opcodes.INVOKESPECIAL
this.visitTypeInsn(Opcodes.NEW, oldHandle.owner)
this.visitInsn(Opcodes.DUP)
}
Opcodes.H_INVOKESTATIC -> accResult = Opcodes.INVOKESTATIC
Opcodes.H_INVOKEVIRTUAL -> accResult = Opcodes.INVOKEVIRTUAL
}
val middleMethodType = Type.getType(methodDesc)
val argumentsType = middleMethodType.argumentTypes
if (argumentsType.isNotEmpty()) {
var loadIndex = 0
for (tmpType in argumentsType) {
val opcode = tmpType.getOpcode(Opcodes.ILOAD)
this.visitVarInsn(opcode, loadIndex)
loadIndex += tmpType.size
}
}
this.visitMethodInsn(
accResult,
oldHandle.owner,
oldHandle.name,
oldHandle.desc,
false
)
val returnType = middleMethodType.returnType
val returnOpcodes = returnType.getOpcode(Opcodes.IRETURN)
this.visitInsn(returnOpcodes)
this.visitEnd()
}
private fun weaveHookCode(mv: MethodVisitor) {
// todo 植入代碼
}
}
最終編譯出來的.class如下:
public class FuncActivity {
private void test2() {
(new Thread(FuncActivity::lambda$run$wsj1)).start();
}
private static void lambda$run$wsj1() {
ThreadTracker.trackOnce("me/wsj/performance/ui/FuncActivity by Lambda:");
lambda$test2$0();
}
}
詳細代碼可以參考:OutSiderAPM/LambdaMethodReferAdapter.kt at master · Jinxqq/OutSiderAPM · GitHub
三,方法引用
有些時候我們可以通過方法引用來簡化Lambda表達式,如下:
public class FuncActivity {
private void test3(){
new Thread(MyThreadPool::getInstance).start();
}
}
直接看其編譯后的ASM如下:
{
methodVisitor = classWriter.visitMethod(ACC_PRIVATE, "test3", "()V", null, null);
methodVisitor.visitCode();
...
methodVisitor.visitTypeInsn(NEW, "java/lang/Thread");
methodVisitor.visitInsn(DUP);
methodVisitor.visitInvokeDynamicInsn("run", "()Ljava/lang/Runnable;", new Handle(Opcodes.H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory", "metafactory", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", false), new Object[]{Type.getType("()V"), new Handle(Opcodes.H_INVOKESTATIC, "me/wsj/performance/test/CibThreadPool", "getInstance", "()Lme/wsj/performance/test/CibThreadPool;", false), Type.getType("()V")});
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Thread", "<init>", "(Ljava/lang/Runnable;)V", false);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "start", "()V", false);
...
methodVisitor.visitInsn(RETURN);
...
methodVisitor.visitEnd();
}
可見跟Lambda一樣都是調用了InvokeDynamic指令,methodVisitor.visitInvokeDynamicInsn()
如下:
methodVisitor.visitInvokeDynamicInsn(
"run",
"()Ljava/lang/Runnable;",
new Handle(
Opcodes.H_INVOKESTATIC,
"java/lang/invoke/LambdaMetafactory",
"metafactory",
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;",
false),
new Object[]{
Type.getType("()V"),
new Handle(
Opcodes.H_INVOKESTATIC,
"me/wsj/performance/test/CibThreadPool",
"getInstance",
"()Lme/wsj/performance/test/CibThreadPool;",
false),
Type.getType("()V")
}
);
跟Lambda不同的是,這里沒有生成新的方法,因為方法引用是一個現成的方法,可以直接訪問。因此對方法引用進行hook的思路和對Lambda表達式hook一樣,都是生成一個方法,方法內調用源方法,最后替換調用處。
使用同Lambda同樣的方式(方案二),最終結果如下:
public class FuncActivity {
private void test3() {
(new Thread(FuncActivity::lambda$run$wsj2)).start();
}
private static void lambda$run$wsj2() {
ThreadTracker.trackOnce("me/wsj/performance/ui/FuncActivity by MethodReference:");
CibThreadPool.getInstance();
}
}
四,總結
匿名內部類的方式只需要根據方法名及方法簽名作為hook點,植入代碼即可。
-
Lambda表達式方式的Hook有兩種方案:
2.1,找到Lambda表達式編譯生成的方法,在其指令集中植入Hook的代碼指令集即可實現Hook。
2.2,生成一個中間方法,在這個方法中調用這個 Lambda 編譯時生成的中間方法,然后將自定義的 MethodHandle 指向生成的方法,最后替換掉Bootstrap Mehtod中的MethodHandle,達到偷梁換柱的效果。
方法引用的方式的思路是:生成一個中間方法,把方法引用里的內容放到生成的中間方法中,然后將自定義的 MethodHandle 指向生成的方法,最后替換掉 Bootstrap Method 中的 MethodHandle,達到偷梁換柱的效果。
版權聲明:本文為博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。 本文鏈接:http://www.lxweimin.com/p/aeaacb5bf367
參考: