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

¿Existe una manera fácil de visualizar el paso entre el ensamblaje de código a código de máquina?

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

Pero, ¿cómo pasamos de ensamblado a binario, qué sucede detrás de escena?

Respuesta

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

ejemplo de instrucción addlw

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

El 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 que debe agregarse.

Es tedioso, pero no tan difícil de codificar y decodificar. Tenía una clase de pregrado en la que teníamos que hacerlo a mano en los exámenes.

Para crear un archivo ejecutable completo, también tienes que hacer cosas como asignar memoria, calcular las compensaciones de las ramas y ponerlo en un formato como ELF , según su sistema operativo.

Responder

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. Entonces, todo lo que tiene que hacer es identificar cada código de operación en el lenguaje ensamblador, asignarlo a la instrucción de la máquina correspondiente y escribir la instrucción de la 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 lo hacen 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).

Respuesta

La primera lo que necesita es algo como este archivo . Esta es la base de datos de instrucciones para los procesadores x86 que utiliza el ensamblador NASM (que ayudé a escribir, aunque no las partes que realmente traducen las instrucciones). Elija 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. Hay 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 la siguiente:

 add eax, 42  

Ahora, necesita 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 EAX y el valor 42). Una vez que tenga esta estructura, recorra la base de datos de instrucciones y busque la línea que coincida 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 («combinación ilegal de código de operación y operandos» o similar es el texto habitual).

Una vez que «veamos Obtuvimos 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 mi es una descripción de los operandos: un operando modr/m (registro o memoria) (lo que significa que «tendremos que añadir un modr/m byte a el final de la instrucción, que veremos más adelante) y una instrucción inmediata (que se utilizará en la descripción de la instrucción).
  • El siguiente es hle. Esto identifica cómo manejamos el prefijo «bloqueo». No hemos usado «bloqueo», así que lo ignoramos.
  • El siguiente es o32. Esto nos dice que si estamos ensamblando código para un 16- formato de salida de bits, la instrucción necesita un prefijo de anulación del tamaño del operando.Si estuviéramos produciendo una salida de 16 bits, «produciríamos el prefijo ahora (0x66), pero asumiré que no lo estamos y continuar.
  • El siguiente es 83. Este es un byte literal en hexadecimal. Lo mostramos.
  • El siguiente es /0. Esto especifica algunos bits extra que necesitaremos en el bytem modr / m, y nos hace generarlo. El byte modr/m se usa para codificar registros o referencias indirectas de memoria. Tenemos un solo operando de este tipo, un registro. El registro tiene un número, que se especifica en otro archivo de datos :

     eax REG_EAX reg32 0  
  • Comprobamos que reg32 concuerda con el tamaño requerido de la instrucción de la base de datos original (lo hace). El 0 es el número del registro. Un modr/m byte 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)  
  • Dado que estamos trabajando con un registro, el campo mod es 0b11.

  • El campo reg es el número del registro que» estamos usando, 0b000
  • Debido a que solo hay un registro en esta instrucción, necesitamos completar el campo rm con algo. Para eso eran los datos adicionales especificados en /0, así que los colocamos en el campo rm, 0b000.
  • El modr/m byte es, por lo tanto, 0b11000000 o 0xC0. Generamos esto.
  • El siguiente es ib,s. Esto especifica un byte inmediato firmado. Observamos los operandos y notamos que tenemos un valor disponible. Lo convertimos a un byte firmado y lo mostramos (42 => 0x2A).

Por lo tanto, la instrucción ensamblada completa 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 que sepa qué insertar cuando se haga referencia a ellas. Agregue funciones para macros y directivas que se pasan a sus módulos de salida de archivos de objetos. Y así es básicamente como funciona un ensamblador.

Comentarios

  • Gracias. Excelente explicación, pero no debería ‘ ser » 0x83 0xC0 0x2A » en lugar de » 0x83 0xB0 0x2A » porque 0b11000000 = 0xC0
  • @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 razón. 🙂

Respuesta

En la práctica, un ensamblador normalmente no producen directamente algunos ejecutables binarios, pero algunos archivo de objeto (que se enviará más tarde al vinculador ). Sin embargo, hay excepciones (puede utilizar algunos ensambladores para producir directamente algunos ejecutables binarios ; son poco comunes).

En primer lugar, observe que muchos ensambladores son actualmente programas de software libre . Así que descargue y compile en su computadora el código fuente código 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 SO muy amigable para desarrolladores y amigable con software libre).

El archivo de objeto producido por un ensamblador contiene en particular un segmento de código y 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 se ingresa posteriormente en el vinculador (que finalmente produce un ejecutable). Las reubicaciones se especifican mediante la ABI (p. Ej., x86-64 ABI ). Lea el libro de Levine Vinculadores y cargadores para obtener más información.

El segmento de código en dicho archivo de objeto contiene código de máquina con agujeros (para ser llenado, con la ayuda de información de reubicación, por el enlazador). El código de máquina (reubicable) generado por un ensamblador es obviamente específico para un conjunto de instrucciones arquitectura .Los ISA x86 o x86-64 (utilizados en la mayoría de los procesadores de computadoras portátiles o de escritorio) son terriblemente complejo en sus detalles. Pero se ha inventado un subconjunto simplificado, llamado y86 o y86-64, con fines didácticos. 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 están trabajando 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 inicia un ejecutable el kernel del SO (p. ej., cómo funciona la llamada al sistema execve en Linux ) es una pregunta diferente (y compleja). Por lo general, configura un espacio de direcciones virtuales (en el proceso haciendo eso execve (2) …) y luego reinicialice el estado interno del proceso (incluidos los registros modo de usuario ). Un enlazador dinámico , como ld-linux.so (8) en Linux, podría participar en tiempo de ejecución. Lea un buen libro, como Sistema operativo: tres piezas fáciles . La wiki de OSDEV también ofrece información útil.

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

Comentarios

  • Con respecto a los formatos de archivos de objetos, para un principiante I ‘ Recomendaría mirar el formato RDOFF producido por NASM. Esto fue diseñado intencionalmente para ser lo más simple posible de manera realista y aún así funcionar en una variedad de situaciones. La fuente NASM incluye un enlazador y un cargador para el formato. (Divulgación completa: diseñé y escribí todos estos)

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *