2009-10-30 15 views
16

Estoy interesado en escribir un ensamblador x86 para un proyecto de hobby.¿Cuál es la mejor manera de escribir un ensamblador x86 simple?

Al principio me pareció bastante directo, pero cuanto más leo en él, más preguntas sin respuesta me encuentro. No soy totalmente inexperto: he utilizado una cantidad justa de ensambles MIP y he escrito un compilador de juguetes para un subconjunto de C en la escuela.

Mi objetivo es escribir un ensamblador x86 simple pero funcional. No estoy buscando hacer un ensamblador comercialmente viable, sino simplemente un proyecto de pasatiempo para fortalecer mi conocimiento en ciertas áreas. Así que no me importa si no implemento todas las funciones y operaciones disponibles.

Tengo muchas preguntas como: ¿Debo usar un método de una o dos pasadas? ¿Debo usar un análisis ad-hoc o definir gramáticas formales y usar un analizador sintáctico para mis instrucciones? ¿En qué etapa y cómo resuelvo las direcciones de mis símbolos?

Dadas mis necesidades, ¿alguien puede sugerir algunas pautas generales para los métodos que debería utilizar en mi ensamblador de proyecto de mascota?

+1

Esto me recuerda a la prueba programador de la década de 1980, debido a un disquete con sólo el command.com y debug.com en él, qué tipo de un entorno de desarrollo crearía para ti. Sé cómo respondieron los chicos de Forth. – zumalifeguard

Respuesta

1

Tendrá que escribir un lexer y un analizador para leer el código fuente y generar el árbol de sintaxis abstracta (AST). El AST puede atravesarse para generar la salida de código de bytes.

Recomiendo investigar libros sobre cómo escribir un compilador. Esta suele ser una clase de nivel universitario, por lo que debe haber muchos libros. Lo siento, no puedo recomendar uno en particular.

También puede leer en la herramienta ANTLR. Puede tomar reglas de gramática y código de salida en varios idiomas para hacer el trabajo de lexer/analizador para usted.

En una o dos pasadas: necesitaría un compilador de dos pasadas para resolver las referencias futuras. Si eso no es importante, entonces un pase de un solo pase. Te recomiendo que lo mantengas simple, ya que este es tu primer compilador.

+0

Si lee mi pregunta, no es mi primer compilador. He usado Lex/Yacc anteriormente, y tengo una comprensión general de ANTLR. Parece que muchos recursos en línea e incluso en SO sugieren utilizar el análisis Ad-hoc al escribir un ensamblador. ¿Estás de acuerdo o en desacuerdo? – mmcdole

+0

Ok, creo que no entendí completamente lo que estabas pidiendo. Sin embargo, si ha escrito un lexer/analizador para un lenguaje tipo C, el ensamblador x86 debería ser fácil. A primera vista, diría que los nodos AST contendrían metadatos para cada primátil, como el desplazamiento de bytes, las referencias de símbolos, etc. un operador de sucursal haría referencia a un nodo de etiqueta, que contiene su desplazamiento. – spoulson

6

Puede encontrar que dragon book es útil.

El título real es Compilers: Principles, Techniques, and Tools (amazon.com).

Consulte la Intel Architectures Software Developer's Manuals para obtener la documentación completa de los conjuntos de instrucciones IA-32 e IA-64.

AMD's architecture technical documents están disponibles en su sitio web también.

Linkers and Loaders (amazon.com) es una buena introducción a los formatos de objetos y problemas de vinculación. (El unedited original manuscript también está disponible en línea.)

+6

Si bien respeto el Libro del Dragón como el texto definitivo en los compiladores, no creo que sea de mucha utilidad cuando se escribe un ensamblador. Los problemas de análisis implicados con los ensambladores son muy diferentes a los de los compiladores reales, y la generación de código es esencialmente un — declaraciones de lenguaje de ensamblado no operativas, correlacionadas individualmente con las instrucciones de la máquina. –

+0

Ni siquiera compiladores. Es más analizadores. Las otras partes obtienen un capítulo como máximo. –

1

Dado que este es un proyecto de pasatiempo, muchas de sus preguntas realmente se reducen a "¿qué aspectos del problema le interesan más a usted observar y conocer?" Si está interesado en ver cómo se correlacionan las herramientas de análisis con el problema de los ensambladores (particularmente cuando se trata del macroprocesamiento y similares), debe usarlos. Por otro lado, si no está demasiado interesado en esas preguntas y solo quiere entrar en las preguntas sobre el empaquetado y el diseño de las instrucciones y está contento de tener un ensamblador mínimo sin macros, entonces el análisis probablemente sea rápido y sucio. camino a seguir.

