Hay un registro especial llamado rflags.
Sus bits actúan como banderas para diversas condiciones.
Algunas de ellas se enumeran a continuación:
| nombre | símbolo | bit |
|---|---|---|
| acarreo | CF | 0 |
| cero | ZF | 6 |
| signo | SF | 7 |
| desbordamiento | OF | 11 |
Las banderas de rflags no se modifican directamente.
En su lugar, las establecen muchas instrucciones diferentes.
Por ejemplo, ZF se activa con muchas operaciones aritméticas o bit a bit cuando el resultado es cero.
Una de las instrucciones más comunes para comprobar condiciones es cmp.
Toma dos operandos y actualiza las banderas, pero no modifica sus operandos.
La instrucción cmp resta el segundo operando al primero y establece las banderas según el resultado.
Si A es el primer operando y B el segundo:
| bandera | se activa cuando |
|---|---|
| CF | A < B (sin signo) |
| ZF | A == B |
| SF | A < B (con signo, sin desbordamiento) |
| OF | desbordamiento en una resta con signo |
Por defecto, el código en x86-64 se ejecuta secuencialmente de arriba a abajo.
Pero hay muchas situaciones en las que es necesario modificar este comportamiento. Por ejemplo, para ejecutar un conjunto diferente de instrucciones en respuesta a una condición.
En lenguajes de nivel superior, esto suele hacerse con abstracciones como los condicionales if...else.
Sin embargo, esos no existen en x86-64.
En su lugar, x86-64 proporciona instrucciones que transfieren efectivamente la ejecución a otra ubicación del código.
Esto se llama branching.
Ya hemos visto dos de esas instrucciones: call y ret.
Cuando se llama a una función, la ejecución se transfiere desde la función que hace la llamada hasta la función llamada. Y, al volver, la ejecución se transfiere de nuevo a la función que hizo la llamada.
Si no se encuentra ningún ret, la ejecución pasa directamente a la siguiente función.
Esto a veces puede usarse para optimizar el flujo del código.
La instrucción jmp transfiere incondicionalmente la ejecución del programa a otro punto del código. Su único operando es una etiqueta que contiene la dirección del punto donde continuará la ejecución.
Considera, por ejemplo, la siguiente función:
fn:
mov rax, 5
jmp end
add rax, 10
end:
ret
Cuando se llama a fn, la ejecución comienza en mov rax, 5.
Esto establece el valor de rax en 5 en ese punto.
La siguiente instrucción es jmp end, que transfiere la ejecución a la etiqueta end.
Después de end, la siguiente instrucción es ret, que transfiere la ejecución de vuelta a la función que hizo la llamada.
Fíjate en que, como add rax, 10 está situada después de jmp end y antes de end, nunca se ejecuta.
El valor de rax cuando fn termina es 5.
La familia de instrucciones jcc transfiere la ejecución del programa a otro punto solo si se cumple una condición específica. De lo contrario, la ejecución continúa secuencialmente.
Cada condición se corresponde con una o más banderas de rflags.
Algunas variantes de jcc comprueban que una bandera está activada; otras comprueban que está desactivada.
El cc de jcc no es literal, sino que hace referencia al sufijo específico asociado a la bandera que se comprueba.
Hay muchos sufijos y muchos de ellos comprueban la misma condición que otro. Algunos de ellos se refieren directamente a una bandera, de modo que la instrucción salta a una etiqueta si la bandera específica está activada:
| sufijo | salta si |
|---|---|
| z | ZF == 1 |
| c | CF == 1 |
| s | SF == 1 |
| o | OF == 1 |
Muchos otros se eligen para hacer referencia a su significado en una instrucción cmp.
Por ejemplo:
| instrucción | sufijo | salta si |
|---|---|---|
| cmp A, B | e | A == B |
| cmp A, B | l | A < B (con signo) |
| cmp A, B | b | A < B (sin signo) |
| cmp A, B | g | A > B (con signo) |
| cmp A, B | a | A > B (sin signo) |
Es posible añadir e después de l, b, g o a para incluir la igualdad en la condición:
cmp rcx, r8
jge two ; this jumps to 'two' if rcx is greater than, or equal to, r8 in a signed comparison
jbe two ; this jumps to 'two' if rcx is lesser than, or equal to, r8 in an unsigned comparison
Para todos los sufijos, hay variantes que comprueban el comportamiento opuesto.
Tienen la misma sintaxis, pero con una n antes del sufijo.
Por ejemplo, jnz salta cuando ZF no está activada.
De manera similar, jnae salta cuando A no es >= B (A y B interpretados como enteros sin signo).
Algunos sufijos son alias de las mismas condiciones.
Por ejemplo, jz (sufijo z, para ZF) y je (sufijo e, para igualdad) ambos saltan cuando ZF está activada.
Esto se debe a que, con cmp, ZF se activa cuando la resta da cero, lo que corresponde a que los dos operandos sean iguales.
Otros sufijos, sin embargo, comprueban una combinación de banderas y no se pueden sustituir directamente por un único sufijo de bandera.
Prefiere el sufijo que describa mejor la semántica de tu comparación.
Las etiquetas son visibles en todo el archivo fuente; no son locales a una función. Por lo tanto, es imposible reutilizar el nombre de una etiqueta.
Para imitar el comportamiento de una etiqueta local, NASM tiene una notación especial para una etiqueta declarada con un punto (.) delante.
Esta notación define una etiqueta que incluye implícitamente el nombre de la etiqueta anterior sin punto:
section .text
fn1:
...
.example: ; this is fn1.example
...
ret
fn2:
...
.example: ; this is fn2.example
...
ret
Todavía es posible saltar a esta etiqueta desde cualquier lugar del código usando el nombre completo de la etiqueta; por ejemplo, jmp fn1.example.
Sin embargo, un salto que use la parte de la etiqueta que comienza en el punto se hará a la etiqueta dentro de la función superior.
Por ejemplo, .example se comporta como si fuera local a la función:
section .text
fn1:
...
.example:
...
jmp .example ; this jumps to fn1.example
fn2:
...
.example:
...
jmp .example ; this jumps to fn2.example
En este ejercicio vas a implementar algunas reglas del Blackjack, como la forma en que se juega y se puntúa.
En este ejercicio, las cartas se representan con números; cada carta numérica se representa con su valor numérico, mientras que las jotas, las reinas, los reyes y los ases se identifican con 11, 12, 13 y 14, respectivamente (en el juego no se usan comodines).
Para que sea más fácil trabajar con esta representación, se definen algunas constantes al principio del archivo; de C2 a C10 son para las cartas numéricas, CJ para una jota, CQ para una reina, CK para un rey y CA para un as.
Se supone una baraja francesa estándar de 52 cartas, pero en la mayoría de las versiones se barajan varias barajas juntas para jugar.
Estas son las instrucciones mencionadas en este concepto:
| Instrucción | Descripción |
|---|---|
| cmp a, b | establece las banderas según a - b |
| jmp a | el código deja de ejecutarse aquí y continúa en la etiqueta a |
| jcc a | el código continúa en la etiqueta a si se cumple la condición en cc
|
Estas son las condiciones que se comprueban en un jcc después de un cmp a, b:
| Instrucción | Salta cuando |
|---|---|
| je | a == b |
| jl | a < b (con signo) |
| jg | a > b (con signo) |
| jb | a < b (sin signo) |
| ja | a > b (sin signo) |
| jle | a <= b (con signo) |
| jge | a >= b (sin signo) |
| jbe | a <= b (sin signo) |
| jae | a >= b (sin signo) |
| jne | a != b |
| jnl | !(a < b) (con signo) |
| jng | !(a > b) (con signo) |
| jnb | !(a < b) (sin signo) |
| jna | !(a > b) (sin signo) |
| jnle | !(a <= b) (con signo) |
| jnge | !(a >= b) (con signo) |
| jnbe | !(a <= b) (sin signo) |
| jnae | !(a >= b) (sin signo) |
En el Blackjack, el valor de un CA es 1 u 11, según la mano (hablaremos de esto más adelante).
Las figuras (CJ, CQ y CK) puntúan 10 puntos y cualquier otra carta vale su valor numérico.
Define la función value_of_card con el parámetro card, un número que representa una carta.
La función debe devolver el valor numérico de la carta que se le pasa.
Como un CA puede tomar varios valores (1 o 11), de momento fijamos el valor de un CA en 1.
Más adelante implementarás una función para determinar el valor de un CA, dada una mano existente.
value_of_card(13)
// => 10
value_of_card(4)
// => 4
value_of_card(14)
// => 1
Define la función higher_card con los parámetros card_one y card_two, dos números que representan cada uno una carta.
A la hora de puntuar, los valores de un CJ, un CQ y un CK son todos 10.
La función debe devolver qué carta tiene el valor más alto para puntuar.
Si ambas cartas tienen el mismo valor, devuelve las dos.
Un CA puede tomar varios valores, así que fijaremos su valor en 1 para esta tarea.
higher_card(13, 11)
// => {13, 11}
higher_card(4, 6)
// => 6
>>> higher_card(13, 14)
// => 13
Para devolver dos números enteros desde una función, debes usar los registros rax y rdx:
returning_two_values:
mov rax, rdi
mov rdx, rsi
ret
Si solo se devuelve una carta, hay que establecer rdx en 0.
Como se ha mencionado antes, un CA vale o bien 1 o bien 11 puntos, según la mano.
Las reglas del Blackjack exigen que los valores de los CA se elijan para maximizar la puntuación de la mano sin pasarse de 21 (lo que se conoce como «pasarse»).
Define la función value_of_ace con los parámetros card_one y card_two, que son dos números que representan un par de cartas que ya están en la mano antes de recibir el último CA.
Tu función debe devolver qué valor, 1 u 11, se asignará al próximo CA.
Recuerda: el valor de la nueva mano (con el CA) debe ser lo más alto posible sin pasarse de 21.
Pista: si ya tenemos un CA en la mano, el valor para el próximo CA sería 1.
value_of_ace(6, 13)
// => 1
value_of_ace(7, 3)
// => 11
Si a un jugador le reparten un CA y una carta de diez (C10, CJ, CQ o CK) como sus dos primeras cartas, entonces el jugador tiene una puntuación de 21.
Esta mano se conoce como blackjack.
Define la función is_blackjack con los parámetros card_one y card_two, que son dos números que representan un par de cartas.
La función debe devolver 1 si la mano de dos cartas es un blackjack, y 0 en caso contrario.
Para que sea más fácil trabajar con los valores, al principio del archivo se definen las constantes TRUE y FALSE, equivalentes a 1 y 0, respectivamente.
Nota: el cálculo de la puntuación se puede hacer de muchas maneras.
Pero, si es posible, nos gustaría que comprobaras si hay un CA y una carta de diez en la mano, en lugar de sumar los valores de las cartas.
is_blackjack(14, 13)
// => 1
is_blackjack(10, 9)
// => 0
Si las dos primeras cartas de un jugador tienen el mismo valor (por ejemplo, una mano de dos C6, o una mano de un CQ y un CK), el jugador puede optar por tratarlas como dos manos separadas.
Esto se conoce como «dividir parejas».
Define la función can_split_pairs con los parámetros card_one y card_two, que son dos números que representan un par de cartas.
La función debe devolver 1 si la mano de dos cartas se puede dividir en dos manos, y 0 en caso contrario.
Para que sea más fácil trabajar con los valores, al principio del archivo se definen las constantes TRUE y FALSE, equivalentes a 1 y 0, respectivamente.
can_split_pair(12, 13)
// => 1
can_split_pair(10, 14)
// => 0
Cuando las dos cartas originales repartidas suman 9, 10 u 11 puntos, un jugador puede hacer una apuesta adicional igual a su apuesta original. Esto se conoce como «doblar».
Define la función can_double_down con los parámetros card_one y card_two, que son dos números que representan un par de cartas.
La función debe devolver 1 si la mano de dos cartas permite al jugador «doblar», y 0 en caso contrario.
Para que sea más fácil trabajar con los valores, al principio del archivo se definen las constantes TRUE y FALSE, equivalentes a 1 y 0, respectivamente.
can_double_down(14, 9)
// => 1
can_double_down(10, 2)
// => 0
Regístrate en Exercism para aprender y dominar x86-64 Assembly con 22 conceptos130 ejercicios y mentoría humana real, todo gratis.