Skip to content

Niveau 1 · Chapitre 1.2

Drapeaux, cmp et sauts conditionnels

Comment un CPU décide sans if : RFLAGS, cmp comme soustraction jetée, et les sauts conditionnels qui la lisent — signés et non signés.

Un CPU n’a pas de if. Il dispose d’une routine en deux temps à partir de laquelle toute décision compilée est construite :

  1. une instruction enregistre des faits sur un résultat — est-il nul ? négatif ? y a-t-il eu dépassement ? — dans un registre appelé RFLAGS ;
  2. un saut conditionnel lit ces faits et saute ailleurs, ou continue à l’instruction suivante.

Une fois ces deux étapes maîtrisées, chaque if, while, for et switch d’un désassemblage devient lisible.

Le registre des drapeaux

RFLAGS est un registre de 64 bits, mais les bits utiles aux décisions sont six drapeaux d’état. Les instructions arithmétiques et logiques (add, sub, cmp, and, test, inc…) les écrivent en effet de bord. mov n’y touche pas.

Les manuels appellent ces bits des codes de condition, rangés dans un registre d’état souvent nommé PSW (program status word, mot d’état du programme). RFLAGS est le PSW du x86, et ZF, SF, OF et CF sont ses versions des bits classiques Z, N, V et C.

DrapeauNomVaut 1 quand…
ZFZérole résultat vaut 0
SFSignele résultat est négatif (son bit de poids fort vaut 1)
CFRetenueune opération non signée a produit une retenue ou un emprunt
OFDépassementune opération signée a débordé
PFParitél’octet bas du résultat contient un nombre pair de bits à 1
AFRetenue auxiliaireil y a eu une retenue sortant du bit 3 (utilisée pour le BCD)

ZF, SF, CF et OF sont ceux qu’utilisent les sauts conditionnels. PF et AF existent pour des raisons historiques ; on les voit rarement testés.

cmp est une soustraction dont on jette le résultat

cmp a, b calcule a − b, positionne les drapeaux d’après le résultat, puis jette le résultat. Rien d’autre que RFLAGS ne change. Toute l’astuce est là : comparer deux nombres, c’est les soustraire et regarder ce que la soustraction aurait produit.

La démo ci-dessous a déjà exécuté mov eax, 7 et cmp eax, 5. Regardez le panneau des drapeaux : 7 − 5 = 2, qui n’est ni nul, ni négatif, et n’a pas emprunté — donc les six drapeaux d’état valent 0. Rien n’est allumé, mais chacun est entouré : le cmp les a tous écrits, simplement avec des zéros. Appuyez sur Step pour exécuter les deux comparaisons suivantes.

Live · cmp : une soustraction dont on jette le résultat
program— ▸ is the next instruction
  1. mov eax, 7
  2. cmp eax, 5 ; 7 − 5 = 2
  3. cmp eax, 7 ; 7 − 7 = 0
  4. cmp eax, 9 ; 7 − 9 = −2
step 0
Loading emulator…
  • cmp eax, 7 : 7 − 7 = 0, donc ZF = 1 — les deux valeurs sont égales.
  • cmp eax, 9 : 7 − 9 = −2, donc SF = 1 (négatif) et CF = 1 (soustraire un nombre non signé plus grand provoque un emprunt).

eax reste à 7 tout du long : cmp n’écrit que les drapeaux.

Les sauts conditionnels lisent les drapeaux

Un saut conditionnel s’écrit j + un code de condition, et chaque condition est une petite formule sur les drapeaux :

SautSignificationPris quand
je / jzégalZF = 1
jne / jnzdifférentZF = 0
jlinférieur (signé)SF ≠ OF
jleinférieur ou égal (signé)ZF = 1 ou SF ≠ OF
jgsupérieur (signé)ZF = 0 et SF = OF
jgesupérieur ou égal (signé)SF = OF
jben dessous (non signé)CF = 1
jbeen dessous ou égal (non signé)CF = 1 ou ZF = 1
jaau-dessus (non signé)CF = 0 et ZF = 0
jaeau-dessus ou égal (non signé)CF = 0

Les mêmes conditions existent aussi sous forme de setcc (écrit 0 ou 1 dans un registre d’un octet) et de cmovcc (copie seulement si la condition est vraie).

Les mêmes bits, deux questions différentes

Pourquoi deux familles, jl/jg et jb/ja ? Parce que le CPU ne sait pas si vos 32 bits représentent un nombre signé ou non signé — c’est l’instruction choisie qui en décide.

Prenez 0xffffffff. En int signé, c’est −1. En unsigned int, c’est 4 294 967 295. Comparez-le à 1 et vous obtenez deux réponses différentes, toutes deux correctes :

