ASM的tree?api對(duì)匿名線程的hook操作詳解
背景
看完本章,你將會(huì)學(xué)習(xí)到用ASM的tree api進(jìn)行對(duì)匿名線程的hook操作,同時(shí)也能夠了解到asm相關(guān)的操作和背景知識(shí)介紹!對(duì)于ASM插樁來(lái)說(shuō),可能很多人都不陌生了,但是大多數(shù)可能都停留在core api上,對(duì)于現(xiàn)在市面上的一些插樁庫(kù),其實(shí)很多都用tree api進(jìn)行編寫(xiě)了,因?yàn)閠ree api的簡(jiǎn)單與明了的特性,也越來(lái)越成為許多開(kāi)源庫(kù)的選擇。(ASM有兩套api類型,分別是core 和 tree)

ASM介紹
ASM其實(shí)就是一個(gè)可以編譯字節(jié)碼的工具,比如說(shuō)我們?nèi)粘i_(kāi)發(fā)會(huì)引入很多的類庫(kù)對(duì)不對(duì),又或者說(shuō)我們的項(xiàng)目太大了,想修改某個(gè)點(diǎn)的時(shí)候,統(tǒng)一修改容易出錯(cuò)(比如隱私合規(guī)問(wèn)題等),這個(gè)時(shí)候如果能有一個(gè)工具對(duì)生成后的class文件進(jìn)行編輯的話,就非常方便我們進(jìn)行后續(xù)的工作了。
本章主要介紹tree api,下文所說(shuō)的ASM都是指tree api的操作哦,對(duì)于core api的介紹可以查看筆者曾經(jīng)寫(xiě)過(guò)的文章Spider。
class文件
我們常說(shuō)的class文件,其實(shí)從二進(jìn)制的角度出發(fā),無(wú)非是分成以下幾個(gè)部分:

可以看到,一個(gè)class文件其實(shí)就是由上圖中的多個(gè)部分組成,而ASM,就是把這些結(jié)構(gòu)進(jìn)行了更進(jìn)一步的抽象,對(duì)于class文件,其實(shí)就是抽象成asm中的class node類

對(duì)于一個(gè)class文件來(lái)說(shuō),通過(guò)以下就可以進(jìn)行唯一性識(shí)別,分別是:version(版本),access(作用域,比如private等修飾符),name(名稱),signature(泛型簽名),superName(父類),interfaces(實(shí)現(xiàn)的接口),fields(當(dāng)前的屬性),methoss(當(dāng)前的方法)。 所以如果想要修改一個(gè)class,我們修改對(duì)應(yīng)的classNode即可
fields
屬性,也是類非常重要的一部分,在字節(jié)碼中,是如此定義的

對(duì)于一個(gè)屬性,ASM將其抽象為FieldNode

對(duì)于一個(gè)屬性field來(lái)說(shuō),通過(guò)以下就可以進(jìn)行唯一性識(shí)別:access(作用域,跟class結(jié)構(gòu)一樣,比如private修飾),name(屬性名稱),desc(簽名),signature(泛型簽名),value(當(dāng)前對(duì)應(yīng)的數(shù)值)
methods
相比于屬性,我們的方法結(jié)構(gòu)更為復(fù)雜

相比于屬性的單一,一個(gè)方法可能由多條指令組成而,一個(gè)方法的成功執(zhí)行,也涉及到局部變量表跟操作數(shù)棧的配合。ASM中把方法抽象成這樣一個(gè)定義 方法 = 方法頭+方法體
方法頭:即標(biāo)識(shí)一個(gè)方法的基本屬性,包括:access(作用域),name(方法名),desc(方法簽名),signature(泛型簽名),exceptions(方法可以拋出的異常)

方法體:相比于方法頭,方法體的概念其實(shí)就比較簡(jiǎn)單了,其實(shí)方法體就是方法的各條指令的集合,主要包括instrutions(方法的指令集),tryCatchBlocks(異常的節(jié)點(diǎn)集),maxStack(操作數(shù)棧的最大深度),maxLocals(本地變量表的最大長(zhǎng)度)

