¿Los emuladores analizan el código binario dentro de los archivos?

¿Los emuladores analizan el código binario dentro de los archivos?

He visto algunos emuladores que afirman que se ejecutan y, aunque lo hacen, su código fuente muestra que no analizan directamente cada 1 y 0 para determinar una instrucción.

Mi pregunta es, si el emulador debe emular los códigos de operación exactos que haría la CPU real, ¿no sería necesario analizar el formato de código de operación binario correcto de un juego para emular la CPU legítimamente (o en absoluto)?

Por ejemplo, en el archivo del juego almaceno una instrucción, un byte, marcado de la siguiente manera:

0000 1111

Mi programa debe verificar que esta instrucción realmente signifique (por ejemplo, "agregar uno al registro A"), pero ¿no necesitaría verificar cada cero y uno en el archivo de texto para asegurarlo?

Luego, los emuladores analizarían bytes completos, pero los bytes completos, nuevamente, son ocho bits y los patrones fluctuantes cambian la salida de la operación.

Por ejemplo, 0000 1111 podría significar agregar uno a A, pero 0000 1110 podría significar agregar A con A.

Respuesta1

Exposición: intentar responder directamente a la pregunta.

Si está leyendo el código fuente de un emulador y no lee ciertos bits de un archivo binario (ejecutable) y aún ejecuta fielmente el código, entonces hay tres resultados posibles:

  1. Eresequivocadoal pensar que el emulador no lee cada bit del archivo, yde hecho lo hace, y simplemente estás equivocado.
  2. Erescorrecto, y el emulador no lee cada bit, porque es capaz de asumir ciertos hechos sobre el comportamiento del programa que está emulando para no necesitar leer cada bit para saber lo que necesita hacer (tal vez porque espera un cierto motor de juego que se ejecutará, o un cierto tipo de API de gráficos, o un cierto tipo de API de sonido, etc.).
  3. Erescorrecto, y el emulador no lee cada bit, porque hay ciertos bits del ejecutable que simplemente no son necesarios para ejecutar el programa correctamente. Pueden ser "cruft" heredados, o metadatos, o cualquier otra cosa que sea simplemente una tontería adicional que en realidad no consiste en la funcionalidad del programa.
  4. Erescorrecto, y el emulador no lee cada bit, porque el emulador está traduciendo ciertas operaciones en el código en operaciones de nivel superior y omite por completo las instrucciones de bajo nivel específicas del procesador/hardware. Por ejemplo, si te piden que imites exactamente lo que una persona está haciendo en una grabación de vídeo de esa persona realizando una operación complicada, y te dicen "Ahora taladra un agujero en el costado de la caja", estarías bastante tentado a dejar de hacerlo. mire el video y use su experiencia existente sobre cómo perforar agujeros en cosas en lugar de seguir los movimientos literales del tipo en el video (suponiendo que esté equipado con un taladro adecuado y, en general, tenga experiencia en la vida). De manera similar, si el emulador puede deducir que el programa solicita dibujar una imagen de 32x32 en la pantalla en un conjunto de coordenadas dado, puede dejar de leer el código tan pronto como comprenda qué imagen es, en qué formato está la imagen, y dónde dibujarlo: no necesita ver cómo lo dibuja el sistema emulado.

Cómo funcionan los emuladores

Un emulador que ejecuta código para otra plataforma y/o CPU (por ejemplo,vino) hace cosas en varias etapas. Algunas etapas son absolutamente necesarias para que el emulador funcione; otras etapas son opcionales y representan posibilidades de optimización del rendimiento.

  • Requerido: "Analizar" el código ejecutable (código de máquina, MSIL, código de bytes de Java, etc.)Analizandoconsiste en:

    • Leyendo cada bit del código ejecutable.
    • Comprender lo suficiente el diseño/formato (sintaxis) y el propósito (semántica) de cada bit/byte (o cualquier otra unidad discreta de medida de información que desee utilizar) del código nativo, para comprender lo que está haciendo.
    • Comprenderquédice un programa, el emulador tiene que entender elsintaxisdel formato binario, y elsemántica. La sintaxis consta de cosas como "expresamos enteros de 32 bits con signo en formato de bit con mínimo signo"; la semántica consiste en cosas como "cuando el código nativo contiene un código de operación52, eso significa hacer una llamada a función."
    • Mnemotécnico(te ayuda a recordar por qué esto es necesario): Si me he dedicado a seguir una receta, si la ignoro por completo y ni siquieraleeres imposible que alguna vez pueda seguir esa receta, a menos que pruebe un montón de cosas al azar ysuertea seguir los mismos pasos que requeriría la receta. De manera similar, a menos que tenga una simulación aleatoria de Monte Carlo que ejecute instrucciones aleatorias de la CPU hasta que tenga suerte de realizar la misma funcionalidad que el programa, cualquier emulador tendrá que comprenderquédice el programa.

  • Requerido: "Traducir" el código analizado (normalmente algún tipo de modelo de datos abstracto, máquina de estados, árbol de sintaxis abstracta o algo así) a cualquiera de los dosnivel altocomandos (por ejemplo, declaraciones en C o Java) onivel bajocomandos (por ejemplo, instrucciones de CPU para un procesador x86). Los comandos de alto nivel tienden a ser más óptimos. Por ejemplo, si analiza el flujo de código de una larga secuencia de instrucciones de la CPU y determina en un nivel alto que lo que está pidiendo es reproducir un determinado archivo MP3 desde el disco, puede omitir toda la emulación a nivel de instrucción y simplemente usar su código nativo. decodificador MP3 de la plataforma (que puede estar optimizado para su procesador) para reproducir el mismo archivo MP3. Por otro lado, si "rastreara" la ejecución del programa emulado lo más literalmente posible, esto sería más lento y menos óptimo, porque estaría renunciando a gran parte de la optimización de la que se beneficia al ejecutar instrucciones de forma nativa.

  • Opcional: "Optimizar" y analizar el flujo de código de una gran parte del código del programa emulado, o del programa completo, para determinar la secuencia completa de ejecución, y construir un modelo muy detallado y sofisticado de cómo su emulador emulará este comportamiento. con las instalaciones de la plataforma nativa. Wine hace esto hasta cierto punto, pero le ayuda el hecho de que el código que está traduciendo es x86 a x86 (lo que significa que en ambos casos la CPU tiene el mismo conjunto de instrucciones, por lo que todo lo que tiene que hacer es conectar el sistema operativo Windows). código al entorno externo basado en UNIX y dejar que se ejecute "de forma nativa").


Analogía del pastel

Al considerar el rendimiento de un emulador, piense en cuántas hojas de papel necesitaría para escribir instrucciones si estuviera viendo a alguien en un video (con audio) horneando un pastel, en los siguientes escenarios:

  • Si nunca antes en tu vida has movido las manos o ejercitado algún músculo de tu cuerpo;(Pista: necesitarías miles de hojas de papel para documentar los pasos detallados del movimiento de la mano, la coordinación ojo-mano, la pesca con caña, la velocidad, la posición, las técnicas básicas como agarrar, sostener utensilios, amasar, etc.)

  • Si tienes control motor básico (puedes caminar y alimentarte solo), pero nunca antes en tu vida has preparado ningún alimento;(pista: necesitarías decenas de hojas de papel para documentar los pasos individuales, y probablemente necesitarías mucha práctica para dominar cosas como amasar y sostener utensilios desconocidos, pero podrías documentarlo en mucho menos tiempo que el caso anterior)

  • Si nunca antes en tu vida has horneado un pastel, pero has preparado alguna comida antes;(pista: necesitarías un par de hojas de papel, pero no más de 10; ya estarías familiarizado con medir ingredientes, revolver, etc.)

  • Si ha horneado un pastel muchas veces antes y está muy familiarizado con el proceso, pero no sabe cómo hornear esta variedad/sabor de pastel en particular.(pista: quizás necesites media hoja de papel para anotar los ingredientes básicos y el tiempo que lleva en el horno, y listo).

Básicamente, en estos niveles crecientes de "competencia del emulador", el emulador puede hacer más cosas de nivel superior "de forma nativa" (usando rutinas y procedimientos que ya conoce) y tiene que hacer menos "rastreo" (usando rutinas y procedimientos que ya conoce). siguiendo literalmente el programa emulado).

Para poner esta analogía en términos informáticos, puedes imaginar un emulador queemula el hardware realque el programa emulado se ejecutaría y "rastrearía" fielmente el comportamiento de ese hardware, tal vez incluso hasta un nivel de hardware (circuitos); esto seríamuylento en comparación con un emulador que analiza el programa a tal nivel de sofisticación que comprende cuándo intenta reproducir un archivo de sonido y puede reproducir "de forma nativa" ese archivo de sonido sin necesidad de seguir las instrucciones del programa emulado para hacerlo.


Sobre el "rastreo" (también conocido como mimetismo de memoria) frente a la "ejecución nativa"

Una última cosa: el rastreo es lento principalmente porque tienes que usar mucha memoria para "replicar" componentes muy detallados e intrincados de lo que estás emulando, y en lugar de simplemente ejecutar las instrucciones en tu CPU host, tienes que ejecutarinstrucciones que ejecutan las instrucciones(¿Ves el nivel de indirección?), lo que conduce a la ineficiencia. Si hicieras todo lo posible y emularas el hardware físico de un sistema informático, así como el programa, estarías emulando la CPU, la placa base, la tarjeta de sonido, etc., lo que a su vez "rastrearía" la ejecución del programa como tu El emulador "rastrea" la ejecución de la CPU, y con tantos niveles de rastreo, todo sería extremadamente lento y engorroso.

A continuación se muestra un ejemplo detallado de dónde un emulador no necesitaría leer cada bit/byte del programa de entrada para emularlo.

Digamos que conocemos una API escrita en C o C++ (los detalles no son importantes) para un entorno de software emulado, donde esta API tiene una función void playSound(string fileName). Digamos que sabemos que la semántica de esta función es abrir el archivo en el disco, leer su contenido, averiguar en qué codificación está el archivo (¿MP3? ¿WAV? ¿algo más?) y luego reproducirlo por los parlantes en el frecuencia de muestreo y tono normal/esperado. Si leemos, del código nativo, un conjunto de instrucciones que dice "ingrese a la rutina playSound para comenzar a reproducir sonido /home/hello/foo.mp3", podemos dejar de leer el código del programa allí mismo y usar nuestropropio(¡optimizada!) Rutina para abrir de forma nativa ese archivo de sonido y reproducirlo. ¿Necesitamos seguir el procesador emulado a nivel de instrucción? No, realmente no lo sabemos, no si confiamos en que sabemos lo que hace la API.


¡Surge una gran diferencia! (problemas en terrenos de alto nivel)

Por supuesto, al leer un montón de instrucciones e "inferir" un plan de ejecución de alto nivel, como en el ejemplo anterior, corre el riesgo de no poderprecisamenteimitar el comportamiento del programa original que se ejecuta en el hardware original. Digamos, por ejemplo, que el hardware original podría haber tenido limitaciones de hardware que solo le permitían reproducir 8 archivos de sonido simultáneamente. Bueno, si tu computadora de última generación puede reproducir 128 archivos de sonido simultáneamente y estás emulando la playSoundrutina a un alto nivel, ¿qué te impide reproducir más de 8 archivos de sonido a la vez? Esto podría causar... un comportamiento extraño (para bien o para mal) en la versión emulada del programa. Estos casos se pueden resolver mediante pruebas cuidadosas o quizás comprendiendo muy bien el entorno de ejecución original.

Por ejemplo, DOSBox tiene una característica que le permite limitar intencionalmente la velocidad de ejecución del programa DOS emulado, porque algunos programas DOS se ejecutarían incorrectamente si se les permitiera ejecutarse a máxima velocidad; en realidad dependían del tiempo de la frecuencia del reloj de la CPU para ejecutarse a la velocidad esperada. Este tipo de "característica" que limita intencionalmente el entorno de ejecución se puede utilizar para proporcionar un buen equilibrio entrefidelidadde ejecución (es decir, hacer que el programa emulado funcione correctamente) yeficienciade ejecución (es decir, construir una representación del programa que sea de nivel suficientemente alto como para que pueda emularse eficientemente con un mínimo de seguimiento).

Respuesta2

Mi pregunta es, si el emulador debe emular los códigos de operación exactos que haría la CPU real, ¿no sería necesario analizar el formato de código de operación binario correcto de un juego para emular la CPU legítimamente (o en absoluto)?

"Analizar" significa "revisar el texto y descubrir qué significa". La sintaxis similar al texto y al lenguaje es compleja porque no sólo las "palabras" individuales significan cosas, sino que el significado puede cambiar según su posición y proximidad a otras palabras.

Probablemente el término más preciso a aplicar con respecto a lo que una CPU hace con un flujo de instrucciones (que es mucho más simple que analizar) es "decodificar" - y sí, un emulador debe "decodificar" las instrucciones de la misma manera. Sin embargo, supongo que "análisis" no es un término tan malo, dada la complejidad del conjunto de instrucciones x86 si estás hablando de eso.

Mi programa debe verificar que esta instrucción realmente signifique (por ejemplo, "agregar uno al registro A"), pero ¿no necesitaría verificar cada cero y uno en el archivo de texto para asegurarlo?

Las CPU realmente no verifican las instrucciones. Cualquier CPU tiene un registro interno que apunta a una ubicación de memoria en la RAM. La CPU lo lee, lee un par de bytes más si es necesario y luego intenta ejecutarlo. Las CPU modernas iniciarán un "proceso de excepción" si la instrucción es ilegal.

Las CPU antiguas, como la antigua 6502 de 8 bits, ni siquiera hacían eso: algunas instrucciones ilegales bloquearían la CPU, otras harían cosas extrañas e indocumentadas. (Algunas búsquedas revelarán una serie de instrucciones x86 no documentadas que se encuentran en todas las CPU del panteón x86; por ejemplo, la CPUIDinstrucción existía en unas 486 CPU antes de que Intel la documentara oficialmente).

Un emulador de CPU bueno y preciso simplemente masticará un flujo de instrucciones a ciegas como lo hace una CPU real, aunque puede hacer cosas diferentes en condiciones que causarían un bloqueo del sistema, como mostrar un cuadro de diálogo que le indica que su CPU virtual está muerta. de bloquear su emulador.

Además, dice "es necesario verificar cada cero y uno en el archivo de texto", pero normalmente no alimenta archivos de texto a los emuladores. Un emulador de CPU necesita memoria emulada y, dado que muchas plataformas utilizan E/S asignadas en memoria, también dispositivos de E/S emulados. Emular cosas como el firmware requiere que tengas imágenes binarias de ese firmware, ya sean copias legales del firmware real o firmware sustituto de código abierto. Emular una plataforma completa (como la "plataforma de PC" de la cual la CPU x86 es un componente) requiere más que emulación de CPU.

información relacionada