Home UC3M
Home IT
Home / Docencia / I. Telecom. / Fundamentos de Ordenadores 2

Fundamentos de Ordenadores 2

Curso 2010-11

Práctica 2

Arquitectura de Ordenadores



El programa depurador o "Debugger".
El programa depurador (conocido genéricamente como "debugger") nos permite ver lo que está sucediendo en el interior nuestro programa mientras está ejecutando. Las facilidades que proporciona este programa son:
  • Ejecutar el programa en un entorno controlado por el usuario.
  • Hacer que el programa detenga su ejecución en determinadas condiciones y en lugares del código seleccionados por el usuario
  • Examinar el entorno de ejecución, registros, variables, memoria, etc.
  • Cambiar el valor del entorno de ejecución para poder ver el efecto de una corrección en nuestro programa o si un error se manifiesta o no.

El depurador que vamos a utilizar para la realización de nuestros ejercicios es gdb que se ejecuta sobre plataformas Unix. Para que un programa genérico pueda ser procesado por gdb se necesita primero advertir al compilador y "linker" para que incluyan datos adicionales en el código. En nuestro entorno, como los programas primero se traducen por el ensamblador as y luego se procesan por el "linker", ambos programas necesitan opciones especiales para generar un ejecutable que pueda ser manipulado por gdb.

En el caso del ensamblador, la opción extra que se precisa es -gstabs. Es decir, el nuevo comando para ensamblar un fichero de código será:

unix$ as -gstabs -o miprograma.o miprograma.s
Es preciso también añadir una nueva opción al "linker" para que esta información se propague al ejecutable final producido. El nuevo comando para obtener el ejecutable será:
unix$ gcc -g -o programa programa.o
Nótese que el nombre de las opciones es diferente en los dos comandos.

Una vez obtenido el fichero ejecutable con estos nuevos comandos podemos invocar al depurador con el comando:

unix$ gdb programa

Veremos que tras un mensaje de bienvenida al programa nos aparece el prompt (gdb) . El depurador es un programa que lee un fichero ejecutable y espera a que el usuario le diga mediante comandos las operaciones a hacer con el fichero. En esta sección vamos a ver los comandos más importantes para manipular la ejecución de nuestro programa. El comando info nos muestra las diferentes formas de obtener información sobre gdb desde el prompt. Para abandonar el depurador se utiliza el comando q.

El primer comando que vamos a probar es list que nos "lista" el contenido de nuestro programa. Si tecleamos ese comando en el prompt vemos como se nos muestran las diez primeras líneas de nuestro programa tal y como aparece en el fichero de texto que contine el código ensamblador.

El mecanismo más importante del depurador son los puntos de parada en nuestro programa. Un punto de parada es una marca en el código que hace que el programa cuando está ejecutándose y llega a ese punto, se pare y devuelva el control en ese preciso instante al depurador. El entorno de ejecución del programa se deja intacto al pasar el control al depurador. Es decir, el contenido de memoria, registros, y todo lo que pueda afectar al procesador se congela y se nos muestra de nuevo el prompt del depurador para que podamos introducir comandos. Podemos introducir un punto de parada en el código mediante el comando:

(gdb) b <número de línea>
El depurador tiene constancia del nombre de todas las etiquetas utilizadas en nuestro código, con lo que también podemos introducir el punto de parada en una etiqueta:
(gdb) b main
Se pueden introducir varios puntos de parada en diferentes partes del código. En todo momento, el depurador mantiene una lista con los puntos de parada, y esta lista se puede visualizar mediante el comando
(gdb) info breakpoints

Una vez que tenemos un punto de parada introducido en nuestro código instruimos al depurador para que comience a ejecutar nuestro programa mediante el comando run. Si hemos introducido un punto de parada en la etiqueta main veremos como el depurador nos vuelve a mostrar el prompt tras mostrarnos la línea de código correspondiente a la etiqueta main. En este preciso instante nuestro programa está en mitad de ejecución a punto de ejecutar la instrucción en donde está el punto de parada.

Con el comando next (abreviado con n) el programa retoma el control, ejecuta una única instrucción y devuelve el control al depurador. Veremos que de nuevo se nos muestra la línea en la que se ha quedado parado nuestro programa. Invocando repetidamente el comando next vemos como nuestro programa avanza instrucción a instrucción.

En cualquier momento de la ejecución el depurador nos permite visualizar el contenido de los registros del procesador tal y como se encuentran en ese preciso instante mediante el comando info registers.

El comando continue (abreviado con c) instruye al depurador a continuar la ejecución sin parar hasta que se llegue a otro punto de parada introducido por el usuario o se termine el programa. Si el programa termina el depurador nos devuelve el prompt (gdb). Si queremos podemos volver a ejecutar nuestro programa con el comando run.

