Capítulo 6. El programa ensamblador

Tabla de contenidos

6.1. Creación de un programa ejecutable en ensamblador
6.2. Definición de datos
6.2.1. Definición de bytes
6.2.2. Definición de enteros
6.2.3. Definición de strings
6.2.4. Definición de espacio en blanco
6.3. Uso de etiquetas
6.4. Gestión de la pila
6.5. Desarrollo de programas en ensamblador
6.6. Ejemplo de programa en ensamblador
6.7. Ejercicios

Los programas escritos en lenguaje ensamblador, a pesar de representar instrucciones del lenguaje máquina del procesador, no son directamente ejecutables por éste sino que es necesario traducirlas a su codificación en binario. Este proceso de traducción es fácilmente automatizable, y por tanto se dispone de programas denominados ensambladores (o más genéricamente compiladores que se encargan de esta tarea.

El ensamblador es un programa que recibe como datos de entrada uno o varios ficheros de texto plano con un conjunto de instrucciones y datos escritos en lenguaje ensamblador y produce un fichero binario y ejecutable que contiene la codificación binaria del programa. La figura 6.1 muestra el funcionamiento del programa ensamblador.

Figura 6.1. El programa ensamblador

El programa ensamblador

En general, a los programas encargados de traducir de un lenguaje de programación a otro se les denomina “compiladores” y todos ellos trabajan de forma similar. Dado un conjunto de ficheros escritos en un lenguaje, producen como resultado otro fichero que contiene la traducción a un segundo lenguaje. En el caso del ensamblador, la traducción es de lenguaje ensamblador a lenguaje máquina.

En adelante se utilizarán los términos “compilador” y “ensamblador” de forma indistinta y siempre en referencia al programa que traduce de lenguaje ensamblador a lenguaje máquina.

Así como el lenguaje máquina de un procesador es único e inmutable (a no ser que se rediseñe el procesador), pueden coexistir múltiples lenguajes ensamblador que representen el mismo lenguaje máquina. La representación de las instrucciones mediante cadenas alfanuméricas es un convenio utilizado para facilitar su escritura, por lo que pueden existir múltiples convenios de este tipo siempre y cuando se disponga del ensamblador los que traduzca al lenguaje máquina del procesador.

En el caso concreto del sistema operativo Linux, se incluye como parte de las herramientas del sistema un compilador capaz de traducir de lenguaje ensamblador a lenguaje máquina. Su nombre es as. En la práctica este programa lo suelen invocar otros compiladores tales como gcc que es un compilador del lenguaje de alto nivel C a lenguaje máquina, pero también permite la traducción de ficheros con código ensamblador invocando internamente el programa as.

6.1. Creación de un programa ejecutable en ensamblador

La figura 6.2 muestra un programa en lenguaje ensamblador creado mediante un editor de texto plano, un programa que guarda únicamente el texto codificado en formato ASCII o UNICODE sin información alguna sobre estilo. El primer paso, por tanto, para la obtención de un programa ejecutable es la creación de un fichero de texto que contenga el código.

Figura 6.2. Estructura de un programa en ensamblador

Estructura de un programa en ensamblador

Un programa consta de varias secciones separadas cada una de ellas por palabras clave que comienzan por el símbolo “.”. La palabra .data que aparece en la primera línea no tiene traducción alguna para la ejecución, sino que es la forma de notificar al ensamblador que a continuación se encuentran definidos conjunto de datos. A este tipo de palabras que comienzan por punto se les denomina “directivas”.

El programa tiene definido un único dato que se representa como una secuencia de caracteres. La línea que contiene .asciz (también una directiva) seguida del string entre comillas es la que instruye al ensamblador para crear una zona de memoria con datos, y almacenar en ella el string que se muestra terminado por un byte con valor cero. El efecto de la directiva .asciz es que, al comienzo de la ejecución de programa, este string esté almacenado en memoria.

Antes de la directiva .asciz se incluye la palabra dato seguida por dos puntos. Esta es la forma de definir una etiqueta o nombre que luego se utilizará en el código para acceder a estos datos.

La línea siguiente contiene la directiva .text que denota el comienzo de la sección de código. La directiva .global main comunica al ensamblador que la etiqueta main es globalmente accesible desde cualquier otro programa.

A continuación se encuentran las instrucciones en ensamblador propiamente dichas. Al comienzo del código se define la etiqueta main que identifica el punto de arranque del programa.

Una vez creado y guardado el fichero de texto con el editor, se debe invocar el compilador. En una ventana en el que se ejecute un intérprete de comandos y situados en el mismo directorio en el que se encuentra el fichero ejemplo.s se ejecuta el siguiente comando:

gcc -o ejemplo ejemplo.s

El compilador realiza una tarea similar a la de un compilador de un lenguaje de alto nivel como Java. Si hay algún error en el programa se muestra la línea y el motivo. Si el proceso de traducción es correcto, se crea un fichero ejecutable. En el comando anterior, se ha instruido al ensamblador, por medio de la opción -o ejemplo para que el programa resultante se deposite en el fichero con nombre ejemplo.

El compilador también es capaz de procesar más de un fichero de forma simultanea. Esto es útil cuando el código de un programa es muy extenso y está fraccionado en varios ficheros que deben combinarse para obtener un único ejecutable. En tal caso el comando para compilar debe incluir el nombre de todos los ficheros necesarios.

Si el compilador no detecta ningún error en la traducción, el fichero ejemplo está listo para ser ejecutado por el procesador. Para ello simplemente se escribe su nombre en el intérprete de comandos (en la siguiente línea, el símbolo $ representa el mensaje que imprime siempre el intérprete de comandos):

$ ejemplo
Mi Primer Programa Ensamblador
$

Todo programa ensamblador debe seguir el siguiente patrón:

        .data           # Comienzo del segmento de datos

        <datos del programa>

        .text           # Comienzo del código
        .global main    # Obligatorio 

main:
        <Instrucciones>

        ret             # Obligatorio

Se pueden incluir comentarios en el código a partir de símbolo “#” hasta el final de línea y son ignorados por el compilador. Basado en este patrón, el programa de la figura 6.2 ha ejecutado las instrucciones:

        push %eax
        push %ecx
        push %edx

        push $dato
        call printf
        add $4, %esp

        pop %edx
        pop %ecx
        pop %eax

        ret

Las primeras tres instrucciones depositan los valores de los registros %eax, %ecx y %edx en la pila. Las tres instrucciones siguientes se encargan de poner la dirección del string también en la pila (instrucción push), invocar la rutina externa printf que imprime el string (instrucción call) y sumar una constante al registro %esp para restaurar el valor inicial del puntero a la cima de la pila. Las tres últimas instrucciones restauran el valor original en los registros previamente guardados en la pila.

A continuación se estudia en detalle la sintaxis de las diferentes construcciones permitidas en el lenguaje ensamblador.

6.2. Definición de datos

Como todo lenguaje de programación, se permiten definir tipos de datos así como su contenido. En el caso del ensamblador, estos tipos no permiten estructuras complejas ni heterogéneas. Todas las definiciones deben incluirse en una sección del código que comience por la directiva .data. Los datos se almacenan en posiciones contiguas de memoria, es decir, dos definiciones seguidas hacen que los datos se almacenen uno a continuación de otro.

La principal dificultad para manipular los datos en ensamblador es que cuando el procesador accede a ellos, no se realiza ningún tipo de comprobación. Aunque se definan datos con cierto tamaño y estructura en memoria, el procesador trata estos datos como una simple secuencia de bytes. Esta es una diferencia sustancial con los lenguajes de programación de alto nivel tales como Java. La definición de datos en ensamblador se realiza a través de directivas (descritas a continuación) que únicamente reservan espacio en memoria con los datos pertinentes, pero no se almacena ningún tipo de información sobre su tamaño.

Los lenguajes de alto nivel contienen lo que se conoce como un “sistema de tipos” que consiste en un conjunto de reglas que permiten la definición de tipos de datos así como el mecanismo para comprobar su corrección. En ensamblador, al tratarse de los datos que manipula directamente el procesador, no se dispone de tal sistema, y por tanto se manejan como si fuese simples secuencias de bytes.

6.2.1. Definición de bytes

La definición de valores numéricos almacenados en bytes se realiza mediante la directiva .byte seguida de uno o varios valores separados por comas. Cuando el programa comienza la ejecución, se han inicializado tantas posiciones en memoria como indica la directiva con los valores dados. El ejemplo 6.1 muestra ejemplos de utilización de la directiva .byte así como los valores almacenados en memoria.

Ejemplo 6.1. Definición de bytes en ensamblador y sus valores en memoria

datos:  .byte 38, 0b11011101, 0xFF, 'A, 'b

Si el valor numérico especificado es menor que cero o mayor que 255 el compilador notifica la anomalía con un error.

6.2.2. Definición de enteros

La definición de enteros de 32 bits se hace mediante la directiva .int seguida de un número o una lista de números enteros separados por comas. Los números se codifican con 4 bytes almacenados en little endian. El ejemplo 6.2 muestra ejemplos de definiciones de enteros.

Ejemplo 6.2. Definiciones de números enteros y sus valores en memoria

nums:   .int 3, 4, 5
        .int 0x12AB, 0x10ab, 0b111000, 0B111000
        .int 21
        .int 07772

La directiva .long es un sinónimo de .int y también define enteros de 32 bits. Las directivas .word y .quad son análogas a las anteriores pero definen enteros de 16 y 64 bits respectivamente.

6.2.3. Definición de strings

La definición de strings se puede hacer con dos formatos diferentes mediante la utilización de tres directivas. La directiva .ascii permite la definición de uno o más strings entre comillas y separadas por comas. Cada símbolo de cada strings codifica con un byte en ASCII utilizando posiciones consecutivas de memoria. Se utilizan tantos bytes como la suma de los símbolos de cada string.

La directiva .asciz es similar a la anterior, se escribe seguida de uno o más strings separados por comas, pero cada uno de ellos se codifica añadiendo un byte con valor cero a final del string. Este formato se suele utilizar para detectar el final del string. La directiva .string es un sinónimo de la directiva .asciz.

El ejemplo 6.3 muestra la utilización de las directivas de definición de strings y los valores que se almacenan en memoria. Los bytes resaltados corresponden son los que añaden las directivas .asciz y .string al final de cada string.

Ejemplo 6.3. Definición de strings y sus valores en memoria

msg:    .ascii "S 1", "S 2"
        .asciz "S 3", "S 4"
        .string "S final"

6.2.4. Definición de espacio en blanco

La directiva .space seguida de dos números separados por una coma permite la reserva de espacio en memoria. El primer valor denota el número de bytes que se reservan y el segundo es el valor que se utiliza para inicializar dichos bytes y debe estar entre 0 y 255. En el caso de que este parámetro se omita, la memoria se reserva pero no se inicializa a ningún valor.

El uso principal de esta directiva es para reservar espacio que, o se debe inicializar al mismo valor, o su valor será calculado y modificado por el propio programa. El ejemplo 6.4 muestra el uso de la directiva así como su efecto en memoria.

Ejemplo 6.4. Definiciones de espacio en blanco y su valor en memoria

result: .space 4, 0
        .space 4
        .space 8, 0xFF

6.3. Uso de etiquetas

En lenguaje ensamblador se permite la definición de un conjunto de datos y las instrucciones para manipularlos que se traducen a su codificación binaria y se produce un fichero ejecutable. Antes de comenzar la ejecución del programa, los datos e instrucciones en binario se cargan en la memoria RAM del sistema. Pero ¿en qué posición de memoria está almacenado el programa?

El valor de esta dirección de memoria, o de la dirección en la que está almacenado cualquier dato o instrucción, no se sabe hasta el momento en el que se ejecuta el programa porque es el sistema operativo el que lo decide, y tal decisión se aplaza hasta el último instante para así poder ubicar cada programa en el lugar más conveniente en memoria. El sistema operativo está ejecutando múltiples programas de forma simultánea, y por tanto, necesita esta flexibilidad para poder hacer un mejor uso de la memoria.

Pero, si no se sabe el valor de la dirección de memoria de ningún dato ni instrucción, ¿cómo se puede, por ejemplo, acceder a un dato en memoria? Para ello se precisa su dirección, pero el valor numérico de esta no se sabe cuando se escribe un programa.

El lenguaje ensamblador soluciona este problema mediante el uso de “etiquetas”. Las etiquetas no son más que nombres que se ponen al comienzo de una línea (ya sea definición de datos o una instrucción) seguido por dos puntos. Dicho nombre representa la posición de memoria en la que está almacenado el dato o instrucción definido justo a continuación.

Estas etiquetas son, por tanto, un punto de referencia en memoria que el ensamblador sabe interpretar de forma correcta y que en el momento de ejecución serán reemplazados por el valor numérico de la dirección de memoria pertinente.

La definición de una etiqueta no sólo permite referirse a los datos almacenados en esa posición, sino que ofrece un mecanismo por el que acceder a los datos en posiciones cercanas a ella mediante simples operaciones aritméticas sobre la dirección que representa. Considérese de nuevo la representación en memoria de los enteros definidos en el ejemplo 6.2. La figura 6.3 ilustra como se pueden deducir las direcciones de los demás enteros en base al símbolo nums:.

Figura 6.3. Etiqueta y direcciones relativas a ella

Etiqueta y direcciones relativas a ella

Dado que la directiva .int define valores enteros representados por 32 bits, de la definición de la etiqueta nums se pueden deducir los valores de las direcciones en las que se almacenan el resto de números enteros definidos. Para efectuar estos cálculos es imprescindible saber el tamaño de la información almacenada a partir de la etiqueta.

Las etiquetas, por tanto, se pueden definir en cualquier lugar del código y son únicamente símbolos que representan una dirección de memoria cuyo valor no se sabe y se utilizan como puntos de referencia para acceder a los datos en memoria de su alrededor. Pero su uso en ensamblador tiene dos versiones igualmente útiles. La primera es acceder al valor contenido en la posición de memoria a la que se refieren. Para ello se incluye en las instrucciones ensamblador el nombre de la etiqueta tal cual se ha definido (sin los dos puntos).

Pero a menudo es necesario manipular la propia dirección de memoria que representa dicha etiqueta. Aunque dicho valor es desconocido, nada impide que se escriban instrucciones máquina que operen con él. El ensamblador permite referirse al valor de la dirección de memoria que representa una etiqueta precediendo su nombre del símbolo $.

Si una instrucción ensamblador contiene como operando el nombre de una etiqueta, este operando es de tipo dirección de memoria (ver la sección 5.3.3). En cambio, si el operando es el nombre de una etiqueta precedido por $, este operando es de tipo constante.

Esta nomenclatura para diferenciar entre el valor al que apunta una etiqueta y su valor como dirección en memoria es consistente con la nomenclatura de operandos. Dada una etiqueta, de los dos valores, al que apunta en memoria y su dirección, es este último el que permanece constante a lo largo de la ejecución, y por tanto se representa con el prefijo $. En cambio, el valor en memoria al que apunta es variable y por ello se representa únicamente por el nombre de la etiqueta.

El ejemplo 6.5 muestra una porción de código en la que se define una etiqueta y se manipula mediante instrucciones máquina.

Ejemplo 6.5. Definición y manipulación de etiquetas

        .data
dato:   .int 3, 4
        .string "Mensaje"
        .byte 17
        ...
        mov dato, %eax
        add $5, %eax
        mov %eax, dato
        movl $4, dato
        ...
        mov $dato, %ebx
        add $8, %ebx
        ...

La etiqueta dato corresponde con la dirección de memoria en la que está almacenado el entero de 32 bits con valor 3. En el primer grupo de instrucciones, la instrucción mov dato, %eax mueve el número 3 al registro %eax. Nótese que el operando carece del prefijo $ y por tanto se refiere al valor almacenado en memoria. A continuación se suma la constante 5 y se transfiere el valor en %eax de nuevo a la posición de memoria referida por dato.

La instrucción movl $4, dato requiere especial atención. El sufijo de tamaño es necesario para desambiguarla porque ni la constante $4 ni el segundo operando contienen información sobre su tamaño.La información de una etiqueta es únicamente la dirección a la que representa sin ningún tipo de información sobre el tamaño de los datos. Por tanto, a pesar de que dato ha sido definida en una línea en la que se reserva espacio para enteros, cuando se utiliza en una instrucción y el otro operando tampoco ofrece información sobre el tamaño, requiere el sufijo.

En el segundo grupo de instrucciones, la instrucción mov $dato, %ebx carga en el registro %ebx el valor de la dirección de memoria que representa la etiqueta. Este valor es imposible de saber en tiempo de programación, pero se puede manipular al igual que cualquier otro número. Tras ejecutar la última instrucción, el registro %ebx contiene la dirección de memoria en la que está almacenada la primera letra del string. Esto se deduce de las definiciones de datos y sus tamaños. Los dos números ocupan 8 bytes, con lo que en la posición $dato + 8 se encuentra la letra “M” del string Mensaje.

Las etiquetas no sólo se utilizan en las definiciones de datos sino también en instrucciones del código. Los destinos de los saltos reciben como operando una dirección de memoria, que por tanto debe ser una etiqueta.

Se pueden definir tantas etiquetas como sea preciso en un programa sin que por ello se incremente el tamaño del programa. Las etiquetas son símbolos que utiliza el programa ensamblador para utilizar en lugar de los valores numéricos de las direcciones que se sabrán cuando el programa comience su ejecución.

El compilador gcc utilizado para traducir de lenguaje ensamblador a lenguaje máquina asume que el punto de comienzo de programa está marcado por la presencia de la etiqueta con nombre main. Por tanto, al escribir un programa que sea traducido por gcc se debe definir la etiqueta main en el lugar del código que contenga su primera instrucción máquina.

6.4. Gestión de la pila

El desarrollo de programas en ensamblador tiene una serie de particularidades derivadas de la proximidad al procesador con la que se trabaja. Uno de los cometidos de los lenguajes de programación de alto nivel tales como Java es precisamente el ofrecer al programador un entorno en el que se oculten los aspectos más complejos de la programación en ensamblador.

La pila se utiliza como depósito temporal de datos del programa en ejecución. Las operaciones push y pop permiten depositar y obtener datos de la pila, pero no son las únicas que modifican su contenido. El propio procesador también utiliza la pila para almacenar datos temporales durante la ejecución. Esto implica que los programas en ensamblador tienen ciertas restricciones al manipular la pila.

La más importante de ellas es que la cima de la pila debe ser exactamente la misma antes del comienzo de la primera instrucción de un programa y antes de la instrucción RET que termina su ejecución. Cuando se arranca un programa, el sistema operativo reserva espacio para la pila y almacena el valor pertinente en el registro %esp. Por motivos que se explican en detalle en el capítulo 8, el valor de este registro debe ser el mismo al terminar la ejecución de un programa.

Nótese que el respetar esta regla no implica que la pila no pueda utilizarse. Al contrario, como la cima debe ser idéntica al comienzo y final del programa, las instrucciones intermedias sí pueden manipular su contenido siempre y cuando al final del programa se restaure el valor de la cima que tenía al comienzo.

En un programa, esta limitación se traduce en que cada dato que se deposite en la pila debe ser descargado antes de que finalice el programa. En el medio del código, la pila puede almacenar los datos que el programador considere oportunos.

Además de inicializar el registro %esp, el sistema operativo también deposita valores en los registros de propósito general. La ejecución del programa escrito en ensamblador la inicia el sistema mediante una llamada a la subrutina con nombre main (de ahí que éste sea el punto de comienzo del programa) y por tanto, los registros tienen todos ciertos valores iniciales.

La regla a respetar en los programas ensamblador es que al término de la ejecución de un programa, el valor de los registros de propósito general debe ser exactamente el mismo que tenían cuando se comenzó la ejecución (en la práctica no todos los registros deben ser restaurados, pero por simplicidad se ha adoptado la regla para todos ellos).

De nuevo, el que el valor de los registros tenga que ser idéntico al comienzo y al final de un programa no quiere decir que no se puedan utilizar. Simplemente se deben guardar los valores iniciales de aquellos registros que se utilicen y restaurarlos antes de terminar la ejecución.

El lugar más apropiado para guardar los valores iniciales de estos registros es precisamente la pila. No es preciso reservar espacio de antemano, pues la pila ya lo tiene reservado, y mediante varias instrucciones PUSH se depositan los registros que se modifiquen en el código antes de ejecutar las instrucciones propias del cálculo. Luego, justo antes del final de la ejecución se restauran mediante las instrucciones POP. El ejemplo 6.6 muestra una porción de un programa que modifica los registros %eax, %ebx, %ecx y %edx,

Ejemplo 6.6. Instrucciones para salvar y restaurar registros

 main:  push %eax
        push %ebx
        push %ecx
        push %edx

        # Instrucciones del programa que modifican los 4 registros

        pop %edx
        pop %ecx
        pop %ebx
        pop %eax

        ret

Las instrucciones para guardar la copia de los registros que se modifican se realiza justo al principio del código. De forma análoga, las instrucciones para restaurar estos valores se realizan justo antes de la instrucción RET. Asimismo, el orden en el que se salvan y restauran los registros es el inverso debido a cómo se almacenan en la pila. El orden en el que se depositan los datos en la pila es irrelevante, tan sólo se deben restaurar en orden inverso al que se han depositado.

Todo programa ensamblador, por tanto, debe comenzar y terminar con instrucciones de PUSH y POP de los registros que se modifiquen en su interior.

La utilización de la pila para almacenar los valores de los registros modificados respeta el convenio de mantener la cima de la pila idéntica al comienzo y final del programa. Como el número de operaciones PUSH es idéntico al número de operaciones POP, mientras que en el código interno del programa todo dato que se deposite en la pila se extraiga, la cima de la pila es idéntica al comienzo y final.

A la hora de desarrollar programas en ensamblador, se recomienda primero escribir el código interno de un programa y cuando dicho código se suponga correcto completarlo con las instrucciones que salvan y restauran los registros que se modifican. En general, los datos que se almacenan en la pila se hace de forma temporal y deben eliminarse una vez terminada la tarea para la que se han almacenado.

6.5. Desarrollo de programas en ensamblador

El desarrollo de programas en ensamblador requiere un conocimiento en detalle de la arquitectura del procesador y una meticulosidad extrema a la hora de decidir qué instrucciones y datos utilizar. Al trabajar con el lenguaje máquina del procesador, la comprobación de errores de ejecución es prácticamente inexistente. Si se ejecuta una instrucción con operandos incorrectos, el procesador los interpretará tal y como estipula su lenguaje máquina, con lo que es posible que la ejecución del programa produzca resultados inesperados.

Desafortunadamente no existe un conjunto de reglas que garanticen un desarrollo simple de los programas. Esta destreza se adquiere mediante la práctica y, más importante, mediante el análisis detenido de los errores, pues ponen de manifiesto aspectos de la programación que se han ignorado.

Las recomendaciones que se hacen para el desarrollo de programas en lenguaje de alto nivel adquieren todavía más relevancia en el contexto del lenguaje ensamblador. Sin ser de ninguna manera una lista exhaustiva, se incluyen a continuación las más relevantes.

  • El valor de los registros y el puntero de pila antes de ejecutar la última instrucción del programa deben ser idénticos a los valores que tenían al comienzo.

  • Se deben evitar las operaciones innecesarias. Por ejemplo, salvar y restaurar todos los registros independientemente de si son utilizados o no.

  • Debido al gran número de instrucciones disponibles y a su simplicidad siempre existen múltiples forma de realizar una operación. Generalmente elige aquella que proporciona una mayor eficiencia en términos de tiempo de ejecución, utilización de memoria o registros, etc.

  • Mantener un estilo de escritura de código que facilite su legibilidad. Escribir las etiquetas a principio de línea, las instrucciones todas a la misma altura (generalmente mediante ocho espacios), separar los operandos por una coma seguida de un espacio, etc.

  • La documentación en el código es imprescindible en cualquier lenguaje de programación, pero en el caso del ensamblador, es crucial. Hacer uso extensivo de los comentarios en el código facilita la comprensión del mismo además de simplificar la detección de errores. Los comentarios deben ser lo más detallados posible evitando comentar instrucciones triviales. Es preferible incluir comentarios de alto nivel sobre la estructura global del programa y los datos manipulados.

  • La mayor parte de errores se detectan cuando el programa se ejecuta. No existe una técnica concreta para detectar y corregir un error, pero se debe analizar el código escrito de manera minuciosa. En ensamblador un simple error en el nombre de un registro puede producir que un programa sea incorrecto.

6.6. Ejemplo de programa en ensamblador

El ejemplo 6.7 muestra un programa de ejemplo escrito en ensamblador que dados cuatro enteros almacenados en memoria, suma sus valores y deposita el resultado en el lugar que denota la etiqueta result.

Ejemplo 6.7. Programa que suma cuatro enteros

	.data				# Comienza la sección de datos
num1:	.int 10
num2:	.int 23
num3:	.int 34
num4:	.int 43
result:	.space 4			# Deposita aquí el resultado

	.text				# Comienza la sección de código
	.global main			# main es un símbolo global

main:
	push %eax			# Salva registros
	
	mov num1, %eax			# Carga primer número y acumula
	add num2, %eax
	add num3, %eax
	add num4, %eax

	mov %eax, result		# Almacena resultado

	pop %eax			# Restaura registros
	ret

La sección de definición de datos contiene los cuatro enteros con sus respectivas etiquetas y los cuatro bytes de espacio vacío en el que se almacenará su suma. La directiva .space tiene un único parámetro, con lo que se reserva espacio en memoria pero no se inicializa.

El programa utiliza el registro %eax para acumular los valores de los números, por lo que necesita ser salvado en la pila al comienzo y restaurado al final.

Tras salvar %eax en la pila la siguiente instrucción simplemente mueve el primer número al registro %eax. No es posible sumar dos números que están almacenados en memoria, por lo que el programa carga el primer valor en un registro y luego suma los restantes números a este registro.

Finalmente, el programa almacena el resultado de la suma en la posición de memoria con etiqueta result, restaura los registros utilizados (en este caso sólo %eax) y termina la ejecución con la instrucción RET.

6.7. Ejercicios

  1. Escribir el equivalente de las siguientes definiciones de datos en ensamblador pero utilizando únicamente la directiva .byte.

    • .int 12, 0x34, 'A
    • .space 4, 0b10101010
    • .ascii "MSG."
    • .asciz "MSG. "
  2. Dada la siguiente definición de datos:

    dato:  .int 0x10203040, 0b10
           .string "Mensaje en ASCII"
           .ascii "Segundo mensaje"

    Si la etiqueta dato ser refiere a la posición de memoria 0x00001000, calcular la dirección de memoria de los siguientes datos:

    • El byte con valor 0x30 del primer entero definido.

    • El byte de más peso del segundo número entero definido.

    • La letra “A” del primer string definido.

    • El espacio en blanco entre las dos palabras de la última definición.

Envío de errata