0% encontró este documento útil (0 votos)
466 vistas20 páginas

Plantilla para Programas en Ensamblador

Este documento explica el proceso de traducción de código ensamblador a lenguaje máquina. Describe cómo un programa ensamblador traduce instrucciones como "mov %eax, %ebx" a una secuencia de ceros y unos que puede entender la CPU. También explica cómo compilar un programa ensamblador, visualizar el código objeto resultante, y depurar programas ensamblador.

Cargado por

txitxo0
Derechos de autor
© Attribution Non-Commercial (BY-NC)
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
466 vistas20 páginas

Plantilla para Programas en Ensamblador

Este documento explica el proceso de traducción de código ensamblador a lenguaje máquina. Describe cómo un programa ensamblador traduce instrucciones como "mov %eax, %ebx" a una secuencia de ceros y unos que puede entender la CPU. También explica cómo compilar un programa ensamblador, visualizar el código objeto resultante, y depurar programas ensamblador.

Cargado por

txitxo0
Derechos de autor
© Attribution Non-Commercial (BY-NC)
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd

Práctica 1: Traducción de ensamblador a lenguaje máquina

Tabla de contenidos

1. El programa ensamblador
2. Traducción de lenguaje ensamblador a lenguaje máquina
3. La función printf
4. Distancia entre dos etiquetas
5. Volcado de valores de registros por pantalla
6. El depurador
6.1. Arranque y parada del depurador
6.2. Visualización de código
6.3. Ejecución controlada de un programa
6.4. Visualización de datos
6.5. Ejercicios
7. Adivinar el valor de un string
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 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

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.
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. La función printf
La función printf, de la biblioteca de funciones auxiliares, incorpora el código necesario para imprimir
números enteros (en decimal, hexadecimal u octal), números en coma flotante, cadenas de texto, etc. Para ello
tan sólo se necesita especificar los datos a imprimir y su formato.

El primer parámetro que se pasa se llama cadena de formato. Esta cadena contiene el mensaje a escribir y,
además, permite indicar el formato en el que se deben imprimir ciertos parámetros adicionales. Por ejemplo,
si se invoca con los tres parámetros "%s tiene %d años\n", "María" y 18 escribe “María tiene 18
años”. En la cadena de formato, el símbolo %s (de string) indica que el parámetro siguiente a la cadena es una
cadena de texto (su dirección en memoria). Análogamente, %d (de decimal) en la cadena de formato indica
que en ese lugar se debe imprimir en formato decimal el siguiente parámetro que es un número. La propia
rutina se encarga de transformar el número en la cadena de texto pertinente. El símbolo “\n” al final de la
cadena de formato representa el carácter de fin de línea. En caso de querer imprimir el símbolo \ se debe
utilizar “\\”

Lo más importante de esta función es proporcionar tantos parámetros adicionales como símbolos de formato
se especifican en la cadena.

En la Tabla 1 se muestran los símbolos que se pueden incluir en la cadena de formato y su interpretación.

Tabla 1. Símbolos utilizados en el formato de printf


Nuevo Significado
%c Imprime el carácter ASCII correspondiente
%d, %i Conversión decimal con signo de un entero
%x, %X Conversión hexadecimal sin signo
%p Dirección de memoria (puntero)
%e, %E Conversión a coma flotante con signo en notación científica
%f Conversión a coma flotante con signo, usando punto decimal
%g, %G Conversión a coma flotante, usando la notación que requiera menor espacio
%o Conversión octal sin signo de un entero
%u Conversión decimal sin signo de un entero
%s Cadena de caracteres (terminada en '\0')
%% Imprime el símbolo %

La elección entre “x” o “X” simplemente determina si las letras que puedan aparecer deben ser minúsculas o
mayúsculas.

Para ejecutar esta función se deben depositar en la pila todos los parámetros necesarios en el orden correcto.
Justo antes de la llamada a printf (instrucción call printf), en la cima de la pila debe estar la dirección de la
cadena de formato y a continuación (en las posiciones siguientes de la pila) todos los parámetros adicionales
en el orden en que se utilizan en dicha cadena. La figura 3 ilustra cómo se han de disponer los parámetros en
la pila para llamar a la rutina e imprimir el ejemplo mencionado anteriormente.

