cmp and the Flags Register
How cmp and test set the flags (ZF, SF, OF, CF) without storing a result.
The Instruction That Stores Nothing
So far every instruction you have read produced a value and put it somewhere: imul eax, eax left a product in eax, mov copied bytes from one place to another. Control flow works differently. Before a CPU can decide whether to take a branch, it has to ask a question, "is this number zero?", "is this one bigger than that one?", and remember the answer. The instruction that asks the question is cmp, and the place it stores the answer is a special register called the flags register.
Here is a tiny function that checks whether its argument is zero:
int classify(int n)
{
if (n == 0)
return 1;
return 0;
}
At -O0 the comparison shows up like this:
classify(int):
push rbp
mov rbp, rsp
mov DWORD PTR -4[rbp], edi
cmp DWORD PTR -4[rbp], 0
jne .L2
mov eax, 1
jmp .L3
.L2:
mov eax, 0
.L3:
pop rbp
ret
The push rbp / mov rbp, rsp opening and the store of edi into -4[rbp] are the function-setup boilerplate you first met in Chapter 1; the next chapter covers them fully, so skim past them here. Ignore the jumps for a moment too (the next lesson is all about them). The line that interests us here is cmp DWORD PTR -4[rbp], 0. That is the comparison. Notice what it does not have: a destination to write a result into. So where does the answer go?
What cmp Actually Does
cmp a, b performs the subtraction a - b, sets the flags based on the result, and then throws the result away. It is sub that does not keep its answer. The only lasting effect is on the flags.
That is the whole trick. The comparison does not produce a number you can use; it produces a handful of single-bit facts about the subtraction, recorded in the flags register. A following instruction then reads those bits to decide what to do.
So cmp DWORD PTR -4[rbp], 0 means "compute n - 0 and set the flags". Since n - 0 is just n, this effectively records whether n was zero, negative, and so on, without disturbing n itself.
The Four Flags That Matter
The flags register holds many bits, but for reading ordinary integer control flow you only need four:
| Flag | Name | Set when... |
|---|---|---|
ZF |
Zero Flag | the result was exactly zero |
SF |
Sign Flag | the result was negative (its top bit is 1) |
OF |
Overflow Flag | the result overflowed as a signed operation |
CF |
Carry Flag | the result overflowed as an unsigned operation (a borrow) |
After cmp a, b, these read out naturally:
ZFis set exactly whena == b(becausea - bwas zero).SF,OF, andCFtogether let later instructions tell whethera < bora > b, and they do it differently depending on whether the values are treated as signed or unsigned. That split is why there are two whole families of conditional jumps, which the next lesson covers.
You do not compute these by hand. The point is to recognise that a cmp prepares the flags, and that whatever instruction follows is going to read them.
test: the Other Question-Asker
There is a second instruction that sets flags without storing a result: test. Where cmp subtracts, test performs a bitwise AND and discards the result, keeping only the flags.
The most common use by far is test reg, reg with the same register twice, which is the compiler's idiom for "is this value zero?". n & n is just n, so test eax, eax sets ZF exactly when eax is zero, and SF when it is negative. It is a cheaper way to ask "is it zero?" than cmp eax, 0.
You see this whenever C++ tests a value for truthiness. Consider:
int isZero(int n)
{
if (n)
return 0;
return 1;
}
At -O0 GCC still writes the comparison as cmp ... 0:
isZero(int):
push rbp
mov rbp, rsp
mov DWORD PTR -4[rbp], edi
cmp DWORD PTR -4[rbp], 0
je .L2
mov eax, 0
jmp .L3
.L2:
mov eax, 1
.L3:
pop rbp
ret
Here if (n) compiled to cmp DWORD PTR -4[rbp], 0 because the value is sitting in memory. Once the optimizer pulls the value into a register, the same check typically becomes test eax, eax. Both ask the identical question, "is it zero?", and both answer it purely through ZF.
Why the Sign of the Comparison Is Visible
Look back at classify. The source said n == 0, but the instruction after the compare was jne ("jump if not equal"). The compiler inverted the test: rather than jump when equal, it falls through when equal and jumps away otherwise. This inversion is extremely common, and it is the first hint that the comparison and the jump are a matched pair. The cmp sets the flags; the jump reads exactly one combination of them.
That pairing is the heart of reading control flow. Whenever you see a cmp or a test, your eye should immediately drop to the next line to see which conditional jump consumes the flags, because that jump tells you what question was really being asked.
Key Takeaways
cmp a, bcomputesa - b, sets the flags, and discards the result; it stores no value.test a, bcomputesa & b, sets the flags, and discards the result;test reg, regis the idiom for "is it zero?".- The flags register records single-bit facts:
ZF(zero),SF(negative),OF(signed overflow),CF(unsigned overflow/borrow). - After
cmp a, b,ZFis set exactly whena == b; the other flags distinguish<from>, differently for signed vs unsigned. - A comparison only prepares the flags. The instruction that follows reads them, so always read a
cmp/testtogether with the jump after it.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
cmp and the Flags Register - Quiz
Test your understanding of the lesson.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!