Planeación de una emulación escalable de x86

Iniciado por ~, 4 Marzo 2013, 02:06 AM

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

~

Hace un tiempo, cerca de hace 6 meses, en el 2012, aproximadamente de Junio a Agosto, estuve trabajando en un emulador de VGA y CPU x86 como manera tanto de entender HTML5 como de llevar mi conocimiento a un lugar más accesible, tal como es el navegador.

Versión Pre-Alpha "Final"

Historial de Versiones Pre-Alpha:
Versión 0.1
Versión 0.2
Versión 0.3
Versión 0.4
Versión 0.5





El código de emulador de CPU x86 que estoy intentando rediseñar para una emulación realmente adecuada, escalable y con código mantenible (por ahora en javascript y HTML5) es este (que recupera un código de boot x86 de floppy, pero obviamente todavía no hay BIOS ni soporte para las instrucciones del CPU globalmente):

Emulador de CPU con visor VGA provisional (2013-03-02).





Durante el repaso del código antiguo me he dado cuenta de varios requerimientos que debería satisfacer.

El requerimiento más importante a superar en este momento es cómo manejar grupos de instrucciones, ya que muchísimas de estas difieren solo en sus opcodes y en la operación que efectúan, pero se rigen por la misma tabla de decisiones. Por ahora, uso como ejemplo la versión de opcode 0x31 de la instrucción xor.

Hay muchas más que igual que esta, usan la misma tabla de operadores en 16 bits (y más adelante, otra para 32 bits, y otra mucho más adelante, para 64 bits).



El código básico para manejar las ramificaciones de esta tabla de la forma más básica es la siguiente (en javascript):

function mod(modrm){
var mod=parseInt("11000000b", 2);
var mod_val=(modrm&mod)>>6;
return mod_val;
}



function reg(modrm){
var reg=parseInt("00111000", 2);
var reg_val=(modrm&mod)>>3;
return reg_val;
}


function rm(modrm){
var rm=parseInt("00000111", 2);
var rm_val=(modrm&rm);
return rm_val;
}



function scale(sib){
var scale = parseInt("11000000b", 2);
var scale_val=(sib&scale)>>6;
return scale_val;
}


function index(sib){
var index = parseInt("00111000b", 2);
var index_val=(sib&index)>>3;
return index_val;
}


function base(sib){
var base  = parseInt("00000111b", 2);
var base_val=sib&base;
return base_val;
}



function xor_16()
{
    var ctr=0x00;

    var str="";
    for(var modrm=0; modrm<256; modrm++)
    {
     str+="db 0x31,"+modrm;

     if(mod(modrm)==0)
     {
      if(rm(modrm)==6)
       str+=",0,0\n";
      else
      {
       str+="\n";
       continue;
      }
     }
      else if(mod(modrm)==1)
      {
       str+=",0\n";
       continue;
      }
       else if(mod(modrm)==2)
       {
        str+=",0,0\n";
        continue;
       }
        else if(mod(modrm)==3)
        {
         str+="\n";
         continue;
        }
    }

return str;
}



Como vemos, lo más importante aquí es el campo Mod del byte Mod/RM, que puede tener un valor de 0 a 3, y dependiendo de este, se usan diferentes desplazamientos de memoria o registros.

Una vez que tenemos este código, lo difícil es decidir cómo reutilizar este código eficientemente para absolutamente todas las demás instrucciones que esperan dicho byte Mod/RM, usando punteros a funciones, y probablemente funciones simples crudas para todas las operaciones básicas (AND, OR, XOR, NOT, NEG, suma, resta, muliplicación, división, desplazamientos de bits, rotaciones de bits, etc.).





Otro aspecto importante es crear un cuerpo de rutinas para recuperar adecuadamente las instrucciones. Para esto en primer lugar necesitamos una tabla con los tamaños esperados de todas las instrucciones existentes. Esto también puede ayudarnos para aislar instrucciones que actualmente no podemos manejar y para que podamos saltarlas y/o recuperarlas por separado.

Esto se complica porque una instrucción puede estar precedida por un número variable de bytes de prefijo, y estos también deben considerarse parte integral de la instrucción. Muchas veces estos cambian el número de bytes esperados a continuación (por ejemplo un número de bytes adecuado para operaciones de 32 bits en código de 16 bits).