Figura 3. Paso de parámetros a la función printf

Nota
La llamada a la función printf siempre modifica el valor de los registros %eax, %ecx y %edx, por lo que si s
conservar sus valores se deben guardar en la pila antes de la llamada y restaurarlos justo a continuación.
4. Distancia entre dos etiquetas
Las etiquetas en lenguaje ensamblador se utilizan para poder referirse a puntos concretos en la definición de
datos o de código. Si existe una etiqueta con nombre label, la dirección de memoria a la que se refiere se
obtiene mediante la expresión $label. Además, el contenido de dicha dirección de memoria se obtiene
utilizando el nombre de la etiqueta label sin prefijo.

Un programa contiene en su sección de datos la definición que se muestra en la siguiente figura.

Escribir el programa distancia.s (del que se

ofrece la estructura inicial) que almacene en las posiciones de memoria con etiquetas v1, v2 y v3 las
direcciones a las que se refieren las etiquetas ms1, ms2 y ms3. Además, en la posición de la etiqueta v2_v1
debe almacenar la distancia entre las direcciones de ms1 y ms2, y en la posición de la etiqueta v3_v2, la
distancia entre las direcciones de ms3 y ms2. Al mismo tiempo que calcula estos valores, y mediante llamadas
a la función printf, el programa debe imprimir para cada uno de los cinco resultados una línea tal y como se
muestra a continuación:

Etiqueta con valor 0x804975b


Etiqueta con valor 0x8049777
Etiqueta con valor 0x804978b
Distancia = 28
Distancia = 20

Los valores de las direcciones de memoria que se imprimen por pantalla en las tres primeras líneas no tienen
por qué coincidir exactamente. El programa sí debe imprimir idénticos valores para la distancia.

5. Volcado de valores de registros por pantalla


La subrutina printf imprime un número en hexadecimal cuando en el string de formato aparecen los
caracteres “%x”.

Dada la sección de datos que se muestra en la siguiente figura:

Escribir el

programa regdump.s (del que se ofrece la estructura inicial) que muestre el contenido en ese preciso instante
de la ejecución de los registros %eax, %ebx, %ecx y %edx por pantalla tal y como se muestra a continuación
(los valores que se imprimen seguramente sean diferentes):

Registro: 0xbf9b86c4
Registro: 0x8cdff4
Registro: 0xa617eb54
Registro: 0x1
6. El depurador
Uno de los principales problemas al escribir programas son los errores de ejecución. Compilar un programa
no es garantía suficiente de que funciona de la manera prevista. Es más, el ciclo de desarrollo de un programa
está ocupado, en su mayoría por las tareas de diagnosticar y corregir los errores de ejecución. A los errores de
ejecución en programas en inglés se les suele denominar bugs (bichos).

El origen de la utilización del término bug para describir los errors en un program es un poco confuso, pero
hay una referencia documentada a la que se le suele atribuir este mérito.

La invención del término se atribuye generalmente a la ingeniera Grace Hopper que en 1946 estaba en el
laboratorio de computación de la universidad de Harvard trabajando en los ordenadores con nombre Mark II y
Mark III. Los operadores descubrieron que la causa de un error detectado en el Mark II era una polilla que se
había quedado atrapada entre los contactos de un relé (por aquel entonces el elemento básido de un
ordenador) que a su vez era parte de la lógica interna del ordenador. Estos operadores estaban familiarizados
con el término bug e incluso pegaron el insecto en su libro de notas con la anotación “First actual case of bug
being found” (primer caso en el que realmente se encuentra un bug) tal y como ilustra la figura 4.

Figura 4. Primer caso en el que realmente se encuentra un bug First actual case of bug being found
(Fuente: U.S. Navel Historical Center Photograph).

Hoy en día, los métodos que se utilizan para depurar los errores de un programa son múltiples y con
diferentes niveles de eficacia. El método consistente en insertar líneas de código que escriben en pantalla
mensajes es quizás el más ineficiente de todos ellos. En realidad lo que se precisa es una herramienta que
permita ejecutar de forma controlada un programa, que permita suspender la ejecución en cualquier punto
para poder realizar comprobaciones, ver el contenido de las variables, etc.

