Cómo pasamos del ensamblado al código de máquina (generación de código)

16

¿Hay una manera fácil de visualizar el paso entre el ensamblaje del código al código de la máquina?

Por ejemplo, si abre sobre un archivo binario en el bloc de notas, verá una representación textualmente formateada del código de máquina. Supongo que cada byte (símbolo) que ve es el carácter ascii correspondiente a su valor binario?

Pero, ¿cómo pasamos del ensamblaje al binario, qué sucede detrás de escena?

usuario12979
fuente

Respuestas:

28

Mire la documentación del conjunto de instrucciones, y encontrará entradas como esta de un microcontrolador pic para cada instrucción:

ejemplo de instrucción addlw

La línea de "codificación" indica cómo se ve esa instrucción en binario. En este caso, siempre comienza con 5 unidades, luego un bit de no importa (que puede ser uno o cero), luego las "k" representan el literal que está agregando.

Los primeros bits se denominan "código de operación", son únicos para cada instrucción. La CPU básicamente mira el código de operación para ver qué instrucción es, luego sabe decodificar las "k" como un número para ser agregado.

Es tedioso, pero no tan difícil de codificar y decodificar. Tuve una clase de pregrado donde tuvimos que hacerlo a mano en los exámenes.

Para crear un archivo ejecutable completo, también debe hacer cosas como asignar memoria, calcular las compensaciones de rama y ponerlo en un formato como ELF , dependiendo de su sistema operativo.

Karl Bielefeldt
fuente
10

Los códigos de operación de ensamblaje tienen, en su mayor parte, una correspondencia uno a uno con las instrucciones subyacentes de la máquina. Por lo tanto, todo lo que tiene que hacer es identificar cada código de operación en el lenguaje ensamblador, asignarlo a la instrucción de máquina correspondiente y escribir la instrucción de máquina en un archivo, junto con sus parámetros correspondientes (si los hay). Luego repite el proceso para cada código de operación adicional en el archivo fuente.

Por supuesto, se necesita más que eso para crear un archivo ejecutable que se cargue y ejecute correctamente en un sistema operativo, y la mayoría de los ensambladores decentes tienen algunas capacidades adicionales más allá de la simple asignación de códigos de operación a las instrucciones de la máquina (como macros, por ejemplo).

Robert Harvey
fuente
7

Lo primero que necesita es algo como este archivo . Esta es la base de datos de instrucciones para procesadores x86 que utiliza el ensamblador NASM (que ayudé a escribir, aunque no las partes que realmente traducen las instrucciones). Vamos a elegir una línea arbitraria de la base de datos:

ADD   rm32,imm8    [mi:    hle o32 83 /0 ib,s]      386,LOCK

Lo que esto significa es que describe la instrucción ADD. Existen múltiples variantes de esta instrucción, y la específica que se describe aquí es la variante que toma un registro de 32 bits o una dirección de memoria y agrega un valor inmediato de 8 bits (es decir, una constante incluida directamente en la instrucción). Una instrucción de ensamblaje de ejemplo que usaría esta versión es esta:

add eax, 42

Ahora, debe tomar su entrada de texto y analizarla en instrucciones y operandos individuales. Para la instrucción anterior, esto probablemente resultaría en una estructura que contiene la instrucción ADD, y una matriz de operandos (una referencia al registro EAXy al valor 42). Una vez que tenga esta estructura, recorre la base de datos de instrucciones y encuentra la línea que coincide tanto con el nombre de la instrucción como con los tipos de operandos. Si no encuentra una coincidencia, es un error que debe presentarse al usuario (el texto habitual es "combinación ilegal de código de operación y operandos" o similar).

Una vez que tenemos la línea de la base de datos, miramos la tercera columna, que para esta instrucción es:

[mi:    hle o32 83 /0 ib,s] 

Este es un conjunto de instrucciones que describen cómo generar la instrucción de código de máquina que se requiere:

  • El mies una descripción de los operandos: uno un operando modr/m(registro o memoria) (lo que significa que tendremos que agregar un modr/mbyte al final de la instrucción, que veremos más adelante) y uno una instrucción inmediata (que ser utilizado en la descripción de la instrucción).
  • El siguiente es hle. Esto identifica cómo manejamos el prefijo de "bloqueo". No hemos usado "bloqueo", por lo que lo ignoramos.
  • El siguiente es o32. Esto nos dice que si estamos ensamblando código para un formato de salida de 16 bits, la instrucción necesita un prefijo de anulación de tamaño de operando. Si estuviéramos produciendo una salida de 16 bits, produciríamos el prefijo ahora ( 0x66), pero asumiré que no lo somos y continuaré.
  • El siguiente es 83. Este es un byte literal en hexadecimal. Lo sacamos.
  • El siguiente es /0. Esto especifica algunos bits adicionales que necesitaremos en el byte modr / m, y hace que lo generemos. El modr/mbyte se usa para codificar registros o referencias indirectas de memoria. Tenemos un solo operando, un registro. El registro tiene un número, que se especifica en otro archivo de datos :

    eax     REG_EAX         reg32           0
  • Verificamos que reg32esté de acuerdo con el tamaño requerido de la instrucción de la base de datos original (lo hace). El 0es el número del registro. Un modr/mbyte es una estructura de datos especificada por el procesador, que se ve así:

     (most significant bit)
     2 bits       mod    - 00 => indirect, e.g. [eax]
                           01 => indirect plus byte offset
                           10 => indirect plus word offset
                           11 => register
     3 bits       reg    - identifies register
     3 bits       rm     - identifies second register or additional data
     (least significant bit)
  • Debido a que estamos trabajando con un registro, el modcampo es 0b11.

  • El regcampo es el número del registro que estamos usando,0b000
  • Debido a que solo hay un registro en esta instrucción, necesitamos completar el rmcampo con algo. Para eso /0estaban los datos adicionales especificados , así que los ponemos en el rmcampo 0b000,.
  • El modr/mbyte es por lo tanto 0b11000000o 0xC0. Nosotros sacamos esto.
  • El siguiente es ib,s. Esto especifica un byte inmediato firmado. Observamos los operandos y observamos que tenemos un valor inmediato disponible. Lo convertimos a un byte firmado y lo enviamos ( 42=> 0x2A).

