Existe um registo especial chamado rflags.
Os seus bits funcionam como flags para várias condições.
Algumas delas estão listadas abaixo:
| nome | símbolo | bit |
|---|---|---|
| transporte | CF | 0 |
| zero | ZF | 6 |
| sinal | SF | 7 |
| transbordo | OF | 11 |
As flags em rflags não são modificadas diretamente.
Em vez disso, são definidas por muitas instruções diferentes.
Por exemplo, ZF é definida por muitas operações aritméticas ou bit a bit quando o resultado é zero.
Uma das instruções mais comuns usadas para testar condições é cmp.
Recebe dois operandos e atualiza as flags, mas não modifica os seus operandos.
A instrução cmp subtrai o segundo operando ao primeiro e define as flags de acordo com o resultado.
Se A é o primeiro operando e B o segundo:
| flag | definida quando |
|---|---|
| CF | A < B (sem sinal) |
| ZF | A == B |
| SF | A < B (com sinal, sem transbordo) |
| OF | transbordo na subtração com sinal |
Por predefinição, o código em x86-64 é executado sequencialmente de cima para baixo.
Mas há muitas situações em que é necessário alterar este comportamento. Por exemplo, para executar um conjunto diferente de instruções em resposta a uma condição.
Nas linguagens de alto nível, isto costuma ser feito com abstrações como as condicionais if...else.
No entanto, essas não existem em x86-64.
Em vez disso, o x86-64 fornece instruções que transferem efetivamente a execução para outro ponto do código.
Isto chama-se branching.
Já vimos duas dessas instruções: call e ret.
Quando uma função é chamada, a execução é transferida de quem a chama para a função chamada. E, ao regressar, a execução é transferida de volta para quem a chamou.
Se não for encontrado nenhum ret, a execução passa diretamente de uma função para a seguinte.
Isto pode, por vezes, ser usado para otimizar o fluxo do código.
A instrução jmp transfere incondicionalmente a execução do programa para outro ponto do código. O seu único operando é um rótulo que tem o endereço do ponto onde a execução vai continuar.
Considera, por exemplo, a seguinte função:
fn:
mov rax, 5
jmp end
add rax, 10
end:
ret
Quando fn é chamada, a execução começa em mov rax, 5.
Isto define o valor em rax como 5 nesse ponto.
A instrução seguinte é jmp end, que transfere a execução para o rótulo end.
Depois de end, a instrução seguinte é ret, que transfere a execução de volta para a função que a chamou.
Repara que, como add rax, 10 está depois de jmp end e antes de end, nunca é executada.
O valor de rax quando fn regressa é 5.
A família de instruções jcc transfere a execução do programa para outro ponto apenas se uma condição específica se verificar. Caso contrário, a execução continua sequencialmente.
Cada condição corresponde a uma ou mais flags em rflags.
Algumas variantes de jcc testam se uma flag está definida, outras testam se está limpa.
O cc em jcc não é literal, mas refere-se ao sufixo específico associado à flag testada.
Há muitos sufixos e muitos deles testam a mesma condição que outro. Alguns referem-se diretamente a uma flag, de modo que a instrução salta para um rótulo se a flag específica estiver definida:
| sufixo | salta se |
|---|---|
| z | ZF == 1 |
| c | CF == 1 |
| s | SF == 1 |
| o | OF == 1 |
Muitos outros são escolhidos para se referirem ao seu significado numa instrução cmp.
Por exemplo:
| instrução | sufixo | salta se |
|---|---|---|
| cmp A, B | e | A == B |
| cmp A, B | l | A < B (com sinal) |
| cmp A, B | b | A < B (sem sinal) |
| cmp A, B | g | A > B (com sinal) |
| cmp A, B | a | A > B (sem sinal) |
É possível acrescentar e depois de l, b, g ou a para incluir a igualdade na condição:
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 os sufixos, existem variantes que verificam o comportamento oposto.
Têm a mesma sintaxe, mas com um n antes do sufixo.
Por exemplo, jnz salta quando ZF não está definida.
Do mesmo modo, jnae salta quando A não é >= B (A e B interpretados como inteiros sem sinal).
Alguns sufixos são aliases das mesmas condições.
Por exemplo, jz (sufixo z, para ZF) e je (sufixo e, para igual) saltam ambos quando ZF está definida.
Isto acontece porque, com cmp, ZF é definida quando a subtração dá zero, o que corresponde a os dois operandos serem iguais.
Outros sufixos, no entanto, testam uma combinação de flags e não podem ser substituídos diretamente por um único sufixo de flag.
Prefere o sufixo que descreve melhor a semântica da tua comparação.
Os rótulos são visíveis em todo o ficheiro fonte; não são locais a uma função. Por isso, é impossível reutilizar o nome de um rótulo.
Para imitar o comportamento de um rótulo local, o NASM tem uma notação especial para um rótulo declarado com um ponto (.) antes do nome.
Esta notação define um rótulo que inclui implicitamente o nome do rótulo anterior sem ponto:
section .text
fn1:
...
.example: ; this is fn1.example
...
ret
fn2:
...
.example: ; this is fn2.example
...
ret
Ainda é possível saltar para este rótulo a partir de qualquer ponto do código, usando o nome completo do rótulo, por exemplo, jmp fn1.example.
No entanto, um salto que use a parte do rótulo a partir do ponto será feito para o rótulo dentro da função superior.
Por exemplo, .example comporta-se como se fosse local à função:
section .text
fn1:
...
.example:
...
jmp .example ; this jumps to fn1.example
fn2:
...
.example:
...
jmp .example ; this jumps to fn2.example
Neste exercício vais implementar algumas regras do Blackjack, como a forma como o jogo se joga e se pontua.
Neste exercício, as cartas são representadas por números; cada carta numérica é representada pelo seu valor numérico, enquanto valetes, damas, reis e ases são identificados por 11, 12, 13 e 14, respetivamente (os jokers não são usados no jogo).
Para facilitar o trabalho com esta representação, algumas constantes são definidas no topo do ficheiro; C2 a C10 são para cartas numéricas, CJ para um valete, CQ para uma dama, CK para um rei e CA para um ás.
Assume-se um baralho padrão de 52 cartas com naipes franceses, mas na maioria das versões, vários baralhos são baralhados em conjunto para jogar.
Estas são as instruções mencionadas neste conceito:
| Instrução | Descrição |
|---|---|
| cmp a, b | define as flags de acordo com a - b |
| jmp a | o código deixa de ser executado aqui e continua na etiqueta a |
| jcc a | o código continua na etiqueta a se a condição em cc se verificar |
Estas são as condições verificadas num jcc depois de um cmp a, b:
| Instrução | Salta quando |
|---|---|
| je | a == b |
| jl | a < b (com sinal) |
| jg | a > b (com sinal) |
| jb | a < b (sem sinal) |
| ja | a > b (sem sinal) |
| jle | a <= b (com sinal) |
| jge | a >= b (sem sinal) |
| jbe | a <= b (sem sinal) |
| jae | a >= b (sem sinal) |
| jne | a != b |
| jnl | !(a < b) (com sinal) |
| jng | !(a > b) (com sinal) |
| jnb | !(a < b) (sem sinal) |
| jna | !(a > b) (sem sinal) |
| jnle | !(a <= b) (com sinal) |
| jnge | !(a >= b) (com sinal) |
| jnbe | !(a <= b) (sem sinal) |
| jnae | !(a >= b) (sem sinal) |
Em Blackjack, o valor de um CA é 1 ou 11, dependendo da mão (mais sobre isto adiante).
As cartas de figura (CJ, CQ e CK) valem 10 pontos e qualquer outra carta vale o seu valor numérico.
Define a função value_of_card com o parâmetro card, um número que representa uma carta.
A função deve devolver o valor numérico da carta recebida.
Como um CA pode assumir vários valores (1 ou 11), fixamos o valor de um CA em 1 para já.
Mais tarde, vais implementar uma função para determinar o valor de um CA, dada uma mão existente.
value_of_card(13)
// => 10
value_of_card(4)
// => 4
value_of_card(14)
// => 1
Define a função higher_card com os parâmetros card_one e card_two, dois números que representam cada um uma carta.
Para efeitos de pontuação, os valores de um CJ, de uma CQ e de um CK são todos 10.
A função deve devolver qual carta tem o valor mais alto para pontuação.
Se ambas as cartas tiverem o mesmo valor, devolve as duas.
Um CA pode assumir vários valores, por isso vamos fixar o seu valor em 1 para esta tarefa.
higher_card(13, 11)
// => {13, 11}
higher_card(4, 6)
// => 6
>>> higher_card(13, 14)
// => 13
Para devolver dois inteiros de uma função, deves usar os dois registos rax e rdx:
returning_two_values:
mov rax, rdi
mov rdx, rsi
ret
Se apenas uma carta for devolvida, rdx tem de ser definido como 0.
Como mencionado anteriormente, um CA vale ou 1 ou 11 pontos, dependendo da mão.
As regras do Blackjack exigem que os valores dos CAs sejam escolhidos para maximizar a pontuação da mão, mas sem ultrapassar 21 (rebentar).
Define a função value_of_ace com os parâmetros card_one e card_two, que são dois números que representam um par de cartas já na mão antes de receber o último CA.
A tua função deve devolver que valor, 1 ou 11, será atribuído ao próximo CA.
Lembra-te: o valor da nova mão (com o CA) tem de ser o mais alto possível sem ultrapassar 21.
Dica: se já tivermos um CA na mão, então o valor para o próximo CA seria 1.
value_of_ace(6, 13)
// => 1
value_of_ace(7, 3)
// => 11
Se um jogador receber um CA e uma carta de valor dez (C10, CJ, CQ ou CK) como as suas duas primeiras cartas, então o jogador tem uma pontuação de 21.
Esta mão é conhecida como blackjack.
Define a função is_blackjack com os parâmetros card_one e card_two, que são dois números que representam um par de cartas.
A função deve devolver 1 se a mão de duas cartas for um blackjack, 0 caso contrário.
Para facilitar o trabalho com os valores, as constantes TRUE e FALSE, equivalentes a 1 e 0, respetivamente, são definidas no topo do ficheiro.
Nota: O cálculo da pontuação pode ser feito de várias formas.
Mas, se possível, gostaríamos que verificasses se há um CA e uma carta de valor dez na mão, em vez de somar os valores das cartas.
is_blackjack(14, 13)
// => 1
is_blackjack(10, 9)
// => 0
Se as duas primeiras cartas de um jogador tiverem o mesmo valor (por exemplo, a mão de dois C6s, ou a mão de uma CQ e um CK), o jogador pode optar por tratá-las como duas mãos separadas.
Isto é conhecido como «dividir pares».
Define a função can_split_pairs com os parâmetros card_one e card_two, que são dois números que representam um par de cartas.
A função deve devolver 1 se a mão de duas cartas puder ser dividida em duas mãos, 0 caso contrário.
Para facilitar o trabalho com os valores, as constantes TRUE e FALSE, equivalentes a 1 e 0, respetivamente, são definidas no topo do ficheiro.
can_split_pair(12, 13)
// => 1
can_split_pair(10, 14)
// => 0
Quando as duas cartas iniciais distribuídas totalizam 9, 10 ou 11 pontos, um jogador pode fazer uma aposta adicional igual à sua aposta original. Isto é conhecido como «dobrar a aposta».
Define a função can_double_down com os parâmetros card_one e card_two, que são dois números que representam um par de cartas.
A função deve devolver 1 se a mão de duas cartas permitir ao jogador «dobrar a aposta», 0 caso contrário.
Para facilitar o trabalho com os valores, as constantes TRUE e FALSE, equivalentes a 1 e 0, respetivamente, são definidas no topo do ficheiro.
can_double_down(14, 9)
// => 1
can_double_down(10, 2)
// => 0
Inscreve-te no Exercism para aprenderes e dominares x86-64 Assembly com 22 conceitos130 exercícios, e mentoria humana real, tudo grátis.