← Voltar aos artigos

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:

CPSR — os quatro bits que os desvios condicionais consultam

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):

e1520003 = cmp r2, r3
  • O S = 1 é o que faz o CMP escrever no CPSR.
  • Internamente, o processador calcula r2 − r3 e 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 = 1 se r2 ≥ r3 (sem borrow).
  • C = 0 se r2 < 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:

Desvios condicionais e suas flags
CondMnemônicoExpressão lógicaQuando usar
0000EQZ == 1Igual (após CMP)
0001NEZ == 0Diferente
0010CS / HSC == 1Carry set (unsigned ≥)
0011CC / LOC == 0Carry clear (unsigned <)
0100MIN == 1Menor que zero (negativo)
0101PLN == 0Positivo ou zero
1010GEN == VMaior ou igual (com sinal) — nosso bge
1011LTN != VMenor (com sinal)
1100GTZ == 0 e N == VMaior (com sinal)
1101LEZ == 1 ou N != VMenor ou igual (com sinal)
1110ALsempreExecuta 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ço

Caso 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ço

Caso 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ço

Começ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 gera N = 1 e C = 0. O bge vê N ≠ V e não pula — o laço continua.
  • Quando r2 = r3, o CMP gera Z = 1. O bge vê 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

Flags e como inspecioná-las
FlagBitTestar no GDB
N31p/t $cpsr → bit 31
Z30p/t $cpsr → bit 30
C29p/t $cpsr → bit 29
V28p/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.