[Código fuente][C] Máquina virtual

Iniciado por Miky Gonzalez, 30 Septiembre 2013, 18:30 PM

0 Miembros y 1 Visitante están viendo este tema.

Miky Gonzalez

Adjunto el código fuente de una máquina simple que he estado haciendo, pero no tengo más tiempo para continuarla ni tampoco para terminar el ensamblador. Actualmente dispone de las siguientes funciones:




Set de instrucciones:
0x00haltdetiene el cpu
0x01litcargar en acumulador un valor inmediato
0x02loadcargar en acumulador valor de un registro
0x03storealmacenar valor acumulador en un registro
0x04incincrementar por 1 el acumulador
0x05decdecrementar por 1 el acumulador
0x06putlponer en memoria un valor inmediato
0x07putrponer en registro un valor de memoria
0x08getacargar en acumulador un valor de memoria
0x09getrcargar en registro un valor de memoria
0x0Acmplcomparar acumulador con valor inmediado. establece flag
0x0Bcmprcomparar acumulador con valor registro. establece flag
0x0Cjmpsaltar a direccion de codigo (inicio == 0x00)
0x0Djmplsaltar a direccion de codigo si flag == 1 ( < )
0x0Ejmpesaltar a direccion de codigo si flag == 2 ( = )
0x0Fjmpg    saltar a direccion de codigo si flag == 3 ( > )



Especificaciones técnicas:
El CPU consta de 4 registros de usuario, un registro flag y el registro acumulador. La máquina dispone de memoria a modo de pila (tengo otra versión con memoria dinámica, en vez de usar una pila); también se dispone de una memoria de sólo lectura para el código.




main.c:
/*!
Miky Gonzalez Virtual Machine
Contact: mikygonzalez95@gmail.com

Miky Gonzalez Virtual Machine por Miky Gonzalez se encuentra bajo una
Licencia Creative Commons Atribución-NoComercial-CompartirIgual 3.0
Unported. Más información:
http://creativecommons.org/licenses/by-nc-sa/3.0/deed.es
*/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
//#include <inttypes.h>
//#include <time.h>

#include "pila.h"

/* MVM - Estructura de la MV */
typedef struct CPU {
short int registro[4];
short int acumulador;
unsigned int inst_pointer;
unsigned char *memoria_codigo;
unsigned char flag;
pila_ pila;
} cpu_t;

unsigned int crear_cpu(cpu_t *cpu, unsigned char *memoria_codigo) {
if(!memoria_codigo || !cpu) return 0;

//memset(cpu->registro, 0, sizeof(cpu->registro) / sizeof(*cpu->registro));
unsigned short int i = 0;
for(; i < 4; i++)
cpu->registro[i] = 0;
cpu->acumulador = 0;
cpu->inst_pointer = 0;
cpu->memoria_codigo = memoria_codigo;
cpu->flag = 0;
cpu->pila = NULL;

return 1;
}

