UC3M
Universidad Carlos III de Madrid. Dept. de Ingeniería Telemática
Dept. IT
Arquitectura de ordenadores 07/08
RSS Disponible RSS Feed

Práctica 2: El programa ensamblador



Enlace a la sala de chat del curso
[Nota] Nota

Recuerda que este enunciado, así como todos los ficheros auxiliares que se mencionan en él, está en la copia de tu directorio de trabajo en el depósito central. Por tanto, los puedes obtener simplemente actualizando tu directorio de trabajo con el comando svn update ejecutado desde la raíz de tu directorio de trabajo (Group_??).

1. El programa ensamblador

El programa ensamblador es la herramienta que realiza la traducción de un fichero que contiene instrucciones para el procesador del estilo mov %eax, %ebx a su correspondiente representación como secuencia de ceros y unos. Este programa, por tanto, sabe cómo codificar todas y cada una de las operaciones posibles en el procesador, así como sus operandos, modos de direccionamiento, etc.

Los datos de entrada al programa ensamblador (de ahora en adelante simplemente ensamblador) es uno o varios ficheros de texto plano que contienen un programa o secuencia de instrucciones a ejecutar por el procesador tal y como muestra la figura 1.

Figura 1. El programa ensamblador

El programa ensamblador

El lenguaje en el que se escriben estas instrucciones se conoce como lenguaje ensamblador. Un mismo procesador puede tener diferentes programas ensambladores con diferentes lenguajes de entrada, pero todos ellos producen idéntico código (o lenguaje) máquina.

El lenguaje ensamblador no sólo permite utilizar los nombres de las instrucciones, operandos y modos de direccionamiento, sino que también permite especificar etiquetas y definir porciones de memoria para almacenar datos. La figura 2 muestra un fichero que contiene un programa escrito en ensamblador:

Figura 2. Estructura de un programa en ensamblador

Estructura de un programa en ensamblador

Un programa está dividido en varias secciones. La palabra .data es una directiva y comunica al ensamblador que a continuación se define un conjunto de datos. El programa tan sólo tiene un único dato que se representa como una secuencia de caracteres. La línea .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. Nótese que el efecto de la directiva .asciz no se traduce en código sino que son órdenes para que el ensamblador haga una tarea, en este caso almacenar un string en memoria.

Antes de la directiva .asciz se incluye la palabra dato seguida por dos puntos. Esta es la forma de definir una etiqueta 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. Nótese que todas las directivas tienen como primer carácter un punto. La línea .globl main contiene la directiva que comunica al ensamblador que la etiqueta con nombre main será globalmente accesible desde otro programa.

A continuación se encuentran las instrucciones en ensamblador propiamente dichas. La primera de ellas es push %eax que almacena el registro %eax en una zona específica de memoria que se denomina la pila.

Al comienzo del código se define la etiqueta main. Esta etiqueta identifica la posición por la que el procesador va a empezar a ejecutar, o lo que es lo mismo, la primera instrucción del programa. Todo programa debe incluir esta etiqueta obligatoriamente. Al final del código se puede ver una segunda etiqueta (esta opcional) con nombre done.

Una vez creado el fichero de texto con el editor y guardado con el contenido de la figura 2 con nombre programa.s, se pasa a ejecutar el compilador. Para ello primero es preciso abrir una ventana con el intérprete de comandos y situarse mediante comandos cd en el mismo directorio en el que se encuentra el fichero programa.s. El comando para compilar el programa se muestra en la siguiente figura (en las siguientes figuras, la palabra shell$ es el mensaje que imprime siempre el intérprete de comandos al comienzo de línea, también conocido como prompt):

El compilador que se utilizará se llama gcc y realiza una tarea similar a la de un compilador de un lenguaje de alto nivel como Java. La cadena -o programa es la forma de decirle al compilador que queremos que el ejecutable resultante se llame programa. Si al compilar se detecta algún error en el programa se muestra la línea y el motivo. Si no se detectan errores, se genera un fichero con el ejecutable resultante.

En el caso de tener el código escrito en varios ficheros que deben combinarse para obtener un único programa, la línea anterior se modifica incluyendo todos los ficheros con extensión .s necesarios.

Si no ha habido ningún error, el fichero programa está listo para ser ejecutado por el procesador. Para ello simplemente se ha de teclear su nombre en el intérprete de comandos:

Volvamos al código para analizar detenidamente lo que acabamos de ejecutar. La etiqueta main marca el punto en el código en el que el programa comienza a ejecutar. Todo programa 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

Nótese que se pueden incluir todo tipo de comentarios utilizando el carácter '#'. Todo lo que se escriba desde este carácter hasta el final de la línea es ignorado por el compilador. Basado en este patrón, el programa anterior 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 zona de memoria denominada la pila. Las tres instrucciones siguientes se encargan de poner la dirección del string también en la pila (instrucción push), invocar una rutina externa que imprime el string (instrucción call) y sumar el valor 4 al registro %esp. Finalmente, las tres últimas instrucciones restauran el valor original en los registros previamente guardados en la pila.

