El lenguaje ensamblador ("assembler" o assembly language en inglés, también la abreviatura asm) es un lenguaje de programación de bajo nivel. Consiste en un conjunto de mnemónicos que representan instrucciones soportadas por computadoras, microprocesadores, microcontroladores y otros circuitos integrados programables. Implementa una representación simbólica de los códigos de máquina binarios y otras constantes necesarias para programar una arquitectura de procesador. En este lenguaje, cada línea de código asm se traduce exactamente en una instrucción máquina ejecutada por el hardware subyacente (procesador o CPU).

El lenguaje ensamblador refleja directamente la arquitectura e instrucciones de una CPU, y pueden ser muy diferentes de una arquitectura de CPU a otra. Cada arquitectura de microprocesador tiene su propio set de instrucciones, y en consecuencia su propio lenguaje ensamblador ya que este se encuentra muy ligado a la estructura del hardware para el cual se programa.

A diferencia de un lenguaje de programación de alto nivel, cuando se escriben programas en lenguaje ensamblador se carece de librerías o cualquier otro tipo de ayuda. Sólo se dispone del conjunto de instrucciones provistas por el procesador (CPU) y las llamadas al sistema operativo (funciones implementadas por el sistema operativo, en este caso Linux). Por lo tanto, para programar en lenguaje ensamblador se debe tener a mano tanto el set de instrucciones destino como la tabla de syscalls del kernel.

Para el caso de las arquitecturas x86 y amd64 existen diferentes ensambladores y sintaxis dependiendo del fabricante. Hay dos principales ramas en cuanto a sintaxis se refiere: Intel y AT&T. Se diferencian principalmente en el orden de los operandos, especificación de tamaños de parámetros, valores inmediatos y direcciones efectivas. No va al caso de este artículo hacer una explicación detallada de ambas sintaxis, sino que dejo varios enlaces interesantes al final del mismo. La sintaxis Intel es la más popular y algunos ensambladores que la utilizan son NASM, YASM, FASM, MASM, TASM y GAS.

Este artículo demuestra el proceso de ensamblado y linkeado de un programa en lenguaje Yasm utilizando el ensamblador yasm y el linker ld desde línea de comandos en GNU/Linux para las arquitecturas x86 y amd64. No se trata de un tutorial de programación en lenguaje ensamblador en sí, aunque se dejan buenos tutoriales en la sección de referencias.

Yasm es una reimplementación del ensamblador NASM bajo licencia BSD. Se trata de un ensamblador modular que soporta los sets de instrucciones x86 y amd64, acepta la sintaxis NASM (Intel) y GAS (AT&T) y muchos formatos de archivo objeto compatibles con sistemas Linux, Windows e iOS (ELF32, ELF64, Mach-O, RDOFF2, COFF, Win32, Wind64). Además es capaz de generar símbolos de debug en formato STABS, DWARF 2y CodeView 8.

Para instalar Yasm en Debian y derivados, ejecutar:

# apt-get install yasm

¡Hola Mundo!

Veamos un ejemplo de programa escrito en lenguaje ensamblador. En la sección de datos se declara y reserva memoria para el string que contiene la frase "¡Hola Mundo!", mientras que en la sección de texto se define el programa:

emi@hal9000:~/yasm$ nano hola.asm
; Ejemplo de "Hola Mundo" en lenguaje ensamblador

section .text

    global _start

_start:

    ; Imprimo "Hola Mundo" por pantalla:
    mov EAX, 4          ; selecciono la llamada sys_write
    mov EBX, 1          ; escribo en stdout
    mov ECX, mensaje    ; puntero al mensaje
    mov EDX, longitud   ; longitud del mensaje
    int 0x80            ; interrupción 80h

    ; Termino el proceso con éxito
    mov EAX, 1          ; selecciono la llamada sys_exit
    mov EBX, 0          ; retorno 0 (exit status)
    int 0x80            ; llamo al sistema operativo

section .data

    mensaje db "¡Hola Mundo!",0xa
    longitud equ $ - mensaje

Este programa hace uso de la llamada al sistema sys_write del kernel Linux, escribiendo el mensaje en la salida estándar (pantalla). Luego invoca a sys_exit para terminar la ejecución. Notar como se hace un fuerte uso de las llamadas al sistema operativo Linux.

La guía x86 Assembly Guide conforma un excelente tutorial en el uso de registros; modos de direccionamiento de memoria; principales instrucciones en el set de instrucciones Intel (de datos, aritméticas, lógicas y de control); convenciones de llamada a funciones y subrutinas; uso de la pila (stack) y programación en general. La recomiendo ampliamente para comenzar a desarrollar programas en lenguaje ensamblador ya que explica todos estos conceptos de forma simple y directa.

Se observa que, como todo código fuente, el tipo de archivo es de texto plano (tipo MIME es "text/plain"):

