Bucles en Ensamblador x86
SOLUCIONARIO
Bucles en Ensamblador x86
2025-08-29
Por
Occam's Razor

    .intel_syntax noprefix

    .global _start
    .extern exit
    .extern printf
    .extern puts

    .text
_start:
    // Bucle de inicialización de array
    xor  rcx, rcx      # RCX = 0
init :
    cmp  rcx, 10       # Si RCX == 10 FIN
    jge  print1
    
    mov  rdx, rcx
    add  rdx, 10
    mov  [a + rcx*4], rdx # a[RCX] = RCX + 10
    inc  rcx
    jmp  init
    
    // Bucle de impresion
print1: 
    xor r12, r12 

    // Llamadas a libC modifican registros
    // rcx, r8, r9 son modificados por printf
    // En este caso usamos r12 
print1_0:
    cmp  r12, 10
    jge  print2
    
    lea  rdi, fmt2
    mov  rsi, r12
    mov  rdx, [a + r12 * 4]
    call printf
    
    inc  r12
    jmp  print1_0
    // ------------------------------------
    

print2:
    lea  rdi, str0
    call puts

    mov dword ptr [i], 0 # Contador en memoria
    
print2_0:
    cmp dword ptr [i], 10
    jge bucle
    
    lea rdi, fmt2
    mov esi, dword ptr [i]     
    mov rdx, [a + esi * 4]
    call printf
    
    incw [i]
    jmp print2_0
    // ------------------------------------
    
bucle:
    lea  rdi, str0
    call puts
    
    mov  dword ptr [i], 10

bucle0:
    dec  dword ptr [i]
    
    lea  rdi, fmt3
    mov  esi, dword ptr [i]     
    mov  rdx, [a + esi * 4]
    call printf

    // i <= 4?
    cmp  dword ptr [i], 4
    jle  bucle0_1

    // i > 4
    lea rdi, [str1 + 3]
    call puts
    jmp continue

bucle0_1:   
    
    lea  rdi, str1
    call puts
    
    cmp  dword ptr [i], 1
    je   fin   # break

continue:   
    // while (i > 0)
    cmp dword ptr [i], 0
    jg  bucle0
    jmp fin
    

fin:    
    call exit

.section .rodata
fmt1:   .asciz "%s\n"
fmt2:   .asciz "%d -> %d\n"
fmt3:   .asciz "%d -> %d"
str0:   .asciz "---------------------------"
str1:   .asciz " **"
    

msg:        .asciz "Bucles ASM"
    
.data
i:  .int 0
a:  .fill 10, 4, 2

Como ya os imaginaréis cuando programamos en ensamblador no tenemos nada parecido a un bucle for o un bucle while, aunque podemos implementar cualquier tipo de bucle simplemente utilizando saltos condicionales y a continuación veremos como. Ya sabemos que un bucle for o while se puede implementar de la siguiente forma, utilizando una especie de pseudo-ensamblador ideado para la ocasión:

contador = valor_inicial;
BUCLE:
      Si contador == valor_final -> Salta FINAL
      // Operaciones del bucle
      contador = contador + 1 // Actualiza el contador
      Salta BUCLE
FINAL:
// El programa continua aquí

Utilizando un registro como contador, podemos implementar cualquier bucle para la plataforma x86_64 de la siguiente forma.

      xor rcx, rcx   # rcx = 0
BUCLE:
      cmp rcx, 10
      je FINAL
      // Operaciones del bucle
      inc rcx
      jmp BUCLE
FINAL:
      // El programa continua aquí

En este ejemplo, hemos utilizado RCX como contador ya que esa era su función inicial en el 8086 original, y aún lo sigue siendo para ciertas instrucciones máquina.

¿SABÍAS QUE?