void ejecutar_cpu(cpu_t *cpu) {
if(!cpu) {
printf("[MVM] Error al ejecutar CPU...\n");
return;
}
while(1) {
switch(cpu->memoria_codigo[cpu->inst_pointer]) {
case 0x00: /*! halt */
printf("halt\n");
return;
case 0x01: /*! lit */
cpu->acumulador = cpu->memoria_codigo[++cpu->inst_pointer];
printf("lit %d\n", cpu->memoria_codigo[cpu->inst_pointer]);
break;
case 0x02: /*! load */
if(cpu->memoria_codigo[++cpu->inst_pointer] > 0x03) {
printf("[CPU] Desbordamiento en numero de registro.\n");
return;
}
cpu->acumulador = cpu->registro[cpu->memoria_codigo[cpu->inst_pointer]];
printf("load r%d\n", cpu->memoria_codigo[cpu->inst_pointer]);
break;
case 0x03: /*! store */
if(cpu->memoria_codigo[++cpu->inst_pointer] > 0x03) {
printf("[CPU] Desbordamiento en numero de registro.\n");
return;
}
cpu->registro[cpu->memoria_codigo[cpu->inst_pointer]] = cpu->acumulador;
printf("store r%d\n", cpu->memoria_codigo[cpu->inst_pointer]);
break;
case 0x04: /*! inc */
cpu->acumulador++;
printf("inc\n");
break;
case 0x05: /*! dec */
cpu->acumulador--;
printf("dec\n");
break;
case 0x06: /*! pushl */
push(&cpu->pila, cpu->memoria_codigo[++cpu->inst_pointer]);
printf("pushl %d\n", cpu->memoria_codigo[cpu->inst_pointer]);
break;
case 0x07: /*! pushr */
if(cpu->memoria_codigo[++cpu->inst_pointer] > 0x03) {
printf("[CPU] Desbordamiento en numero de registro.\n");
return;
}
push(&cpu->pila, cpu->registro[cpu->memoria_codigo[cpu->inst_pointer]]);
printf("pushr r%d\n", cpu->memoria_codigo[cpu->inst_pointer]);
break;
case 0x08: /*! pop */
if(pila_vacia(&cpu->pila)) {
printf("[CPUSTACK] No hay elemento en pila.\n");
return;
}
pop(&cpu->pila);
printf("pop\n");
break;
case 0x09: /*! loadt */
if(pila_vacia(&cpu->pila)) {
printf("[CPUSTACK] No hay elemento en pila.\n");
return;
}
cpu->acumulador = pop(&cpu->pila);
printf("loadt\n");
break;
case 0x0A: /*! cmpl */
if(cpu->acumulador < cpu->memoria_codigo[++cpu->inst_pointer])
cpu->flag = 1;
else if(cpu->acumulador == cpu->memoria_codigo[cpu->inst_pointer])
cpu->flag = 2;
else if(cpu->acumulador > cpu->memoria_codigo[cpu->inst_pointer])
cpu->flag = 3;
printf("cmpl %d\n", cpu->memoria_codigo[cpu->inst_pointer]);
break;
case 0x0B: /*! cmpr */
if(cpu->acumulador < cpu->registro[cpu->memoria_codigo[++cpu->inst_pointer]])
cpu->flag = 1;
else if(cpu->acumulador == cpu->registro[cpu->memoria_codigo[cpu->inst_pointer]])
cpu->flag = 2;
else if(cpu->acumulador > cpu->registro[cpu->memoria_codigo[cpu->inst_pointer]])
cpu->flag = 3;
printf("cmpr r%d\n", cpu->memoria_codigo[cpu->inst_pointer]);
break;
case 0x0C: /*! jmp */
printf("jmp %d\n", cpu->memoria_codigo[++cpu->inst_pointer]);
cpu->inst_pointer = cpu->memoria_codigo[cpu->inst_pointer] - 1;
break;
case 0x0D: /*! jmpl */
printf("jmpl %d\n", cpu->memoria_codigo[++cpu->inst_pointer]);
if(cpu->flag == 1)
cpu->inst_pointer = cpu->memoria_codigo[cpu->inst_pointer] - 1;
break;
case 0x0E: /*! jmpe */
printf("jmpe %d\n", cpu->memoria_codigo[++cpu->inst_pointer]);
if(cpu->flag == 2)
cpu->inst_pointer = cpu->memoria_codigo[cpu->inst_pointer] - 1;
break;
case 0x0F: /*! jmpg */
printf("jmpg %d\n", cpu->memoria_codigo[++cpu->inst_pointer]);
if(cpu->flag == 3)
cpu->inst_pointer = cpu->memoria_codigo[cpu->inst_pointer] - 1;
break;
default:
printf("[CPU] Instruccion desconocida. Cerrando proceso CPU.\n");
return;
}
cpu->inst_pointer++;
printf("r0: %d\tr1: %d\tr2: %d\tr3: %d\tac: %d\n", cpu->registro[0], cpu->registro[1], cpu->registro[2], cpu->registro[3], cpu->acumulador);
contar_nodos(cpu->pila);
}
}

int main(int argc, char *argv[]) {
/* Inicializar datos y comprobar argumentos */
if(argc < 2) {
printf("Uso: %s <archivo>\n", argv[0]);
return 0;
}
FILE *codigo_archivo = fopen(argv[1], "rb");
if(!codigo_archivo) {
printf("[MVM] Fallo al abrir el archivo %s\n", argv[1]);
return 0;
}

/* Calcular tamaño (volumen) del código */
unsigned int codigo_volumen;
fseek(codigo_archivo, 0, SEEK_END);
codigo_volumen = ftell(codigo_archivo);
rewind(codigo_archivo);
if(!codigo_volumen) {
printf("[MVM] No hay instrucciones...\n");
fclose(codigo_archivo);
return 0;
}

/* Reservar el espacio necesario para almacenar
* el código en memoria. Si existe, copiarlo. */
unsigned char *codigo = (unsigned char *)malloc(codigo_volumen);
if(!fread(codigo, 1, codigo_volumen, codigo_archivo)) {
printf("[MVM] Error en lectura de archivo...\n");
fclose(codigo_archivo);
return 0;
} else fclose(codigo_archivo);

/* Crear CPU e inicializar los datos */
cpu_t cpu;
if(!crear_cpu(&cpu, codigo)) {
printf("[MVM] Error al crear CPU...\n");
return 0;
}

/* Ejecutar CPU hasta instrucción fin o error */
ejecutar_cpu(&cpu);
printf("r0: %d\tr1: %d\tr2: %d\tr3: %d\tac: %d\n", cpu.registro[0], cpu.registro[1], cpu.registro[2], cpu.registro[3], cpu.acumulador);
contar_nodos(cpu.pila);
eliminar_pila(&cpu.pila);
free(codigo);

getchar();
return 0;
}


