Java/Kotlinで書かれたJVMバイトコード実行エンジン

JAVAバイトコード実行エンジン

従来のJava動的デバッグはソースコードレベルに基づくものしかなく、ソースコードがない場合や難読化されたJavaクラスファイルを動的デバッグすることはできません。

Javaプログラムの実行はJVM仮想マシンに基づいており、バイトコードを実行します。私たちはKotlinを使用してJVMバイトコード実行エンジンを構築しました。これにより、IntelliJ IDEAなどの最新のIDEを使用して、バイトコードレベルでJavaプログラムをデバッグし、実行中のプログラムの動作を観察できます。

注意:このプロジェクトはJVMの動作原理の研究と悪意のあるプログラムの分析のみを目的としています。違法な目的での使用は固く禁じられています。

前提知識

このプロジェクトを使用する前に、以下の知識基盤を持っていることを確認してください。

  1. Javaクラスファイルのフォーマットの理解
  2. JVMの各バイトコードの役割と意味の理解

IDEAを使用したバイトコードレベルのデバッグ

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

IDEAでこのプロジェクトを開き(JDK 17が必要)、TestCasesに移動します。

TestCasesには2つのテストケースがあります。1つは静的メソッドの実行用で、もう1つはインスタンスメソッドの実行用です。それぞれexecuteStaticMethodexecuteVirtualMethodと名付けられています。

対応するメソッドにclassPathclassNamemethodNamemethodSignatureなどの情報を入力してください。クラスファイルの詳細情報はClassViewerを使用して確認できます。

直接実行

以下のコードのコンパイル済みクラスファイルを例として使用します。

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に変更してください。