Realmente los procesadores Intel ofrecen algunas instrucciones especiales para trabajar con bucles. La primera es LOOP, la cual decrementa el registro rcx y realiza un salto condicional basada en el valor resultante. Sin embargo, esta instrucción es muy lenta y en la práctica no se utiliza. La instrucción JRCXZ realiza un salto condicional basada en el valor del registro rcx, sin embargo, en la práctica el registro rcx no se suele usar como contador, siendo común usar una posición de memoria. Al menos así lo hacen los compiladores. Finalmente Intel ofrece una serie de instrucciones de cadena que, junto con el modificador REP pueden ejecutar varias iteraciones basadas en el valor del registro rcx. Si, el registro rcx inicialmente se consideró como el registro Contador (Counter en inglés) ya que esa era la función que se esperaba hiciera. Con el paso del tiempo, esto ha ido cambiando y ahora solo conserva el nombre y estas instrucciones que raramente se utilizan (con la excepción de las de cadena).

Como podéis ver, el bucle del ejemplo anterior, contará hasta 10, y utilizando la instrucción je (Jump if Equal o Salta si Igual) para terminar el bucle.

Los procesadores Intel ofrecen un montón de instrucciones para saltar de forma condicional. Estas instrucciones utilizan los flags del procesador para permitirnos implementar las mismas condiciones que escribimos en lenguajes de alto nivel. En lo que se refiere a bucles, nos interesa ser capaces de utilizar los operadores relacionales básicos. Estás son las equivalencias.

  OPERADOR  INSTRUCCIÓN SIGNIFICADOR
  ==        jz, je      Salta si ZERO/IGUAL
  !=        jnz, jne    Salta si distinto de ZERO/IGUAL
  <         jl          Salta si MENOR
  <=        jle         Salta si MENOR o IGUAL
  >         jg          Salta si MAYOR
  >=        jgl         Salta si MAYOR o IGUAL
 

Con estas instrucciones podremos implementar la mayoría de los bucles sencillos que necesitemos en nuestros programas.

SABÍAS QUE

Los saltos jg (Greater Than) y jl (Lesser Than) se aplican a resultados con signo, mientras que ja (Above) y jb (Below) se utilizan cuando no queremos tener en cuenta el signo del resultado.

El bucle que hemos implementado más arriba es el clásico for owhile, puesto que implica que puede que no se produzca ni una sola iteración del mismo. Los bucles until o do ... while que ofrecen varios lenguajes tienen la peculiaridad de que siempre se ejecutan al menos una vez. Su implementación en ensamblador es trivial… solo tenemos que poner la comprobación de la condición al final:

      xor rcx, rcx
BUCLE:
      // Operaciones del bucle
      inc rcx
      cmp rcx, 10
      je FINAL
      jmp BUCLE
FINAL:
      // El programa continua aquí

Como podéis ver en ensamblador, si bien hay que escribir más, es bastante evidente lo que el programa hace.

SOY NOVATO

La forma en la que se llama a las funciones en Linux está definida en el ABI System V y, en concreto esa parte del ABI, depende de la arquitectura. Aquí podéis encontrar la especificación para x86_64. En ese documento, podéis encontrar una sección sobre como se debe llamar a una función. Si leéis esa parte, veréis que cuando se llama a una función, solo podemos estar seguros de que los registros rbx y r12-r15 (si bien r15 puede ser usado para otras cosas) no son modificados por la función. Por esa razón, en el segundo bucle utilizamos como contador r12. Cualquier otro registro puede ser modificado por la llamada a printf y destruir su valor y produciendo resultados indeseados (normalmente un bucle infinito).

Observad el uso de la instrucción inc que equivale al operador ++ de lenguajes como C o C++.

En programas pequeños en fácil utilizar registros como contadores en los bucles, esto hace los bucles muchísimo más rápidos y es una forma de optimizar algoritmos complejos que requieran un gran número de iteraciones. Sin embargo, en el caso general (y en el código generado por los compiladores), en lugar de un registro utilizaremos, generalmente, una variable, o una posición de memoria en una sección en el que podamos escribir. Normalmente, la sección .data es generada por defecto para todos los programas y es el lugar en el que se almacenan las variables globales. La otra opción para almacenar nuestras variables es utilizar la pila. En nuestro código de ejemplo hemos utilizado el segmento de datos.