pila.c:
/*!
Miky Gonzalez Virtual Machine
Contact: mikygonzalez95@gmail.com

Miky Gonzalez Virtual Machine por Miky Gonzalez se encuentra bajo una
Licencia Creative Commons Atribución-NoComercial-CompartirIgual 3.0
Unported. Más información:
http://creativecommons.org/licenses/by-nc-sa/3.0/deed.es
*/

/*! Implementación de una pila para memoria de datos MVM */

#include <stdio.h>
#include <stdlib.h>
#include "pila.h"

void push(pila_ *pila, int valor) {
nodo_ nodo = (nodo_)malloc(sizeof(nodo_t));

if(nodo != NULL) {
nodo->valor = valor;
nodo->nodo_siguiente = *pila;
*pila = nodo;
}
}

int pop(pila_ *pila) {
nodo_ nodo;
int valor = 0;

nodo = *pila;
valor = (*pila)->valor;
*pila = (*pila)->nodo_siguiente;

free(nodo);
return valor;
}

//#define pila_vacia(a) *a == NULL ? 1:0;
unsigned short int pila_vacia(pila_ *pila) {
return (*pila == NULL ? 1 : 0);
}

unsigned short int contar_nodos(nodo_ nodo) {
if(nodo == NULL) return 0;

unsigned short int nodos = 0;
while(nodo != NULL) {
nodos++;
printf("n%d: %d\t", nodos, nodo->valor);
nodo = nodo->nodo_siguiente;
}
printf("\n");

return nodos;
}

// #define eliminar_pila(a) {if(pila_vacia(a))return;while(!pila_vacia(a))pop(a);}
void eliminar_pila(pila_ *pila) {
if(pila_vacia(pila)) return;
while(!pila_vacia(pila)) pop(pila);
return;
}


pila.h:
/*!
Miky Gonzalez Virtual Machine
Contact: mikygonzalez95@gmail.com

Miky Gonzalez Virtual Machine por Miky Gonzalez se encuentra bajo una
Licencia Creative Commons Atribución-NoComercial-CompartirIgual 3.0
Unported. Más información:
http://creativecommons.org/licenses/by-nc-sa/3.0/deed.es
*/

#ifndef MVM_PILA
#define MVM_PILA

/* Estructura de nodos de pila */
typedef struct NODO {
int valor;
struct NODO *nodo_siguiente;
} nodo_t;

typedef nodo_t *nodo_;
typedef nodo_t *pila_;

void push(pila_ *pila, int valor);
int pop(pila_ *pila);
unsigned short int pila_vacia(pila_ *pila);
unsigned short int contar_nodos(nodo_ nodo);
void eliminar_pila(pila_ *pila);

#endif


Actualmente no dispongo de tiempo para terminar el ensamblador, así que tendrán que crear los ejecutables editandolos hexadecimalmente, hice un programa para facilitarme el trabajo de prueba:

#include <stdio.h>

int main(int argc, char **argv) {
char codigo[] = {0x00};
FILE *fd;
fd = fopen("test", "wb");
if(!fd) return 0;
fwrite(codigo, sizeof(char), sizeof(codigo), fd);
fclose(fd);
return 0;
}





Algunos ejemplos ya programados, con sus respectivos opcodes:

#========================================
# CALCULAR RESTA DE DOS NUMEROS
#========================================
# Usando 2 registros se pueden hacer
# funciones de resta de números.
# resultado: r0
#========================================

# Inicializacion de los datos

lit 25 # ac: 25
store r0 # r0: 25
lit 17 # ac: 17
store r1 # r1: 17
lit 0 # ac: 0

# Bucles

load r1 # ac: r1
dec # ac: ac--
store r1 # r1: ac
load r0 # ac: r0
dec # ac: ac--
store r0 # r0: ac

lit 1 # ac: 1
cmpr r1 # comparar ac & r1
jmpg 16 # ac > r1 --> jmp 16
jmp 6 # jmp 6

halt # stop