La principal utilidad del depurador es cuando nuestro programa tiene un error de ejecución. En este caso lo podemos ejecutar paso a paso y ver exactamente el punto en el que se produce el error, así como el entorno de ejecución para detectar la causa y corregirla.

 Ejercicio 1: Uso del depurador

En este ejercicio se trabajará con un programa en ensamblador muy sencillo. Copia el siguiente código en un fichero, llamado suma.s:


.data # segmento de datos

formato:
.asciz "El resultado es: %d\n"

operando1:
.int 240

operando2:
.int -400

resultado:
.int 0

# segmento de código
.text
.global main # hace de main un símbolo global

main:
# guarda los registros que se modifican en el programa
push %eax
push %ecx
push %edx

mov operando1, %eax
sub operando2, %eax
mov %eax, resultado


# invoca a printf para imprimir el mensaje
push %eax
push $formato
call printf
add $8, %esp

# restaura los registros modificados
pop %edx
pop %ecx
pop %eax

# finaliza
ret


El programa ensamblador se puede invocar con la opción -gstabs para que el resultado pueda ser procesado por el depurador gdb. Asimismo también hay que ejecutar el "linker" en modo depuración, con la opción -g:

unix$ as -gstabs -a -o suma.o suma.s
unix$ gcc -g -o suma suma.o
Una vez obtenido el fichero ejecutable, podemos invocar el depurador:
unix$ gdb suma

Se pide: Depura el programa con gdb y sigue los pasos que se indican para responder las siguientes cuestiones:

  1. Prueba el comando list. Observa que si introduces un comando vacío, se vuelve a ejecutar el último comando introducido.
  2. Ahora vas a colocar un punto de parada en la primera instrucción del programa (comando br main). Con el comando run inicias la ejecución, que se detendrá al encontrar el punto de parada que has introducido. Observa que el depurador presenta la próxima instrucción a ejecutar.
  3. Puedes examinar el contenido de los registros (info registers). Puedes ver uno en particular, si lo prefieres, con p $eax (cambia eax por el nombre del registro que desees ver). Toma nota del valor del puntero de pila.
  4. Escribiendo el comando s (o step), ejecutas la siguiente instrucción.
  5. El comando x/12xw direccion permite examinar el contenido de una zona de memoria cuya dirección se indica, en formato hexadecimal, y formando palabras de 32 bits. En total, se verán 12 palabras a partir de dicha dirección de memoria. NOTA: si cambias "w" por "b", los datos aparecerán byte a byte.
  6. Puedes utilizar el comando anterior para examinar la pila, utilizando como dirección el contenido del registro esp. Prueba por ejemplo x/12xw $esp-16.
  7. El depurador te permite también examinar la memoria del segmento de datos. Ejecuta el comando p &formato, para obtener la dirección en memoria de la cadena de formato. Con x/16xb &formato puedes ver 16 bytes a partir de esa dirección, en hexadecimal. Prueba también los comandos x/16xc &formato y x/1xs &formato.
  8. Ejecuta el programa paso a paso hasta llegar a la instrucción call printf (para justo antes de ejecutar dicha instrucción)
  9. Pregunta a: ¿Cuántos bytes se han introducido en la pila desde el principio del programa hasta la llamada a printf?
  10. Pregunta b: Indica el contenido de los dos datos (de 32 bits) de la cima de la pila.
  11. Pregunta c: ¿Cuál es el contenido de eax?
Instrucciones mul/imul: Multiplicación de enteros sin/con signo.

La instrucción imul multiplica dos números enteros con signo, mientras que la instrucción mul realiza la multiplicación de enteros pero sin signo.

La intrucción imul contiene operandos implícitos. Utiliza registros que no aparecen en la instrucción para realizar la operación de multiplicación. El resultado de esta operación siempre se asume que es de tamaño doble al del operando especificado en la instrucción.

  • Si el multiplicador es un entero de 8 bits:
    • El multiplicando está IMPLÍCITO en AL.
    • El multiplicador es el operando proporcionado en la instrucción.
    • El resultado de 16 bits se deposita en AX.
  • Si el multiplicador es un entero de 16 bits:
    • El multiplicando está IMPLÍCITO en AX.
    • El multiplicador es el operando proporcionado en la instrucción.
    • El resultado de 32 bits se deposita en DX:AX.
  • Si el multiplicador es un entero de 32 bits:
    • El multiplicando está IMPLÍCITO en EAX.
    • El multiplicador es el operando proporcionado en la instrucción.
    • El resultado de 64 bits se deposita en EDX:EAX (tal y como se puede ver en el dibujo).

Ejemplo:

mov $0, %edx
mov $0x10000000, %eax
mov $0x00000010, %ecx
imul %ecx

Esta secuencia de instrucciones multiplican ECX * EAX (0x10000000 * 0x00000010 = 0x100000000) y deposita el resultado en EDX:EAX (EDX = 0x1; EAX = 0x0).

Consulta el manual de instrucciones para comprobar qué registros se utilizan en las otras variantes de esta instrucción.

