Движок выполнения байт-кода JVM, написанный на Java/Kotlin

Движок выполнения байт-кода JAVA

Традиционная динамическая отладка Java может выполняться только на уровне исходного кода. Без исходного кода или при наличии обфусцированных файлов классов Java динамическая отладка невозможна.

Выполнение Java-программ основано на виртуальной машине JVM, которая выполняет байт-код. Мы создали движок выполнения байт-кода JVM с помощью Kotlin, который позволяет отлаживать Java-программы на уровне байт-кода, используя современные IDE, такие как IntelliJ IDEA, для наблюдения за поведением программы во время выполнения.

Внимание, этот проект предназначен только для изучения и исследования принципов работы JVM и анализа вредоносных программ. Использование в незаконных целях строго запрещено.

Необходимые базовые знания

Перед использованием этого проекта убедитесь, что у вас есть следующие знания:

  1. Понимание формата файлов классов Java
  2. Понимание роли и значения каждого байт-кода в JVM

Отладка на уровне байт-кода с помощью IDEA

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

Откройте этот проект в IDEA (требуется JDK 17) и перейдите к TestCases.

В TestCases есть два тестовых примера: один для выполнения статических методов и один для выполнения методов экземпляра, с именами executeStaticMethod и executeVirtualMethod соответственно.

Для соответствующего метода заполните такую информацию, как classPath, className, methodName и methodSignature. Подробную информацию о файле класса можно просмотреть с помощью 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"

Вывод в консоль отображает все инструкции байт-кода этого метода, изменения стека во время выполнения инструкций и результаты каждой инструкции.

Отладка

Если вам нужно отладить инструкции байт-кода, вы можете установить точки останова до и после метода execute() в VMExecutor.

 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.