emi@hal9000:~/yasm$ file hola.asm 
hola.asm: UTF-8 Unicode text
emi@hal9000:~/yasm$ mimetype hola.asm 
hola.asm: text/plain
emi@hal9000:~/yasm$ mimetype -d hola.asm 
hola.asm: plain text document

Para ensamblar el programa con Yasm para la arquitectura x86 (32 bit), incluyendo los símbolos de depuración, ejecutar:

emi@hal9000:~/yasm$ yasm -f elf -g stabs hola.asm 

La opción -g agrega los símbolos de depuración (debug), en este ejemplo en formato STABS. Esto es necesario si se requiere depurar el programa utilizando un debugger como ddd u otro.

Si no se desean incluir los símbolos de depuración (por ejemplo al liberar la versión distribuible del programa) ejecutar:

emi@hal9000:~/yasm$ yasm -f elf hola.asm 

El ensamblador yasm genera el archivo objeto:

emi@hal9000:~/yasm$ ll hola*
-rw-r--r-- 1 emi emi 554 Nov 21 11:18 hola.asm
-rw-r--r-- 1 emi emi 976 Nov 21 11:19 hola.o

Se trata del código binario del programa, en formato ELF32, pero sin enlazar:

emi@hal9000:~/yasm$ file hola.o
hola.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

Para enlazar ("linkear") el binario y convertirlo en ejecutable, utilizar el linker ld:

$ ld -o hola hola.o

La opción -o define el nombre del archivo ejecutable de salida. En caso de enlazar el archivo objeto en formato ELF32 desde u sistema de 64 bit (amd64), es necesario emular la arquitectura con la opción -m. Esta indica que el formato del archivo de salida debe ser para la arquitectura i386:

emi@hal9000:~/yasm$ ld -o hola -m elf_i386 hola.o

El linker genera el binario ejecutable:

emi@hal9000:~/yasm$ ll hola*
-rwxr-xr-x 1 emi emi 9064 Nov 21 11:19 hola
-rw-r--r-- 1 emi emi  554 Nov 21 11:18 hola.asm
-rw-r--r-- 1 emi emi  976 Nov 21 11:19 hola.o
emi@hal9000:~/yasm$ file hola
hola: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped

Ejecutarlo para verificar el funcionamiento:

emi@hal9000:~/yasm$ ./hola 
¡Hola Mundo!

Si se desea ensablar y linkear el mismo programa para arquitectura amd64 (64 bit), ejecutar los siguientes pasos:

emi@hal9000:~/yasm$ yasm -f elf64 hola.asm 
emi@hal9000:~/yasm$ ld -o hola hola.o
emi@hal9000:~/yasm$ file hola
hola: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

Script de compilación

Para automatizar el ensamblado y linkeo, es posible crear un pequeño script en lenguaje Bash (al que he llamado simplemente ensamblar32):

#!/bin/bash

