Introducción
La contribución del ensamblador a la tecnología moderna es innegable. Desde la
aviación y la exploración espacial hasta los sistemas embebidos en dispositivos
cotidianos, el ensamblador ha sido instrumental en el desarrollo de sistemas
críticos y de alto rendimiento. Su precisión y eficiencia siguen siendo esenciales
en aplicaciones donde cada ciclo de reloj cuenta y donde el control directo del
hardware es necesario.
El lenguaje ensamblador ha sido una piedra angular en la evolución de la
informática, remontándose a los primeros días de las computadoras. En la década
de 1940, pioneros como Kathleen Booth desarrollaron los primeros lenguajes
ensambladores, proporcionando a los programadores una herramienta para escribir
instrucciones que pudieran ser interpretadas directamente por el procesador.
Desde entonces, el ensamblador ha sido esencial no sólo para maximizar el
rendimiento y la eficiencia de los sistemas, sino también para formar a
generaciones de informáticos e ingenieros. Trabajar con ensamblador ofrece una
comprensión profunda de la arquitectura de los computadores, revelando cómo
interactúan el hardware y el software a nivel fundamental, conocimiento que ha
sido la base sobre la cual se han desarrollado habilidades de programación más
avanzadas y se ha comprendido la arquitectura de los sistemas modernos.
Aunque el enfoque de la programación ha evolucionado hacia lenguajes de alto
nivel y tecnologías de abstracción, el ensamblador sigue siendo relevante.
Proporciona una herramienta invaluable para aplicaciones que requieren un
control absoluto y una optimización precisa del hardware ,y que, con el avance de
nuevas tecnologías, como la computación cuántica, es posible que surjan nuevas
variantes del ensamblador adaptadas a estos entornos.
3
1. Programas y depuración.
El lenguaje ensamblador es el vínculo directo entre el software y el hardware, por
ello, los programas escritos en este lenguaje utilizan instrucciones que
corresponden a comandos específicos que el procesador puede ejecutar. Debido a
esta relación directa, los programas en ensamblador son extremadamente
eficientes, lo cual fue crucial durante los primeros días de la computación y sigue
siendo importante en aplicaciones que requieren un control absoluto sobre el
rendimiento y los recursos del sistema.
1.1. Archivos de extensión .asm
Los archivos con extensión .asm son aquellos que contienen código fuente escrito
en lenguaje ensamblador. Como anteriormente se comentó, este lenguaje de
programación de bajo nivel se encuentra muy cerca del código máquina, lo que
significa que las instrucciones escritas en ensamblador están directamente
relacionadas con las operaciones que realiza el procesador.
Características de los archivos .asm:
● Textuales: Los archivos .asm son archivos de texto plano, lo que significa
que pueden ser creados y editados con cualquier editor de texto.
● Código máquina simbólico: El código dentro de un archivo .asm utiliza
mnemónicos (palabras clave) que representan las instrucciones del
procesador en un formato legible para los humanos.
● Dependencia de la arquitectura: El código ensamblador es específico
para una arquitectura de procesador determinada (x86, ARM, etc.). Esto
significa que un programa escrito en ensamblador para un procesador x86
no funcionará en un procesador ARM sin modificaciones.
4
1.2. Secciones de un programa.
Un archivo .asm típicamente contiene varias secciones fundamentales que
organizan el código y los datos necesarios para la ejecución del programa. Estas
secciones incluyen:
- Sección de datos (.data):
La sección .data es utilizada para definir datos que están inicializados
cuando el programa comienza a ejecutarse. Estos datos suelen incluir
constantes y cadenas de texto que el programa necesita desde el inicio, por
lo que almacenar datos en esta sección permite un acceso más rápido y
eficiente durante la ejecución del programa. En esta sección de data es
posible trabajar con:
● Cadenas de Texto: Estas se definen utilizando db (define byte) y pueden
incluir cadenas terminadas en nulo (0), que son necesarias para algunas
funciones de sistema que trabajan con cadenas. Por ejemplo:
section .data
hello_msg db 'Hello, World!', 0 ; Cadena de texto terminada en nulo
number db 10 ; Define un byte con el valor 10
● Constantes Numéricas: Se pueden definir constantes numéricas con dw
(define word), dd (define double word), etc., dependiendo del tamaño de
los datos. Por ejemplo:
num1 dw 1000 ; Define una palabra (16 bits) con el valor 1000
num2 dd 123456 ; Define una doble palabra (32 bits) con el valor
123456
5
- Sección de Variables no Inicializadas (.bss)
La sección .bss (Block Started by Symbol) se utiliza para declarar
variables que no están inicializadas al inicio del programa. Estas variables
se reservan en memoria, pero no se les asigna un valor hasta que el
programa les asigne uno durante su ejecución, por lo que esta sección es
particularmente útil para reservar espacio para buffers y otros datos
temporales.
● Reserva de Espacio en Memoria: Utiliza directivas como resb
(reserve byte), resw (reserve word), resd (reserve double word),
etc., para reservar bloques de memoria. Por ejemplo:
section .bss
buffer resb 64 ; Reserva un buffer de 64 bytes
array resw 10 ; Reserva un array de 10 palabras (16 bits c/u)
- Sección de Texto (.text)
La sección .text usualmente se declara con la directiva section .text y
contiene instrucciones de ensamblador que se traducen directamente en
código máquina. Esto quiere decir que, aquí es donde se maneja la lógica
del programa, desde operaciones aritméticas simples hasta estructuras de
control más complejas, llamadas a subrutinas y manejo de interrupciones.
Un programa en ensamblador normalmente comienza con una etiqueta que
actúa como el punto de entrada. Este punto de entrada es referenciado en
el proceso de vinculación para saber dónde debe comenzar la ejecución del
programa. Por ejemplo:
section .text
global _start ; Hace la etiqueta '_start' visible para el vinculador
6
_start:
… ; Instrucciones del programa
1.3. Depuración de Programas.
La depuración de programas es una tarea fundamental y detallada que
exige un conocimiento profundo del hardware y del propio lenguaje. En el
caso del ensamblador, esto implica examinar el código máquina generado
a partir de las instrucciones en lenguaje ensamblador para encontrar y
solucionar problemas que impidan que el programa se ejecute
correctamente o produzca resultados inesperados. Dado que este se
encuentra a un nivel muy bajo en la jerarquía de lenguajes de
programación, la depuración se realiza a menudo examinando
directamente los registros, la memoria y la ejecución paso a paso de las
instrucciones.
Los errores en el ensamblador pueden ser especialmente difíciles de
rastrear debido a la naturaleza del lenguaje. A diferencia de lenguajes de
alto nivel, donde los errores a menudo se manifiestan de manera más clara,
en ensamblador los problemas pueden surgir por diversas razones, como
un uso incorrecto de los registros, un direccionamiento de memoria
erróneo o incluso un bug en el propio hardware. Por ello, la depuración en
ensamblador requiere un pensamiento lógico y habilidades de resolución
de problemas altamente desarrolladas.
Técnicas de Depuración.
- Breakpoints (Puntos de Interrupción)
Los breakpoints son marcadores que se establecen en puntos específicos del
código donde deseas que la ejecución del programa se detenga. Esto te permite
inspeccionar el estado del programa en momentos críticos. Se pueden establecer
7
breakpoints en líneas específicas del código donde sospechas que pueden ocurrir
errores o donde quieras verificar el estado de los registros y la memoria. Ejemplo:
mov eax, 1 ; Establecer breakpoint aquí
- Ejecución Paso a Paso
La ejecución paso a paso permite avanzar una instrucción a la vez, lo que es útil
para observar cómo se modifican los registros y la memoria con cada instrucción.
● Step Into (Entrar en): Entra en una subrutina para depurar línea por línea
dentro de ella.
● Step Over (Pasar por encima): Ejecuta la subrutina como un todo y pasa
a la siguiente instrucción fuera de ella.
- Inspección de Registros y Memoria
Inspeccionar los registros y la memoria es fundamental para asegurarte de que los
datos estén siendo manejados correctamente y que contengan los valores
esperados. Ejemplo:
info registers ; Comando de GDB para ver los registros
● Volcado de Memoria: Examina áreas específicas de la memoria para
detectar posibles errores. Ejemplo:
x/20xw $esp ; Comando de GDB para ver 20 palabras en la pila
- Observación de Variables
La observación de variables implica monitorizar cambios en variables específicas
durante la ejecución del programa, lo que puede ayudar a identificar cuándo y
dónde ocurren errores inesperados.
8
GDB (GNU Debugger)
GDB es una de las herramientas más poderosas para depurar programas en
ensamblador y otros lenguajes. Proporciona una amplia gama de comandos para
establecer puntos de interrupción, examinar y modificar registros y memoria, y
ejecutar el programa instrucción por instrucción.
Características de GDB
● Una de sus principales funcionalidades es la ejecución paso a
paso, lo que permite al desarrollador ejecutar un programa
instrucción por instrucción, facilitando así el seguimiento del flujo
de control y la inspección del valor de las variables en cada etapa.
● GDB permite establecer puntos de interrupción en líneas
específicas del código, lo cual detiene la ejecución del programa
en ese punto preciso, permitiendo al desarrollador examinar a
fondo el estado del programa en ese momento. Esta capacidad,
combinada con la posibilidad de inspeccionar y modificar el valor
de las variables sobre la marcha, hace de GDB una herramienta
invaluable para encontrar y corregir errores en el código.
● Otra característica importante de GDB es su capacidad para
mostrar la secuencia de llamadas a funciones que llevó a un
determinado punto del programa, lo que se conoce como
backtrace. Esta funcionalidad es crucial para entender cómo se
llegó a un estado particular del programa y para identificar
posibles errores en la lógica del código.
● Asimismo, GDB permite visualizar el contenido de la pila de
llamadas, lo que proporciona información valiosa sobre el estado
de la ejecución del programa y ayuda a detectar problemas
relacionados con la gestión de la memoria.
● GDB tiene la capacidad de mostrar el código máquina
correspondiente al código fuente, lo que resulta especialmente útil
cuando se trabaja a nivel de ensamblador.
9
● Finalmente, GDB es altamente extensible, lo que significa que
puede ser personalizado y ampliado mediante scripts para
adaptarse a las necesidades específicas de cada desarrollador.
¿Cómo funciona?
GDB Funciona estableciendo una conexión con un programa en ejecución, lo
que permite al desarrollador interactuar con él de manera controlada a través de
una interfaz de línea de comandos.
Una vez conectado, el usuario puede iniciar el programa que desea depurar,
especificando el archivo ejecutable y cualquier argumento necesario. A
continuación, puede establecer puntos de interrupción en líneas específicas del
código, lo que provoca que la ejecución del programa se pause en esos puntos.
Esto permite al desarrollador examinar detenidamente el estado del programa en
ese momento, incluyendo el valor de las variables, el contenido de los registros
del procesador y la estructura de la pila de llamadas. Además de pausar la
ejecución, GDB permite continuar la ejecución del programa hasta el siguiente
punto de interrupción, lo que facilita el seguimiento del flujo de control.
Una de las grandes ventajas de GDB es la posibilidad de inspeccionar de
manera detallada el estado del programa, esto incluye la capacidad de examinar
el valor de cualquier variable en cualquier momento de la ejecución, así como de
explorar el contenido de la memoria y los registros del procesador. Además,
GDB permite modificar el estado del programa sobre la marcha, lo que resulta
útil para simular diferentes escenarios y probar distintas hipótesis sobre la causa
de un error. Por ejemplo, se puede cambiar el valor de una variable para ver
cómo afecta al comportamiento del programa, o se puede saltar a una línea
diferente del código para omitir una sección que se sospecha que está causando
problemas.
10
Comandos más comunes de GDB
Ejecución
● run: Inicia la ejecución del programa.
● start: Inicia el programa y se detiene en la primera línea del main.
● continue (o c): Continua la ejecución hasta el próximo breakpoint.
Breakpoints
● break [línea|función|dirección]: Establece un breakpoint en una línea
específica, función o dirección.
● info breakpoints: Muestra una lista de todos los breakpoints definidos.
● delete [n]: Elimina el breakpoint número n.
Inspección
● print [expresión]: Muestra el valor de una expresión.
● x/[n][format] [dirección]: Examina la memoria en la dirección
especificada, donde n es el número de unidades y format el formato de
salida (e.g., x/16x $esp).
● info registers: Muestra los valores actuales de todos los registros.
Depuración Paso a Paso
● step (o s): Ejecuta la siguiente instrucción de la función actual.
● next (o n): Ejecuta la siguiente instrucción, pero si es una llamada a
función, la ejecuta como un todo.
2. Directiva Tlink
Tlink es un enlazador, una herramienta fundamental en el proceso de creación de
programas a partir de código ensamblador. Una vez que el código ensamblador ha
sido convertido en código objeto (generalmente con extensión .obj), Tlink entra
en acción para combinar estos módulos objeto con otras bibliotecas y recursos,
generando así un archivo ejecutable (generalmente con extensión .exe).
11
¿Qué son las directivas Tlink?
Las directivas Tlink son comandos especiales que se incluyen en el código
ensamblador para proporcionar instrucciones al enlazador sobre cómo debe llevar
a cabo el proceso de enlace. Estas directivas influyen en la forma en que los
diferentes módulos se combinan, cómo se resuelven las referencias externas y
cómo se organiza el archivo ejecutable final.
Directivas Básicas (Recapitulación):
● MODEL: Define el modelo de memoria (pequeño, mediano, grande,
enorme) y establece los segmentos de datos y código.
● STACK: Especifica el tamaño de la pila, una región de memoria esencial
para el manejo de funciones y variables locales.
● DATA: Define la sección de datos, donde se almacenan variables y
constantes.
● CODE: Define la sección de código, donde se encuentran las
instrucciones ejecutables.
● EXTRN: Declara que un símbolo (variable o función) se define en otro
módulo.
● PUBLIC: Declara que un símbolo está definido en este módulo y puede
ser utilizado por otros.
● ASSUME: Indica al ensamblador qué segmentos de memoria se utilizarán
para los registros de segmento.
Directivas Adicionales:
● SEGMENT: Define un segmento de memoria personalizado, permitiendo
un mayor control sobre la organización de la memoria.
● ENDS: Marca el final de un segmento definido con SEGMENT.
● EQU: Define una constante.
● DB, DW, DD: Reservan espacio en la sección de datos para bytes,
palabras o doble palabras, respectivamente.
12
● PROC, ENDP: Definen el inicio y el final de un procedimiento (función).
● NEAR, FAR: Especifican el alcance de un salto o llamada a una función
(dentro o fuera del segmento actual).
● COMMON: Define una variable común, que puede ser accedida desde
diferentes módulos.
● EXTERN: Sinónimo de EXTRN.
Ejemplo de uso:
.MODEL SMALL: Define el modelo de memoria small, que limita la cantidad
de memoria disponible para el programa.
.STACK 100h: Reserva 100 hexadecimales de memoria para la pila.
.DATA, .CODE y MAIN PROC/ENDP: Definen las secciones de datos, código
y el punto de entrada del programa, respectivamente.
3. Directiva Tasm
Las directivas TASM son comandos especiales que se utilizan en el ensamblador
TASM (Turbo Assembler) para controlar la estructura y el comportamiento del
programa. Estas directivas proporcionan instrucciones al ensamblador sobre cómo
debe procesar el código fuente y generar el código objeto correspondiente.
13
Tipos de directivas TASM:
● Directivas de segmentación: Definen los segmentos de memoria
utilizados en el programa.
● Directivas de asignación de memoria: Reservan espacio en la memoria
para variables y constantes.
● Directivas de procedimiento: Definen el inicio y el final de los
procedimientos (funciones).
● Directivas de enlace: Controlan el proceso de enlace entre diferentes
módulos.
● Directivas de ensamblado: Proporcionan información al ensamblador
sobre el proceso de ensamblado.
14
Características e importancia de estas directivas Tasm
Las directivas TASM son fundamentales en la programación en ensamblador, ya
que proporcionan las herramientas necesarias para estructurar y organizar el
código de manera eficiente, además de que actúan como guías para el
ensamblador, indicando cómo debe interpretar el código fuente y generar el
código máquina correspondiente.
● En primer lugar, las directivas definen la estructura del programa al
organizar el código en segmentos. Esto permite separar las diferentes
secciones del programa, como el código, los datos y la pila, lo que facilita
la lectura, escritura y mantenimiento del código.
● También son cruciales para la gestión de la memoria. Permiten reservar
espacio en memoria para variables, constantes y estructuras de datos, así
como controlar el tamaño y la disposición de los segmentos de memoria,
lo que es esencial para garantizar que el programa tenga suficiente espacio
para ejecutarse correctamente.
● Otro aspecto importante de las directivas es su papel en el enlazado de
diferentes módulos de código. Las directivas permiten definir referencias a
variables y funciones que se encuentran en otros módulos, lo que facilita la
creación de programas modulares y reutilizables.
● Finalmente, las directivas también influyen en el comportamiento del
ensamblador durante el proceso de traducción. Por ejemplo, algunas
directivas permiten generar listados del código ensamblado, mientras que
otras controlan la optimización del código generado.
4. Turbo Debugger y Teclas de función (F2, F5, F6, F7, F8 Y F9)
Turbo Debugger fue una herramienta de depuración muy popular en su tiempo,
especialmente para programas desarrollados en ensamblador. Su interfaz de línea
de comandos y su conjunto de teclas de función proporcionaban un control
preciso sobre la ejecución del programa, permitiendo a los programadores
identificar y corregir errores en el código, gracias a su capacidad para mostrar el
15
contenido de los registros del microprocesador y la memoria, establecer puntos de
interrupción, y ejecutar el programa paso a paso. A través de múltiples ventanas,
se pueden observar diferentes aspectos del programa simultáneamente, facilitando
una depuración más eficiente y exhaustiva.
Las teclas de función (F1-F12) en Turbo Debugger tienen funciones específicas
que hacen la depuración más accesible y eficaz. La tecla F1 abre la ayuda
contextual, proporcionando información sobre el uso del debugger y sus
características, lo cual es muy útil para los nuevos usuarios que necesitan
orientación inmediata. F2 se utiliza para configurar puntos de interrupción,
permitiendo al programador detener la ejecución del programa en líneas
específicas para investigar el estado del sistema en ese punto exacto.
La tecla F3 abre el buscador de archivos, permitiendo acceder rápidamente a
archivos de código fuente o de configuración relacionados con el programa que se
está depurando. F4 se usa para la búsqueda de cadenas dentro del código,
facilitando la localización de variables, funciones o cualquier otra referencia de
interés. F5 ejecuta la siguiente instrucción del programa, avanzando paso a paso y
permitiendo al programador observar los cambios inmediatos en los registros y la
memoria.
Con F6, el usuario puede saltar sobre las llamadas a funciones sin entrar en ellas,
lo que es útil para avanzar rápidamente a través del código de una función cuando
se sabe que no es la fuente del problema. F7, en cambio, permite entrar en las
funciones para depurarlas línea por línea. F8 se utiliza para salir de la función
actual y regresar al contexto de la llamada anterior, permitiendo al programador
continuar depurando en el nivel superior.
La tecla F9 ejecuta el programa hasta el siguiente punto de interrupción o hasta
que se complete la ejecución, mientras que F10 continúa la ejecución desde el
punto actual hasta el siguiente punto de interrupción. F11 permite detener
inmediatamente la ejecución del programa, una función crítica si se detecta un
comportamiento inesperado o potencialmente dañino. Finalmente, F12 activa el
16
modo de observación, en el cual el programador puede establecer observadores
para monitorear cambios en variables específicas en tiempo real.
5. Opciones de Manejo de Turbo Debugger
● Ejecutar hasta un punto específico: Esta opción permite al programador
ejecutar el programa hasta una dirección de memoria específica, lo que es
útil para observar el comportamiento del programa en un punto particular.
● Mostrar contenido de registros y memoria: Turbo Debugger permite ver
el contenido de los registros del procesador y la memoria, lo que ayuda a
analizar variables y otros datos importantes durante la ejecución del
programa.
● Establecer puntos de interrupción: Los puntos de interrupción
(breakpoints) permiten detener la ejecución del programa en líneas
específicas, facilitando la inspección del estado del sistema en ese punto.
● Modo de observación: En este modo, se pueden establecer observadores
para monitorear cambios en variables específicas en tiempo real, lo que es
útil para detectar problemas que pueden no ser evidentes durante la
ejecución normal.
● Ventanas múltiples: Turbo Debugger permite tener varias ventanas
abiertas al mismo tiempo, cada una mostrando diferentes aspectos del
programa, como el código fuente, el desensamblado, y el contenido de la
memoria.
● Buscar y reemplazar: Esta opción permite buscar cadenas específicas
dentro del código fuente y reemplazarlas, lo que es útil para realizar
cambios rápidos y precisos.
● Depuración remota: Turbo Debugger también soporta la depuración
remota, permitiendo a los programadores depurar programas que se
ejecutan en otro sistema.
17