# 0x01 25 0x03 0x00 0x01 17 0x03 0x01 0x01 0x00 0x02 0x01 0x05
# 0x03 0x01 0x02 0x00 0x05 0x03 0x00 0x01 0x01 0x0B 0x01 0x0F 28
# 0x0C 10 0x00


#========================================
# CALCULAR SUMA DE DOS NUMEROS
#========================================
# Usando 2 registros se pueden hacer
# funciones de suma de números.
# resultado: r0
#========================================

# Inicializacion de los datos

lit 17 # ac: 17
store r0 # r0: 17
lit 25 # ac: 25
store r1 # r1: 25
lit 0 # ac: 0

# Bucles

load r1 # ac: r1
dec # ac: ac--
store r1 # r1: ac
load r0 # ac: r0
inc # ac: ac++
store r0 # r0: ac

lit 1 # ac: 1
cmpr r1 # comparar ac & r1
jmpg 16 # ac > r1 --> jmp 16
jmp 6 # jmp 6

halt # stop
# 0x01 25 0x03 0x00 0x01 17 0x03 0x01 0x01 0x00 0x02 0x01 0x05
# 0x03 0x01 0x02 0x00 0x04 0x03 0x00 0x01 0x01 0x0B 0x01 0x0F 28
# 0x0C 10 0x00


#========================================
# CALCULAR MULTIPLICACION DE DOS NUMEROS
#========================================
# Utilizando 3 registros (incluso menos) se
# pueden hacer funciones de multiplicación
# de números.
# resultado: r2.
#========================================

# Inicialización de los datos

lit 10 # ac: 10
store r0 # r0: 10
lit 3 # ac: 3
store r1 # r1: 3
lit 0 # ac: 0


# Bucles

load r1 # ac: r1
dec # ac: ac--
store r1 # r1: ac
cmpl 0 # comparar ac & 0
jmpe 23 # ac == 0 --> jmp 23

lit 10 # ac: 10
store r0 # r0: 10

load r2 # ac: r2
inc # ac: ac++
store r2 # r2: ac
load r0 # ac: r0
dec # ac: ac--
store r0 # r0: ac
lit 0 # ac: 0
cmpr r0 # comparar ac & r0
jmpl 13 # ac < r0 --> jmp 13
jmp 6 # jmp 6

lit 0 # ac: 0
store r1 # r1: 0

halt # stop
#0x01 0x0A 0x03 0x00 0x01 0x03 0x03 0x01 0x01 0x00  
#0x02 0x01 0x05 0x03 0x01 0x0A 0x00 0x0D 0x29 0x01
#0x0A 0x03 0x00 0x02 0x02 0x04 0x03 0x02 0x02 0x00
#0x05 0x03 0x00 0x01 0x00 0x0B 0x00 0x0D 23 0x0C 10
#0x01 0x00 0x03 0x01 0x00


Espero que este proyecto les guste, y si alguien puede continuarlo, espero que lo haga y tenga buenas ideas, tal vez aunque no puedo no seria malo proponerlas, nunca se sabe si volvere a continuar con la máquina virtual.

Saludos,
  Miky Gonzalez
Mi blog personal, con información acerca de programación, seguridad, desarrollo y electrónica:

EN CONSTRUCCIÓN

eferion

Deberías plantearte empezar a subdividir el código de la cpu. Conforme vayas añadiendo instrucciones tu código va a empezar a ser un poco caótico.

Si te atreves a migrar el código a C++ avísame y te comento algunas ideas.

Si no, una opción para liquidar ese switch que va a acabar siendo infernal es que te crees un array de punteros a funciones... en la posición 0 pones un puntero a la función que va a ejecutar la instrucción 0x00, en la posición 1 la correspondiente a la instrucción 0x01... Cuando te llegue una instrucción ejecutas la función que corresponda dado el índice de la instrucción y listo. Además ganarás velocidad.

Ten en cuenta que con este diseño tendrás que pasar como parámetro un puntero a la estructura cpu, para poder acceder a los acumuladores, registros y demás.

Si hay huecos puedes definir un valor que determine que la instrucción en concreto no existe... para mostrar mensajes de error por ejemplo.

Este nuevo diseño te va a permitir mantener un código más limpio y ordenado, con funciones pequeñas y fáciles de depurar.

Por lo demás, tiene buena pinta.

Enhorabuena.

Miky Gonzalez

En primer lugar, gracias por comentar eferion. Soy consciente que con más instrucciones y más funciones el código será "sucio" y poco optimizable, pero para empezar, me parece que está claro.

Decidí programar la máquina virtual en C, porque no soy amigo de C++, aunque la programación orientada a objetos sería una buena idea, incluso tal vez mas editable.

En cambiar el switch por un array, o una estructura ya lo pensé, pero decidí no implementarlo por no tener que cambiar el código, aunque en implementarlo no se tardaría mucho, tan solo sería separar cada instrucción como una función del programa. Sería un paso a hacer si se continuara el proyecto añadiendo más instrucciones y funciones.

Estuve buscando cómo implementar y hacer funciones más rápidas y de hecho tengo una versión que podría proporcionar perfectamente libre de licencia, pero con un código mal organizado y poco ordenado, pero está bastante más optimizado comparandolo con este código.

Otro de los pasos a hacer si continuara el proyecto sería terminar el ensamblador, publicar la versión con uso de memoria y punteros en vez de una pila de datos; entre otras cosas, añadir 'labeles' para hacer saltos relativos en el código, de este manera, el problema actual de tener que cambiar de salto por cada nueva instrucción añadida se solventaría, convirtiendo los saltos relativos a saltos absolutos (esto lo haría el ensamblador).

Saludos,
  Miky Gonzalez
Mi blog personal, con información acerca de programación, seguridad, desarrollo y electrónica:

EN CONSTRUCCIÓN

x64core

Esto es más bien emulacion de codigo y no una maquina virtual ya que se tendria que virtualizar el entorno también.

Karcrack

Me gustan mucho este tipo de códigos :P Yo me hice un emulador para un set de instrucciones reducizo del MIPS cuando lo dí en la universidad ;D



Cita de: eferion en 30 Septiembre 2013, 20:35 PM
Si no, una opción para liquidar ese switch que va a acabar siendo infernal es que te crees un array de punteros a funciones... en la posición 0 pones un puntero a la función que va a ejecutar la instrucción 0x00, en la posición 1 la correspondiente a la instrucción 0x01... Cuando te llegue una instrucción ejecutas la función que corresponda dado el índice de la instrucción y listo. Además ganarás velocidad.
Los compiladores modernos ya hacen esa optimización de forma automática. (http://en.wikipedia.org/wiki/Branch_table)

eferion

Cita de: Karcrack en  1 Octubre 2013, 09:46 AM
Me gustan mucho este tipo de códigos :P Yo me hice un emulador para un set de instrucciones reducizo del MIPS cuando lo dí en la universidad ;D


Los compiladores modernos ya hacen esa optimización de forma automática. (http://en.wikipedia.org/wiki/Branch_table)

Las optimizaciones de los procesadores están bien... pero no puedes depender de ellas para hacer un código optimizado. Entre otras cosas las optimizaciones son condicionales, ya que no siempre pueden aplicarse, además cada compilador aplica sus propias optimizaciones, luego la optimización final varía de un compilador a otro.

Además, por muchas optimizaciones que haga el compilador, el mantenimiento del código fuente siempre te va a tocar a ti, por lo que te interesa que ese código, al margen de posibles optimizaciones del compilador ) sea lo más legible y sencillo posible... y si no intenta tu pegarte con un bug en una función de 2484 líneas de código, 20 parámetros y punteros y variables sin inicializar ( sí, yo me he encontrado con eso y con cosas peores ).

Karcrack

Completamente de acuerdo pero a no ser que uses un compilador de los 90 tu intento de optimizar puede no ser tan óptimo como el que haría el compilador...

eferion

Cita de: Karcrack en  1 Octubre 2013, 22:26 PM
Completamente de acuerdo pero a no ser que uses un compilador de los 90 tu intento de optimizar puede no ser tan óptimo como el que haría el compilador...

No me refería exactamente a eso.

Tu piensa que, en función de cómo configures el compilador, el mismo compilador, puede proporcionarte unas optimizaciones u otras. El mismo código fuente puede servir para generar varias decenas de códigos binarios distintos... dependiendo de las optimizaciones que se vayan a aplicar.

Además hay optimizaciones incompatibles entre sí, como pueden ser algunas relativas a velocidad de ejecución y las de tamaño del ejecutable.

A lo que yo iba es que, independientemente de eso, el código fuente es algo que tiene que mantener el programador, por lo que merece la pena invertir el tiempo y el dinero en tener un código limpio, manejable y escalable. A corto, medio y largo plazo merece la pena.


Miky Gonzalez

Totalmente de acuerdo con todos ustedes, si continuo el proyecto, intentaré implementar lo mejor posible para optimizar en velocidad.
  Saludos
Mi blog personal, con información acerca de programación, seguridad, desarrollo y electrónica:

EN CONSTRUCCIÓN