Existe um registrador especial chamado rflags.
Seus bits funcionam como flags para várias condições.
Algumas delas estão listadas abaixo:
| nome | símbolo | bit |
|---|---|---|
| carry | CF | 0 |
| zero | ZF | 6 |
| sign | SF | 7 |
| overflow | OF | 11 |
Para uma lista completa, consulte o Manual da Intel.
As flags em rflags não são modificadas diretamente.
Em vez disso, elas são definidas por muitas instruções diferentes.
Por exemplo, a 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 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 | 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 mudar 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 geralmente é feito com abstrações como as condicionais if...else.
No entanto, elas não existem em x86-64.
Em vez disso, o x86-64 fornece instruções que, na prática, transferem a execução para outro ponto do código.
Isso se chama branching.
Já vimos duas dessas instruções: call e ret.
Quando uma função é chamada, a execução é transferida de quem chamou para a função chamada. E, no retorno, a execução é transferida de volta para quem chamou.
Se nenhum ret for encontrado, a execução passa direto de uma função para a próxima.
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 em que a execução vai 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 naquele 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 que chamou.
Repare que, como add rax, 10 está 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.
Do mesmo modo, jnae salta quando A não é >= B (A e B interpretados como inteiros sem sinal).
Alguns sufixos são apelidos 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, a ZF é definida quando a subtração resulta em zero, o que corresponde a os dois operandos serem iguais.
Outros sufixos, porém, testam uma combinação de flags e não podem ser substituídos diretamente por um sufixo de flag única.
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. 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.
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 mais externa.
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
Repare que um rótulo sem ponto dentro de uma função, na prática, define outra função:
section .text
fn1:
...
.example: ; this is 'fn1.example'
...
non_dotted:
...
.example: ; this is 'non_dotted.example'
...
ret