La subrutina printf modifica el contenido de los registros %eax, %ecx y %edx. Por tanto, antes de llamar a esta rutina es necesario salvar su contenido en memoria (en este caso en la pila) y restaurar su valor al terminar. Con las instrucciones de push al comienzo y pop al final se garantiza que el programa termina con idénticos valores con los que comenzó la ejecución en los registros.

2. Traducción de lenguaje ensamblador a lenguaje máquina

El programa ensamblador, aparte de traducir de lenguaje ensamblador a lenguaje máquina, también puede mostrar por pantalla el resultado de la codificación de las instrucciones y datos en binario

Para la resolución de este ejercicio se trabaja con el siguiente programa que suma dos números (valor1 y valor2) y almacena el resultado multiplicado por 2 en la posición de memoria etiquetada como result. Además se muestra el valor de la suma por pantalla. El fichero debe tener como nombre nombre sumadospordos.s y su código se muestra en la siguiente figura.

        .data           # Comienza la sección de datos
                        # Se define el formato a imprimir
string: .asciz "Resultado = %d\n"
valor1: .int 530        # Variable que almacena el primer valor
valor2: .int -927       # Variable que almacena el segundo valor
result: .space 4        # Espacio reservado para el resultado

        .text           # Comienza la sección de código
        .globl main     # Declaración de main como símbolo global

main:   push %eax
        push %ecx
        push %edx

        mov valor1, %eax
        add valor2, %eax
        mov %eax, result
        add %eax, result
        
        push %eax
        push $string
        call printf
        add $8, %esp
        
        pop %edx
        pop %ecx
        pop %eax
        ret            # termina el programa

El compilador o ensamblador es el programa que traduce este fichero a un fichero ejecutable en lenguaje máquina. Sin embargo, se puede ejecutar este programa con la orden para que, en lugar de generar un ejecutable, muestre por pantalla el resultado de la traducción de ensamblador a lenguaje máquina. Esto se consigue incluyendo la opción -Wa,-a (nótese la ausencia de espacios en blanco). El comando para ensamblar el programa y ver el resultado por pantalla es:

gcc -Wa,-a -o [ejecutable] [fichero.s]

donde [ejecutable] se debe reemplazar por el nombre del fichero ejecutable a crear y [fichero.s] por el del fichero que contiene el código ensamblador.

El compilador muestra por pantalla el resultado del proceso de traducción a lenguaje máquina en un formato específico. Para analizar en detalle este listado se puede capturar el resultado en un fichero y visualizarlo mediante un editor. Para obtener un fichero que contenga el resultado producido por el compilador, es preciso ejecutar el comando anterior añadiendo al final el sufijo > listado.txt:

gcc -Wa,-a -o [ejecutable] [fichero.s] > listado.txt

donde de nuevo [ejecutable] se debe reemplazar por el nombre del fichero ejecutable a crear y [fichero.s] por el del fichero que contiene el código ensamblador.

Al ejecutar este comando el compilador no muestra ningún dato por pantalla y únicamente el intérprete de comandos escribe de nuevo el prompt. Sin embargo, en el directorio actual se ha creado un fichero listado.txt. Este fichero se puede abrir y ver su contenido con el editor.

El listado producido por el compilador está organizado en columnas. El compilador muestra el código del fichero en la parte derecha, y en la parte izquierda incluye tres columnas de números. En la primera columna se muestra el número de línea del fichero original. Los números incluidos en la segunda y tercera columna están escritos en hexadecimal. La segunda columna muestra en hexadecimal el número del primer byte de la codificación que se muestra en la tercera columna. El número de la segunda columna se puede obtener sumando al número de esta columna de la línea anterior, el número de bytes de la tercera columna. La codificación comienza a partir del byte 0 y se van almacenando los bytes en posiciones correlativas.

En la tercera columna se muestra el resultado de la codificación de cada línea (también en hexadecimal). Cuando dicha codificación ocupa más de una línea, se repite en la primera columna el número de línea incluyendo el valor de los siguientes bytes. La siguiente figura muestra un ejemplo de este listado.

GAS LISTING sumadospordos.s 			page 1


   1              	        .data           # Comienza la sección de datos
   2              	                        # Se define el formato a imprimir
   3 0000 52657375 	string: .asciz "Resultado = %d\n"
   3      6C746164 
   3      6F203D20 
   3      25640A00 
   4 0010 12020000 	valor1: .int 530        # Variable que almacena el primer valor
   5 0014 61FCFFFF 	valor2: .int -927       # Variable que almacena el segundo valor
   6 0018 00000000 	result: .space 4        # Espacio reservado para el resultado
   7              	
   8              	        .text           # Comienza la sección de código
   9              	        .globl main     # Declaración de main como símbolo global

