.intel_syntax noprefix
.global _start
.extern exit
.extern printf
.extern puts
.text_start:
// Bucle de inicialización de arrayxor 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 impresionprint1:
xor r12, r12
// Llamadas a libC modifican registros, r8, r9 son modificados por printf
// rcxr12
// En este caso usamos 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
[i]
incw 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
<= 4?
// i cmp dword ptr [i], 4
jle bucle0_1
> 4
// i 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:
= valor_inicial;
contador :
BUCLE== valor_final -> Salta FINAL
Si contador // Operaciones del bucle
= contador + 1 // Actualiza el contador
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 bucleinc 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 registrorcx
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ónJRCXZ
realiza un salto condicional basada en el valor del registrorcx
, sin embargo, en la práctica el registrorcx
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 modificadorREP
pueden ejecutar varias iteraciones basadas en el valor del registrorcx
. Si, el registrorcx
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) yjl
(Lesser Than) se aplican a resultados con signo, mientras queja
(Above) yjb
(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 bucleinc 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
yr12-r15
(si bienr15
puede ser usado para otras cosas) no son modificados por la función. Por esa razón, en el segundo bucle utilizamos como contadorr12
. Cualquier otro registro puede ser modificado por la llamada aprintf
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 unaw
al final de las instrucciones que lo necesitan, quedando el código de la siguiente forma:[i], 0 # Contador en memoria movw BUCLE: [i], 10 cmpw jge FINAL // Instrucciones a repetir [i] incw 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 infinitamentejmp 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
odo while
. - Ofrece el equivalente a la instrucción
goto
- No ofrece
break
ocontinue
. Ambos deben implementarse con saltos condicionales.
■