可以看到,方法其中的InsnList對(duì)象,是特指方法的指令集的抽象,這里繼續(xù)講解
InsnList
public class InsnList implements Iterable<AbstractInsnNode> {
private int size;
private AbstractInsnNode firstInsn;
private AbstractInsnNode lastInsn;
AbstractInsnNode[] cache;
...
可以看到,主要的對(duì)象就是firstInsn,與lastInsn,代表著方法指令集的頭指令與尾指令,每一個(gè)指令其實(shí)都被抽象成了AbstractInsnNode的子類,AbstractInsnNode定義了一條指令最基礎(chǔ)的信息,我們可以看看這個(gè)類的子類

這里我們?cè)倏纯次覀冏畛S玫膍ethodInsnNode
public class MethodInsnNode extends AbstractInsnNode {
/**
* The internal name of the method's owner class (see {@link
* org.objectweb.asm.Type#getInternalName()}).
*
* <p>For methods of arrays, e.g., {@code clone()}, the array type descriptor.
*/
public String owner;
/** The method's name. */
public String name;
/** The method's descriptor (see {@link org.objectweb.asm.Type}). */
public String desc;
/** Whether the method's owner class if an interface. */
public boolean itf;
這個(gè)就是一個(gè)普通方法指令最根本的定義了,owner(方法調(diào)用者),name(方法名稱),desc(方法簽名)等等,他們都有著相似的結(jié)構(gòu),這個(gè)也是我們接下來(lái)會(huì)實(shí)戰(zhàn)的重點(diǎn)。
Signature
嗯!我們最后介紹一下這個(gè)神奇的東西!不知道大家在看介紹的時(shí)候,有沒(méi)有一臉疑惑,這個(gè)我解釋為泛型簽名,這個(gè)跟desc(函數(shù)簽名)參數(shù)有什么區(qū)別呢?當(dāng)然,這個(gè)不僅僅在函數(shù)上有出現(xiàn),在屬性,類的結(jié)構(gòu)上都有出現(xiàn)!是不是非常神奇!
其實(shí)Signature屬性是在JDK 1.5發(fā)布后增加到了Class文件規(guī)范之中,它是一個(gè)可選的定長(zhǎng)屬性, 可以出現(xiàn)于類、屬性表和方法表結(jié)構(gòu)的屬性表中。我們想想看,jdk1.5究竟是發(fā)生什么了!其實(shí)就是對(duì)泛型的支持,那么1.5版本之前的sdk怎么辦,是不是也要進(jìn)行兼容了!所以java標(biāo)準(zhǔn)組就想到了一個(gè)折中的方法,就是泛型擦除,泛型信息編譯(類型變量、參數(shù)化類型)之后 都通通被擦除掉,以此來(lái)進(jìn)行對(duì)前者的兼容。那么這又導(dǎo)致了一個(gè)問(wèn)題,擦除的泛型信息有時(shí)候正是我們所需要的,所以Signature就出現(xiàn)了,把這些泛型信息存儲(chǔ)在這里,以提供運(yùn)行時(shí)反射等類型信息的獲??!實(shí)際上可以看到,我們大部分的方法或者屬性這個(gè)值都為null,只有存在泛型定義的時(shí)候,泛型的信息才會(huì)被存儲(chǔ)在Signature里面
實(shí)戰(zhàn)部分
好啦!有了理論基礎(chǔ),我們也該去實(shí)戰(zhàn)一下,才不是口水文!以我們線程優(yōu)化為例子,在工作項(xiàng)目中,或者在老項(xiàng)目中,可能存在大多數(shù)不規(guī)范的線程創(chuàng)建操作,比如直接new Thread等等,這樣生成的線程名就會(huì)被賦予默認(rèn)的名字,我們這里先把這類線程叫做“匿名線程”!當(dāng)然!并不是說(shuō)這個(gè)線程沒(méi)有名字,而是線程名一般是“Thread -1 ”這種沒(méi)有額外信息含量的名字,這樣對(duì)我們后期的線程維護(hù)會(huì)帶來(lái)很大的干擾,時(shí)間長(zhǎng)了,可能就存在大多數(shù)這種匿名線程,有可能帶來(lái)線程創(chuàng)建的oom crash!所以我們的目標(biāo)是,給這些線程賦予“名字”,即調(diào)用者的名字
解決“匿名”Thread
為了達(dá)到這個(gè)目的,我們需要對(duì)thread的構(gòu)造有一個(gè)了解,當(dāng)然Thread的構(gòu)造函數(shù)有很多,我們舉幾個(gè)例子
public Thread(String name) {
init(null, null, name, 0);
}
public Thread(ThreadGroup group, String name) {
init(group, null, name, 0);
}
可以看到,我們Thread的多個(gè)構(gòu)造函數(shù),最后一個(gè)參數(shù)都是name,即Thread的名稱,所以我們的hook點(diǎn)是,能不能在Thread的構(gòu)造過(guò)程,調(diào)用到有name的構(gòu)造函數(shù)是不是就可以實(shí)現(xiàn)我們的目的了!我們?cè)倏匆幌缕胀ǖ膎ew Thread()字節(jié)碼

那么我們?cè)趺床拍馨裯ew Thread()的方式變成 new Thread(name)的方式呢?很簡(jiǎn)單!只需要我們把init的這條指令變成有參的方式就可以了,怎么改變呢?其實(shí)就是改變desc!方法簽名即可,因?yàn)橐粋€(gè)方法的調(diào)用,就是依據(jù)方法簽名進(jìn)行匹配的。我們?cè)诤瘮?shù)后面添加一個(gè)string的參數(shù)即可
node是methidInsnNode
def desc =
"${node.desc.substring(0, r)}Ljava/lang/String;${node.desc.substring(r)}"
node.desc = desc
那么這樣我們就可以完成了嗎,非也非也,我們只是給方法簽名對(duì)加了一個(gè)參數(shù),但是這并不代表我們函數(shù)就是這么運(yùn)行的!因?yàn)榉椒▍?shù)的參數(shù)列表中的string參數(shù)我們還沒(méi)放入操作數(shù)棧呢!那么我們就可以構(gòu)造一個(gè)string參數(shù)放入操作數(shù)棧中,這個(gè)指令就是ldc指令啦!asm為我們提供了一個(gè)類是LdcInsnNode,我們可以創(chuàng)建一個(gè)該類對(duì)象即可,構(gòu)造參數(shù)需要傳入一個(gè)字符串,那么這個(gè)就可以把當(dāng)前方法的owner(解釋如上,調(diào)用者名稱)放進(jìn)去了,是不是就達(dá)到我們想要的目的了!好啦!東西我們又了,我們要在哪里插入呢?

