Existe um registrador especial chamado rflags.
Seus bits funcionam como flags para diversas condições.
Algumas delas estão listadas abaixo:
| nome | símbolo | bit |
|---|---|---|
| carry | CF | 0 |
| zero | ZF | 6 |
| sinal | SF | 7 |
| overflow | OF | 11 |
As flags em rflags não são modificadas diretamente.
Em vez disso, elas 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.
Ela recebe dois operandos e atualiza as flags, mas não modifica os seus operandos.
A instrução cmp subtrai o segundo operando do 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 overflow) |
| OF | há overflow na subtração com sinal |
Por padrão, o código em x86-64 é executado sequencialmente de cima para baixo.
Mas há muitas situações em que é necessário modificar esse comportamento. Por exemplo, para executar um conjunto diferente de instruções em resposta a uma condição.
Em linguagens de nível mais alto, isso costuma ser feito com abstrações como as condicionais if...else.
No entanto, elas não existem em x86-64.
Em vez disso, x86-64 fornece instruções que transferem efetivamente a execução para outro ponto do código.
Isso é chamado de branching.
Já vimos duas dessas instruções: call e ret.
Quando uma função é chamada, a execução é transferida do chamador para a função chamada. E, no retorno, a execução é transferida de volta para o chamador.
Se nenhum ret for encontrado, a execução passa direto de uma função para a seguinte.
Isso às vezes pode 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. Seu único operando é um rótulo que contém o endereço do ponto onde a execução continuará.
Considere, 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.
Isso define o valor em rax como 5 nesse ponto.
A próxima instrução é jmp end, que transfere a execução para o rótulo end.
Depois de end, a próxima instrução é ret, que transfere a execução de volta para a função chamadora.
Repare que, como add rax, 10 está localizada depois de jmp end e antes de end, ela nunca é executada.
O valor de rax quando fn retorna é 5.
A família de instruções jcc transfere a execução do programa para outro ponto apenas se uma condição específica for satisfeita. 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 ela está limpa.
O cc em jcc não é literal, mas se refere ao sufixo específico associado à flag testada.
Existem muitos sufixos, e muitos deles testam a mesma condição que outro. Alguns se referem 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 referir ao seu significado em uma 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 adicionar 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.
Elas têm a mesma sintaxe, mas com um n antes do sufixo.
Por exemplo, jnz salta quando ZF não está definida.
Da mesma forma, jnae salta quando A não é >= B (A e B interpretados como inteiros sem sinal).
Alguns sufixos são aliases para as mesmas condições.
Por exemplo, jz (sufixo z, para ZF) e je (sufixo e, para igual) saltam ambos quando ZF está definida.
Isso acontece porque, com cmp, ZF é definida quando a subtração resulta em zero, o que corresponde a dois operandos iguais.
Outros sufixos, porém, testam uma combinação de flags e não podem ser substituídos diretamente por um único sufixo de flag.
Prefira o sufixo que descreve melhor a semântica da sua comparação.
Os rótulos são visíveis em todo o arquivo-fonte, eles não são locais a uma função. Então é 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 dele.
Essa 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 esse rótulo de qualquer lugar do código usando o nome completo do rótulo, por exemplo, jmp fn1.example.
No entanto, um salto que usa a parte do rótulo a partir do ponto será feito para o rótulo dentro da função superior.
Por exemplo, .example se comporta 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, você vai implementar algumas regras do Blackjack, como a forma como o jogo é jogado e pontuado.
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, respectivamente (os coringas não são usados no jogo).
Para facilitar o trabalho com essa representação, algumas constantes são definidas no topo do arquivo; 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 embaralhados juntos para o jogo.
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 para de executar aqui e continua no rótulo a |
| jcc a | o código continua no rótulo a se a condição em cc for atendida |
Estas são as condições verificadas em um 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) |
No Blackjack, o valor de um CA é ou 1 ou 11, dependendo da mão (mais sobre isso depois).
As cartas de figura (CJ, CQ e CK) valem 10 pontos, e qualquer outra carta vale o seu valor numérico.
Defina a função value_of_card com o parâmetro card, um número que representa uma carta.
A função deve retornar 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 por enquanto.
Mais adiante, você vai implementar uma função para determinar o valor de um CA a partir de uma mão existente.
value_of_card(13)
// => 10
value_of_card(4)
// => 4
value_of_card(14)
// => 1
Defina a função higher_card com os parâmetros card_one e card_two, dois números, cada um representando uma carta.
Para fins de pontuação, os valores de um CJ, de uma CQ e de um CK são todos 10.
A função deve retornar qual carta tem o valor mais alto para a pontuação.
Se as duas cartas tiverem o mesmo valor, retorne as duas.
Um CA pode assumir vários valores, então vamos fixar o valor dele em 1 para esta tarefa.
higher_card(13, 11)
// => {13, 11}
higher_card(4, 6)
// => 6
>>> higher_card(13, 14)
// => 13
Para retornar dois inteiros de uma função, você deve usar os dois registradores rax e rdx:
returning_two_values:
mov rax, rdi
mov rdx, rsi
ret
Se apenas uma carta for retornada, rdx deve ser definido como 0.
Como mencionado antes, 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 (estourar).
Defina 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.
Sua função deve retornar qual valor, 1 ou 11, será atribuído ao próximo CA.
Lembre-se: o valor da nova mão (com o CA) precisa ser o mais alto possível sem ultrapassar 21.
Dica: se já temos um CA na mão, o valor do próximo CA seria 1.
value_of_ace(6, 13)
// => 1
value_of_ace(7, 3)
// => 11
Se um jogador recebe um CA e uma carta de dez (C10, CJ, CQ ou CK) como as duas primeiras cartas, então o jogador tem uma pontuação de 21.
Essa mão é conhecida como blackjack.
Defina 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 retornar 1 se a mão de duas cartas for um blackjack, e 0 caso contrário.
Para facilitar o trabalho com os valores, as constantes TRUE e FALSE, equivalentes a 1 e 0, respectivamente, são definidas no topo do arquivo.
Nota: O cálculo da pontuação pode ser feito de várias maneiras.
Mas, se possível, gostaríamos que você verificasse se há um CA e uma carta de 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 um CQ e um CK), o jogador pode optar por tratá-las como duas mãos separadas.
Isso é conhecido como "dividir pares".
Defina 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 retornar 1 se a mão de duas cartas puder ser dividida em duas mãos, e 0 caso contrário.
Para facilitar o trabalho com os valores, as constantes TRUE e FALSE, equivalentes a 1 e 0, respectivamente, são definidas no topo do arquivo.
can_split_pair(12, 13)
// => 1
can_split_pair(10, 14)
// => 0
Quando as duas cartas originais distribuídas somam 9, 10 ou 11 pontos, o jogador pode fazer uma aposta adicional igual à aposta original. Isso é conhecido como "dobrar a aposta".
Defina 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 retornar 1 se a mão de duas cartas permitir que o jogador "dobre a aposta", e 0 caso contrário.
Para facilitar o trabalho com os valores, as constantes TRUE e FALSE, equivalentes a 1 e 0, respectivamente, são definidas no topo do arquivo.
can_double_down(14, 9)
// => 1
can_double_down(10, 2)
// => 0
Crie sua conta no Exercism para aprender e dominar x86-64 Assembly com 22 conceitos130 exercícios e mentoria humana de verdade, tudo de graça.