Cómo programar un emulador.
por Marat Fayzullin
Prohibida la distribución no autorizada. Enlaza a esta página, no
la copies.
Escribí este documento tras recibir grandes cantidades de emails
de gente que quería saber cómo escribir un emulador de una u otra
computadora pero no sabía por donde empezar. Cualquier opinión o
consejo contenido en el presente texto es solamente mio y no debe
ser tomado como una verdad absoluta. El documento principalmente
cubre los emuladores "interpretadores" en contraposición a los
"compiladores", ya que no tengo mucha experiencia en técnicas de
recompilación. Encontrarás varios enlaces a lugares donde puedes
encontrar información sobre estas técnicas.
Si piensas que en este documento falta algo o quieres hacer una
corrección, sé libre de enviarme por email tus comentarios. No
contestaré a flames/idiocy o peticiones de ROMs. Probablemente faltan
algunas direcciones FTP/WWW importante en la lista de recursos de
este documento de modo que si conoces alguna que valga la pena
poner aquí, dimelo. Y lo mismo para algunas FAQs (Preguntas
Frecuentemente Contestadas) que no están en este documento.
Este documento ha sido
traducido al japonés por Bero. Hay también
traducciones al Chino disponibles, cortesía de Jean-Yuan Chen, y una
traducción al Francés
hecha por Guillaume Tuloup. Esta traducción al castellano está realizada
por Santiago Romero. Nota del traductor: La
versión original en Inglés
del documento está en la página de
Marat Fayzullin.
Contenidos
¿Así que has decidido escribir un emulador? Bien, entonces este
documento puede ser una ayuda para ti. Cubre varias cuestiones
técnicas comunes que la gente suele preguntar sobre la programación
de emuladores. También te proveerá de las pautas que alguna manera
podrás seguir en el funcionamiento interno del emulador.
- General
- ¿Qué puede ser emulado?
- ¿Qué es "emulación y en qué se diferencia de "simulación"?
- ¿Es legal emular el hardware propietario?
- ¿Qué es un emulador "interpretador" y en qué se diferencia de
un emulador "recompilador"?
- Quiero programar un emulador: ¿Por dónde debería empezar?
- ¿Qué lenguaje de programación debería usar?
- ¿Dónde puedo obtener información del hardware emulado?
- Implementación
- ¿Cómo emulo una CPU?
- ¿Cómo manejo los accesos a la memoria emulada?
- Tareas cíclicas: ¿Qué son?
- Técnicas de Programación
- ¿Cómo optimizo el código C?
- ¿Qué es low/high-endianess?
- ¿Cómo hago el programa portable?
- ¿Por qué debería hacer mi programa modular?
- Más por venir aquí.
¿Qué puede ser emulado?
Basicamente, cualquier cosa que tenga un microprocesador dentro. Por
supuesto, sólo los dispositivos ejecutando programas más o menos
flexibles son interesantes para emular. Esto incluye:
- Computadoras
- Calculadoras
- Consolas de VídeoJuegos
- VídeoJuegos Arcade
- etc.
Es necesario que te des cuenta de que puedes emular cualquier sistema
o computadora, incluso si éste es muy completo (como la computadora
Commodore Amiga, por ejemplo). No obstante, el rendimiento de una
emulación así puede ser bastante lento.
¿Qué es "emulación" y en qué se diferencia de "simulación"?
Emulación es un intento de imitar el diseño interno de un dispositivo.
Simulación es un intento de imitar las funciones de un dispositivo.
Por ejemplo, un programa imitando el hardware del arcade Pacman y
ejecutando la ROM real de Pacman en él es un emulador. Un juego de
Pacman escrito para tu computadora pero usando gráficos similares
a los del arcade real es un simulador.
¿Es legal emular el hardware propietario?
Aunque esta materia entra dentro del área "gris", parece legal
emular el hardware propietario, siempre que la información sobre
él no haya sido obtenida de manera ilegal. Debes tener cuidado
también la distribución ilegal de ROMs del sistema (BIOS, etc.)
con un emulador si estas ROMS tienen copyright.
¿Qué es un emulador "interpretador" y en qué se diferencia de un
emulador "recompilador"?
Hay tres esquemas básicos que pueden ser usados para un emulador.
Pueden ser combinados para obtener el mejor resultado.
- Interpretación
Un emulador lee código emulado byte a byte desde la memoria,
lo decodifica y realiza las operaciones apropiadas en los
registros emulados, memoria y E/S. El algoritmo general para
este tipo de emuladores es:
while(CPUIsRunning)
{
Fetch OpCode
Interpret OpCode
}
Las virtudes de este modelo incluyen facilidad de depuración,
portabilidad y facilidad de sincronización (puedes simplemente
contar los cíclos de reloj que han ocurrido y adaptar el resto
de tu emulación a este contaje de ciclos).
Una debilidad simple, grande y obvia es el bajo rendimiento.
La interpretación toma mucho tiempo de CPU y puedes necesitar
una computadora bastante rápida para ejecutar el código a una
velocidad decente.
- Recompilación estática
En esta técnica se toma un programa escrito en el código
emulado y se intenta traducir éste al código ensamblador de
tu computadora. El resultado será un ejecutable que puedes
hacer funcionar en tu computadora sin ninguna herramienta
especial. Aunque esto suena muy bien, no siempre es posible.
Por ejemplo, no puedes recompilar estáticamente código que
se automodifique a él mismo ya que no hay manera de saber a
qué hay que convertirlo sin ejecutarlo. Para evitar ese tipo
de situaciones, es posible combinar la recompilación estática
con un intérprete o recompilador dinámico.
- Recompilación dinámica
La recompilación dinámica es esencialmente la misma cosa que
la estática, pero ocurre durante la ejecución del programa.
En lugar de intentar recompilar todo el código de una vez,
se hace al vuelo cuando se encuentra una instrucción CALL o JUMP.
Para incrementar la velocidad, esta técnica puede ser combinada
con la recompilación estática. Puedes leer más sobre recompilación
dinámica en el "white paper" de Ardi, creadores del emulador
recompilador de Macintosh.
Quiero programar un emulador. ¿Por dónde debería empezar?
Para programar un emulador, debes tener un buen conocimiento general
de programación de computadoras y de electrónica digital. La experiencia
en programación en ensamblador es muy útil también.
- Selecciona un lenguaje de programación para usar.
- Encuentra toda la información disponible sobre el hardware emulado.
- Escribe la emulación de la CPU o utiliza código ya existenet
para la emulación de la misma.
- Escribe algo de código en forma de borrador para emular el resto
del hardware, al menos parcialmente.
- En este punto, resulta muy útil escribir un pequeño depurador
interno que te permita detener la emulación y ver qué está
haciendo el programa. Necesitarás también un desensamblador
para el lenguaje ensamblador del sistema emulado. Escribe el
tuyo propio si no existe ninguno disponible.
- Intenta ejecutar programas en tu emulador.
- Utiliza el desensamblador y el depurador para ver cómo los
programas utilizan el hardware y adapta tu código de forma apropiada.
¿Qué lenguaje de programación debería usar?
Las alternativas más obvias son C y Ensamblador. A continuación
enumero los pros y contras de cada uno:
- Lenguaje ensamblador
- Generalmente, permite producir código más rápido.
- Los registros de la CPU que realiza la emulación
pueden ser utilizados directamente para almacenar
los registros de la CPU emulada.
- Muchos opcodes (códigos de operación) pueden ser
emulados directamente con opcodes similares de la
CPU emuladora.
- El código es no portable, es decir, no puede ser ejecutado
en una computadora con diferente arquitectura.
- El código se hace difícil de depurar y mantener.
- C
- El código puede ser hecho portable de forma que funcione
en diferentes computadoras y Sistemas Operativos.
- Es relativamente fácil de depurar y mantener.
- Diferentes hipótesis de cómo funciona el hardware real
pueden ser testeadas rápidamente.
- C es generalmente más lento que el código ensamblador puro.
Un buen conocimiento del lenguaje elegido es una necesidad absoluta
para escribir un emulador que funcione, ya que es un proyecto bastante
completo y el código debe ser optimizado para ejecutarse tan rápido
como sea posible. La emulación no es definitivamente uno de esos
proyectos donde se aprende un lenguaje de programación.
¿Dónde puedo obtener información del hardware emulado?
Lo siguiente es una lista de lugar en donde te puede interesar echar
un vistazo.
- Grupos de News
- comp.emulators.misc
Este es un grupo de news para la discusión general sobre emulación
de computadoras. Muchos autore de emuladores lo leen, aunque el
nivel de "ruido" es bastante alto. Lee la FAQ de c.e.m. antes de
postear mensajes en este grupo de news.
- comp.emulators.game-consoles
Lo mismo que comp.emulators.misc, pero dedicado específicamente a
los emuladores de videojuegosd e consola. Lee la FAQ de c.e.m antes
de postear mensajes en este grupo de news.
- comp.sys./emulated-system/
La jerarquía comp.sys.* contiene grupos de news dedicados a
computadoras específicas. Puedes obtener un montón de información
técnica leyendo estos grupso de news. Ejemplos típicos:
- comp.sys.msx: Computadoras MSX/MSX2/MSX2+/TurboR
- comp.sys.sinclair: Sinclair ZX80/ZX81/ZXSpectrum/QL
- comp.sys.apple2: Apple ][
- etc.
Por favor, lee las FAQs apropiadas antes de postear en estos grupos.
- alt.folklore.computers
- rec.games.video.classic
FTP
WWW
¿Cómo emulo una CPU?
Lo primero de todo, si necesitas emular una CPU Z80 o 6502 estándar,
puedes usar uno de los emuladores de CPUs que yo escribí. No obstante,
deben aplicarse ciertas condiciones a su uso.
Para aquellos que quieren escribir su propio núcleo de emulación CPU
o están interesados en cómo funciona, a continuación puede verse un
esqueleto en C de un emulador de CPU típico. En el emulador real,
tal vez desees evitar algunas partes de él o escribir algunas otras
por tí mismo.
Counter=InterruptPeriod;
PC=InitialPC;
for(;;)
{
OpCode=Memory[PC++];
Counter-=Cycles[OpCode];
switch(OpCode)
{
case OpCode1:
case OpCode2:
...
}
if(Counter<=0)
{
/* Chequear interrupciones y */
/* hacer tareas cíclicas aquí... */
...
Counter+=InterruptPeriod;
if(ExitRequired) break;
}
}
Primero asignamos valores iniciales al contador de ciclos de la
CPU (Counter) y al Contador de Programa (PC):
Counter=InterruptPeriod;
PC=InitialPC;
Counter contiene el número de cíclos de CPU que quedan para la
próxima posible interrupción. Nota que la interrupción no debe
de ocurrir necesariamente cuando este contador expire: puedes
aprovecharlo para otros propósitos, como sincronizar temporizadores,
o actualizar scanlines en la pantalla. Veremos más sobre esto más
adelante. El PC contiene la dirección de memoria de la cual la
CPU emulada leerá el siguiente opcode.
Tras la asignación inicial de valores, empezamos el bucle principal:
for(;;)
{
Nota que este bucle podría haberse implementado también como:
while(CPUIsRunning)
{
Donde CPUIsRunning es una variable booleana. Esto tiene ciertas
ventajas, ya que podemos terminar el bucle en cualquier momento
poniendo CPUIsRunning=0. Desafortunadamente, chequear esta variable
en cada paso del bucle toma mucho tiempo de CPU y debe ser evitado
en la medida de lo posible. Por otra parte, no implementes este
bucle como
while(1)
{
porque en este caso, algunos compiladores generarán código
chequeando si 1 es verdadero o no. Realmente no deseamos que el
compilador haga este trabajo innecesario en cada paso del bucle.
Ahora, mientras estamos en el bucle, la primera cosa a hacer es
leer el siguiente opcode, y modificar el Contador de Programa:
OpCode=Memory[PC++];
Notése que aunque este es el método más simple y rápido de acceder
a la memoria emulada, no es siempre posible. Un método más universal
de acceder a la memoria será cubierto más tarde en este documento.
Una vez que el opcode ha sido leido (fetch), decrementamos el
contador de ciclos de la CPU en el número de ciclos requeridos
para dicho opcode:
Counter-=Cycles[OpCode];
La tabla de ciclos debe contener el número de ciclos de CPU para
cada opcode. Algunos opcodes (como saltos condicionales o llamadas
a subrutinas) pueden tardar un diferente número de ciclos dependiendo
de sus argumentos. Esto puede ser ajustado más tarde en el código.
Ahora viene el tiempo de interpretar el opcode y ejecutarlo:
switch(OpCode)
{
Es un fallo común el pensar que la construcción switch() es
ineficiente, porque compila como una cadena de instrucciones
if() ... else if(). Aunque esto es cierto para construcciones
con un pequeño número de casos, las construcciones grandes
(típicamente 100-200 o más casos) parecen compilarse como una
tabla de saltos, lo que lo hace bastante eficiente.
Hay 2 maneras alternativas de interpretar los opcodes. La primera
es crear una tabla de funciones y llamar a la apropiada. Este método
parece menos eficiente que un switch() ya que se tiene la sobrecarga
de la llamada a la función. El segundo método sería utilizar una
tabla de etiquetas (labels) y utilizar la sentencia goto. Aunque
este método es ligeramente más rápido que un switch(), sólo
funcionará en compiladores que soporten "etiquetas precalculadas".
Otros compiladores no te permitirán crear un vector de direcciones
de etiquetas.
Una vez que hemos interpretado y ejecutado con éxito el opcode,
llega un momento en que tenemos que chequear algunas interrupciones.
En este momento podemos también aprovechar para realizar cualquier
tarea que necesite ser sincronizada con el reloj del sistema:
if(Counter<=0)
{
/* Chequear interrupciones y realizar otras
emulaciones de hardware aquí */
...
Counter+=InterruptPeriod;
if(ExitRequired) break;
}
Estas tareas cíclicas serán cubiertas más tarde en este documento.
Nota que no asignamos simplemente Counter=InterruptPeriod, y que
en cambio hacemos Counter+=InterruptPeriod: esto hace el contaje
de ciclos más preciso, ya que puede haber un valor negativo de
ciclos en el contador Counter.
También fíjate en la línea:
if(ExitRequired) break;
Como es muy costoso testear la salida del programa en cada paso del
bucle, lo podemos hacer sólo cuando el contador Counter expire:
esto hará también finalizar la emulación cuando hagamos ExitRequired=1,
pero no tomará tanto tiempo de CPU.
¿Cómo manejo los accesos a la memoria emulada?
La manera más simple de acceder a la memoria emulada es tratarlo
como un vector (array) de bytes (words, etc.). Accederla es
trivial entonces:
Data=Memory[Address1]; /* Leer de Address1 */
Memory[Address2]=Data; /* Escribir a Address2 */
Ese simple método de acceso a memoria no es siempre posible por las
siguientes razones:
- Memoria paginada
El espacio de direccionamiento puede estar fragmentado entre
páginas modificables (también llamadas bancos). Esto se hace
habitualmente para expandir la memoria cuando el espacio de
direccionamiento es pequeño (64Kb).
- "Mirrored Memory"
Un área de memoria puede ser accesible desde diferentes
direcciones. Por ejemplo, el data que intentamos escribir en
la dirección $4000 aparecerá también en la $6000, $8000, etc.
Las ROMS también pueden aparecer repetidas debido a la
decodificación incompleta.
- Protección de la ROM
Hay software basado en cartuchos (como algunos juegos de MSX,
por ejemplo) que intenta escribir en su propia ROM y se niega
a funcionar si la escritura tiene éxito. Esto se hace normalmente
como protección anticopia. Para hacer funcionar este software
en tu emulador debes desactivar la escritura en la ROM.
- E/S mapeada en memoria
Pueden haber en el sistema dispositivos de E/S mapeados en la
memoria. Accesos a dichas posiciones de memoria producen "efectos
especiales" y deben ser tenidos en cuenta.
Para cubrir todos estos problemas introduciremos un par de funciones:
Data=ReadMemory(Address1); /* Read from Address1 */
WriteMemory(Address2,Data); /* Write to Address2 */
Todo el procesado especial como acceso de páginas, mirroring, control
de E/S, etc. se hace dentro de estas funciones.
ReadMemory() y WriteMemory() normalmente causan bastante sobrecarga
en la emulación si son llamadas muy frecuentemente. Por ello deben
ser creadas de la manera más eficiente posible. A continuación
tenemos un ejemplo de dichas funciones escritas para acceder a una
dirección de memoria paginada:
static inline byte
ReadMemory(register word Address)
{
return(
MemoryPage[Address>>13][Address&0x1FFF]
);
}
static inline void
WriteMemory(register word Address,register byte Value)
{
MemoryPage[Address>>13]
[Address&0x1FFF]=Value;
}
Date cuenta de la palabra clave inline. Esta palabra le dice al
compilador que embeba la función dentro del código, en lugar de
realizar llamadas a ella. Si tu compilador no soporta inline o
_inline, intenta hacer la función static (con static): algunos
compiladores (WatcomC, por ejemplo) optimizarán funciones estáticas
cortas haciéndolas inline.
También ten claro que la mayoría de las veces la función ReadMemory()
es llamada con mucha más frecuencia que WriteMemory(). Por ello, es
útil implementar la mayoría del código en WriteMemory(), dejando
ReadMemory() tan simple y corta como sea posible.
-
Una pequeña nota sobre "memory mirroring":
Como se ha dicho antes, muchas computadoras disponen de RAM
con zonas reflejadas donde un valor escrito en una posición
de memoria aparece también en otras. Aunque esta situación
puede ser gestionada en ReadMemory(), esto no es recomendable,
ya que ReadMemory() es llamada mucho más frecuentemente que
WriteMemory(). Una manera más eficiente de implementar la
mirrored memory consistiría en realizar toda su gestión en
la función WriteMemory().
Tareas cíclicas: ¿Qué son?
Las tareas cíclicas son cosas que deben ocurrir periódicamente en
una máquina emulada, como:
- Refresco de pantalla
- Interrupcions VBlank y HBlank
- Actualización de temporizadores
- Actualización de parámetros de sonido
- Actualización del estado de teclado/joystick
- etc.
Con el fin de emular estas tareas deberás ejecutarlas tras el
número apropiado de ciclos de CPU. Por ejemplo, si la CPU se
supone que corre a 2.5Mhz y el display usa una frecuencia de
refresco de 50Hz (estándar para el vídeo PAL), entonces la
interrupción VBlank ocurrirá cada
2500000/50 = 50000 ciclos de CPU
Si asumimos que la pantalla entera (incluyendo VBlank) es de 256
scanlines de alto y 212 de ellas son mostradas en el display (las
otras 44 caen en el VBlank), entonces obtenemos que nuestra
emulación debe refrescar un scanline cada:
50000/256 ~= 195 ciclos de CPU
Tras eso, debemos generar una interrupción VBlank y no hacer
nada hasta que se haya acabado con el VBlank, es decir, durante
(256-212)*50000/256 = 44*50000/256 ~= 8594 ciclos de CPU
Calcula cuidadosamente el número de ciclos de reloj necesarios para
cada tarea, y entonces utiliza el divisor común mayor para
InterruptPeriod, ajustando todas las otras tareas a él (no tienen
porqué ejecutarse en cada expiración del contador Counter).
¿Cómo optimizo el código C?
Se puede conseguir mucho rendimiento adicional eligiendo las
opciones correctas de optimización del compilador. Basadas en mi
experiencia, las siguientes combinaciones de opciones te darán
la mejor velocidad de ejecución:
Watcom C++ -oneatx -zp4 -5r -fp3
GNU C++ -O3 -fomit-frame-pointer
Borland C++
Si encuentras una mejor combinación de opciones para cualquiera de
estos compiladores o para otro diferente, por favor házmelo saber.
-
Una pequeña nota sobre desenrollamiento de bucles:
Puede parecer muy útil activar el "loop unrolling"
(desenrrollamiento de bucles) del compilador. Esta opción
intentará convertir los bucles cortos en piezas lineales
de código. Mi experiencia me muestra, no obstante, que
esta opción no produce ningún incremento del rendimiento.
Activarla puede incluso romper tu código en algunos casos
especiales.
Optimizar el código C por ti mismo es algo más laborioso que
simplemente elegir opciones del compilador, y normalmente dependerá
de la CPU para la que compilemos el código, aunque hay muchas reglas
generales que se aplican a todas las CPUs. No te las tomes como
verdades absolutas, ya que pueden variar según muchos factores:
- ¡Usa el profiler!
Una ejecución de tu programa bajo cualquier utilidad de
profiling decente (GPROF viene inmediatamente a mi mente)
puede revelarte cosas maravillosas que no habías sospechado.
Puedes encontrar que porciones de código insignificantes
son ejecutadas con mucha más frecuencia que el resto y que
éstas ralentizan tu programa. Optimizar estas porciones de
código o reescribirlas en lenguaje ensamblador puede incrementar
el rendimiento.
- Evita el C++
Evita utilizar cualquier construcción que te fuerce a compilar
el programa con un compilador de C++ en lugar de simple C: los
compiladores de C++ normalmente añaden mucha sobrecarga al
código generado.
- Tamaño de los enteros
Intenta utilizar sólo enteros del tamaño base soportado por la
CPU (int en lugar de short o long). Esto reducirá la cantidad
de código generado por el compilador debido a conversiones
entre diferentes longitudes de enteros. También reducirá el
tiempo de acceso a memoria, ya que algunas CPUs trabajan más
rápido cuando leen/escriben datos del tamaño base y alineados
a direcciones múltiples del tamaño base.
- Uso de registros
Utiliza tan pocas variables como te sea posible en cada bloque
y declara las más usadas como tipo register (muchos nuevos
compiladores pueden poner automáticamente variables en los
registros, no obstante). Esto tiene sentido para las CPUs
con muchos registros de propósito general (PowerPC) mas que
para aquellas con unos pocos registros dedicados (Intel 80x86).
- Desenrrolla los bucles pequeños
Si tienes un pequeño bucle que se ejecuta unas pocas veces, es
una buena idea desenrrollarlo manualmente en porciones de código
lineales. Mira la nota más arriba sobre el desenrrollamiento
automático.
- Desplazamientos contra multiplicación/división
Utiliza desplazamientos cada vez que necesites multiplicar o
dividir por 2^ (J/128==J>>7). Estos se ejecutan mucho más
rápido en muchas CPUs. Usa también el operador de bits AND
para obtener el módulo en estos casos (J%128==J&0x7F).
¿Qué es low/high-endianess?
Ls CPUs se dividen generalmente en muchas clases, dependiendo
de cómo almacenan los datos en memoria. Aunque hay algunos
especímenes muy particulares, la mayoría de las CPUs caen en
una de estas dos clases:
- Las CPUs High-endian almacenarán los datos de modo que los bytes
más altos de una palabra aparezcan primero en memoria. Por ejemplo,
si almacenas el número 0x1234567 en una de estas CPUs, la memoria
aparecerá como:
0 1 2 3
+--+--+--+--+
|12|34|56|78|
+--+--+--+--+
- Las CPUs Low-endian CPUs almacenan los datos de forma que los
bytes más bajo de una palabra aparezcan primero en memoria. El
ejemplo anterior aparece bastante diferente en estas CPUs:
0 1 2 3
+--+--+--+--+
|78|56|34|12|
+--+--+--+--+
Ejemplos típicos de CPUs high-endian so las 6809, las seriers
Motorola 680x0, PowerPC, y Sun SPARC. Las CPUs Low-endian incluyen
el 6502 y su sucesor el 65816, el Zilog Z80, muchos chips Intel
(incluyendo el 8080 y los 80x86), DEC Alpha, etc.
Cuando se programa un emulador debes tener cuidado en con el
formato que usan ambas CPUs (la emulada y la emuladora). Digamos
que quieres emular una cpu Z80 que es low-endian. Esto quiere
decir que el Z80 almacena sus palabras de 16 bits con el byte
inferior primero. Si utilizas una CPU low-endia (como por ejemplo
el Intel 80x86) para esto, todo ocurre naturalmente. Pero si usas
una CPU high-endian (PowerPC) aparece un problema al almacenar
los datos de 16 bits del Z80 en memoria. Peor aún, si tu programa
debe trabajar en ambas arquitectureas necesitarás algún sistema
para tratar este problema.
Una manera de gestionar este problema se da a continuación:
typedef union
{
short W; /* Acceso a Word */
struct /* Acceso a Byte... */
{
#ifdef LOW_ENDIAN
byte l,h; /* ...en arquitectura low-endian */
#else
byte h,l; /* ...en arquitectura high-endian */
#endif
} B;
} word;
Como puedes ver, una palabra puede ser accedida por completo usando
W. Cada vez que la emulación necesite acceder a ella por sus bytes
por separado, utilizaremos B.l y B.h, que preservan el orden.
Si tu programa va a ser compilado en diferentes plataformas, puedes
querer saber si ha sido compilado con el flag de endianess correcto
antes de ejecutar algo realmente importante. Aquí podemos ver una
manera de realizar dicho test:
int *T;
T=(int *)"\01\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
if(*T==1) printf("Esta máquina es high-endian.\n");
else printf("Esta máquina es low-endian.\n");
¿Cómo hago el programa portable?
Todavía por escribir.
¿Por qué debería hacer mi programa modular?
Muchos sistemas están formados por muchos chips cada uno de los
cuales realiza cierta parte de las funciones del sistema. Suele
haber una CPU, una controladora de vídeo, un generador de sonido,
etc. Algunos de estos chips pueden tener su propia memoria u
otro hardware asociado a ellos.
Un emulador típico debe repetir el diseño del sistema original
implementando cada subsistema y sus funciones en un módulo
separado. Primero, esto hace la depuración más fácil ya que todos
los fallos están localizados en los módulos. Segundo, la arquitectura
modular permite reutilizar los módulos en otros emuladores. El
hardware de computadoras está bastante estandarizado: puedes encontrar
la misma CPU o chip de vídeo en muchos modelos diferentes de
computadoras. Es mucho más fácil emular el chip una sola vez que
implementarlo una y otra vez por cada computadora que lo use.
1997-1999 Copyright por Marat Fayzullin [fms@cs.umd.edu]
Traducción al Castellano por Santiago Romero
Subir al principio de esta página
Santiago Romero alias NoP
sromero©gmail·com
Ultima actualización
: 11-03-2005