Hackear un ensamblador universal

Siempre me he reído de las personas que tienen muchas herramientas, esas modernas navajas del ejército suizo, en su equipo. Para mí, toda la premisa de una herramienta multitarea es que me impiden ir al engranaje. Si tengo tiempo para ir al taller, conseguiré la herramienta adecuada para el trabajo.

No es que no me guste una buena multicapa. Están en forma y son buenos para trabajar. Eso es lo que siento por axasma, un ensamblador universal que pirateé. Llamarlo un truco de ensamblador cruzado no lo hace justo. Es un truco enorme y feo, pero hace el trabajo. Si necesitaba algo serio, iría a la caja de herramientas y obtendría un ensamblaje real, pero a veces solo quieres usar lo que tienes en el bolsillo.

¿Por qué un cruzado?

Probablemente se esté preguntando por qué quería escribir una asamblea. El problema es que me gusta diseñar CPU personalizadas (generalmente implementadas en Verilog en FPGA). Estas CPU tienen instrucciones únicas, y eso significa que no hay montaje libre. Hay algunos ensambladores controlados por tablas que puede configurar, pero luego necesita aprender alguna sintaxis departamental arcana para esa herramienta. Mi objetivo no era escribir una asamblea. Era un código para escribir para mi nueva CPU.

Para mis dos primeras CPU, acabo de cancelar un compilador en C o awk. Noté que todos se ven similares. Casi todos los lenguajes ensambladores que he usado tienen un formato bastante regular: una etiqueta opcional seguida de dos puntos, un mnemónico opnode y quizás algunos argumentos separados por comas. Punto y coma marcan los comentarios. También obtienes algunos comandos especiales bastante comunes como ORG, END y DATA. Eso me hizo pensar, que suele ser peligroso.

El hack

El procesador C tiene mala reputación, probablemente porque parece dinamita. Es increíblemente útil y también increíblemente peligroso, especialmente en las manos equivocadas. Pensé que si mi lenguaje ensamblador se parecía a las macros C, podría crear fácilmente un ensamblado personalizado a partir de un esqueleto fijo. Probablemente todos los procesadores a los que me dirigiría tienen memorias relativamente pequeñas (según los estándares informáticos), así que ¿por qué no usar macros para completar una matriz en un programa C.? Luego, el compilador hará todo el trabajo y algunas rutinas estándar pueden escupir el resultado en formato binario o hexadecimal de Intel o cualquier otro formato que se le ocurra.

Mi plan era simple: usar un script awk para convertir código con formato de ensamblador convencional en macros. Esto convertiría una línea como:

      add r1,r2

En tal macro:

     ADD(r1,r2);

Tenga en cuenta que el código de operación siempre se fuerza en mayúsculas. Las etiquetas son un poco especiales. Cuando el script de ensamblaje encuentra una etiqueta, genera una macro DEFLABEL en un archivo adicional especial. Luego escribe una macro LABEL en el archivo principal. Esto es necesario porque es posible que esté utilizando una etiqueta antes de que se defina (salto hacia adelante) y el ensamblador necesitará conocerla de antemano.

El resultado

A diferencia de un ensamblador normal, el archivo de salida del script no es el código de la máquina. Son dos conjuntos de macros en lenguaje C que se incluyen con el código fuente estándar para el ensamblador. Un script de controlador dirige todo. Lanza el script, llama al compilador y luego ejecuta el programa resultante (temporal) (entregándole las opciones que usted especificó). El código fuente estándar simplemente se llena un búfer con el código de su máquina y lo transmite en uno de los varios formatos disponibles. Puede ver el flujo general del proceso a continuación.

Si eso no fue lo suficientemente claro, el programa generado tiene una función: imprimir su programa específico en lenguaje ensamblador en código de máquina en algún formato. Aquí lo tienes. No conserva los ejecutables. Una vez que se ejecutan, ya no son útiles.

La función que usa el ensamblador para generar código es genasm(). El conductor lo llama dos veces: una vez como prueba para averiguar cuáles son todos los valores de la etiqueta y la segunda vez para emitir el código de la máquina. La función genasm se crea a partir de su código de vinculación. Cada definición de procesador tiene una macro ORG que organiza todo, incluido el jefe de una función de genasm. La macro END lo cierra junto con alguna otra gestión.

