Flags e desvios condicionais: N/Z/C/V, bge/blt/bne
No artigo anterior a gente viu o BL salvando o retorno em LR e o BX copiando de volta. Mas como o processador decide se um desvio acontece ou não? A resposta está nos 4 bits do topo do CPSR: N, Z, C, V — as flags. Cada CMP atualiza essas flags, e cada desvio condicional (bge, blt, bne…) consulta exatamente elas. É o que este texto explica em detalhe.
O CPSR: onde as flags vivem
O CPSR (Current Program Status Register) é um registrador de 32 bits. Os 4 bits mais significativos são as flags que nos interessam:
Quando uma instrução tem o bit S = 1 (ex.: CMP, ADDS, MOVS), ela escreve esses 4 bits no CPSR. Quando S = 0 (a maioria das MOV/ADD), o CPSR não mexe.
Como o CMP atualiza as flags
O CMP é um SUBS que só atualiza flags — o resultado (a subtração) é descartado. Olhe os bytes reais do sum.elf (do artigo de carga e armazenamento):
- O
S = 1é o que faz o CMP escrever no CPSR. - Internamente, o processador calcula
r2 − r3e joga o resultado fora — mas as flags ficam.
As quatro flags, uma por uma
N — Negative
N = result[31] — é o bit de sinal do resultado. Se o resultado da operação é negativo em complemento de dois (bit 31 = 1), N = 1.
Exemplo: cmp r2, r3 com r2 = 0, r3 = 5 → resultado interno = 0 − 5 = −5 = 0xFFFFFFFB → N = 1.
Z — Zero
Z = 1 se o resultado é zero. É a flag mais intuitiva: se r2 == r3, o CMP dá zero e Z = 1.
Exemplo: cmp r2, r3 com r2 = 5, r3 = 5 → 5 − 5 = 0 → Z = 1.
C — Carry
Para subtração (CMP/SUB), o C é o inverso do borrow (convenção ARM, diferente do x86):
C = 1ser2 ≥ r3(sem borrow).C = 0ser2 < r3(borrow).
Para soma (ADD/ADC), C = 1 se houve carry para fora do bit 31 (overflow unsigned).
Exemplo: cmp r2, r3 com r2 = 0, r3 = 5 → 0 < 5 → C = 0 (borrow).
V — Overflow
V = 1 se o resultado overflowou em complemento de dois (signed overflow). Para SUB:
V = (Rn[31] != op2[31]) && (result[31] != Rn[31])Ou seja: se os sinais dos operandos são diferentes e o sinal do resultado é diferente do sinal do primeiro operando, deu overflow.
Nos exemplos com valores pequenos (0, 3, 5), V sempre é 0 — não cabe num inteiro de 32 bits com sinal.
Tabela completa: como cada desvio consulta as flags
Cada condição (bits 31..28) é uma combinação lógica de N, Z, C, V. Tabela com os mais usados:
| Cond | Mnemônico | Expressão lógica | Quando usar |
|---|---|---|---|
0000 | EQ | Z == 1 | Igual (após CMP) |
0001 | NE | Z == 0 | Diferente |
0010 | CS / HS | C == 1 | Carry set (unsigned ≥) |
0011 | CC / LO | C == 0 | Carry clear (unsigned <) |
0100 | MI | N == 1 | Menor que zero (negativo) |
0101 | PL | N == 0 | Positivo ou zero |
1010 | GE | N == V | Maior ou igual (com sinal) — nosso bge |
1011 | LT | N != V | Menor (com sinal) |
1100 | GT | Z == 0 e N == V | Maior (com sinal) |
1101 | LE | Z == 1 ou N != V | Menor ou igual (com sinal) |
1110 | AL | sempre | Executa sempre (default) |
Exemplo trabalhado: por que o laço funciona
Voltemos ao sum.elf. O laço compara r2 (índice) com r3 = 5 (tamanho). O bge done pula quando r2 ≥ r3 — ou seja, quando terminou de varrer o vetor. Vamos ver as flags para diferentes valores de r2:
Caso 1: r2 = 0, r3 = 5 (laço continua)
cmp r2, r3 @ 0 − 5 = −5 (0xFFFFFFFB)
N = 1 (bit 31 = 1, resultado negativo)
Z = 0 (resultado ≠ 0)
C = 0 (0 < 5, borrow)
V = 0 (sem overflow)
bge done @ N == V ? 1 == 0 → NÃO → continua no laçoCaso 2: r2 = 5, r3 = 5 (laço termina)
cmp r2, r3 @ 5 − 5 = 0
N = 0 (bit 31 = 0)
Z = 1 (resultado == 0)
C = 1 (5 ≥ 5, sem borrow)
V = 0 (sem overflow)
bge done @ N == V ? 0 == 0 → SIM → sai do laçoCaso 3: r2 = 3, r3 = 0 (laço continua)
cmp r2, r3 @ 3 − 0 = 3
N = 0 (bit 31 = 0)
Z = 0 (resultado ≠ 0)
C = 1 (3 ≥ 0, sem borrow)
V = 0 (sem overflow)
bge done @ N == V ? 0 == 0 → SIM → sai do laçoComeçando do zero: onde o CMP pega carona
O primeiro passo do laço é mov r2, #0 — ele começa com zero. Mas de onde vem o valor que define o resultado do CMP? Do CMP r2, r3 (comparação entre r2 e r3) e das flags que ele gera.
- Quando
r2 < r3, o CMP geraN = 1eC = 0. ObgevêN ≠ Ve não pula — o laço continua. - Quando
r2 = r3, o CMP geraZ = 1. ObgevêN = V(ambos 0) e pula — o laço termina.
Quando N ≠ V: o caso do BLT
O BLT (branch if less than) é o "inverso" do BGE: ele pula quando N ≠ V. No caso 1 (r2 = 0, r3 = 5), N = 1, V = 0, então N ≠ V → o BLT pularia. É a condição correta para "r2 é menor que r3".
Glossário: como testar cada flag
| Flag | Bit | Testar no GDB |
|---|---|---|
| N | 31 | p/t $cpsr → bit 31 |
| Z | 30 | p/t $cpsr → bit 30 |
| C | 29 | p/t $cpsr → bit 29 |
| V | 28 | p/t $cpsr → bit 28 |
O que o arm-jitter faz com isso
Quando o arm-jitter executa um CMP, ele calcula a subtração e escreve os 4 bits no CPSR do core emulado. Quando chega o BGE, ele lê o CPSR e verifica se N == V. É exatamente isso que vimos no artigo do GDB: o info registers cpsr mostrava esses bits mudando instrução a instrução. Agora você sabe por que o bit Z vira 1 quando r2 == r3 e o laço termina.
Próximo passo
Até agora só vimos instruções de 32 bits fixos. O próximo texto quebra essa regra de vez: o Thumb e o Thumb-2, onde instruções têm 16 ou 32 bits e o decodificador precisa ler o tamanho antes do opcode — e onde o BX e o bit 0 viram essenciais para trocar de modo. Até lá, tente calcular as flags manualmente para CMP r0, r1 com r0 = 7, r1 = 3 (resposta: N=0, Z=0, C=1, V=0) e verifique se o BLT pularia ou não.