所以我們的目標(biāo)很明確,就是在init指令調(diào)用前插入即可,asm也提供了insertBefore方法,提供在某個(gè)指令前插入的便捷操作。
method.instructions.insertBefore(
node,
new LdcInsnNode(klass.name)
)
我們看看最后插入后的字節(jié)碼

當(dāng)然,我們插入asm代碼一般是在android提供給我們的Transform階段進(jìn)行的(agp新版有改變,但是大體工作流程一致),所以我們?cè)趖ransfrom中為了避免對(duì)類的過(guò)度干擾,我們還需要把不必要的階段提早剔除!比如我們只在new Thread操作,那么就把非Opcodes.INVOKESPECIAL的操作過(guò)濾即可。還有就是非init階段(即非構(gòu)造函數(shù)階段)或者owner不為T(mén)hread類就可以提前過(guò)濾,不參與更改即可。
那我們看到完整的代碼(需要在Transform中執(zhí)行的代碼)
static void transform(ClassNode klass) {
println("ThreadTransformUtils")
// 這里只處理Thread
klass.methods?.forEach { methodNode ->
methodNode.instructions.each {
// 如果是構(gòu)造函數(shù)才繼續(xù)進(jìn)行
if (it.opcode == Opcodes.INVOKESPECIAL) {
transformInvokeSpecial((MethodInsnNode) it, klass, methodNode)
}
}
}
}
private static void transformInvokeSpecial(MethodInsnNode node, ClassNode klass, MethodNode method) {
// 如果不是構(gòu)造函數(shù),就直接退出
if (node.name != "<init>" || node.owner != THREAD) {
return
}
println("transformInvokeSpecial")
transformThreadInvokeSpecial(node, klass, method)
}
private static void transformThreadInvokeSpecial(
MethodInsnNode node,
ClassNode klass,
MethodNode method
) {
switch (node.desc) {
// Thread()
case "()V":
// Thread(Runnable)
case "(Ljava/lang/Runnable;)V":
method.instructions.insertBefore(
node,
new LdcInsnNode(klass.name)
)
def r = node.desc.lastIndexOf(')')
def desc =
"${node.desc.substring(0, r)}Ljava/lang/String;${node.desc.substring(r)}"
// println(" + $SHADOW_THREAD.makeThreadName(Ljava/lang/String;Ljava/lang/String;) => ${this.owner}.${this.name}${this.desc}: ${klass.name}.${method.name}${method.desc}")
println(" * ${node.owner}.${node.name}${node.desc} => ${node.owner}.${node.name}$desc: ${klass.name}.${method.name}${method.desc}")
node.desc = desc
break
}
}
最后
看到這里,應(yīng)該可以了解到asm tree api相關(guān)用法與實(shí)戰(zhàn)了,希望能有所幫助!
以上就是ASM的tree api對(duì)匿名線程的hook操作詳解的詳細(xì)內(nèi)容,更多關(guān)于ASM tree api匿名線程hook的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android中使用的定時(shí)針(刷新頁(yè)面請(qǐng)求服務(wù)器)詳解
這篇文章主要介紹了Android中使用的定時(shí)針(刷新頁(yè)面請(qǐng)求服務(wù)器)詳解的相關(guān)資料,需要的朋友可以參考下2016-12-12
Android自定義ViewGroup之FlowLayout(三)
這篇文章主要為大家詳細(xì)介紹了Android自定義ViewGroup之FlowLayout,常用于關(guān)鍵字標(biāo)簽,搜索熱詞列表等功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-09-09
Flutter 實(shí)現(xiàn)網(wǎng)易云音樂(lè)字幕的代碼
這篇文章主要介紹了Flutter 實(shí)現(xiàn)網(wǎng)易云音樂(lè)字幕的代碼,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04
Android 系統(tǒng)相機(jī)拍照后相片無(wú)法在相冊(cè)中顯示解決辦法
這篇文章主要介紹了Android 系統(tǒng)相機(jī)拍照后相片無(wú)法在相冊(cè)中顯示解決辦法的相關(guān)資料,需要的朋友可以參考下2016-12-12
安卓 獲取手機(jī)IP地址的實(shí)現(xiàn)代碼
本篇文章主要介紹 Android 4.0 獲取手機(jī)IP地址的方法,附有實(shí)現(xiàn)代碼,具有參考價(jià)值,希望對(duì)有需要的小伙伴有幫助2016-07-07
Android中TextView和ImageView實(shí)現(xiàn)傾斜效果
這篇文章主要為大家詳細(xì)介紹了Android中TextView和ImageView實(shí)現(xiàn)傾斜效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08

