認識 JavaAgent --獲取目標程式已載入的所有類
作者:Longofo@知道創宇404實驗室
時間:2019年12月10日
之前在一個應用中搜尋到一個類,但是在反序列化測試的時出錯,錯誤不是
class notfound
,是其他
0xxx
這樣的錯誤,透過搜尋這個錯誤大概是類沒有被載入。最近剛好看到了JavaAgent,初步學習了下,能進行攔截,主要透過Instrument Agent來進行位元組碼增強,可以進行
位元組碼插樁,bTrace,Arthas 等操作,結合ASM,javassist,cglib框架能實現更強大的功能。
Java RASP也是基於JavaAgent實現的。趁熱記錄下JavaAgent基礎概念,以及簡單使用JavaAgent
實現一個獲取目標程式已載入的類的測試。
JVMTI與Java Instrument
Java平臺偵錯程式架構(Java Platform Debugger Architecture, )是一組用於除錯Java程式碼的API(摘自維基百科):
- Java偵錯程式介面(Java Debugger Interface,JDI)——定義了一個高層次Java介面,開發人員可以利用JDI輕鬆編寫遠端除錯工具
- Java虛擬機器工具介面(Java Virtual Machine Tools Interface,JVMTI)——定義了一個原生(native)介面,可以對執行在Java虛擬機器的應用程式檢查狀態、控制執行
- Java虛擬機器除錯介面(JVMDI)——JVMDI在J2SE 5中被JVMTI取代,並在Java SE 6中被移除
- Java除錯線協議(JDWP)——定義了除錯物件(一個 Java 應用程式)和偵錯程式程式之間的通訊協議
JVMTI 提供了一套"代理"程式機制,可以支援第三方工具程式以代理的方式連線和訪問 JVM,並利用 JVMTI 提供的豐富的程式設計介面,完成很多跟 JVM 相關的功能。JVMTI是基於事件驅動的,JVM每執行到一定的邏輯就會呼叫一些事件的回撥介面(如果有的話),這些介面可以供開發者去擴充套件自己的邏輯。
JVMTIAgent是一個利用JVMTI暴露出來的介面提供了代理啟動時載入(agent on load)、代理透過attach形式載入(agent on attach)和代理解除安裝(agent on unload)功能的動態庫。Instrument Agent可以理解為一類JVMTIAgent動態庫,別名是JPLISAgent(Java Programming Language Instrumentation Services Agent),是 專門為java語言編寫的插樁服務提供支援的代理。
Instrumentation介面
以下介面是Java SE 8 [1]提供的(不同版本可能介面有變化):
void addTransformer(ClassFileTransformer transformer, boolean canRetransform)//註冊ClassFileTransformer例項,註冊多個會按照註冊順序進行呼叫。所有的類被載入完畢之後會呼叫ClassFileTransformer例項,相當於它們透過了redefineClasses方法進行重定義。布林值引數canRetransform決定這裡被重定義的類是否能夠透過retransformClasses方法進行回滾。void addTransformer(ClassFileTransformer transformer)//相當於addTransformer(transformer, false),也就是透過ClassFileTransformer例項重定義的類不能進行回滾。boolean removeTransformer(ClassFileTransformer transformer)//移除(反註冊)ClassFileTransformer例項。void retransformClasses(Class<?>... classes)//已載入類進行重新轉換的方法,重新轉換的類會被回撥到ClassFileTransformer的列表中進行處理。void appendToBootstrapClassLoaderSearch(JarFile jarfile)//將某個jar加入到Bootstrap Classpath裡優先其他jar被載入。void appendToSystemClassLoaderSearch(JarFile jarfile)//將某個jar加入到Classpath裡供AppClassloard去載入。Class[] getAllLoadedClasses()//獲取所有已經被載入的類。Class[] getInitiatedClasses(ClassLoader loader)//獲取所有已經被初始化過了的類。long getObjectSize(Object objectToSize)//獲取某個物件的(位元組)大小,注意巢狀物件或者物件中的屬性引用需要另外單獨計算。boolean isModifiableClass(Class<?> theClass)//判斷對應類是否被修改過。boolean isNativeMethodPrefixSupported()//是否支援設定native方法的字首。boolean isRedefineClassesSupported()//返回當前JVM配置是否支援重定義類(修改類的位元組碼)的特性。boolean isRetransformClassesSupported()//返回當前JVM配置是否支援類重新轉換的特性。void redefineClasses(ClassDefinition... definitions)//重定義類,也就是對已經載入的類進行重定義,ClassDefinition型別的入參包括了對應的型別Class<?>物件和位元組碼檔案對應的位元組陣列。void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix)//設定某些native方法的字首,主要在找native方法的時候做規則匹配。
:
重新定義功能在Java SE 5中進行了介紹, 重新轉換功能在Java SE 6中進行了介紹,一種猜測是將 重新轉換作為 更通用的功能引入,但是必須保留 重新定義以實現向後相容,並且 重新轉換操作也更加方便。
Instrument Agent兩種載入方式
在官方 [1]中提到,有兩種獲取Instrumentation介面例項的方法 :
- JVM在指定代理的方式下啟動,此時Instrumentation例項會傳遞到代理類的premain方法。
- JVM提供一種在啟動之後的某個時刻啟動代理的機制,此時Instrumentation例項會傳遞到代理類程式碼的agentmain方法。
premain對應的就是VM啟動時的Instrument Agent載入,即
agent on load
,agentmain對應的是VM執行時的Instrument Agent載入,即
agent on attach
。兩種載入形式所載入的
Instrument Agent
都關注同一個
JVMTI
事件 –
ClassFileLoadHook
事件,這個事件是在讀取位元組碼檔案之後回撥時用,也就是說
premain和agentmain方式的回撥時機都是類檔案位元組碼讀取之後(或者說是類載入之後),之後對位元組碼進行重定義或重轉換,不過修改的位元組碼也需要滿足一些要求,在最後的侷限性有說明。
premain與agentmain的區別:
premain
和
agentmain
兩種方式最終的目的都是為了回撥
Instrumentation
例項並啟用
sun.instrument.InstrumentationImpl#transform()
(InstrumentationImpl是Instrumentation的實現類)從而回撥註冊到
Instrumentation
中的
ClassFileTransformer
實現位元組碼修改,本質功能上沒有很大區別。兩者的非本質功能的區別如下:
-
premain方式是JDK1.5引入的,agentmain方式是JDK1.6引入的,JDK1.6之後可以自行選擇使用
premain
或者agentmain
。 -
premain
需要透過命令列使用外部代理jar包,即-javaagent:代理jar包路徑
;agentmain
則可以透過attach
機制直接附著到目標VM中載入代理,也就是使用agentmain
方式下,操作attach
的程式和被代理的程式可以是完全不同的兩個程式。 -
premain
方式回撥到ClassFileTransformer
中的類是虛擬機器載入的所有類,這個是由於代理載入的順序比較靠前決定的,在開發者邏輯看來就是:所有類首次載入並且進入程式main()
方法之前,premain
方法會被啟用,然後所有被載入的類都會執行ClassFileTransformer
列表中的回撥。 -
agentmain
方式由於是採用attach
機制,被代理的目標程式VM有可能很早之前已經啟動,當然其所有類已經被載入完成,這個時候需要藉助Instrumentation#retransformClasses(Class<?>... classes)
讓對應的類可以重新轉換,從而啟用重新轉換的類執行ClassFileTransformer
列表中的回撥。 - 透過premain方式的代理Jar包進行了更新的話,需要重啟伺服器,而agentmain方式的Jar包如果進行了更新的話,需要重新attach,但是agentmain重新attach還會導致重複的位元組碼插入問題,不過也有
Hotswap
和DCE VM
方式來避免。
透過下面的測試也能看到它們之間的一些區別。
premain載入方式
premain方式編寫步驟簡單如下:
1.編寫premain函式,包含下面兩個方法的其中之一:
java
public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);
如果兩個方法都被實現了,那麼帶Instrumentation引數的優先順序高一些,會被優先呼叫。
agentArgs
是
premain
函式得到的程式引數,透過命令列引數傳入
2.定義一個 MANIFEST.MF 檔案,必須包含 Premain-Class 選項,通常也會加入Can-Redefine-Classes 和 Can-Retransform-Classes 選項
3.將 premain 的類和 MANIFEST.MF 檔案打成 jar 包
4.使用引數 -javaagent: jar包路徑啟動代理
premain載入過程如下:
1.建立並初始化 JPLISAgent
2.MANIFEST.MF 檔案的引數,並根據這些引數來設定 JPLISAgent 裡的一些內容
3.監聽
VMInit
事件,在 JVM 初始化完成之後做下面的事情:
(1)建立 InstrumentationImpl 物件 ;
(2)監聽 ClassFileLoadHook 事件 ;
(3)呼叫 InstrumentationImpl 的
loadClassAndCallPremain
方法,在這個方法裡會去呼叫 javaagent 中 MANIFEST.MF 裡指定的Premain-Class 類的 premain 方法
下面是一個簡單的例子(在JDK1.8.0_181進行了測試):
PreMainAgent
package com.longofo;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.lang.instrument.Instrumentation;import java.security.ProtectionDomain;public class PreMainAgent { static { System.out.println("PreMainAgent class static block run..."); } public static void premain(String agentArgs, Instrumentation inst) { System.out.println("PreMainAgent agentArgs : " + agentArgs); Class<?>[] cLasses = inst.getAllLoadedClasses(); for (Class<?> cls : cLasses) { System.out.println("PreMainAgent get loaded class:" + cls.getName()); } inst.addTransformer(new DefineTransformer(), true); } static class DefineTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println("PreMainAgent transform Class:" + className); return classfileBuffer; } }}
MANIFEST.MF:
Manifest-Version: 1.0 Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-Class: com.longofo.PreMainAgent
Testmain
package com.longofo;public class TestMain { static { System.out.println("TestMain static block run..."); } public static void main(String[] args) { System.out.println("TestMain main start..."); try { for (int i = 0; i < 100; i++) { Thread.sleep(3000); System.out.println("TestMain main running..."); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("TestMain main end..."); }}
將PreMainAgent打包為Jar包(可以直接用idea打包,也可以使用maven外掛打包),在idea可以像下面這樣啟動:
命令列的話可以用形如
java -javaagent:PreMainAgent.jar路徑 -jar TestMain/TestMain.jar
啟動
結果如下:
PreMainAgent class static block run...PreMainAgent agentArgs : nullPreMainAgent get loaded class:com.longofo.PreMainAgentPreMainAgent get loaded class:sun.reflect.DelegatingMethodAccessorImplPreMainAgent get loaded class:sun.reflect.NativeMethodAccessorImplPreMainAgent get loaded class:sun.instrument.InstrumentationImpl$1PreMainAgent get loaded class:[Ljava.lang.reflect.Method;......PreMainAgent transform Class:sun/nio/cs/ThreadLocalCodersPreMainAgent transform Class:sun/nio/cs/ThreadLocalCoders$1PreMainAgent transform Class:sun/nio/cs/ThreadLocalCoders$CachePreMainAgent transform Class:sun/nio/cs/ThreadLocalCoders$2......PreMainAgent transform Class:java/lang/Class$MethodArrayPreMainAgent transform Class:java/net/DualStackPlainSocketImplPreMainAgent transform Class:java/lang/VoidTestMain static block run...TestMain main start...PreMainAgent transform Class:java/net/Inet6AddressPreMainAgent transform Class:java/net/Inet6Address$Inet6AddressHolderPreMainAgent transform Class:java/net/SocksSocketImpl$3......PreMainAgent transform Class:java/util/LinkedHashMap$LinkedKeySetPreMainAgent transform Class:sun/util/locale/provider/LocaleResources$ResourceReferenceTestMain main running...TestMain main running.........TestMain main running...TestMain main end...PreMainAgent transform Class:java/lang/ShutdownPreMainAgent transform Class:java/lang/Shutdown$Lock
可以看到在PreMainAgent之前已經載入了一些必要的類,即PreMainAgent get loaded class:xxx部分,這些類沒有經過transform。然後在main之前有一些類經過了transform,在main啟動之後還有類經過transform,main結束之後也還有類經過transform,可以和agentmain的結果對比下。
agentmain載入方式
agentmain方式編寫步驟簡單如下:
1.編寫agentmain函式,包含下面兩個方法的其中之一:
public static void agentmain(String agentArgs, Instrumentation inst); public static void agentmain(String agentArgs);
如果兩個方法都被實現了,那麼帶Instrumentation引數的優先順序高一些,會被優先呼叫。
agentArgs
是
premain
函式得到的程式引數,透過命令列引數傳入
2.定義一個 MANIFEST.MF 檔案,必須包含 Agent-Class 選項,通常也會加入Can-Redefine-Classes 和 Can-Retransform-Classes 選項
3.將 agentmain 的類和 MANIFEST.MF 檔案打成 jar 包
4.透過attach工具直接載入Agent,執行attach的程式和需要被代理的程式可以是兩個完全不同的程式:
// 列出所有VM例項 List<VirtualMachineDescriptor> list = VirtualMachine.list(); // attach目標VM VirtualMachine.attach(descriptor.id()); // 目標VM載入Agent VirtualMachine#loadAgent("代理Jar路徑","命令引數");
agentmain方式載入過程類似:
1.建立並初始化JPLISAgent
2.解析MANIFEST.MF 裡的引數,並根據這些引數來設定 JPLISAgent 裡的一些內容
3.監聽
VMInit
事件,在 JVM 初始化完成之後做下面的事情:
(1)建立 InstrumentationImpl 物件 ;
(2)監聽 ClassFileLoadHook 事件 ;
(3)呼叫 InstrumentationImpl 的
loadClassAndCallAgentmain
方法,在這個方法裡會去呼叫javaagent裡 MANIFEST.MF 裡指定的
Agent-Class
類的
agentmain
方法。
下面是一個簡單的例子(在JDK 1.8.0_181上進行了測試):
SufMainAgent
package com.longofo;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.lang.instrument.Instrumentation;import java.security.ProtectionDomain;public class SufMainAgent { static { System.out.println("SufMainAgent static block run..."); } public static void agentmain(String agentArgs, Instrumentation instrumentation) { System.out.println("SufMainAgent agentArgs: " + agentArgs); Class<?>[] classes = instrumentation.getAllLoadedClasses(); for (Class<?> cls : classes) { System.out.println("SufMainAgent get loaded class: " + cls.getName()); } instrumentation.addTransformer(new DefineTransformer(), true); } static class DefineTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println("SufMainAgent transform Class:" + className); return classfileBuffer; } }}
MANIFEST.MF
Manifest-Version: 1.0 Can-Redefine-Classes: true Can-Retransform-Classes: true Agent-Class: com.longofo.SufMainAgent
TestSufMainAgent
package com.longofo;import com.sun.tools.attach.*;import java.io.IOException;import java.util.List;public class TestSufMainAgent { public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException { //獲取當前系統中所有 執行中的 虛擬機器 System.out.println("TestSufMainAgent start..."); String option = args[0]; List<VirtualMachineDescriptor> list = VirtualMachine.list(); if (option.equals("list")) { for (VirtualMachineDescriptor vmd : list) { //如果虛擬機器的名稱為 xxx 則 該虛擬機器為目標虛擬機器,獲取該虛擬機器的 pid //然後載入 agent.jar 傳送給該虛擬機器 System.out.println(vmd.displayName()); } } else if (option.equals("attach")) { String jProcessName = args[1]; String agentPath = args[2]; for (VirtualMachineDescriptor vmd : list) { if (vmd.displayName().equals(jProcessName)) { VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent(agentPath); } } } }}
Testmain
package com.longofo;public class TestMain { static { System.out.println("TestMain static block run..."); } public static void main(String[] args) { System.out.println("TestMain main start..."); try { for (int i = 0; i < 100; i++) { Thread.sleep(3000); System.out.println("TestMain main running..."); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("TestMain main end..."); }}
將SufMainAgent和TestSufMainAgent打包為Jar包(可以直接用idea打包,也可以使用maven外掛打包),首先啟動Testmain,然後先列下當前有哪些Java程式:
attach SufMainAgent到Testmain:
在Testmain中的結果如下:
TestMain static block run...TestMain main start...TestMain main running...TestMain main running...TestMain main running.........SufMainAgent static block run...SufMainAgent agentArgs: nullSufMainAgent get loaded class: com.longofo.SufMainAgentSufMainAgent get loaded class: com.longofo.TestMainSufMainAgent get loaded class: com.intellij.rt.execution.application.AppMainV2$1SufMainAgent get loaded class: com.intellij.rt.execution.application.AppMainV2......SufMainAgent get loaded class: java.lang.ThrowableSufMainAgent get loaded class: java.lang.System......TestMain main running...TestMain main running.........TestMain main running...TestMain main running...TestMain main end...SufMainAgent transform Class:java/lang/ShutdownSufMainAgent transform Class:java/lang/Shutdown$Lock
和前面premain對比下就能看出,在agentmain中直接getloadedclasses的類數目比在premain直接getloadedclasses的數量多,而且premain getloadedclasses的類+premain transform的類和agentmain getloadedclasses基本吻合(只針對這個測試,如果程式中間還有其他通訊,可能會不一樣)。也就是說某個類之前沒有載入過,那麼都會透過兩者設定的transform,這可以從最後的java/lang/Shutdown看出來。
測試Weblogic的某個類是否被載入
這裡使用weblogic進行測試,代理方式使用agentmain方式(在jdk1.6.0_29上進行了測試):
WeblogicSufMainAgent
import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.lang.instrument.Instrumentation;import java.security.ProtectionDomain;public class WeblogicSufMainAgent { static { System.out.println("SufMainAgent static block run..."); } public static void agentmain(String agentArgs, Instrumentation instrumentation) { System.out.println("SufMainAgent agentArgs: " + agentArgs); Class<?>[] classes = instrumentation.getAllLoadedClasses(); for (Class<?> cls : classes) { System.out.println("SufMainAgent get loaded class: " + cls.getName()); } instrumentation.addTransformer(new DefineTransformer(), true); } static class DefineTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println("SufMainAgent transform Class:" + className); return classfileBuffer; } }}
WeblogicTestSufMainAgent:
import com.sun.tools.attach.*;import java.io.IOException;import java.util.List;public class WeblogicTestSufMainAgent { public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException { //獲取當前系統中所有 執行中的 虛擬機器 System.out.println("TestSufMainAgent start..."); String option = args[0]; List<VirtualMachineDescriptor> list = VirtualMachine.list(); if (option.equals("list")) { for (VirtualMachineDescriptor vmd : list) { //如果虛擬機器的名稱為 xxx 則 該虛擬機器為目標虛擬機器,獲取該虛擬機器的 pid //然後載入 agent.jar 傳送給該虛擬機器 System.out.println(vmd.displayName()); } } else if (option.equals("attach")) { String jProcessName = args[1]; String agentPath = args[2]; for (VirtualMachineDescriptor vmd : list) { if (vmd.displayName().equals(jProcessName)) { VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent(agentPath); } } } }}
列出正在執行的Java應用程式:
進行attach:
Weblogic輸出:
假如在進行Weblogic t3反序列化利用時,如果某個類之前沒有被載入,但是能夠被Weblogic找到,那麼利用時對應的類會透過Agent的transform,但是有些類雖然在Weblogic目錄下的某些Jar包中,但是weblogic不會去載入,需要一些特殊的配置Weblogic才會去尋找並載入。
Instrumentation侷限性
大多數情況下,使用Instrumentation都是使用其位元組碼插樁的功能,籠統說是 類重轉換的功能,但是有以下的侷限性:
- premain和agentmain兩種方式修改位元組碼的時機都是類檔案載入之後,就是說必須要帶有Class型別的引數,不能透過位元組碼檔案和 自定義的類名重新定義一個本來 不存在的類。這裡需要注意的就是上面提到過的重新定義,剛才這裡說的 不能重新定義是指不能重新換一個類名,位元組碼內容依然能重新定義和修改,不過位元組碼內容修改後也要滿足第二點的要求。
- 類轉換其實最終都回歸到類重定義Instrumentation#retransformClasses()方法,此方法有以下限制:
1.新類和老類的父類必須相同;
2.新類和老類實現的介面數也要相同,並且是相同的介面;
3.新類和老類訪問符必須一致。 新類和老類欄位數和欄位名要一致;
4.新類和老類新增或刪除的方法必須是private static/final修飾的;
5.可以刪除修改方法體。
實際中遇到的限制可能不止這些,遇到了再去解決吧。如果想要重新定義一全新類(類名在已載入類中不存在),可以考慮基於類載入器隔離的方式:建立一個新的自定義類載入器去透過新的位元組碼去定義一個全新的類,不過只能透過反射呼叫該全新類的侷限性。
小結
- 文中只是描述了JavaAgent相關的一些基礎的概念,目的只是知道有這個東西,然後驗證下之前遇到的一個問題。寫的時候也借鑑了其他大佬寫的幾篇文章[4]&[5]
- 在寫文章的過程中看了一些如 [6],利用了汙點跟蹤、hook、語法樹分析等技術,也看了幾篇大佬們整理的Java RASP相關文章[2]&[3],如果自己要寫基於RASP的漏洞檢測/利用工具的話也可以借鑑到這些思路
程式碼放到了 上,有興趣的可以去測試下,注意pom.xml檔案中的jdk版本,在切換JDK測試如果出現錯誤,記得修改pom.xml裡面的JDK版本。
參考
1.
2.
3.
4.
5.
https://www.cnblogs.com/rickiyang/p/11368932.html
6.
本文由 Seebug Paper 釋出,如需轉載請註明來源。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912109/viewspace-2668431/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- javascript如何獲取事件目標物件JavaScript事件物件
- iOS 執行時獲取類的所有屬性iOS
- jquery獲取指定元素下所有指定子元素的數目程式碼例項jQuery
- chrome獲取書籤目錄下收藏的所有連結Chrome
- 未能載入基類“DevExpress.XtraEditors.XtraForm”。請確保已引用該程式集並已生成所有專案devExpressORM
- linux入門——基本目錄認識Linux
- 獲取裝置上的某個目錄下的所有檔案
- Java 獲取Word中的標題大綱(目錄)Java
- 【android】獲取手機安裝的所有程式Android
- python獲取指定目錄下的所有指定字尾的檔名Python
- python獲取指定目錄所有檔案絕對路徑Python
- 想獲取JS載入網頁的源網頁的原始碼,不想獲取JS載入後的資料JS網頁原始碼
- Java知識點總結(反射-獲取類的資訊)Java反射
- 獲取gridview所有行的idView
- 目標識別程式碼解讀整理
- jQuery獲取所有兄弟元素jQuery
- 相容所有瀏覽器的獲取事件源物件程式碼瀏覽器事件物件
- jQuery獲取所有的li元素程式碼例項jQuery
- 求助,JAVA如何獲取系統當前所有程式Java
- 【目標區域捕獲-2】目標區域捕獲簡介
- Java類載入知識總結Java
- 在spring中獲取代理物件代理的目標物件工具類Spring物件
- 獲取Android裝置唯一標識碼Android
- 中獲取當前程式本身所在目錄
- Bash 指令碼例項:獲取符號連結的目標位置指令碼符號
- 易優Channel獲取欄目列表-Eyoucms標籤手冊
- 如何確保已正確識別和捕獲所有業務流程? - modernanalystNaN
- 淘寶API,獲取店鋪的所有商品API
- 獲取ul元素下的所有li元素
- 獲取所有鑰匙的最短路徑
- java中獲取類載入路徑和專案根路徑的5種方法Java
- 獲取指定元素下所有li元素程式碼例項
- js獲取頁面中所有元素程式碼例項JS
- .NET Core 反射獲取所有控制器及方法上特定標籤反射
- rust學習五、認識所有權Rust
- 認識類和物件物件
- 從一道面試題來認識java類載入時機與過程面試題Java
- React 穿透獲取被高階元件裝飾的目標元件例項React穿透元件