Configuración

La clave es configurar los archivos de macro. Debido a que el script convierte todo a mayúsculas, el archivo de macro debe usar códigos de operación en mayúsculas (pero el programa no tiene que hacerlo). Como mencioné, debe generar de alguna manera la función genasm, por lo que generalmente toma una macro ORG y END. Por lo general, configuran un espacio de direcciones falso (ninguno de mis procesadores tiene más de 4 MB de almacenamiento de programas, por lo que puedo crear fácilmente una matriz en la computadora). Luego, definiré una macro para cada formato de instrucción y la usaré para definir más macros utilizables. La macro ORG también necesita configurar algunos elementos de configuración en la estructura solo_info (cosas como el tamaño de la palabra y la ubicación de la tabla de códigos de máquina).

Debido a que ORG configura las cosas una vez, no puede usarlo varias veces. Eso significa que generalmente doy una macro REORG que simplemente se mueve a una nueva dirección. A veces, un truco requiere un pequeño compromiso, y ese es uno justo ahí.

Por ejemplo, considere CARDIAC. Esta es una simple computadora de cartón que Bell Labs ofreció a las escuelas para enseñar computación en la década de 1960 (todavía puedes comprar algunas, y también la recreé en FPGA). Aquí está parte de la definición ORG para CARDIAC:

#define ORG(n) unsigned int genasm(int _solo_pass) { 
 unsigned _solo_add=n;
 _solo_info.psize=16; 
 _solo_info.begin=n; 
 _solo_info.end=n; 
 _solo_info.memsize=MAXMEM; 
 _solo_info.ary=malloc(_solo_info.memsize*_solo_info.psize); 
 _solo_info.err=(_solo_info.ary==NULL)

La dirección es n (el argumento del comando ORG) y el tamaño del programa es de 16 bits. En este momento, el inicio y el final de la matriz también son n, pero eso cambiará, por supuesto.

La función __setary carga valores de código de máquina en la matriz, y otras instrucciones usan esta macro para facilitar la escritura:

#define INP(r) __setary(_solo_add++,bcd(r))
#define LOD(r) __setary(_solo_add++,bcd(100+(r)))

Debido a que CARDIAC es una máquina BCD, la macro bcd ayuda a crear los números de salida en el formato correcto (por ejemplo, 100 decimal se convierte en 100 hexadecimal). Esto no es muy común, pero muestra que puede acomodar casi todo escribiendo un pequeño código C en el archivo de definición.

Corriendo

Una vez que tenga su programa de lenguaje ensamblador y su procesador de definición adecuado, es fácil iniciar un axioma de línea de comandos. Aquí está el mensaje de uso que recibirá si solo trabaja con axasm:

Usage: axasm [-p processor] [ -H | -i | -b | -8 | -v | -x ] [-D define] [-o file] inputfile
 -p processor = processor.inc file to use (default=soloasm)
 -D = Set C-style preprocessor define (multiple allowed)
 -H = Raw hex output
 -i = Intel hex output
 -v = Verilog output
 -x = Xilinx COE format
 -b = Binary raw (32-bit only)
 -8 = Binary raw (8-bit only)
 -o = Set output file (stdout default)

La bandera -p es la definición que desea utilizar. El programa puede generar formato hexadecimal sin formato, hexadecimal Intel, Verilog, Xilinx-COE y binario sin formato (use od si desea convertir eso a, digamos, octal, para el 8080).

¿Cuál es el propósito?

Puede parecer un poco extraño pervertir el procesador C de esta manera, pero ofrece muchas ventajas. Primero, puede definir las instrucciones de su CPU en un lenguaje cómodo y usar construcciones poderosas en funciones y macros para realizar el trabajo. En segundo lugar, puede utilizar todas las funciones del compilador de C. Las expresiones matemáticas constantes funcionan bien, por ejemplo.

Incluso puede usar el código C para generar su programa de montaje prefijando las líneas C con # (y las líneas del preprocesador, por lo tanto, tienen dos caracteres #). Por ejemplo:

##define CT 10
# { int i; for (i=3;i<10;i++) LDRIQ(i); }

Esto generará instrucciones LDRIQ con i que van de 3 a 9. Tenga en cuenta que el bucle for no termina en su código. Genera tu código. Incluso puede definir códigos de operación simples o alias en su programa usando el preprocesador:

##define MOVE MOV
##define CLEAR(r) XOR(r,r)

Por supuesto, dado que AXASM funciona para procesadores personalizados, también puede definir procesadores estándar. Github tiene definiciones para RCA1802, 8080 y PIC16F84. Si está creando una nueva definición, haga una solicitud de extracción en Github y compártala.

No estoy seguro de querer sugerir este truco como técnica general para procesar textos. Como la dinamita, es poderosa, útil y peligrosa al mismo tiempo. Nuevamente, como herramienta múltiple, es conveniente y cumple la tarea. Si necesita una actualización en el preprocesador C (y C ++), vea el video a continuación.

  • Ren dice:

    ¡Guau! Me gusta…
    Todavía estoy tratando de dar la vuelta a mi cabeza, pero eso es porque soy tan ignorante de C,

    Uno de mis primeros pensamientos fue ... a medida que AX-ASM (mi guión) se vuelve más refinado, ¿cambiará su nombre a
    ChainSawAsm, TableSawAsm, BandSawAsm, y nombre Mini-versión HatchetAsm? B ^)

    • Congreso Nacional Africano dice:

      ¡No cambie su nombre en honor a la macro ORG!

      • ROBÓ dice:

        Regla 35

  • remo dice:

    Esta:
    ## definir CT 10
    # {int i; por (i = 3; i

    Podría ser mejor como:
    ## definir CT 10
    ## definir LW 3 // rango inferior
    # {int i; por (i = LW; i

    O si no, ¿por qué molestarse en definir algo?

    • Al Williams dice:

      ¡Mi culpa! Cambié eso de 0 a 3 en el último minuto solo para ser menos aburrido. Tienes razón, por supuesto. El código real cuando de 0 a CT y decidí que no me gustaba el cero en un ejemplo.

  • jaromirs dice:

    Esto me recuerda a TASM (montaje en placa). Lo usé para combinar fuentes Z80 con binarios CP / M en mi computadora CP / M, pero también fuentes 6502 u 8080 para mis antiguas placas de CPU, pero también proyectos 8051. Diferentes arquitectos necesitaban una nueva tabla de definición.
    http://home.comcast.net/~tasm/tasmsum.htm
    Muy útil.

    • JD dice:

      El autor de esto necesita encontrar mejor un anfitrión para ese sitio, porque todos los sitios "home.comcast.net" se irán pronto ...

    • BobbyMac99 dice:

      Dos ensamblajes de pase estaban de moda ese día: si no tenía el correcto para su procesador, podría usar MASM y un archivo de macro para redefinir las directivas de compilación reales en sí mismas, lo cual es un poco ordenado de alguna manera. También hubo un libro a principios y mediados de la década de 1980 llamado "Lenguaje ensamblador universal", que describía un conjunto general de instrucciones que se convertirían mediante una macro sustitución intermedia para definir la topología del procesador (estoy seguro de que muchos toman eso tema ahora). Entonces hubo mucha discusión sobre producir un producto con un código universal básico y luego compilarlo para que el sistema operativo funcione, por lo que podría compilarlo para una MAC y luego una computadora, o un sistema UNIX, o un parche específico si hicieras trabajo incorporado. Por supuesto, encontraron problemas, ya que la mayoría de ellos tenían requisitos específicos del dispositivo que debían abordarse. Un buen ejemplo de esto es el entorno MAME32 para jugar en entornos de videojuegos (si no conoce este producto de código abierto, mire la fuente, es algo muy bueno). Básicamente rompió los bloques "cerrados" utilizados (IO - Teclado, joystick, unidades de disco, etc. y video, junto con procesadores específicos) en los videojuegos - y estaba lo suficientemente cerca como para poder producir las cajas "negras". necesita ejecutarse, tomó las ROM originales del juego original y lo lanzó en este sistema copiado, casi un verdadero emulador de procesador en términos reales. Debo admitir que unirme fue todo lo que hice durante muchos años (porque el producto en el que estaba trabajando era una base de datos escrita con ensamblaje, por valor de 150,000 líneas), y todavía me gusta hoy, y sí, codifiqué a mano muchos procesadores los años. , pueden haber sido radicalmente diferentes entre sí en el direccionamiento de las páginas, los registros y el uso de la pila. Solo el uso masivo de "C" no es parte de la diligencia sistema integrado (a menos que, por supuesto, escriba uno). Ahora, tal vez esto sea completamente extraño para la mayoría de ustedes, hace mucho tiempo cuando los dinosaurios codificaban programas, y si sabían algo sobre algo, eran adorados. Ahora un niño escribe una bomba de correo electrónico y la gente impresiona, me entristece.

      • BobbyMac99 dice:

        http://www.drdobbs.com/embedded-systems/a-universal-cross-assembler/222600279

  • nik pfirsig dice:

    Hace años, utilicé un soporte cruzado universal llamado "PCMAC" escrito por Peter Verhaus. Usó un lenguaje de macros para definir los nombres del procesador de destino.
    http://peter.verhas.com/progs/c/pcmac/

  • cplamb dice:

    Es divertido ver cómo los desarrolladores siguen inventando cosas. Usé Univac Meta-Assember en 1108 en la década de 1970. Lo configuré para varios procesadores diferentes y lo usé productivamente para componer código para ellos. Me sorprendió cuando una búsqueda en Internet mostró que todavía está en uso.

    • Al Williams dice:

      Ah Execution8 y .... CI creo? Traerme de vuelta. Hice un montón de 1100 en ese mismo tiempo, pero sin compilación cruzada. Eso fue todo Avocet en Quasar QDP 100 a principios de los 80.

      • ROBÓ dice:

        Hola [Al Williams],
        La mayoría de la gente hoy en día ni siquiera usa ASM, con bastante frecuencia el lenguaje de nivel más bajo que la gente usa es algún derivado de "C". Por lo tanto, las UC modernas están optimizadas para el compuesto "C", es decir, las CPU de tipo RISC frente a las CPU de tipo CISC con las que todavía componen muchas personas.

        Así que tengo una pregunta para la que no he encontrado una respuesta y, mientras diseña varias CPU en HDL, me preguntaba si existe alguna forma científica, matemática o lógica de evaluar el rendimiento informático total de una CPU como análisis. es un conjunto de instrucciones. ¿Existe alguna opción "ideal" de funciones lógicas (cmp, inc, dec, srl) que pueda tener una CPU que permita un rendimiento mayor?

        La única forma que puedo considerar para analizar esto es hacer varias complejidades diferentes de máquinas de estado y usar algo como la síntesis HDL para llevar cada una de ellas a una definición mínima. Y según la definición mínima, conviértalos en un conjunto para ejecutarse en los proyectos de CPU probados. La cantidad de ciclos de reloj para completar cada estado y transición de estado indicaría la eficiencia de codificación de la CPU.

        ¿Un centavo por tus pensamientos?

        • Al Williams dice:

          No sé si existe una forma universal de cuantificar eso, porque creo que dependería de la tarea. Un procesador visual tendrá diferentes necesidades que un motor de base de datos.

          Sin embargo, una de las cosas que me interesaron mucho es un compilador que tomaría algo como C ++, lo evaluaría, decidiría que le gustaría tener 4 unidades de diapositivas, 3 unidades enteras y 4 puertos de memoria. Cree HDL para la CPU "ideal", luego compile el programa para ella. Trabajé mucho con CPU Transfer Triggered porque lo prestaban muy bien, pero aparte de proyectar TTA, ya no seguí con eso.

          Sin embargo, buena pregunta.

          • mm0zct dice:

            Eso suena un poco como parte del trabajo realizado por nuestro departamento cuando era estudiante, y mi proyecto de último año involucró http://groups.inf.ed.ac.uk/pasta/ La idea básica es perfilar automáticamente su objetivo . programa para extraer CFG, luego analice esto en busca de bits de computación adecuados para generar instrucciones especiales, muchos de los detectados son ejemplos de extensiones de instrucciones comunes, p. Multiplicar-acumular, o módulo / división en paralelo, pero algunas aplicaciones tendrán características más inusuales que tienen sentido solo dentro de la aplicación específica, como una variedad aleatoria de adiciones, cambios y restas, compartir algunos registros de origen y algunas constantes. Ahora que tiene estos ISE, necesita que el compilador reconozca las plantillas de código que pueden usarlos y usarlos. Esto funcionó bastante bien en teoría, pero el diseño de la CPU era demasiado limitado en su ancho de banda de memoria entre el caché y la canalización (no hay acceso directo a la memoria desde los ISE, por lo que tenían que cargarse en los registros antes, y solo admitía una carga por ciclo) . Sin embargo, la computación de núcleos con un flujo de datos / computadora de estilo diamante pequeño funcionó bien, ya que los elementos de datos pequeños se llevaron a cabo, se expandieron a varios valores intermedios en vuelo y se mantuvieron en registros, luego se condensaron nuevamente en una pequeña cantidad de valores almacenados.

            Si recuerdo correctamente a un amigo que intentó utilizar las herramientas para los algoritmos de cifrado obtuvo buenos resultados, mis intentos con el algoritmo h264 fueron menos exitosos.

          • ROBÓ dice:

            @[Al Williams] Me concentré más en la especificidad sobre el "rendimiento de la computadora" que en el "rendimiento de los datos". Pero definir incluso ese límite (o distinción) sería en sí mismo muy difícil. Acabo de leer sobre TTA y definitivamente lo daré en VHDL. Parece que un puñado de piezas "personalizadas" permitiría una amplia gama de instrucciones personalizadas.

            @[mm0zct] Gracias por el enlace. (PASTA) Una lectura muy interesante.
            Encontré esto muy interesante -
            http://groups.inf.ed.ac.uk/pasta/papers/SAMOS10_JIT_DBT_ISS.pdf

            El TLDR; es que su proceso automatizado: diseñar instrucciones de CPU, ejecutar una prueba de simulación, optimizar, repetir -
            se retrasó debido a los largos tiempos necesarios para el nivel de puerta y la simulación cíclica precisa en un motor HDL sintético.

            Por lo tanto, crean un ciclo de nivel de instrucción (inmediatamente) preciso * Intérprete * y reducen en gran medida el tiempo de simulación.

            Desafortunadamente para mí, no hay ninguna indicación de cómo se puede encontrar la combinación "mágica" de instrucciones para un recorrido de computadora óptimo (uso general). Los procesos utilizados en el proyecto PASTA se parecen más a prueba / error o aproximación secuencial que a predicción.

            Gracias por el gran artículo [Al Williams]Definitivamente probaré suerte con las CPU TTA en VHDL.

          • Al Williams dice:

            [Rob] (lo siento, soy demasiado vago para encontrar la diéresis) - Ver: http://www.drdobbs.com/embedded-systems/the-one-instruction-wonder/221800122

            Aunque está en Verilog no VHDL

            y

            http://www.drdobbs.com/architecture-and-design/the-commando-forth-compiler/222000477

            AXASM se encargará del ensamblaje de esta CPU, entre otras cosas.

          • GRC dice:

            [Al Williams]

            ¿Quizás el siguiente paso sea la "Síntesis avanzada"?
            http://www.ida.liu.se/~petel71/SysSyn/lect3.frm.pdf
            http://www.xilinx.com/support/documentation/sw_manuals/ug998-vivado-intro-fpga-design-hls.pdf

          • ROBÓ dice:

            Hola [Al Williams], estos se ven muy interesantes, pero me perdí en el Verilog. No creo que llegue tan lejos con la "R" de "RISC" jejeje.

            Me voy por otra madriguera ahora buscando un buen conversor gratuito de Verilog a VHDL.

          • BobbyMac99 dice:

            Obviamente, los conjuntos de instrucciones se pueden dividir en categorías, y algunas instrucciones duran mucho más que otras (generalmente matemáticas ...). Si considera lo que llamamos "microcodificación" (no sé en absoluto cómo se llama ahora), se trata básicamente de la creación de otra instrucción única para un procesador determinado que no forma parte del conjunto predeterminado. En el día a día, los fabricantes crearían una instrucción cuyo propósito solo ellos sabían, que generalmente se puede descifrar eventualmente, pero puede ocupar mucho menos espacio de código o más rápido que el código equivalente, pero ya entiendes. El proceso real de tomar la instrucción del puntero del programa y ejecutar la máscara de instrucción en sí es interesante porque define las superposiciones reales que realiza. Por ejemplo: podría crear una instrucción llamada 'bob', y cubriría el acumulador en un procesador con un valor, y luego movería las piezas con aquellas con otro valor de su elección, una especie de instrucción de encriptación. Hacían estas cosas cada vez que intentaba descompilar algo, no se descomponía debido a la instrucción microcodificada y necesitaba averiguar qué hacía. Por lo general, hacían esto para ahorrar espacio o por seguridad, porque la instrucción en sí misma no podía tener parámetros o tener varios parámetros. Pero vale la pena observar el proceso para comprender cómo funciona realmente una instrucción dentro del procesador.

          • Greenaum dice:

            Pregunto esto como un espectador interesado ... si está diseñando una CPU para que coincida con un algoritmo específico, ¿por qué no ir hasta el final y usar algo como VHDL para hacer todo en hardware? La gente usa CPU de propósito general porque las computadoras hacen mucho trabajo diferente. Pero para un algoritmo en particular, ¿no habría siempre un circuito de hardware separado que, en teoría, sería más rápido que el software en una CPU? Basado en una presentación idéntica de puertas.

            Parece que el software CPU + habitual está a medio camino entre un software limpio y un hardware limpio. ¿Hay alguna forma de encontrar el punto ideal entre ellos? ¿Qué debería ser hardware dedicado y qué debería ser de uso general, y cómo de uso general, y qué instrucciones necesitaría? ¿Como se dice esto? ¿Es matemáticamente posible una forma automática de elaborar esto o es incomputable?

            Solo me preguntaba por curiosidad, cómo. Solo me gustaría tener una idea general, sin necesidad de respuestas específicas a preguntas que pueden no ser exactamente apropiadas.

  • Gravis dice:

    espera ... ¿qué pasa con el IR de LLVM? está bien documentado.

    • Al Williams dice:

      ¿Propone traducir IR al código de máquina de destino? Eso es lo que hace que LLVM vuelva, ¿verdad? Pero no es realmente un montaje en cruz. Compilador cruzado, sí.

      • Caza Rayfield dice:

        Es parte del compilador ... pero yo diría que es una línea de ensamblaje de optimización general ...

      • aparentemente inteligente dice:

        Esto me hace preguntarme si podría usar la infraestructura ASMParser de LLVM (generalmente utilizada para ensamblajes en línea, etc.) como una herramienta para hacer montajes independientes ...

  • Tomás dice:

    He visto algunas publicaciones sobre computadoras normales, pero debo admitir que realmente no entiendo de qué se trata.

    ¿Es un ejercicio para hacer algo para disfrutarlo / educarlo, o una CPU personalizada ayuda a resolver un problema práctico?

    • jaromirs dice:

      No sé sobre los demás, pero he creado algunas CPU personalizadas solo por diversión y educación personal. Realmente no puedo pensar en ninguna situación en la que realmente sea necesario para resolver un problema. Pero después de que surja la situación, estoy listo 😉

    • Al Williams dice:

      He fabricado CPU comerciales para fines especiales, aunque la mayoría de las veces también te conviene usar una CPU compatible y agregarle cosas. Sin embargo, si está explorando nuevas funciones de procesador ...

  • fabricante de acero dice:

    Es posible que desee comprobar qhasm.

Gloria Vega
Gloria Vega

Deja una respuesta

Tu dirección de correo electrónico no será publicada.