# Si no hay parámetros de entrada, imprimir la ayuda
if [ $# -lt 1 ]
then
	echo "Uso: $0 <archivo>.asm"
	exit 1
fi

# Eliminar extensión del nombre de archivo pasado como parámetro
FILE=$(echo $1 | cut -d '.' -f 1)

# Ensamblar
yasm -f elf -g stabs $1

# Si no hay error, linkear el archivo objeto
if [ $? -eq 0 ]
then
	ld -o $FILE -m elf_i386 $FILE.o
fi

El mismo script, pero para 64 bits (ensamblar64):

#!/bin/bash

# Si no hay parámetros de entrada, imprimir la ayuda
if [ $# -lt 1 ]
then
	echo "Uso: $0 <archivo>.asm"
	exit 1
fi

# Eliminar extensión del nombre de archivo pasado como parámetro
FILE=$(echo $1 | cut -d '.' -f 1)

# Ensamblar
yasm -f elf64 -g stabs $1

# Si no hay error, linkear el archivo objeto
if [ $? -eq 0 ]
then
	ld -o $FILE $FILE.o
fi

Comparación con un lenguaje de alto nivel (C)

Los lenguajes ensambladores son utilizados en la actualidad en ambientes académicos y de investigación cuando se requiere la manipulación directa del hardware, alto rendimiento, o un uso de recursos controlado y reducido. igualmente es utilizado en el desarrollo de controladores de dispositivo (drivers) y en el desarrollo de sistemas operativos, debido a la necesidad del acceso directo a las instrucciones de la máquina para implementar secciones críticas. Muchos dispositivos programables (como los microcontroladores) aún cuentan con el ensamblador como la única manera de ser manipulados.

Al hacer un uso mínimo y eficiente de las instrucciones máquina, los programas escritos en lenguaje ensamblador resultan más eficientes y reducidos que aquellos escritos en lenguajes de alto nivel. Veamos un ejemplo comparando el mismo programa, pero escrito en lenguaje C:

emi@hal9000:~/yasm$ nano holac.c

Como es de esperarse, el código resulta mucho más reducido:

#include <stdio.h>

int main(int argc, char** argv) {
    printf("¡Hola Mundo!\n");
}

Compilar el programa con gcc:

emi@hal9000:~/yasm$ gcc -o holac -Wall holac.c

Comparar el tamaño del ejecutable en C versus la versión de ensamblador para x86 incluyendo los símbolos de debug:

emi@hal9000:~/yasm$ ll hola holac
-rwxr-xr-x 1 emi emi  9064 Nov 21 11:43 hola
-rwxr-xr-x 1 emi emi 16608 Nov 21 11:43 holac

Si se ensambla sin símbolos de debug, el tamaño se reduce aún más:

emi@hal9000:~/yasm$ yasm -f elf64 hola.asm 
emi@hal9000:~/yasm$ ld -o hola hola.o
emi@hal9000:~/yasm$ ll hola holac
-rwxr-xr-x 1 emi emi  8992 Nov 21 12:34 hola
-rwxr-xr-x 1 emi emi 16608 Nov 21 11:43 holac

Ahora, la misma comparación eliminando todos los símbolos de los ejecutables con strip:

emi@hal9000:~/yasm$ strip hola holac
emi@hal9000:~/yasm$ ll hola holac
-rwxr-xr-x 1 emi emi  8568 Nov 21 12:36 hola
-rwxr-xr-x 1 emi emi 14408 Nov 21 12:36 holac

Por último, quisiera compartir mi propia versión de la herramienta cat escrita para ensamblar con Yasm, con el objetivo de comprender la complejidad que acarrea desarrollar programas en lenguaje ensamblador.

acat.asm:

; Mi implementación del comando 'cat' en lenguaje ensamblador

%define STDIN 0
%define STDOUT 1
%define STDERR 2

%define sys_exit 1
%define sys_read 3
%define sys_write 4
%define sys_open 5

%define O_RDONLY 0
%define bfsize 256         ; tamaño del buffer a utilizar

section .data

    fdarchivo dd 0x0       ; descriptor de archivo

section .bss

    buffer resb 256        ; reservo un buffer de 256 bytes

section .text

    global _start

_start:

    ; Obtener los parámetros
    pop EAX                ; argc
    cmp EAX,1              ; argc < 2 ?
    je salirerr            ; fin
    pop EAX                ; arg[0] - nombre del programa
    pop EBX                ; arg[1] - nombre de archivo

    ; ahora EBX contiene un puntero al string que contiene el nombre
    ; de archivo pasado como parámetro


    ; Abrir el archivo
    mov EAX,sys_open       ; abrir archivo
    ;mov EBX,archivo       ; descriptor de archivo a abrir
    mov ECX,O_RDONLY       ; modo de sólo lectura
    int 0x80               ; llamada al sistema

    cmp EAX,0              ; verifico si EAX < 0 (error)
    jl salirerr            ; hubo un error

    ; ahora EAX contiene el descriptor de archivo
    mov [fdarchivo],EAX    ; guardo el descriptor de archivo en memoria

leer:

    ; comienzo a leer el archivo, de a bfsize bytes por vez
    mov EAX, sys_read      ; leer archivo
    mov EBX, [fdarchivo]   ; descriptor de archivo a leer
    mov ECX, buffer        ; buffer de lectura
    mov EDX, bfsize        ; leer bfsize bytes
    int 0x80               ; llamada al sistema

    cmp EAX,0              ; verifico si EAX < 0 (error)
    jl salirerr            ; hubo un error

    ; ahora EAX contiene la cantidad de bytes leídos
    mov EDX,EAX            ; guardo la cantidad de bytes leídos en EDX

    ; imprimo por pantalla lo leído desde el archivo
    mov EAX,sys_write      ; escribir archivo
    mov EBX,STDOUT         ; descriptor de archivo a escribir
    mov ECX,buffer         ; puntero al buffer
    ;EDX ya contiene la cantidad de bytes a imprimir
    int 0x80               ; llamada al sistema

    ; verifico si hubo error
    cmp EAX,0              ; comparo para ver si EAX < 0
    jl salirerr            ; salgo con el error cargado en EBX

    ; ahora ya imprimí por pantalla lo leído del archivo
    cmp EDX,bfsize         ; comparo la cantidad de bytes leídos con bfsize
    je leer                ; leyo bfsize? seguir leyendo, sino fin

salirok:

    ; salir
    mov EAX,sys_exit      ; salir
    mov EBX,0             ; no hubo errores
    int 0x80              ; llamada al sistema

salirerr:

    ; salir
    mov EBX,EAX           ; cargo el error que haya en EAX
    mov EAX,sys_exit      ; salir
    int 0x80              ; llamada al sistema

Este programa toma un nombre de archivo como parámetro, lo abre en modo de sólo lectura, lee su contenido de a 256 bytes por vez, al tiempo en que lo muestra por pantalla.

Referencias

Compartí este artículo