Esta herramienta se conoce con el nombre de depurador o, su término inglés, debugger. El depurador es un
ejecutable cuya misión es permitir la ejecución controlada de un segundo ejecutable. Se comporta como un
envoltorio dentro del cual se desarrolla una ejecución normal de un programa, pero a la vez permite realizar
una serie de operaciones específicas para visualizar el entorno de ejecución en cualquier instante.

Más concretamente, el depurador permite:

• ejecutar un programa línea a línea


• detener la ejecución temporalmente en una línea de código concreta
• detener temporalmente la ejecución bajo determinadas condiciones
• visualizar el contenido de los datos en un determinado momento de la ejecución
• cambiar el valor del entorno de ejecución para poder ver su efecto de una corrección en el programa.

Uno de los depuradores más utilizados en entornos Linux es gdb (Debugger de GNU). En este documento se
describen los comandos más relevantes de este depurador para ser utilizados con un programa escrito en
ensamblador. Todos los ejemplos utilizados en el resto de esta sección se basan en el programa cuyo código
fuente se muestra en la Tabla 2 y que se incluye en el fichero gdbuse.s.

Tabla 2. Programa en ensamblador utilizado como ejemplo


1 .data # Comienza sección de datos
2 nums: .int 2, 3, 2, 7, 5, 4, 9
3 # Secuencia de números a imprimir
4 tamano: .int 7 # Tamaño de la secuencia
5 formato:.string "%d\n" # String para imprimir un número
6 .text # Comienza la sección de código
7 .globl main # main es un símbolo global
8 main: push %ebp # Bloque de activación
9 mov %esp, %ebp
10 push %eax # Guardar copia de los registros en la pila
11 push %ebx
12 push %ecx
13 push %edx
14 mov $0, %ebx
15 bucle: cmp %ebx, tamano
16 je termina
17 push nums(,%ebx,4) # pone el número en la pila
18 push $formato # pone el formato en la pila
19 call printf # imprime los datos que recibe
20 add $8, %esp # borra los datos de la cima de la pila
21 inc %ebx
22 jmp bucle
23 termina:pop %edx # restaurar el valor de los registros
24 pop %ecx
25 pop %ebx
26 pop %eax
27 mov %ebp, %esp # Deshacer bloque de activación
28 pop %ebp
29 ret # termina el programa

6.1. Arranque y parada del depurador

Para que un programa escrito en ensamblador pueda ser manipulado por gdb es preciso realizar una
compilación que incluya como parte del ejecutable, un conjunto de datos adicionales. Esto se consigue
incluyendo la opción -gstabs+ al invocar el compilador:

shell$ gcc -gstabs+ -o gdbuse gdbuse.s

Si el programa se ha escrito correctamente este comando ha generado el fichero ejecutable con nombre
gdbuse. Tras obtener este fichero, el depurador con el comando:

shell$ gdb gdbuse

Tras arrancar el depurador se muestra por pantalla un mensaje seguido del prompt (gdb):

shell$ gdb gdbuse


GNU gdb Red Hat Linux (6.0post-0.20040223.19rh)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu"...Using host libthread_db library
"/lib/tls/libthread_db.so.1".

(gdb)
En este instante, el programa depurador ha arrancado, pero la ejecución del programa gdbuse (que se ha
pasado como primer argumento) todavía no. La interacción con el depurador se realiza a través de comandos
introducidos a continuación del prompt, de forma similar a como se proporcionan comandos a un shell o
intérprete de comandos en Linux.

Para arrancar la ejecución del programa se utiliza el comando run (o su abreviatura r). Tras introducir este
comando, el programa se ejecuta de forma normal y se muestra por pantalla de nuevo el prompt (gdb). Por
ejemplo:

En el ejemplo, se puede comprobar como el programa termina

correctamenta (tal y como denota el mensaje que aparece por pantalla). Cuando se produce un error en la
ejecución, el depurador se detiene y muestra de nuevo el prompt.