Por tanto, la instrucción de ensamblado completo es: 0x83 0xC0 0x2A. Envíelo a su módulo de salida, junto con una nota de que ninguno de los bytes constituye referencias de memoria (el módulo de salida puede necesitar saber si lo hacen).

Repita para cada instrucción. Mantenga un registro de las etiquetas para saber qué insertar cuando se hace referencia a ellas. Agregue funciones para macros y directivas que se pasan a los módulos de salida de su archivo de objeto. Y así es básicamente cómo funciona un ensamblador.

Jules
fuente
1
Gracias. Gran explicación pero no debería ser "0x83 0xC0 0x2A" en lugar de "0x83 0xB0 0x2A" porque 0b11000000 = 0xC0
Kamran
@Kamran - $ cat > test.asm bits 32 add eax,42 $ nasm -f bin test.asm -o test.bin $ od -t x1 test.bin 0000000 83 c0 2a 0000003... sí, tienes toda la razón. :)
Jules
2

En la práctica, un ensamblador generalmente no produce directamente algún ejecutable binario , sino algún archivo de objeto (para alimentar más tarde al enlazador ). Sin embargo, hay excepciones (puede usar algunos ensambladores para producir directamente algún ejecutable binario; son poco comunes).

Primero, observe que muchos ensambladores son hoy programas de software gratuitos . Así que descargue y compile en su computadora el código fuente de GNU como (una parte de binutils ) y de nasm . Luego estudie su código fuente. Por cierto, recomiendo usar Linux para ese propósito (es un sistema operativo muy amigable para desarrolladores y software libre).

El archivo objeto producido por un ensamblador contiene notablemente un segmento de código e instrucciones de reubicación . Está organizado en un formato de archivo bien documentado, que depende del sistema operativo. En Linux, ese formato (utilizado para archivos de objetos, bibliotecas compartidas, volcados de núcleo y ejecutables) es ELF . Ese archivo de objeto luego se ingresa al enlazador (que finalmente produce un ejecutable). Las reubicaciones son especificadas por el ABI (por ejemplo, x86-64 ABI ). Lea el libro de Levine Linkers and Loaders para más información.

El segmento de código en dicho archivo de objeto contiene código de máquina con agujeros (para que el vinculador lo llene, con la ayuda de información de reubicación). El código de máquina (reubicable) generado por un ensamblador es obviamente específico para una arquitectura de conjunto de instrucciones . Los ISA x86 o x86-64 (utilizados en la mayoría de los procesadores de computadoras portátiles o de escritorio) son terriblemente complejos en sus detalles. Pero un subconjunto simplificado, llamado y86 o y86-64, se ha inventado con fines de enseñanza. Lea las diapositivas sobre ellos. Otras respuestas a esta pregunta también explican un poco de eso. Es posible que desee leer un buen libro sobre arquitectura de computadoras .

La mayoría de los ensambladores trabajan en dos pasadas , la segunda emite reubicación o corrige parte de la salida de la primera pasada. Ahora usan las técnicas habituales de análisis (así que lea quizás The Dragon Book ).

Cómo el núcleo del sistema operativo inicia un ejecutable (por ejemplo, cómo funciona la execvellamada del sistema en Linux) es una pregunta diferente (y compleja). Por lo general, configura un espacio de direcciones virtuales (en el proceso que realiza ese execve (2) ...) y luego reinicializa el estado interno del proceso (incluidos los registros en modo de usuario ). Un enlazador dinámico , como ld-linux.so (8) en Linux, podría estar involucrado en tiempo de ejecución. Lea un buen libro, como Sistema operativo: tres piezas fáciles . El wiki de OSDEV también brinda información útil.

PD. Su pregunta es tan amplia que necesita leer varios libros al respecto. He dado algunas referencias (muy incompletas). Deberías encontrar más de ellos.

Basile Starynkevitch
fuente
1
Con respecto a los formatos de archivos de objetos, para un principiante, recomendaría mirar el formato RDOFF producido por NASM. Esto fue diseñado intencionalmente para ser lo más simple posible y aún así funcionar en una variedad de situaciones. La fuente NASM incluye un vinculador y un cargador para el formato. (Divulgación completa: diseñé y escribí todo esto)
Julio