2012-01-26 12 views
8

Tengo la intención de escribir mi propio intérprete JIT como parte de un curso sobre máquinas virtuales. Tengo mucho conocimiento sobre lenguajes de alto nivel, compiladores e intérpretes, pero tengo poco o ningún conocimiento sobre el ensamblaje x86 (o C para el caso).Escribo mi propio intérprete JIT. ¿Cómo ejecuto las instrucciones generadas?

En realidad, no sé cómo funciona un JIT, pero aquí está mi opinión: Lea en el programa en algún idioma intermedio. Compila eso con las instrucciones x86. Asegúrese de que la última instrucción regrese a algún lugar sano en el código de VM. Guarde las instrucciones en algún lugar de la memoria. Haz un salto incondicional a la primera instrucción. Voila!

Entonces, con esto en mente, tengo el siguiente pequeño programa C:

#include <stdlib.h> 
#include <stdio.h> 
#include <unistd.h> 

int main() { 
    int *m = malloc(sizeof(int)); 
    *m = 0x90; // NOP instruction code 

    asm("jmp *%0" 
       : /* outputs: */ /* none */ 
       : /* inputs: */ "d" (m) 
       : /* clobbers: */ "eax"); 

    return 42; 

}

bien, así que mi intención es que este programa para almacenar la instrucción NOP en algún lugar de la memoria, salto a esa ubicación y luego probablemente se cuelgue (porque no configuré ninguna forma para que el programa regrese a main).

Pregunta: ¿Estoy en el camino correcto?

Pregunta: ¿Podría mostrarme un programa modificado que logra encontrar su camino de regreso a algún lugar dentro de main?

Pregunta: Otros problemas que debería tener cuidado?

PD: Mi objetivo es obtener comprensión, no necesariamente hacer todo de la manera correcta.


Gracias por todo el comentario. El siguiente código parece ser el lugar para iniciar y funciona en mi máquina Linux:

#include <stdlib.h> 
#include <stdio.h> 
#include <unistd.h> 
#include <sys/mman.h> 

unsigned char *m; 

int main() { 
     unsigned int pagesize = getpagesize(); 
     printf("pagesize: %u\n", pagesize); 

     m = malloc(1023+pagesize+1); 
     if(m==NULL) return(1); 

     printf("%p\n", m); 
     m = (unsigned char *)(((long)m + pagesize-1) & ~(pagesize-1)); 
     printf("%p\n", m); 

     if(mprotect(m, 1024, PROT_READ|PROT_EXEC|PROT_WRITE)) { 
       printf("mprotect fail...\n"); 
       return 0; 
     } 

     m[0] = 0xc9; //leave 
     m[1] = 0xc3; //ret 
     m[2] = 0x90; //nop 

     printf("%p\n", m); 


asm("jmp *%0" 
        : /* outputs: */ /* none */ 
        : /* inputs: */ "d" (m) 
        : /* clobbers: */ "ebx"); 

     return 21; 
} 
+0

Otra opción es simplemente interpretar las instrucciones o código intermedio w/o ejecutar cualquier cosa directamente. –

+1

@Alex: esa es otra opción para implementar un idioma, pero por definición no es un JIT. –

Respuesta

8

Pregunta: ¿Estoy en el camino correcto?

Yo diría que sí.

Pregunta: ¿Podría mostrarme un programa modificado que logra encontrar su camino de regreso a algún lugar dentro de main?

no tengo ningún código para usted, pero una mejor manera de llegar al código generado y la espalda es usar un par de call/ret instrucciones, ya que se encargará de la dirección de retorno automáticamente.

Pregunta: Otros problemas que debería tener cuidado?

Sí, como medida de seguridad, muchos sistemas operativos le impedirían ejecutar código en el montón sin hacer arreglos especiales. Esos arreglos especiales normalmente equivalen a tener que marcar las páginas de memoria relevantes como ejecutables.

En Linux esto se hace usando mprotect() con PROT_EXEC.

+1

Además, la caché de instrucciones generalmente no supervisa la memoria subyacente, por lo que puede requerirse un vaciado de caché explícito antes de ejecutar el salto. –

+1

@Simon: estuvo de acuerdo, y en general eso es "después de que haya terminado de escribir las instrucciones en la memoria, pero antes de ejecutarla". Entonces, en mi experiencia, escribes el código para hacerlo justo después de que terminas de escribir, en lugar de justo antes de ejecutarlo. En este código de ejemplo, esos son el mismo lugar, pero en la práctica puede ejecutar más veces de las que escribe. Y es importante que, como comenta Simon, vacíe el caché de instrucciones en lugar de la caché de datos. –

+0

No necesita vaciar el caché I en x86, SMC solo no funciona si está muy cerca de eip. Moderadamente cerca SMC impone enormes penalizaciones. – harold

3

Si su código generado sigue el buen calling convention, entonces se puede declarar un tipo puntero a la función e invocar la función de esta manera:

typedef void (*generated_function)(void); 

void *func = malloc(1024); 
unsigned char *o = (unsigned char *)func; 
generated_function *func_exec = (generated_function *)func; 

*o++ = 0x90;  // NOP 
*o++ = 0xcb;  // RET 

func_exec(); 
Cuestiones relacionadas