Live · Signé ou non signé
program— ▸ is the next instruction
  1. mov eax, -1 ; 0xffffffff
  2. cmp eax, 1
  3. setl bl ; signé : -1 < 1 ?
  4. setb cl ; non signé : 4294967295 < 1 ?
step 0
Loading emulator…

Après le cmp, SF = 1, OF = 0 et CF = 0. Avancez de deux pas :

  • setl bl teste SF ≠ OF → 1 ≠ 0 → bl = 1 : en nombres signés, −1 est inférieur à 1.
  • setb cl teste CF = 1 → CF vaut 0 → cl = 0 : en nombres non signés, 4 294 967 295 n’est pas en dessous de 1.

C’est exactement ce que fait un compilateur avec vos types C : comparer deux int produit jl/jg, comparer deux unsigned produit jb/ja. En lisant un désassemblage, le saut vous dit si les variables sont signées.

Pourquoi OF existe

Si « inférieur » voulait seulement dire « la soustraction est négative », jl pourrait tester SF seul. Impossible, car une soustraction signée peut déborder et inverser le bit de signe.

Le plus petit int vaut −2 147 483 648 (0x80000000). Retirez 1 : le vrai résultat, −2 147 483 649, ne tient pas sur 32 bits et le résultat reboucle sur 0x7fffffff, un nombre positif.

Live · Dépassement signé
program— ▸ is the next instruction
  1. mov eax, 0x80000000 ; INT_MIN
  2. cmp eax, 1 ; INT_MIN − 1 reboucle sur 0x7fffffff
  3. setl bl ; INT_MIN < 1 ?
  4. sets cl ; le résultat (rebouclé) était-il négatif ?
step 0
Loading emulator…

Après le cmp, SF = 0 (le résultat rebouclé semble positif) mais OF = 1 (le résultat signé a débordé). jl et setl testent SF ≠ OF, soit 0 ≠ 1 → vrai : INT_MIN est bien inférieur à 1. sets cl, qui ne regarde que le signe, se trompe et écrit 0. OF est la correction qui rend les comparaisons signées justes même quand la soustraction déborde.

Comment un if en C devient cmp + un saut

Voici if (x > 10) return 1; else return 0; tel qu’un compilateur l’organise, avec x = 12 :

Live · if (x > 10)
program— ▸ is the next instruction
  1. mov edi, 12 ; x = 12
  2. cmp edi, 10 ; compare x à 10
  3. jle else_branch ; x <= 10 ? on saute le bloc « alors »
  4. mov eax, 1 ; alors : return 1
  5. jmp done
  6. else_branch:
  7. mov eax, 0 ; sinon : return 0
  8. done:
step 0
Loading emulator…

Remarquez que le saut est jle, l’inverse de la condition C >. Le compilateur place le bloc « alors » juste après la comparaison et saute par-dessus quand la condition est fausse. Le panneau des drapeaux l’annonce avant même d’avancer : next: jle tests ZF = 0, SF = 0, OF = 0 → falls through. Remplacez 12 par 7 (Edit → Load) et le même jle est pris.

Cette inversion est la première chose à intégrer pour lire du code compilé : la condition du saut est en général la négation de celle du code source.

test : comparer à zéro et tester des bits

test a, b est à and ce que cmp est à sub : il calcule a & b, positionne ZF, SF et PF d’après le résultat, met CF et OF à 0, et jette le résultat.

test eax, eax     ; eax & eax = eax, donc ZF = 1 exactement quand eax == 0
jz   is_zero      ; if (eax == 0) goto is_zero

test reg, reg suivi de jz/jnz, c’est ainsi que les compilateurs écrivent if (x == 0) et if (ptr) quand ils optimisent. test eax, 4 teste un seul bit — l’idiome derrière if (flags & 4).

Aide-mémoire

Condition Cint (signé)unsigned / pointeurs
a == bjeje
a != bjnejne
a < bjljb
a <= bjlejbe
a > bjgja
a >= bjgejae

Dans un binaire non optimisé, pensez à inverser : le saut que vous lisez évite le bloc quand la condition est fausse.

Pour vous entraîner, ouvrez le simulateur et choisissez l’exemple Signed vs unsigned compare : c’est un code C dont les deux if se compilent en jge et jae, que vous pouvez exécuter pas à pas.

Dans ce niveau

  1. 1.1Les registres
  2. 1.2Drapeaux, cmp et sauts conditionnels
  3. 1.3Conventions d’appel (System V, cdecl)
  4. 1.4Assembleur, éditeur de liens et chargeur
  5. 1.5Directives, sections et macros