En pocas palabras, necesitamos escribir un buen "fetcher" de instrucciones, que sea manejable a pesar de la gran cantidad de instrucciones existentes y combinaciones de dichas instrucciones, y operadores.





Voy a seguir escribiendo lo que haga a medida avance. Podría avanzar rápidamente con el código actual, pero sé que es suficientemente fragil como para volverse un problema de escalabilidad o de una correcta emulación, así que voy a irme por el camino más largo e implementar todo pensando en iniciar con las funciones más reutilizables, y una vez que estén bien desarrolladas, construir las capas superiores de la emulación.
Sitio web (si la siguiente imagen no aparece es porque está offline):

~

Un detalle del que me di cuenta al correr este programa en Firefox y en Chrome, es que Chrome e Internet Explorer no soportan argumentos de funciones con valores predeterminados, pero Firefox sí, y es posible que esta característica sea parte de ECMAScript 6.

Por detalles como estos es que siempre prefiero usar características y sintaxis más al estilo de C, para que el programa sea más portable a otros lenguajes, y no propenso a estos detalles técnicos.

Así que lo siguiente no corre bien en Chrome, pero sí en Firefox (en otras palabras, es mejor no usarlo, tal como si usáramos la simplicidad de C):function test(var1, var2=0xFFFF)
{
alert("var2 "+var2);
}

test();


javascript Functions and default parameters, not working in IE and Chrome

No deja de ser algo muy inesperado, especialmente de Chrome (que se supone que incluso tiene una implementación completa de HTML5), porque esto claramente puede dar problemas a mucho código existente.


Incluso PHP soporta valores de argumentos de función predeterminados: <?php
    
function doFoo($Name "Paul") {
        return 
"Foo $Name!\n";
    }

    
doFoo();
    
doFoo("Paul");
    
doFoo("Andrew");
?>
Sitio web (si la siguiente imagen no aparece es porque está offline):

~

Quiero usar imágenes de floppy de 1.44 Megabytes, pero esos son demasiados datos.

Así que pienso comprimirlos con PHP, con un código como el siguiente:

<?php


function print_gzipped_output($filez="LowEST_Kern.img")
{
    
$HTTP_ACCEPT_ENCODING $_SERVER["HTTP_ACCEPT_ENCODING"];
    if( 
headers_sent() )
        
$encoding false;
    else if( 
strpos($HTTP_ACCEPT_ENCODING'x-gzip') !== false )
        
$encoding 'x-gzip';
    else if( 
strpos($HTTP_ACCEPT_ENCODING,'gzip') !== false )
        
$encoding 'gzip';
    else
        
$encoding false;
//print "afds";   
    
if( $encoding )
    {
//        $contents = ob_get_clean();
        
$contents=file_get_contents("LowEST_Kern.img");
        
$_temp1 strlen($contents);
        if (
$_temp1 2048)    // no need to waste resources in compressing very little data
            
print($contents);
        else
        {
            
header('Content-Type: application/octet-stream');
            
header('Content-Encoding: '.$encoding);
            print(
"\x1f\x8b\x08\x00\x00\x00\x00\x00");
            
$contents gzcompress($contents9);
            
$contents substr($contents0$_temp1);
            
header('Content-Length: '.strlen($contents));
            print(
$contents);
        }
    }
    else
        print(
file_get_contents("LowEST_Kern.img"));
}





print_gzipped_output();
?>


Con esto, una imagen de floppy casi vacía puede pasar de 1.44 Megabytes a solo unos 13.5 Kilobytes.

Esto necesito integrarlo al XMLHttpRequest que lee datos de floppy en el emulador.

Otro truco más, de PHP, globalmente útil para muchas otras cosas, que vamos a usar para este proyecto.

Y la compresión GZIP no la tengo activada globalmente en el servidor porque así no puedo ver mi sitio desde mis Palm LifreDrive, así que voy a usar compresión solo para aplicaciones especiales como esta, que de todas formas no pueden correr en la Palm (no hay HTML5 ahí).


Para modo de 32 bits necesitamos una nueva versión del byte Mod/RM y el nuevo byte SIB.

Las cosas se vuelven difíciles porque se pueden usar instrucciones de 32 bits en código de 16 bits, e instrucciones de 16 bits en código de 32 bits.

Así que es difícil pensar cómo implementar esa funcionalidad sin duplicar código, y que sea usable globalmente por todas las instrucciones.

Las tablas que usamos son las siguientes (de los manuales de Intel):






Este es el código capaz de generar opcodes para todos estos casos. Pero igual que con la tabla para 16 bits, direccionar los registros en orden de acuerdo a los diferentes casos de la tabla (registros generales, MMX o XMM), o una ubicación de memoria, requiere un poco más de código.

Eso sé cómo hacerlo en la mayor parte. Lo que no sé es concretamente, cómo recuperar la instrucción con todo y sus prefijos, y llamar la funcionalidad particular de cada instrucción a través de una función que interprete los bytes Mod/RM y SIB, y les pase los operadores.

Creo que antes de esto, sería necesario crear un desensamblador para cada instrucción, y así distinguir si nuestro código está siendo capaz de descifrar las instrucciones.

Así que creo que ese va a ser el siguiente paso: Crear un desensamblador comenzando con las instrucciones XOR, como prueba de concepto.


function modrmwords(modrm)
{
       if(mod(modrm)==1)
       {
        return "0";
       }


       if(mod(modrm)==2)
       {
        return "0,0,0,0";
       }

return "";
}









function xor_32()
{
    var str="bits 32\n\n";
    for(var modrm=0; modrm<256; modrm++)
    {
     if(mod(modrm)<=2)
     {
       if(mod(modrm)==0 && rm(modrm)==5)
       {
        str+="db 0x31,"+modrm+",0,0,0,0\n";
        continue;
       }


           if(rm(modrm)==4)
           {
            for(var sib=0; sib<256; sib++)
            {
             var sibstr=SIB32(modrm, sib);
             if(sibstr!="")
             {
              str+="db 0x31,"+modrm+","+sibstr+"\n";
             }
            }

            continue;
           }
            else
            {
              str+="db 0x31,"+modrm;
            }


           var modrmstr=modrmwords(modrm);
           if(modrmstr!="")
           str+=","+modrmstr+"\n";

           else
           str+="\n";





      continue;
     }







     //If we are here, the Mod field is 3:
     ///
//     if(mod(modrm)==3)
//     {
//      str+="db 0x31,"+modrm+"\n";
      str+="\n";
//     }
    }

return str;
}





function SIB32(modrm, sib)
{
if(base(sib)==5)
{
  if(mod(modrm)==0)
    return sib+",9,9,9,9";
  if(mod(modrm)==1)
    return sib+",8";
  if(mod(modrm)==2)
    return sib+",32,32,32,32";
}

// if(index(sib)!=4)
{
  return sib+","+modrmwords(modrm);
}

return "";
}




Estoy reuniendo y depurando el repositorio final de definiciones de valores de registros VGA.

Por ahora tengo los registros para el modo 320x200x256 colores, 640x480x16 colores, y 320x240x256 colores (Mode X).

Al principio tuve problemas en depurar el modo 640x480x16 colores, pero ahora los valores de registros son coherentes con el resto de referencias, y el video funciona bien.

Esta es una captura de pantalla rápida que muestra que el modo Gráfico VGA 640x480x16 colores ahora lo hemos depurado y ya funciona correctamente, al entrar a este sin usar el BIOS:


Para ver las definiciones en código fuente, ir aquí:

Definición Concisa de Registros VGA

Lo que nos interesa para esto es la sección Configuración de Registros para Modos de Video.



La "basura" que se veía anteriormente en la captura de pantalla es el contenido de la fuente en modo texto, que aparentemente comienza en la dirección 0xA0000, la misma en la que comienza la memoria de modo gráfico VGA estándar.

Esto también demuestra cómo se logra poner un logo y texto en la pantalla de inicio del BIOS. Es posible que para BIOSes que realmente usan modo gráfico y aun así son capaces de mostrar un logo, lo que hagan sea modificar algunos caracteres de la fuenta ASCII, desde los caracteres 128 a 255 como fragmentos del logo, y así logran estar en modo texto y mostrar por ejemplo el logo de Energy Star, etc., en modo de texto.

Ahora pongo aquí un índice que pienso rellenar con la información pertinente, que demuestra todo lo que necesitamos entender en principio para continuar con un emulador de VGA:

Guía de Programación a la EGA, VGA y Super VGA
Sitio web (si la siguiente imagen no aparece es porque está offline):

~

Este es un script que escribí en javascript para convertir un archivo de paleta de colores de 768 bytes usualmente (256 colores en grupos de 3 bytes RGB) a código fuente de Ensamblador:

PAL2src