Si se desea detener un programa mientras se está ejecutando se debe pulsar Crtl-C (la tecla control, y mientras
se mantiene pulsada, se pulsa C). La interrupción del programa es capturada por el depurador, y el control lo
retoma el intérprete de comandos de gdb. En este instante, la ejecución del programa ha sido detenida pero
no terminada. Prueba de ello, es que la ejecución puede continuarse mediante el comando continue (que se
puede abreviar simplemente con la letra c).

Para salir del depurador se utiliza el comando quit (abreviado por la letra q). Si se pretende terminar la sesión
del depurador mientras el programa está en ejecución se pide confirmación para terminar dicha ejecución.

El comando help muestra la información

referente a todos los comandos y sus opciones. Si se invoca sin parámetros, se muestran las categorías en las
que se clasifican los comandos. El comando help seguido del nombre de una categoría, proporciona
información detallada sobre sus comandos. Si se invoca seguido de un comando, describe su utilización.

6.2. Visualización de código

El código fuente del programa en ejecución se puede mostrar por pantalla mediante el comando list
(abreviado l). Sin opciones, este comando muestra la porción de código alrededor de la línea que está siendo
ejecutada en el instante en el que está detenido el programa. Si el programa no está en ejecución, se muestra
el código a partir de la etiqueta main. El comando list acepta opciones para mostrar una línea en concreto,
una línea en un fichero, una etiqueta en un fichero, e incluso el código almacenado en una dirección de
memoria completa. El comando help list muestra todas las opciones posibles.

6.3.

Ejecución controlada de un programa

Aparte de detener la ejecución de un programa con Crtl-C, lo más útil es detener la ejecución en una línea
concreta del código. Para ello es preciso insertar un punto de parada (en inglés breakpoint). Dicho punto es
una marca que almacena el depurador, y cada vez que la ejecución del programa pasa por dicho punto,
suspende la ejecución y devuelve el control al usuario. Para insertar un punto de parada se utiliza el comando
break (abreviado b) seguido de la línea en la que se desea introducir.

Se

pueden introducir tantos puntos de parada como sean necesarios en diferentes lugares del código. El
depurador asigna un número a cada uno de ellos comenzando por el 1. En la última línea del mensaje anterior
se puede ver como al punto introducido en la línea 14 del fichero gdbuse.s se le ha asignado el número 1.

El comando info breakpoints (o su abreviatura info b) muestra por pantalla la lista de puntos de parada que
contiene el depurador.
Los puntos

de parada se pueden introducir en cualquier momento de la ejecución de un proceso. Una vez introducidos, si
se comienza la ejecución del programa mediante el comando run (o su abreviatura r), ésta se detiene en
cuanto se ejecuta una línea con un punto de parada.

Nótese que el depurador primero se ha detenido en el punto

de parada 1, tras introducir el comando continue se ha detenido en el punto de parada 2.

Cada punto de parada puede ser temporalmente desactivado/activado de manera independiente. Los
comandos enable y disable seguido de un número de punto de parada activan y desactivan respectivamente
dichos puntos.

Para reanudar la ejecución del programa previamente suspendida hay tres comandos posibles. El primero que
ya se ha visto es continue (o c). Este comando continua la ejecución del programa y no se detendrá hasta que
se encuentre otro punto de parada, se termine la ejecución, o se produzca un error. El segundo comando para
continuar la ejecución es stepi (o su abreviatura si). Este comando ejecuta únicamente la instrucción en la que
está detenido el programa y vuelve de nuevo a suspender la ejecución.
Con la utilización de este comando se

puede conseguir ejecutar un programa ensamblador instrucción a instrucción de forma que se pueda ver qué
está sucediendo en los registros del procesador y en los datos en memoria. Mediante la combinación del
mecanismo de puntos de parada y el comando stepi se puede ejecutar un programa hasta un cierto punto, y a
partir de él ir instrucción a instrucción. Este proceso es fundamental para detectar los errores en los
programas.