El siguiente código declara una variable capaz de almacenar un long o si lo preferís 4 bytes. .long es una directiva del ensamblador de GNU y no tiene que ver con el tipo long del lenguaje C.

.data
    i:  .long 0

Con esta declaración, nuestro bucle tendrá una pinta como esta:

    mov dword ptr [i], 0 # Contador en memoria
    
BUCLE:
    cmp dword ptr [i], 10
    jge FINAL
    
    // Instrucciones a repetir
    
    inc dword ptr [i]
    jmp BUCLE
    // ------------------------------------
    
FINAL:

En este código debemos comentar varias cosas. La primera son esos dword ptr. Os habéis dado cuenta de cual es la peculiaridad de las instrucciones en las que tenemos que usar este modificador?. Efectivamente, en aquellas instrucciones que acceden a memoria y no especificamos de forma explicita algún registro. En ese caso, el ensamblador no puede inferir el tamaño del dato que queremos leer de memoria y por lo tanto, somos nosotros los que se lo tenemos que especificar. En nuestro ejemplo dword significa palabra doble que, una vez más, para el ensamblador de GNU es 32 bits o cuatro bytes.

¿SABÍAS QUE? En el ejemplo anterior podemos sustituir dword ptr por una w al final de las instrucciones que lo necesitan, quedando el código de la siguiente forma:

  movw [i], 0 # Contador en memoria
  
BUCLE:
  cmpw [i], 10
  jge FINAL
  
  // Instrucciones a repetir
  
  incw [i]
  jmp BUCLE
  // ------------------------------------
  
FINAL:

En ensamblador no tenemos nada parecido a un break o un continue, lo cual es lógico ya que el concepto de bucle como tal no existe (no hay instrucciones especificas para implementar un bucle). Tanto break como continue no son más que saltos a puntos concretos dentro del bucle y como tales se pueden implementar fácilmente como saltos condicionales a etiquetas colocadas estratégicamente.

El último bucle del programa de ejemplo muestra la pinta que tendrían estos constructores. La etiqueta fin justo después del bucle es la utilizada para simular un break, mientras que la etiqueta continue en la que se evalúa la condición para saber si es necesaria una nueva iteración nos permite simular la instrucción continue de otros lenguaje. En el código de ejemplo, este último bucle se ha implementado como un bucle do.. while a modo de ejemplo y por esa razón la comprobación de la condición esta al final, en lugar de al principio, sin embargo el concepto es el mismo y debemos saltar a la parte del código donde realizamos el test de terminación del bucle.

SOY NOVATO

Los procesadores Intel ofrece un modo de direccionamiento indexado que es perfecto para manejar matrices. Este modo de direccionamiento nos permite utilizar expresiones como [puntero + registro * tamaño]. El puntero es una dirección de memoria en la que queremos almacenar nuestros datos, en nuestro caso un bloque de memoria capaz de contener 10 palabras de 4 bytes, inicializadas al valor 2 (a: .fill 10, 4, 2). El tamaño es el número de bytes de cada una de las entradas en la matriz, y el registro lo podemos es el índice dentro de la matriz. Verás el uso de este modo de direccionamiento en el programa de ejemplo para acceder a la matriz.

Finalmente, implementar bucles infinitos en ensamblador es trivial, solo necesitamos utilizar un salto incondicional jmp.

infinito:
   // Esto se ejecutara infinitamente
   jmp infinito

Como podéis ver, muchos lenguajes de alto nivel conservan esta funcionalidad a través de la instrucción goto, la cual es básica cuando programamos en ensamblador.

Resumen

  • No ofrece bucles for, while o do while.
  • Ofrece el equivalente a la instrucción goto
  • No ofrece break o continue. Ambos deben implementarse con saltos condicionales.

SOBRE Occam's Razor
Somos una revista libre que intenta explicar de forma sencilla conceptos tecnológicos y científicos de los que no podemos escapar. Al final, todo es más fácil de lo que parece!

 
Tu publicidad aquí :)