Mecanismo de execução de bytecode JVM escrito com Java/Kotlin

Mecanismo de Execução de Bytecode JAVA

A depuração dinâmica tradicional do Java só pode ser baseada no nível do código-fonte, e não pode ser depurada dinamicamente sem código-fonte ou com arquivos class Java ofuscados.

A execução de programas Java é baseada na máquina virtual JVM, que executa bytecode. Construímos um mecanismo de execução de bytecode JVM usando Kotlin, que nos permite depurar programas Java no nível do bytecode usando IDEs modernos como IntelliJ IDEA para observar o comportamento do programa durante a execução.

Atenção, este projeto é apenas para estudo e pesquisa do princípio de operação da JVM e análise de programas maliciosos. É estritamente proibido usá-lo para fins ilegais.

Conhecimento Prévio Necessário

Antes de usar este projeto, certifique-se de que você tem o seguinte conhecimento base.

  1. Compreensão do formato de arquivos class Java
  2. Compreensão do papel e significado de cada bytecode na JVM

Depuração no nível do bytecode usando IDEA

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

Abra este projeto usando IDEA (requer JDK 17) e navegue até TestCases

Existem dois casos de teste em TestCases, um para executar métodos estáticos e outro para executar métodos de instância, respectivamente chamados executeStaticMethod e executeVirtualMethod.

No método correspondente, preencha as informações como classPath, className, methodName e methodSignature. As informações detalhadas do arquivo class podem ser visualizadas usando ClassViewer.

Execução direta

Usando o arquivo class compilado do seguinte código como exemplo

public class Hello {

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

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

Execute executeVirtualMethod, execute o método hello desta classe.

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)

Você pode obter a seguinte saída no console.

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"

A saída do console exibe todas as instruções de bytecode deste método, as mudanças na pilha durante a execução da instrução e os resultados de cada instrução de bytecode.

Depuração

Se você precisar depurar instruções de bytecode, pode definir pontos de interrupção antes e depois do método execute() em 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)
        }

    }
}

Depuração de bytecode de submétodos

Por padrão, o mecanismo virtual apenas interpreta e executa o bytecode do método especificado. Os submétodos chamados dentro do método especificado ainda são executados na JVM para evitar sobrecarga significativa de desempenho de múltiplas camadas de chamadas. Se você quiser que todos os métodos sejam interpretados e executados pelo mecanismo virtual, modifique io.vlinx.vmengine.Options e altere handleSubMethod para true.