El comando stepi tiene un inconveniente. Cuando la instrucción a ejecutar es una llamada a subrutina (por
ejemplo la instrucción call printf), el depurador ejecuta la instrucción call y se detiene en la primera
instrucción de la subrutina. Este comportamiento es deseable siempre y cuando se quiera ver el código de la
subrutina, pero si dicho código pertenece a una librería del sistema, lo que se necesita es un comando que
permita ejecutar la llamada a la subrutina entera y detenerse en la instrucción que le sigue. Esto se puede
conseguir si, al estar a punto de ejecutar una instrucción call se utiliza el comando nexti en lugar de stepi.

En general,

cuando se produce un error en un programa ensamblador, mediante la utilización de los puntos de parada se
permite llegar al programa al lugar aproximado del código en el que se supone que está el error, y luego
mediante la utilización de stepi se ejecuta instrucción a instrucción teniendo cuidado de utilizar nexti cuando
se quiera ejecutar una instrucción call que incluya la llamada entera.

6.4. Visualización de datos

Los comandos descritos hasta ahora permiten una ejecución controlada de un programa, pero cuando el
depurador es realmente eficiente es cuando hay que localizar un error de ejecución. Generalmente, ese error
se manifiesta como una terminación abrupta (por ejemplo segmentation fault). Cuando el programa se ejecuta
desde el depurador, esa terminación retorna el control al depurador con lo que es posible utilizar comandos
para inspeccionar el estado en el que ha quedado el programa.

Uno de los comandos más útiles del depurador es print (o su abreviatura p). Como argumento recibe una
expresión, y su efecto es imprimir el valor resultante de evaluar dicha expresión. Este comando puede recibir
el nombre de cualquier símbolo que esté visible en ese instante en la ejecución del programa. Por ejemplo, el
contenido de uno de estos símbolos se muestra por pantalla simplemente escribiendo el comando print
seguido del nombre.

Aparte de nombres de etiquetas que

apuntan a datos, print acepta expresiones que se refieren a los registros del procesador: $eax, $ebx, etc.

Nótese que el último comando print tiene el sufijo /x que

hace que el resultado se muestre en hexadecimal, en lugar de decimal. Si se quiere ver el contenido de todos
los registros del procesador se puede utilizar el comando info registers.
Nótese que para cada registro se muestra su valor en

hexadecimal seguido por su representación en decimal. No todos los registros que muestra este comando son
manipulables desde un programa ensamblador, tan sólo los ocho primeros.

El comando print permite igualmente visualizar arrays de valores consecutivos en memoria. Para ello es
preciso especificar en el comando el tipo de datos que contiene el array y su longitud. El formato utilizado es
incluir entre paréntesis el tipo seguido por el tamaño entre corchetes. En el programa dado como ejemplo el
comando para imprimir los siete números enteros que se definen en la etiqueta nums es:

Si lo que se necesita es visualizar los bytes

almacenados en un lugar concreto de memoria, el comando examine (o su abreviatura “x”) imprime una
determinada porción de memoria por pantalla. La sintaxis de este comando es “x/NFU dirección”. Las letras
NFU representan opciones del comando. La N representa un entero que codifica el número de unidades de
información en memoria a mostrar. La F representa el formato en el que se muestran los datos (al igual que el
comando print, la “x” quiere decir hexadecimal). La letra U representa el tamaño de las unidades a mostrar.
Sus posibles valores son “b” para bytes, “h” para palabras de 2 bytes, “w” para palabras de 4 bytes y “g” para
palabras de ocho bytes.

La dirección a partir de la cual se muestra el contenido se puede dar como una constante en hexadecimal, o
como el nombre de una etiqueta precedido del carácter “&”. Por ejemplo, para mostrar el contenido de las 7
palabras de 4 bytes almacenadas a partir de la etiqueta nums el comando es:

Este

comando se puede utilizar también para mostrar el contenido de una porción de memoria a la que apunta un
determinado registro. Por ejemplo, para mostrar las cuatro palabras de memoria almacenadas en la cima de la
pila se puede utilizar el siguiente comando:
Además de

