x86 - instrucciones - lenguaje ensamblador ejemplos pdf
¿Cuál es la mejor manera de escribir un simple ensamblador x86? (9)
A menudo he fantaseado con tratar de construir (otro) lenguaje informático de alto nivel. El objetivo sería tratar de empujar la envolvente de la rapidez del desarrollo y el rendimiento del resultado. Intentaría crear bibliotecas de una clase de operaciones mínimas, bastante altamente optimizadas, y luego trataré de desarrollar las reglas del lenguaje de tal manera que cualquier declaración o expresión expresable en el lenguaje resultara en un código óptimo ... a menos que lo que se expresara fuera simplemente inherentemente subóptima.
Se compilaría a un código de bytes, que se distribuiría, y luego a un código de máquina cuando se instale, o cuando cambie el entorno del procesador. Entonces, cuando se cargue un ejecutable, habrá una pieza del cargador que verificará el procesador y unos pocos bytes de datos de control en el objeto, y si los dos coincidieran, la parte ejecutable del objeto podría cargarse de inmediato, pero si no. , entonces el código de byte para ese objeto tendría que volver a compilarse, y la parte ejecutable se actualizaría. (Por lo tanto, no es una compilación Just In Time, está en la instalación del programa o en la compilación de CPU cambiada). La parte del cargador sería muy breve y fácil, 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 si fuera así, cargaría un objeto de compilador pequeño y ajustado, y optimizado para la arquitectura detectada. Idealmente, el cargador y el compilador permanecerían residentes, una vez cargados, y solo habría una instancia de ambos.
De todos modos, quería responder a la idea de que debes tener al menos dos pases, no creo que esté del todo de acuerdo. Sí, usaría una segunda pasada a través del código compilado, pero no a través del código fuente.
Lo que usted hace es, cuando se encuentra con un símbolo, verifique su tabla hash de símbolos, y si no hay una entrada allí, cree uno y almacene un marcador de ''referencia hacia adelante'' en su código compilado con un puntero a la entrada de la tabla. Cuando encuentre las definiciones de etiquetas y símbolos, actualice (o incluya nuevos datos) en su tabla de símbolos.
Los objetos individuales compilados nunca deben ser tan grandes que ocupen mucha memoria, por lo que, definitivamente, todo el código compilado debe mantenerse en la memoria hasta que todo esté listo para ser escrito. La forma en que mantiene pequeña la huella de su memoria es simplemente tratando con un objeto a la vez, y nunca manteniendo 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 tan grande que la sobrecarga que implica realizar la llamada para cargar el búfer desde el disco es pequeña en comparación con el tiempo que se tarda en leer los datos del disco, de modo que se optimiza la transmisión).
Entonces, una vez que pase la secuencia de origen de un objeto, encadene sus piezas y recopile la información de referencia hacia adelante necesaria desde la tabla hash a medida que avanza, y si los datos no están allí, es un error de compilación. Ese es el proceso que estaría tentado de intentar.
Estoy interesado en escribir un ensamblador x86 para un proyecto de hobby.
Al principio me pareció bastante sencillo, pero cuanto más leo, más preguntas sin respuesta tengo. No estoy totalmente inexperto: he usado el ensamblaje de los MIP una buena cantidad 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 busco hacer un ensamblador comercialmente viable, sino simplemente un proyecto de pasatiempo para fortalecer mi conocimiento en ciertas áreas. Por lo tanto, 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 el análisis ad-hoc o definir gramáticas formales y usar un generador de analizador para mis instrucciones? ¿En qué etapa y cómo resuelvo las direcciones de mis símbolos?
Teniendo en cuenta mis requisitos, ¿alguien puede sugerir algunas pautas generales para los métodos que debería utilizar en mi ensamblador de proyectos de mascotas?
Dado que este es un proyecto de pasatiempo, muchas de sus preguntas realmente se reducen a "¿en qué aspectos del problema le interesa más mirar y aprender?" Si está interesado en ver cómo se asignan las herramientas de análisis al problema de los ensambladores (especialmente en lo que se refiere al procesamiento de macros y similares), debe usarlos. Por otro lado, si no está demasiado interesado en esas preguntas y solo quiere hacer preguntas sobre el empaquetado de instrucciones y el diseño y está contento de tener un ensamblador mínimo sin macros, entonces es rápido y sucio para el análisis. camino a seguir.
Para un solo paso vs multipass, ¿está interesado en jugar con un ensamblador muy rápido con una huella de memoria minimizada? Si es así, esta pregunta se vuelve relevante. Si no, simplemente arrastre todo el programa en la memoria, trátela allí, cree una imagen del objeto en la memoria y luego escríbala. No hay necesidad real de preocuparse por los "pases" como tales. En este modelo, puedes jugar más fácilmente haciendo cosas en diferentes órdenes para ver cuáles son las compensaciones, lo que es una gran parte del objetivo de un proyecto de pasatiempo.
Deberá escribir un lexer y un analizador para leer el código fuente y generar el árbol de sintaxis abstracta (AST). El AST se puede atravesar para generar la salida del código de byte.
Recomiendo investigar libros sobre cómo escribir un compilador. Esto suele ser una clase de nivel universitario, por lo que debe haber un montón de libros. Lo siento, no puedo recomendar uno en particular.
También puede leer sobre la herramienta ANTLR . Puede incluir reglas gramaticales y código de salida en varios idiomas para hacer el trabajo de lexer / parser por usted.
En el paso uno o dos pasos: necesitaría un compilador de dos pasos para resolver las referencias hacia adelante. Si eso no es importante, entonces un pase será suficiente. Te recomiendo que lo mantengas simple, ya que este es tu primer compilador.
Hay un excelente libro electrónico gratuito (pdf) sobre cómo construir ensambladores y cargadores de David Salomon. Lo puedes encontrar en:
He escrito un par de analizadores. Escribí un par de analizadores hechos a mano y probé el tipo de analizadores yacc también ...
Los analizadores hechos a mano proporcionan más flexibilidad. Yacc proporciona un marco al que se debe adaptar o no. Parser Yacc ofrece un analizador rápido por defecto, pero ir después de cambiar / reducir y reducir / reducir puede requerir un gran esfuerzo si no está familiarizado con uno de esos medios y el entorno de su analizador no lo es. el mejor. Sobre la ventaja de Yacc. Te da un sistema si lo necesitas. El analizador hecho a mano te da libertad, pero ¿puedes archivarlo? El lenguaje ensamblador parece ser lo suficientemente simple como para ser manejado por yacc o analizadores similares.
Mi analizador hecho a mano contendría un tokenizer / lexer y yo iría a través de la matriz de tokens for for y realizaría algún tipo de manejo de eventos colocando ifs o case en el loop y examinando el token actual o el siguiente / anterior. Es posible que use un analizador por separado para las expresiones ... Pongo el código de traducción en un conjunto de cadenas y "anota" partes no calculadas del código traducido para que el programa pueda acceder a ellas más tarde y llenar los espacios en blanco. pueden ser espacios en blanco y no todo se conoce de antemano cuando se analiza el código. Por ejemplo, la ubicación de los saltos.
Por otra parte, independientemente de la forma en que hagas tu analizador por primera vez y tengas tiempo, puedes convertir tu analizador de un tipo a otro. Dependiendo de quién eres, incluso puede que te guste.
Hay otros analizadores que no son Yacc y prometen más flexibilidad y menos "errores", pero eso no significa que no se cometan errores, no serán tan visibles y puede que no sean tan rápidos. Si eso es importante.
Por cierto, si los tokens estuvieran almacenados, uno podría estar pensando en un yacc mixto y un analizador hecho a mano.
Para responder a una de sus preguntas, un pase no es viable, a menos que emita un código después del pase.
Imagina esto:
JMP some_label
.. code here
some_label:
¿Qué emite como el valor de distancia 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 pases debe ser un mínimo.
Si bien muchas personas sugieren analizadores ad hoc, creo que en estos días uno debería usar un generador de analizadores porque realmente simplifica el problema de la creación de toda la sintaxis compleja que se necesita para un ensamblador moderno interesante. Vea mi ejemplo de BNF / respuesta a : Z80 ASM BNF .
"Una pasada" frente a "Dos pasadas" se refiere a si lees el código fuente dos veces. Siempre se puede hacer un ensamblador de una sola pasada. Aquí hay dos maneras:
1) Genere resultados binarios sobre la marcha (piense en estos como pares en el resumen que tienden a tener direcciones que aumentan de manera monótona), y emita parches retroactivos como arreglos cuando encuentre información que le permita resolver referencias hacia adelante (piense que son solo pares donde las direcciones se utilizan para sobrescribir ubicaciones previamente emitidas). Para los JMP, confirme 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 "usar 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 funky).
2) En el (primer) pase, genere datos a los buffers en la memoria. JMP predeterminados (y otras instrucciones dependientes del intervalo) para desplazamientos cortos. Registre las ubicaciones de todos los JMP (instrucciones dependientes del tramo, etc.). Al final de este paso, vuelva a los JMP y revise aquellos que son "demasiado cortos" para ser más largos; baraja el código y ajusta los otros JMPs. Un esquema inteligente para hacer esto y lograr conjuntos casi óptimos de JMP cortos es el documento en este documento de 1978: Código de ensamblaje para máquinas con instrucciones que dependen del tramo / Szymanski
Tome las tablas de NASM e intente implementar las instrucciones más básicas, utilizando las tablas para decodificar
Usted puede encontrar el libro del dragón para ser útil.
El título real es Compiladores: Principios, Técnicas y Herramientas ( amazon.com ).
Consulte los manuales del desarrollador de software de Intel Architectures para obtener la documentación completa de los conjuntos de instrucciones IA-32 e IA-64.
Los documentos técnicos de arquitectura de AMD también están disponibles en su sitio web.
Linkers and Loaders ( amazon.com ) es una buena introducción a los formatos de objetos y problemas de enlaces. (El manuscrito original sin editar también está disponible en línea.)