Para un solo paso frente a multipaso, ¿le interesa jugar con un ensamblador muy rápido con memoria minimizada? Si es así, esta pregunta se vuelve relevante.Si no, simplemente sorbe todo el programa en la memoria, trate con él allí, cree una imagen de objeto en la memoria y luego anótelo. No hay necesidad real de preocuparse por 'pases' como tal. En este modelo, puede jugar más fácilmente haciendo cosas en diferentes órdenes para ver cuáles son las compensaciones, que es mucho más que un proyecto de hobby.

2

Para responder a una de sus preguntas, one-pass no es viable, a menos que emita código después del pase.

Imagínese esto:

JMP some_label 
    .. code here 
some_label: 

¿qué es lo que ustedes emiten como la distancia-valor para la instrucción JMP? ¿Qué instrucción JMP emite, la que requiere un valor cercano o la etiqueta está muy lejos?

Así que dos pasadas deben ser un mínimo.

+0

Un pase está bien. Ver mi respuesta –

1

A menudo he fantaseado con intentar construir (otro más) lenguaje de computadora de alto nivel. El objetivo sería tratar de impulsar la envolvente de la rapidez del desarrollo y la ejecución del resultado. Intentaría crear bibliotecas de operaciones mínimas, bastante optimizadas, y luego tratar de desarrollar las reglas del lenguaje de tal manera que cualquier enunciado o expresión expresable en el lenguaje resultaría en un código óptimo ... a menos que lo que se estaba expresando fuera simplemente inherentemente por debajo de lo óptimo.

Compilaría el código de bytes, que se distribuiría, y luego el código de máquina cuando se instalara, o cuando el entorno del procesador cambiara. Entonces, cuando se carga un ejecutable, habría una pieza de cargador que verificaría el procesador y unos pocos bytes de datos de control en el objeto, y si los dos coincidían, entonces la parte ejecutable del objeto podría cargarse directamente, pero si no , entonces el código de bytes para ese objeto debería ser recompilado y la parte ejecutable actualizada. (Por lo tanto, no se trata de la compilación Just In Time, sino de la instalación del programa o de la compilación de la CPU modificada). La parte del cargador sería muy corta y agradable, estaría en el código '386 por lo que no sería necesario compilarla. Solo cargaría el compilador de código de bytes si fuera necesario y, de ser así, cargaría un objeto de compilación pequeño y ajustado, y se optimizaría para la arquitectura detectada. Idealmente, el cargador y el compilador permanecerían como residentes, una vez cargados, y solo habría una instancia de ambos.

De todos modos, quería responder a la idea de que tienes que tener al menos dos pases, no creo que esté completamente de acuerdo. Sí, utilizaría una segunda pasada a través del código compilado, pero no a través del código fuente.

Lo que debes hacer es, cuando te encuentres con un símbolo, ver la tabla hash de símbolos, y si no hay ninguna entrada allí, crea una y almacena un marcador de referencia directa en tu código compilado con un puntero a la tabla entrada. Cuando encuentre las definiciones de etiquetas y símbolos, actualice (o coloque datos nuevos) su tabla de símbolos.

Los objetos compilados individualmente nunca deben ser tan grandes que ocupen mucha memoria, por lo tanto, definitivamente todo el código compilado debe mantenerse en la memoria hasta que todo esté listo para ser escrito. La forma de mantener la huella de la memoria pequeña es simplemente tratando con un objeto a la vez, y nunca guardando más de un pequeño búfer lleno de código fuente en la memoria a la vez. Tal vez 64k o 128k o algo así. (Algo lo suficientemente grande como para que la sobrecarga involucrada al realizar la llamada para cargar el búfer desde el disco sea pequeña en comparación con el tiempo que lleva leer los datos del disco, para que la transmisión esté optimizada)

Entonces, un pase a través de la secuencia fuente para un objeto, luego encadena sus piezas, recopilando la información de referencia hacia adelante necesaria de la tabla hash sobre la marcha, y si los datos no están allí, eso es un error de compilación. Ese es el proceso que estaría tentado de probar.

0

He escrito un par de analizadores. Escribí un par de analizadores sintácticos hechos a mano y también probé el tipo de analizadores de yacc ...