visualizar datos en registros o memoria, el depurador permite también manipular estos datos mientras el
programa está detenido. El comando set permite la asignación de un valor numérico tanto a porciones de
memoria como a registros. Para asignar el valor 10 al registro %eax se utiliza el comando:

Este comando es útil cuando se detecta un valor erróneo en un registro y se puede

corregir para mostrar si el programa puede continuar normalmente.

Al igual que se permite modificar datos en registros, también se pueden modificar datos en memoria. Para
ello es necesario especificar el tipo de dato que se está almacenando entre llaves seguido de la dirección de
memoria. De esta forma se especifica dónde almacenar el valor que se proporciona a continuación tras el
símbolo de igual. Por ejemplo:

El comando anterior almacena el valor 4 en la posición de memoria cuya

dirección es 0x83040 y almacena 4 bytes porque se refiere a ella como un entero.

6.5. Ejercicios

Para la realización de los siguientes ejercicios se utiliza el código fuente utilizado como ejemplo, mostrado en
la Tabla 2 y contenido en el fichero gdbuse.s. Se supone que el programa ha sido compilado, el ejecutable
producido y el depurador arrancado.

1. ¿Qué comando hay que utilizar para mostrar por pantalla todos los bytes que codifican la cadena con
nombre formato?

¿Cuál es el valor del último byte?

2. Se sabe que las instrucciones del tipo push %registro se codifican mediante un único byte y mov
%registro, %registro mediante dos bytes. Utilizando únicamente el depurador, decir cuál es el
código hexadecimal de las siguientes instrucciones:
3. Situar un punto de parada en la instrucción call printf. ¿Qué comando es necesario
para mostrar por pantalla el valor que deposita en la cima de la pila la instrucción push $formato?
4. Introducir un punto de parada en la línea 10 del código (en la instrucción push %eax). Mostrar por
pantalla mediante el comando print el valor de los registros %eax, %ebx, %ecx y %edx. Apuntar estos
valores.

A continuación introducir un segundo punto de parada en la línea 14 (en la instrucción mov $0,
%ebx). Mediante el comando continue continuar la ejecución hasta ese punto.

¿Qué comando hay que utilizar para mostrar por pantalla el contenido de las cuatro palabras de
memoria que se encuentran en la cima de la pila? Comprobar que estos valores son idénticos a los
mostrados en el primer punto de parada.

5. La instrucción inc %ebx aumenta el valor de dicho registro en una unidad. Este registro contiene el
índice del siguiente elemento a imprimir. Poner un punto de parada en la instrucción siguiente a esta y
con el programa detenido modificar el valor de este registro con un número entre cero y seis (ambos
inclusive). Explica qué es lo que sucede y por qué.
6. Utilizando la ejecución instrucción a instrucción que permite el depurador, ¿qué instrucción se ejecuta
justo antes de la instrucción pop %edx?
7. La instrucción push nums(,%ebx,4) deposita un cierto valor en la pila. Introducir un punto de parada
en la siguiente instrucción, y una vez detenido el programa, poner en la cima de la pila otro número
arbitrario mediante el comando set. Explica qué efecto tiene esto y por qué.

7. Adivinar el valor de un string


La empresa ACME Programming distribuye un programa que realiza la autenticación de un alumno en base a
su NIA mediante un número de cuatro dígitos. El programa solicita primero el NIA y a continuación la clave
de cuatro dígitos que compara con una almacenada internamente. El programa está escrito en ensamblador y
debido a un despiste, el ejecutable ha sido ensamblado de tal forma que incluye la información utilizable por
el depurador.

Descargar el programa compare. La rutina que realiza la comprobación entre la clave de entrada y la
introducida por el usuario se llama compare y su código está incluido en el fichero compare.s que debe estar
presente en el mismo directorio que el ejecutable. Analizar detenidamente la ejecución del programa
utilizando el depurador y obtener la clave que permite la entrada al programa con tu NIA. Si la clave es
correcta, el programa imprime el mensaje:

==== ¡ACCESO PERMITIDO! ====


Nota
Tras descargar el programa compare en el directorio local, es preciso dotarlo de permiso para que pueda ser
mediante el comando unix chmod +x compare.

También podría gustarte