El lenguaje ensamblador es un lenguaje de programación de bajo nivel que tiene una correspondencia casi directa con el código máquina que ejecuta el procesador. Cada familia de procesadores tiene su propio conjunto de instrucciones y, por lo tanto, su propio lenguaje ensamblador.
Nota: Este manual se enfoca en ensamblador para arquitectura x86 (32 bits) que es una de las más comunes.
Para programar en ensamblador necesitamos:
sudo apt-get update
sudo apt-get install nasm gdb make
Descargar NASM desde nasm.us y agregarlo al PATH.
Un programa básico en ensamblador tiene la siguiente estructura:
section .data ; Sección para variables inicializadas
mensaje db 'Hola Mundo!', 0 ; Cadena terminada en null
section .bss ; Sección para variables no inicializadas
buffer resb 100 ; Reserva 100 bytes
section .text ; Sección de código
global _start ; Punto de entrada para el linker
_start:
; Código del programa aquí
; Terminar programa
mov eax, 1 ; Número de llamada al sistema para exit
mov ebx, 0 ; Código de retorno 0
int 0x80 ; Llamada al kernel
Sección | Propósito |
---|---|
.data | Contiene datos inicializados (variables con valores) |
.bss | Contiene datos no inicializados (reserva espacio) |
.text | Contiene el código ejecutable del programa |
Los registros son pequeñas ubicaciones de almacenamiento dentro del CPU. En x86 tenemos varios tipos:
Registro | Descripción |
---|---|
EAX | Acumulador - usado para operaciones aritméticas y retorno de valores |
EBX | Base - usado como puntero a datos |
ECX | Contador - usado en bucles |
EDX | Datos - extensión para EAX en operaciones grandes |
ESI | Índice fuente - puntero para operaciones con cadenas |
EDI | Índice destino - puntero para operaciones con cadenas |
ESP | Puntero de pila (Stack Pointer) |
EBP | Puntero de base (Base Pointer) |
Registro | Descripción |
---|---|
CS | Segmento de código |
DS | Segmento de datos |
ES | Segmento extra |
FS, GS | Segmentos adicionales |
SS | Segmento de pila |
Contiene banderas que indican el estado del procesador:
mov eax, 10 ; EAX = 10
mov ebx, eax ; EBX = EAX (10)
mov [var], eax ; Mueve EAX a la dirección de memoria de var
add eax, ebx ; EAX = EAX + EBX
sub eax, ecx ; EAX = EAX - ECX
inc edx ; EDX = EDX + 1
dec esi ; ESI = ESI - 1
mul ebx ; EAX = EAX * EBX (resultado en EDX:EAX)
div ecx ; EAX = EDX:EAX / ECX (cociente en EAX, resto en EDX)
and eax, ebx ; EAX = EAX AND EBX
or eax, ecx ; EAX = EAX OR ECX
xor edx, edx ; EDX = EDX XOR EDX (forma rápida de poner EDX a 0)
not eax ; EAX = NOT EAX
shl eax, 2 ; Desplazamiento a la izquierda 2 bits (multiplica por 4)
shr ebx, 1 ; Desplazamiento a la derecha 1 bit (divide entre 2)
cmp eax, ebx ; Compara EAX con EBX (establece flags)
je etiqueta ; Salta si igual (ZF=1)
jne etiqueta ; Salta si no igual (ZF=0)
jg etiqueta ; Salta si mayor (signed)
jl etiqueta ; Salta si menor (signed)
ja etiqueta ; Salta si mayor (unsigned)
jb etiqueta ; Salta si menor (unsigned)
jmp etiqueta ; Salto incondicional
La pila es una estructura LIFO (Last In, First Out) usada para:
push eax ; Guarda EAX en la pila (ESP disminuye)
pop ebx ; Saca valor de la pila a EBX (ESP aumenta)
pushad ; Guarda todos los registros de propósito general
popad ; Restaura todos los registros de propósito general
Importante: La pila crece hacia direcciones de memoria más bajas. ESP siempre apunta al último elemento agregado.
mov eax, 10
push eax ; Pila: [10]
mov eax, 20
push eax ; Pila: [20, 10]
pop ebx ; EBX = 20, Pila: [10]
pop ecx ; ECX = 10, Pila: []
Las llamadas al sistema permiten interactuar con el sistema operativo. En Linux se usan a través de la interrupción 0x80.
Número | Nombre | Descripción |
---|---|---|
1 | sys_exit | Termina el programa |
3 | sys_read | Lee de un descriptor de archivo |
4 | sys_write | Escribe a un descriptor de archivo |
5 | sys_open | Abre un archivo |
section .data
msg db 'Hola Mundo!', 0xA ; Cadena con salto de línea
len equ $ - msg ; Longitud de la cadena
section .text
global _start
_start:
; sys_write
mov eax, 4 ; Número de llamada al sistema (sys_write)
mov ebx, 1 ; Descriptor de archivo (1 = stdout)
mov ecx, msg ; Dirección de la cadena
mov edx, len ; Longitud de la cadena
int 0x80 ; Llamada al kernel
; sys_exit
mov eax, 1 ; Número de llamada al sistema (sys_exit)
mov ebx, 0 ; Código de retorno
int 0x80
En ensamblador, las funciones son bloques de código que pueden ser llamados y retornan al punto de llamada.
; Función que suma dos números
; Entrada: EAX = primer número, EBX = segundo número
; Salida: EAX = suma
sumar:
add eax, ebx ; Suma los dos números
ret ; Retorna al llamador
; Llamada a la función
mov eax, 10
mov ebx, 20
call sumar ; EAX ahora contiene 30
Es una convención común donde:
; Función que suma dos números (cdecl)
; int sumar(int a, int b)
sumar:
push ebp ; Guarda el base pointer anterior
mov ebp, esp ; Establece nuevo base pointer
mov eax, [ebp+8] ; Primer parámetro (a)
add eax, [ebp+12] ; Suma segundo parámetro (b)
pop ebp ; Restaura base pointer
ret ; Retorna (EAX contiene el resultado)
; Llamada a la función
push dword 20 ; Segundo parámetro (b)
push dword 10 ; Primer parámetro (a)
call sumar
add esp, 8 ; Limpia la pila (2 parámetros * 4 bytes cada uno)
En ensamblador, los arreglos son bloques contiguos de memoria.
section .data
arreglo dd 10, 20, 30, 40, 50 ; Arreglo de 5 enteros (32 bits cada uno)
section .text
global _start
_start:
mov eax, [arreglo] ; EAX = primer elemento (10)
mov ebx, [arreglo+4] ; EBX = segundo elemento (20)
mov ecx, [arreglo+8] ; ECX = tercer elemento (30)
; Acceso con índice (ESI)
mov esi, 3 ; Índice 3 (cuarto elemento)
mov edx, [arreglo + esi*4] ; EDX = 40 (3*4 bytes por elemento)
Las cadenas son arreglos de bytes terminados usualmente con 0 (null-terminated).
section .data
cadena db 'Ensamblador', 0 ; Cadena terminada en null
section .text
global _start
_start:
; Recorrer cadena
mov esi, cadena ; ESI apunta al inicio de la cadena
bucle:
mov al, [esi] ; AL = carácter actual
cmp al, 0 ; ¿Es el terminador null?
je fin_cadena ; Si es así, terminar
; Procesar carácter (aquí podríamos imprimirlo, etc.)
inc esi ; Mover al siguiente carácter
jmp bucle
fin_cadena:
; Continuar con el programa...
Las macros permiten definir bloques de código reutilizables que se expanden durante el ensamblado.
%macro imprimir 2 ; Macro con 2 parámetros
mov eax, 4 ; sys_write
mov ebx, 1 ; stdout
mov ecx, %1 ; Primer parámetro: cadena
mov edx, %2 ; Segundo parámetro: longitud
int 0x80
%endmacro
section .data
msg1 db 'Hola', 0xA
len1 equ $ - msg1
msg2 db 'Mundo', 0xA
len2 equ $ - msg2
section .text
global _start
_start:
imprimir msg1, len1 ; Expande a las instrucciones de la macro
imprimir msg2, len2
mov eax, 1
mov ebx, 0
int 0x80
Directiva | Descripción |
---|---|
%define | Define una constante simbólica |
%include | Incluye otro archivo fuente |
equ | Define una constante |
times | Repite una instrucción o dato |
GDB (GNU Debugger) es una herramienta esencial para depurar programas en ensamblador.
Comando | Descripción |
---|---|
break | Establece un punto de interrupción |
run | Inicia la ejecución del programa |
stepi | Ejecuta una sola instrucción (entra en llamadas) |
nexti | Ejecuta una sola instrucción (no entra en llamadas) |
info registers | Muestra el contenido de los registros |
x/ | Examina memoria (ej: x/10x $esp para ver la pila) |
Evalúa e imprime una expresión | |
quit | Sale de GDB |
# Ensamblar con información de depuración
nasm -f elf -g programa.asm -o programa.o
ld programa.o -o programa
# Iniciar GDB
gdb ./programa
# Dentro de GDB:
(gdb) break _start # Punto de interrupción al inicio
(gdb) run # Ejecutar programa
(gdb) stepi # Ejecutar instrucción por instrucción
(gdb) info registers # Ver registros
(gdb) x/10x $esp # Examinar pila
(gdb) quit # Salir
La optimización en ensamblador busca maximizar el rendimiento y minimizar el uso de recursos.
; Multiplicación lenta (usando mul)
mov eax, 10
mov ebx, 5
mul ebx ; EDX:EAX = EAX * EBX
; Multiplicación rápida (usando desplazamientos y sumas)
mov eax, 10
mov ebx, eax ; EBX = 10
shl eax, 2 ; EAX = 10*4 = 40
add eax, ebx ; EAX = 40 + 10 = 50 (10*5)
Nota: Las técnicas de optimización pueden hacer el código menos legible. Siempre hay que equilibrar entre rendimiento y mantenibilidad.
section .data
msg1 db 'Ingrese primer numero: ', 0
len1 equ $ - msg1
msg2 db 'Ingrese segundo numero: ', 0
len2 equ $ - msg2
msg3 db 'La suma es: ', 0
len3 equ $ - msg3
newline db 0xA
section .bss
num1 resb 5
num2 resb 5
res resb 5
section .text
global _start
_start:
; Pedir primer número
mov eax, 4
mov ebx, 1
mov ecx, msg1
mov edx, len1
int 0x80
; Leer primer número
mov eax, 3
mov ebx, 0
mov ecx, num1
mov edx, 5
int 0x80
; Pedir segundo número
mov eax, 4
mov ebx, 1
mov ecx, msg2
mov edx, len2
int 0x80
; Leer segundo número
mov eax, 3
mov ebx, 0
mov ecx, num2
mov edx, 5
int 0x80
; Convertir y sumar
mov eax, [num1]
sub eax, '0' ; Convertir ASCII a número
mov ebx, [num2]
sub ebx, '0'
add eax, ebx
add eax, '0' ; Convertir número a ASCII
mov [res], eax
; Mostrar resultado
mov eax, 4
mov ebx, 1
mov ecx, msg3
mov edx, len3
int 0x80
mov eax, 4
mov ebx, 1
mov ecx, res
mov edx, 1
int 0x80
; Nueva línea
mov eax, 4
mov ebx, 1
mov ecx, newline
mov edx, 1
int 0x80
; Salir
mov eax, 1
mov ebx, 0
int 0x80
section .data
msg db 'Factorial de 5 es: ', 0
len equ $ - msg
newline db 0xA
section .bss
result resb 1
section .text
global _start
; Función factorial recursiva
; Entrada: EAX = número
; Salida: EAX = factorial
factorial:
cmp eax, 1
jg calcular
mov eax, 1
ret
calcular:
push eax ; Guarda el valor actual
dec eax ; n-1
call factorial ; factorial(n-1)
pop ebx ; Recupera el valor original
mul ebx ; EAX = EAX * EBX
ret
_start:
mov eax, 5 ; Calcular factorial de 5
call factorial
; Convertir resultado a ASCII
add eax, '0'
mov [result], eax
; Mostrar mensaje
mov eax, 4
mov ebx, 1
mov ecx, msg
mov edx, len
int 0x80
; Mostrar resultado
mov eax, 4
mov ebx, 1
mov ecx, result
mov edx, 1
int 0x80
; Nueva línea
mov eax, 4
mov ebx, 1
mov ecx, newline
mov edx, 1
int 0x80
; Salir
mov eax, 1
mov ebx, 0
int 0x80
Este manual ha cubierto los conceptos fundamentales de la programación en ensamblador para arquitectura x86. Aunque el lenguaje ensamblador puede parecer intimidante al principio, su comprensión proporciona un conocimiento profundo de cómo funcionan realmente las computadoras a bajo nivel.
Para continuar aprendiendo:
Recuerda: La programación en ensamblador es un arte que requiere paciencia y práctica. ¡Sigue experimentando y aprendiendo!