Los analizadores sintácticos hechos a mano brindan más flexibilidad. Yacc proporciona un marco al que uno debe adaptarse o fallar. El analizador de Yakc da un analizador rápido de manera predeterminada, pero ir después del cambio/reducir y reducir/reducir puede requerir un gran esfuerzo si no está familiarizado con uno de esos medios y su entorno analizador no es el mejor. Sobre la ventaja de Yacc. Te da un sistema si lo necesitas. El analizador sintáctico te da libertad pero ¿puedes filtrarlo? El lenguaje ensamblador parece ser lo suficientemente simple como para ser manejado por yacc o analizadores similares.

Mi analizador sintáctico a mano contendría un tokenizer/lexer y revisaría la matriz de tokens con for loop y realizaría algún tipo de manejo de eventos colocando ifs o case statement en el ciclo y examinando el token actual o el next/anterior. Es posible que use un analizador separado para expresiones ... Pondría el código de traducción en una matriz de cadenas y "anotaré" partes no calculadas del código traducido para que el programa pueda llegar a ellas más tarde y llenar los espacios en blanco. Puede haber espacios en blanco y no todo se conoce de antemano cuando uno analiza el código. P.ej. la ubicación de los saltos.

Por otro lado, cualquiera que sea la forma en que realice su analizador por primera vez y tenga tiempo, puede convertir su analizador sintáctico de un tipo a otro. Dependiendo de quién seas, incluso te puede gustar eso.

Hay otros analizadores sintácticos que Yacc y prometen más flexibilidad y menos "errores", pero eso no significa que no obtenga errores, no serán tan visibles y puede que no sean tan rápidos. Si eso es importante

Por cierto, si se almacenaran los tokens, uno incluso podría estar pensando en un analizador de yacc mixto y hecho a mano.

1

Tome tablas de NASM, y tratar de poner en práctica las instrucciones más básicas, utilizando las tablas para la decodificación de

4

Mientras que muchas personas sugieren programas de análisis ad hoc, creo que estos días se debe utilizar un generador de análisis, ya que realmente simplifica el problema de construir toda la sintaxis compleja que se necesita para un ensamblador moderno e interesante. Ver mi ejemplo/respuesta BNF a StackOverflow: Z80 ASM BNF.

"One pass" vs. "Two pass" se refiere a si leyó el código fuente dos veces. Siempre puedes hacer un ensamblador de un solo paso. Aquí hay dos maneras:

1) Genere resultados binarios sobre la marcha (piense en estos como pares en el resumen que tienden a tener direcciones monótonamente crecientes), y emita parches de respaldo como correcciones cuando encuentre información que le permita resolver referencias (piense en estos como solo pares donde las direcciones se usan para sobrescribir las ubicaciones emitidas previamente). Para los JMP, ingrese el tipo/tamaño del código de operación JMP cuando lo encuentre. El valor predeterminado puede ser corto o largo según el gusto o incluso una opción de ensamblador. Un poco de sintaxis ingresada por el codificador que dice "use el otro tipo" o "insisto en este tipo" es suficiente (por ejemplo, "JMP long target") para manejar aquellos casos en los que la elección predeterminada del ensamblador es incorrecta. (Esto es ensamblador, está bien tener reglas originales).

2) En la (primera) pasada, genere datos a los almacenamientos intermedios en la memoria. JMP predeterminados (y otras instrucciones dependientes del span) para desplazamientos cortos. Registre las ubicaciones de todos los JMP (instrucciones dependientes del tramo, etc.). Al final de este pase, regrese a los JMP y revise los que son "demasiado cortos" para ser más largos; baraja el código y ajusta los otros JMP.Un esquema inteligente para hacer esto y lograr conjuntos casi óptimos de JMP cortos es el documento en este documento de 1978: Assembling code for machines with span-dependent instructions/Szymanski

+0

Bueno, si solo se trata de un proyecto de juguete, es posible que no necesite respaldar todas las cosas que debería hacer un ensamblador moderno. Además, no sabemos (ya que él no lo dice) si una de las áreas en las que OP podría mejorar es el análisis sintáctico. –

+0

El problema es que los ensambladores x86 tienden a tener una sintaxis desordenada para los operandos de instrucciones. Especialmente para un proyecto de hobby, un generador de analizadores tiene sentido; no hay mucho que aprender sobre el código de ensamblaje. –

Cuestiones relacionadas