A la vista del código mostrado por el compilador, responder a las siguientes preguntas:

  1. ¿Cuántos bytes ocupa la traducción a lenguaje máquina de la línea 3?

  2. En la tercera columna de la línea con el número 5 aparece el número 0x61FCFFFF. ¿Cómo se ha obtenido este número?

  3. En la línea 6 se utiliza la orden .space seguida de un número. ¿Qué significa este número? (puedes probar a modificarlo y ver qué efecto tiene en el listado que muestra el compilador).

  4. A la vista del resultado de la traducción de la línea 3, ¿cuál es el código ASCII en hexadecimal que representa el símbolo “=”?

  5. ¿Cuál es la codificación en hexadecimal de la instrucción push %eax?

  6. La instrucción push seguida de un registro se codifica con 8 bits. El procesador dispone de ocho registros con nombres %eax, %ebx, %ecx, %edx, %esi, %edi, %ebp y %esp. Deducir con esta información qué bits del byte que codifica la instrucción son los que más probablemente se utilicen para codificar el registro (se permite modificar el código).

  7. ¿Por qué crees que la codificación de las instrucciones en las líneas 20 y 21, a pesar de ser ambas del tipo push tiene diferente tamaño?

  8. La instrucción add %eax, result se codifica con 6 bytes. El procesador dispone de ocho registros con nombres %eax, %ebx, %ecx, %edx, %esi, %edi, %ebp y %esp. Deducir con esta información qué bits de los seis bytes que codifican la instrucción son los que más probablemente se utilicen para codificar el registro (se permite modificar el código).

  9. La instrucción add $8, %esp se codifica con tres bytes. ¿Cuántos bits se utilizan para codificar la constante que aparece como primer operando de la suma?

  10. Explica qué sucede con la codificación de la instrucción anterior si la constante del primer operando es mayor que 127.

  11. Para definir un string, además de la directiva .asciz que se utiliza en la línea 3, el ensamblador también permite la directiva .ascii. ¿En qué se diferencian?

  12. En la línea 6 se utiliza la directiva .space. Esta directiva permite que en lugar de ir seguida de un número, vaya seguida de dos números separados por comas. ¿Cuál es el efecto del segundo número?

  13. ¿Cuál es la longitud en bytes de la codificación de los datos del programa?

  14. ¿Cuál es la longitud en bytes de la codificación del código del programa?

  15. En las líneas finales del listado se incluye una sección con título DEFINED SYMBOLS. Esta sección consta de cinco líneas que comienzan por el nombre del fichero seguido de dos puntos, seguido de un número, luego aparece la palabra .data o .text, a continuación un número, y termina con un nombre. ¿Qué crees que significa el número que aparece en penúltimo lugar?

  16. La representación en lenguaje máquina de la instrucción mov valor1, %eax debe contener en su interior un conjunto de bits que codifique el símbolo valor1. ¿Qué bits de su codificación se utilizan para esto? (se permite modificar el código y se sugiere consultar la información de la sección DEFINED SYMBOLS al final del listado).

  17. La última sección del listado lleva por título UNDEFINED SYMBOLS. ¿Por qué crees que esta sección incluye el símbolo printf?

3. Envío de resultados al depósito central de Subversion

En este ejercicio vamos a enviar las respuestas del ejercicio anterior al depósito central de ficheros con la herramienta subversion. El fichero Respuestas.txt que se ha creado en el directorio de trabajo de esta práctica contiene una plantilla para responder a las 17 preguntas del apartado anterior. Puedes editar este fichero e incluir las respuestas a tus preguntas. Modifica este fichero (no hace falta que incluyas todas las respuestas) y manda la versión modificada al depósito mediante el comando svn commit -m 'Mensaje aclaratorio'. El mensaje aclaratorio es para explicar qué cambios has hecho y se suele poner para facilitar la búsqueda de una versión anterior de tu trabajo y para comunicar al resto de usuarios de los cambios que has introducido.

Tras ejecutar el comando, y utilizando el navegador, vete a la URL de tu grupo en el depósito central y cerciórate de que los cambios que has mandado a subversion están en el fichero del depósito. A continuación realiza un segundo cambio y esta vez ejecuta el comando svn commit pero sin ninguna opción -m. Verás que el programa abre un editor para que introduzcas un comentario. Toda operación de commit debe incluir un comentario, ya sea en la propia línea del comando mediante la opción -m o introduciendo un texto en el editor que se abre cuando esta opción no está presente. Es muy recomendable (y práctica extendida en equipos de trabajo industriales) enviar los cambios al depósito, al menos, cada vez que se termina una sesión de trabajo.


© Universidad Carlos III de Madrid Creative Commons License
Material bajo licencia Creative Commons

Última modificación: