Arithmetic Instructions
El segundo tipo de instrucciones básicas son las Arithmetic Instructions. Con estas instrucciones, podemos realizar varios cálculos matemáticos en datos almacenados en registros y direcciones de memoria. Estas instrucciones suelen ser procesadas por la ALU en la CPU, entre otras instrucciones. Dividiremos las instrucciones aritméticas en dos tipos: instrucciones que toman solo un operando (Unary
) e instrucciones que toman dos operandos (Binary
).
Unary Instructions
Las siguientes son las principales Unary Arithmetic Instructions (asumiremos que rax
comienza como 1
para cada instrucción):
Instruction | Description | Example |
---|---|---|
inc |
Incrementar en 1 | inc rax -> rax++ o rax += 1 -> rax = 2 |
dec |
Decrementar en 1 | dec rax -> rax-- o rax -= 1 -> rax = 0 |
Practiquemos estas instrucciones volviendo a nuestro código fib.s
. Hasta ahora, hemos inicializado rax
y rbx
con los valores iniciales 0
y 1
usando la instrucción mov
. En lugar de mover el valor inmediato de 1
a bl
, movamos 0
y luego usemos inc
para convertirlo en 1
:
global _start
section .text
_start:
mov al, 0
mov bl, 0
inc bl
Recuerda, usamos al
en lugar de rax
por eficiencia. Ahora, ensamblamos nuestro código y lo ejecutamos con gdb
:
$ ./assembler.sh fib.s -g
...SNIP...
─────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
→ 0x401005 <_start+5> mov al, 0x0
───────────────────────────────────────────────────────────────────────────────────── registers ────
$rbx : 0x0
...SNIP...
─────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
→ 0x40100a <_start+10> inc bl
───────────────────────────────────────────────────────────────────────────────────── registers ────
$rbx : 0x1
Como podemos ver, rbx
comenzó con el valor 0
, y con inc rbx
, se incrementó a 1
. La instrucción dec
es similar a inc
, pero decrementa en 1
en lugar de incrementar.
Este conocimiento será muy útil más adelante.
Binary Instructions
A continuación, tenemos las Binary Arithmetic Instructions, y las principales son: (asumiremos que tanto rax
como rbx
comienzan como 1
para cada instrucción).
Instruction | Description | Example |
---|---|---|
add |
Suma ambos operandos | add rax, rbx -> rax = 1 + 1 -> 2 |
sub |
Resta el origen del destino (i.e rax = rax - rbx ) |
sub rax, rbx -> rax = 1 - 1 -> 0 |
imul |
Multiplica ambos operandos | imul rax, rbx -> rax = 1 * 1 -> 1 |
Nota que en todas las instrucciones anteriores, el resultado siempre se almacena en el operando de destino, mientras que el operando de origen no se ve afectado.
Comencemos discutiendo la instrucción add
. Sumar dos números es el paso central para calcular una Fibonacci Sequence, ya que el número Fibonacci actual (Fn
) es la suma de los dos anteriores (Fn = Fn-1 + Fn-2
).
Entonces, agreguemos add rax, rbx
al final de nuestro código fib.s
:
global _start
section .text
_start:
mov al, 0
mov bl, 0
inc bl
add rax, rbx
Ahora, ensamblamos nuestro código y lo ejecutamos con gdb
:
$ ./assembler.sh fib.s -g
gef➤ b _start
Breakpoint 1 at 0x401000
gef➤ r
...SNIP...
─────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x401004 <_start+4> inc bl
→ 0x401006 <_start+6> add rax, rbx
───────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x1
$rbx : 0x1
Como podemos ver, después de que la instrucción se procesa, rax
es igual a 0x1 + 0x0
, que es 0x1
. Siguiendo el mismo principio, si tuviéramos otros números de Fibonacci en rax
y rbx
, obtendríamos el nuevo Fibonacci usando add
.
Tanto sub
como imul
son similares a add
, como se muestra en los ejemplos de la tabla anterior. Prueba agregar sub
e imul
al código anterior, ensamblarlo y luego ejecutarlo en gdb
para ver cómo funcionan.
Bitwise Instructions
Ahora, pasemos a las Bitwise Instructions, que son instrucciones que trabajan a nivel de bits (asumiremos que rax = 1
y rbx = 2
para cada instrucción):
Instruction | Description | Example |
---|---|---|
not |
NOT a nivel de bits (invierte todos los bits, 0->1 y 1->0) | not rax -> NOT 00000001 -> 11111110 |
and |
AND a nivel de bits (si ambos bits son 1 -> 1, si son diferentes -> 0) | and rax, rbx -> 00000001 AND 00000010 -> 00000000 |
or |
OR a nivel de bits (si cualquiera de los bits es 1 -> 1, si ambos son 0 -> 0) | or rax, rbx -> 00000001 OR 00000010 -> 00000011 |
xor |
XOR a nivel de bits (si los bits son iguales -> 0, si son diferentes -> 1) | xor rax, rbx -> 00000001 XOR 00000010 -> 00000011 |
Estas instrucciones pueden parecer confusas al principio, pero son sencillas una vez que las entendemos. Por ejemplo, not
irá a cada bit e invertirá su valor. Intenta agregar not rax
al final de nuestro código anterior, ensamblarlo y ejecutarlo en gdb
para ver cómo funciona.
La instrucción más utilizada será xor
. Esta tiene varios usos, pero un uso clave es poner cualquier valor en 0
al aplicar xor
a un registro consigo mismo:
global _start
section .text
_start:
xor rax, rax
xor rbx, rbx
inc rbx
add rax, rbx
Este código debería realizar exactamente las mismas operaciones, pero ahora de una manera más eficiente. Vamos a ensamblar nuestro código y ejecutarlo con gdb
:
$ ./assembler.sh fib.s -g
gef➤ b _start
Breakpoint 1 at 0x401000
gef➤ r
...SNIP...
─────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
→ 0x401001 <_start+1> xor eax, eax
0x401003 <_start+3> xor ebx, ebx
───────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x0
$rbx : 0x0
...SNIP...
─────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
→ 0x40100c add BYTE PTR [rax], al
───────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x1
$rbx : 0x1
Como podemos ver, al hacer xor
con nuestros registros y ellos mismos, cada uno se establece en 0
. El resto del código ejecuta las mismas operaciones que antes, por lo que terminamos con los mismos valores finales para ambos registros, rax
y rbx
.