Go Up
 Instrucción div/idiv: División de enteros sin/con signo.

La instrucción div realiza la división de enteros sin signo, calculando el cociente y el resto de la operación. La instrucción idiv calcula la división de enteros con signo.

La intrucción div (o idiv) trabaja con registros implícitos para realizar la división de enteros.

Si el divisor es un entero de 32 bits (tal y como se puede ver en el dibujo):

  • operandos:
    • dividendo: IMPLÍCITO en EDX:EAX
    • divisor : operando proporcionado en la instrucción
  • resultado:
    • cociente en EAX
    • resto en EDX

div e idiv también pueden trabajar con datos de otros tamaños. Consulta el manual de instrucciones para comprobar qué registros se utilizan en otros casos.

Go Up
Ejercicio 2: Cuadrado de un número entero

Escribir el programa square.s que en su zona de datos incluya las definiciones que se muestran en la figura 2.1.

Figura 2.1. Sección de datos del programa square.s

        .data
num: .int 523
res: .space 8
msg: .string "Resultado %08x:%08x\n"

Dado el entero de 32 bits almacenado en la etiqueta num, almacena en las posiciones de memoria referenciadas por las etiquetas res el entero de 64 bits resultante de elevarlo al cuadrado. Además, utilizando el formato definido por la etiqueta msg, imprime por pantalla los 64 bits del resultado de más significativo a menos en hexadecimal. Se debe utilizar la instrucción imul (multiplicación con signo). El resultado se debe almacenar en memoria con estructura little endian a partir de la posición de memoria res.

Con los datos definidos en la figura 2.1, el programa debe mostrar por pantalla:

Resultado 00000000:00042c79

Go Up
Ejercicio 3: Número es divisor de otro

Escribir el programa isdivider.s que en su zona de datos incluya las definiciones que se muestran en la figura 3.1

Figura 3.1. Sección de datos del programa isdivider.s

	.data
numA: .int 33245
numB: .int 5
adivb: .space 1
bdiva: .space 1
msgYes: .string "%d divide a %d. Cociente = %d, resto = %d\n"
msgNo: .string "%d no divide a %d. Cociente = %d, resto = %d\n"

Dados los enteros almacenados en las etiquetas numA y numB el programa debe comprobar si se dividen el uno al otro y viceversa. En las posiciones de memoria con etiquetas adivb y bdiva se debe almacenar el valor 1 si el número numA divide al número numB o el número numB divide al número numA respectivamente, y el valor cero en caso contrario. Además, utilizando los formatos de texto almacenados en las etiquetas msgYes y msgNo el programa debe imprimir el resultado por pantalla..

Con los datos definidos en la figura 3.1, el programa debe mostrar por pantalla:

33245 no divide a 5. Cociente = 0, resto = 5
5 divide a 33245. Cociente = 6649, resto = 0

El programa debe funcionar para cualquier valor almacenado en las posiciones numA y numB y no sólo para los dados.

Go Up
  Ejercicio 4: Caracteres ASCII. Comprobar si un carácter es mayúscula, minúscula o no alfabético.

El ordenador utiliza el código ASCII para representar los caracteres. Este código asocia un número entre 0 y 255 a cada carácter, de modo que se utiliza un byte para su codificación. Este sistema permite comparar entre sí los caracteres como si fueran números enteros, puesto que se compara en realidad sus respectivos códigos ASCII.

Escribe el programa comprobarCaracter.s que comprueba si el carácter almacenado en la posición de memoria caracter es una letra mayúscula, minúscula o no es un carácter alfabético, e imprime el mensaje correspondiente por pantalla.

Para clasificarlo, simplemente comprueba si está:

  • en el rango 'a'..'z' (minúscula)
  • en el rango 'A'..'Z' (mayúscula)
  • fuera de ambos (carácter no alfabético)
  Ejercicio 5: Caracteres ASCII. Paso a mayúsculas de una cadena de texto.

Escribe el programa mayusculas.s que recorre una cadena de texto compuesta por letras minúsculas y convierte cada carácter a mayúsculas.

Dada la siguiente definición de datos:

	.data
texto:
.asciz "minusculas"
formato:
.asciz "%s\n"
El programa debe presentar la cadena de caracteres texto almacenada en memoria, convertirla a mayúsculas y presentar el resultado de nuevo por pantalla. La cadena se debe recorrer utilizando el modo de direccionamiento DESPLAZAMIENTO + INDICE.
Recuerda que el final de una cadena de caracteres se marca con un byte nulo ('\0').

Sugerencia: Observa que para pasar de minúsculas a mayúsculas simplemente hay que restar 32 al código ASCII del carácter (también se puede realizar con una máscara)


Localización | Personal | Docencia | Investigación | Novedades | Intranet
inicio | mapa del web | contacta

Last Revision: 10/14/2008 16:32:33