Para quienes están más interesados en javascript aquí hay un truco.

Uno puede definir un link generado con javascript codificado en Base64, y también puede definir el nombre de archivo con la nueva propiedad download="nombre_de_archivo.exe".

Así que podemos tener un link de la forma:

<a href="data:application/octet-stream;base64,SG9sYQ==" download="Prueba.txt">Descargar archivo generado</a>



Y esto descargaría un archivo llamado Prueba.txt que contiene Hola.

Aquí usamos este truco para generar el código fuente y descargarlo automáticamente al dar clic al link generado, ya con un nombre de archivo de código fuente de Ensamblador adecuado:

PAL2src


<body bgcolor="#aaaaaa">

<pre id="a1">

</pre>


<script>

var file1="PALETTE_80x25x16.BIN";
var file2="PAL320x200x256.BIN";
var buff=new Array();
document.getElementById("a1").innerHTML="";

function genLink(idx, file)
{
var xhr=new XMLHttpRequest();

xhr.open("GET", file, true);
xhr.responseType="arraybuffer";

xhr.onload=function()
{
  buff[idx]=new DataView(this.response);
  document.getElementById("a1").innerHTML+='<a href="data:application/octet-stream;base64,'+btoa(PAL2src(idx, file))+'" download="'+file+'.asm">'+file+".asm</a>\n";
}

xhr.send();
return xhr;
}




function PAL2src(idx, file)
{
var str=file.substring(0, file.indexOf("."))+":\r\n";
for(var x=0; x<buff[idx].byteLength; x+=3)
{
  str+="  Color_"+(x/3)+": db ";
  str+="0x"+buff[idx].getUint8(x).toString(16).toUpperCase()+",";
  str+="0x"+buff[idx].getUint8(x+1).toString(16).toUpperCase()+",";
  str+="0x"+buff[idx].getUint8(x+2).toString(16).toUpperCase()+"\r\n";
}

return str;
}



genLink(0, file1);
genLink(1, file2);


</script>
Sitio web (si la siguiente imagen no aparece es porque está offline):

~

Después de todo este tiempo, he logrado decidir qué cosas deberían haber sido las primeras en implementar para nuestra emulación.

Actualmente he implementado nuevamente el CPU actualmente con unas 50 instrucciones de la 8086 (modo de 16 bits puro de una 386).

Aquí está el emulador actual corriendo (se puede tardar unos segundos en descargar la imagen actual de BIOS provisional, incluida en el código fuente):

Emulador de PC Pre-Alpha en HTML5


Z86Emu Version 2013-04-23; 0001

RAX=B800
RCX=0000
RDX=0000
RBX=0000
RSP=0500
RBP=0000
RSI=0011
RDI=0000

RIP=002B

ES=F000
CS=F000
DS=F000
SS=0000
FS=0000
GS=0000

;_eflags_
         dd 00000000000000000000000001000110b
;                     |||||| ||||||||| | | |
;                     |||||| ||||||||| | | |___ (00)Carry Flag                (CF)  -- STATUS
;                     |||||| ||||||||| | |_____ (02)Parity Flag               (PF)  -- STATUS
;                     |||||| ||||||||| |_______ (04)Auxiliary Carry Flag      (AF)  -- STATUS
;                     |||||| |||||||||_________ (06)Zero Flag                 (ZF)  -- STATUS
;                     |||||| ||||||||__________ (07)Sign Flag                 (SF)  -- STATUS
;                     |||||| |||||||___________ (08)Trap Flag                 (TF)  -- SYSTEM
;                     |||||| ||||||____________ (09)Interrupt Enable Flag     (IF)  -- SYSTEM
;                     |||||| |||||_____________ (10)Direction Flag            (DF)  -- CONTROL
;                     |||||| ||||______________ (11)Overflow Flag             (OF)  -- SYSTEM
;                     |||||| |++------------(12)(13)I/O Privilege Level     (IOPL)  -- SYSTEM
;                     |||||| |_________________ (14)Nested Task               (NT)  -- SYSTEM
;                     ||||||___________________ (16)Resume Flag               (RF)  -- SYSTEM
;                     |||||____________________ (17)Virtual-8086 Mode         (VM)  -- SYSTEM
;                     ||||_____________________ (18)Alignment Check           (AC)  -- SYSTEM
;                     |||______________________ (19)Virtual Interrupt Flag    (VIF) -- SYSTEM
;                     ||_______________________ (20)Virtual Interrupt Pending (VIP) -- SYSTEM
;                     |________________________ (21)ID Flag                   (ID)  -- SYSTEM




Para evaluar el código actual (en el Dominio Público), descargar este archivo:

Z86Emu_PreAlpha_2013-05-04.zip

Lo Que Queda por Hacer

A diferencia de las versiones Pre-Alpha anteriores que había puesto aquí, desde esta versión particular del emulador, se ha implementado toda una infraestructura formal que permite capturar los puertos de Entrada/Salida, y regiones de memoria (de video, de otros periféricos, etc.), y manejarlas tanto a nivel lógico como de datos de dichos puertos.

Como se ve en la emulación, podemos manejar la memoria VGA, pero a diferencia de versiones anteriores, he averiguado hasta el cansancio la forma de cómo estructurar dicha memoria exactamente igual que una VGA estándar, en cada uno de sus modos de texto y gráficos (por ahora solo está implementada la funcionalidad del modo de texto 80x25x16).

Como se ve, también se ha implementado capacidad de hacer un dump de todos los registros del CPU (y luego de regiones de memoria, de la memoria VGA, de los registros de cada periférico) y a la vez detener la emulación hasta que dejemos de revisar sus valores en busca de errores.

Esto literalmente hace posible que nuestro emulador se vuelva tan y más poderoso que emuladores tradicionales como 8086emu, con un alto grado educativo para el desarrollador, una vez que implementemos más cosas.


Lo siguiente por hacer es implementar el 100% de las instrucciones de la 8086/8088, y esto nos dará la posibilidad de implementar un BIOS estándar a emular. También necesitamos implementar un teclado PS/2, tal como se muestra preliminarmente aquí:

Referencia Visual Interactiva de Valores de Teclas del Teclado


Con esas cosas (CPU 8086 completo, teclado PS/2 y depuración mediante dumps de registros, memoria y periféricos), se nos hará posible depurar lo suficiente como para implementar una unidad de floppy, DMA, el timer, el controlador de interrupciones, y un BIOS estándar, y completar la emulación de la VGA estándar, y afinar la emulación de las regiones de memoria aunque una lectura o escritura atraviese dos regiones de tipos diferentes.

Teniendo esto, ya tendremos un emulador completo de PC 8086, el cual podremos escalar a una 386, y a una Pentium con PCI, FPU, gráficos Super VGA, mouse PS/2, CD/DVD emulado, disco duro emulado, sonido Sound Blaster y/o AC'97, y emulación de una conexión a Internet (emulando los paquetes de bajo nivel encapsulados en un XMLHttpRequest ordinario).

Una vez logardo eso, solo nos queda terminar de aprender sobre los estándares de hardware mediante su emulación/implementación, sino que gracias a esto podremos finalmente aprender de forma fácil y absolutamente completa, lenguajes como C, C++, y desde ahí profesionalizarnos. Unos 3 años sería un tiempo estimado bastante justo para lograr al menos el inicio de esto.

Todo desde un navegador con soporte para HTML5, incluso uno móvil con suficiente memoria, lo que le daría mucho más valor a todo el código clásico de PC, y le daría enorme portabilidad directo en el navegador.




El Siguiente Paso Concreto

El siguiente paso concreto es terminar de implementar el 100% de instrucciones de una 8086/8088. Juzgando por el tiempo que me llevó implementar las primeras 40-50 instrucciones, implementar el resto de instrucciones de 16 bits puros se llevaría no menos de 1 o 2 semanas (especialmente para instrucciones con prefijos, que las modifican y les dan un tamaño variable, impredecible de antemano, pero determinable).

Creo que voy a seguir escribiendo detalles variados a medida que termine de pensar bien cómo serían los siguientes pasos en el tramo actual de camino de implementación. Lo que he hecho hasta ahora, especialmente determinar las funciones de la VGA estándar, con decenas de registros y por lo menos unas 300 funciones internas a implementar, comenzando por la memoria, han sido un dolor de cabeza literal, pero que ya he superado, y con mayor habilidad como compensación, y la posibilidad de comenzar a ver la forma de un emulador de PC realmente útil en el mundo real, y extremadamente portable en infinidad de dispositivos móviles y de escritorio gracias a HTML5, y de volvernos especialistas en emulación a lo largo de los años.
Sitio web (si la siguiente imagen no aparece es porque está offline):