使用 Java/Kotlin 編寫的 JVM 位元組碼執行引擎

JAVA 位元組碼執行引擎

傳統的 Java 動態除錯只能基於原始碼層級,沒有原始碼或經過混淆的 Java class 檔案無法進行動態除錯。

Java 程式的執行基於 JVM 虛擬機,由其執行位元組碼。我們使用 Kotlin 建構了一個 JVM 位元組碼執行引擎,使我們能夠使用 IntelliJ IDEA 等現代 IDE 在位元組碼層級除錯 Java 程式,以觀察執行過程中的程式行為。

注意,此專案僅用於學習和研究 JVM 的運作原理以及分析惡意程式。嚴禁將其用於非法用途。

前提知識基礎

在使用此專案之前,請確保您具備以下知識基礎。

  1. 了解 Java class 檔案的格式
  2. 了解 JVM 中每個位元組碼的作用和含義

使用 IDEA 進行位元組碼層級的除錯

git clone https://github.com/vlinx-io/vlx-vmengine-jvm.git

使用 IDEA(需要 JDK 17)開啟此專案並導航到 TestCases

TestCases 中有兩個測試案例,一個用於執行靜態方法,一個用於執行實例方法,分別命名為 executeStaticMethodexecuteVirtualMethod

在對應的方法上,填寫 classPathclassNamemethodNamemethodSignature 等資訊。class 檔案的詳細資訊可以使用 ClassViewer 查看。

直接執行

以下列程式碼的編譯 class 檔案為例

public class Hello {

    public void hello() {
        System.out.println("hello");
    }

    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

執行 executeVirtualMethod,執行此類別的 hello 方法。

val classPath = "your-classpath"
val className = "Hello"
val methodName = "hello"
val methodSignature = "()V"
val args = listOf<Any?>()

val url = File(classPath).toURI().toURL()
val urls = arrayOf(url)
val loader = VlxClassLoader(urls)

val clazz = loader.loadClass(className)

val method = ClassUtils.getMethod(clazz, methodName, methodSignature, loader)
val instance = clazz.getDeclaredConstructor().newInstance()


val thread = VMThread(VMEngine.instance, loader)
thread.execute(instance, method!!, args, true, 0)

您可以在控制台上得到以下輸出。

2023-05-21 17:51:10 [DEBUG] Execute method: public void Hello.hello()
2023-05-21 17:51:10 [DEBUG] Receiver: Hello@3daf7722
2023-05-21 17:51:10 [DEBUG] Args: []
2023-05-21 17:51:11 [DEBUG] LocalVars: [kotlin.Unit]
2023-05-21 17:51:11 [DEBUG] "L0: GETSTATIC"
2023-05-21 17:51:11 [DEBUG] "#7"
2023-05-21 17:51:11 [DEBUG] public static final java.io.PrintStream java.lang.System.out
2023-05-21 17:51:11 [DEBUG] "push" org.gradle.internal.io.LinePerThreadBufferingOutputStream@6aa3a905
2023-05-21 17:51:11 [DEBUG] "L3: LDC"
2023-05-21 17:51:11 [DEBUG] "#13"
2023-05-21 17:51:11 [DEBUG] "hello"
2023-05-21 17:51:11 [DEBUG] "push" "hello"
2023-05-21 17:51:11 [DEBUG] "L5: INVOKEVIRTUAL"
2023-05-21 17:51:11 [DEBUG] "#15"
2023-05-21 17:51:11 [DEBUG] "class java.io.PrintStream, NameAndType(name='println', type='(Ljava/lang/String;)V')"
2023-05-21 17:51:11 [DEBUG] public void java.io.PrintStream.println(java.lang.String)
2023-05-21 17:51:11 [DEBUG] "pop" "hello"
2023-05-21 17:51:11 [DEBUG] "pop" org.gradle.internal.io.LinePerThreadBufferingOutputStream@6aa3a905
2023-05-21 17:51:11 [DEBUG] 	Execute method: public void org.gradle.internal.io.LinePerThreadBufferingOutputStream.println(java.lang.String)
2023-05-21 17:51:11 [DEBUG] 	Receiver: org.gradle.internal.io.LinePerThreadBufferingOutputStream@6aa3a905
2023-05-21 17:51:11 [DEBUG] 	Args: [org.gradle.internal.io.LinePerThreadBufferingOutputStream@6aa3a905, hello]
2023-05-21 17:51:11 [ERROR] Can't parse class class org.gradle.internal.io.LinePerThreadBufferingOutputStream
hello
2023-05-21 17:51:11 [DEBUG] "L8: RETURN"

控制台輸出顯示了此方法的所有位元組碼指令、指令執行期間堆疊的變化,以及每個位元組碼指令的結果。

除錯

如果您需要除錯位元組碼指令,可以在 VMExecutorexecute() 方法的前後設定中斷點。

 fun execute() {
    while (true) {
        try {
            pc = sequence.index()
            val opcode = sequence.readUnsignedByte().toShort()
            if (execute(opcode)) {
                break
            }
        } catch (vme: VlxVmException) {
            Logger.FATAL(vme)
            exitProcess(1)
        } catch (t: Throwable) {
            handleException(t)
        }

    }
}

除錯子方法位元組碼

預設情況下,虛擬引擎只會解譯和執行指定方法的位元組碼。在指定方法中呼叫的子方法仍然在 JVM 中執行,以避免多層呼叫帶來的顯著效能開銷。如果您希望所有方法都由虛擬引擎解譯和執行,請修改 io.vlinx.vmengine.Options 並將 handleSubMethod 改為 true。