Existe um registo especial chamado rflags.
Os seus bits funcionam como flags para várias condições.
Alguns deles estão listados abaixo:
| nome | símbolo | bit |
|---|---|---|
| transporte | CF | 0 |
| zero | ZF | 6 |
| sinal | SF | 7 |
| transbordo | OF | 11 |
Para uma lista completa, consulta o Manual da Intel.
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 for o primeiro operando e B o segundo:
| flag | definido 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 executa sequencialmente de cima para baixo.
Mas há muitas situações em que é necessário modificar este comportamento. Por exemplo, para executar um conjunto diferente de instruções em resposta a uma condição.
Em linguagens de alto nível, isto é normalmente 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 local do código.
A isto chama-se 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, ao retornar, a execução é transferida de volta para o chamador.
Se não for encontrado nenhum ret, a execução passa 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 contém o endereço do ponto onde a execução 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 chamadora.
Repara que, como add rax, 10 está localizada depois de jmp end e antes de end, 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 está limpa.
O cc em jcc não é literal, mas refere-se ao sufixo específico associado à flag testada.
Existem muitos sufixos e muitos deles testam a mesma condição que outro. Alguns deles 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 de forma a referir o 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 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.
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 designações alternativas para as 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 ao facto de 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 dele.
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 usa 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
Repara que um rótulo sem ponto dentro de uma função define, na prática, outra função:
section .text
fn1:
...
.example: ; this is 'fn1.example'
...
non_dotted:
...
.example: ; this is 'non_dotted.example'
...
ret