x86-64 assembly from the ground up

Comparisons and branches

cmp keeps the flags and throws the answer away, a jump reads one of those flags, and the pair is how a program chooses between two paths.

Part 5 of 12

Intermediate16 min readBack to Courses

cmp subtracts and keeps only the flags

cmp eax, 60 subtracts sixty from eax and then throws the result away. What it keeps is the flags: a small set of bits beside the registers that record what the subtraction was like. The one that matters most is the zero flag, set when the two values were equal.

Nothing has branched yet. cmp only leaves a description of the last operation behind, and any jump reads that description and decides. This is why a comparison and the jump that follows it are written as a pair, and why an instruction placed between them can ruin the comparison by setting the flags to something else.

Choosing the jump

je jumps when the zero flag is set, and jne jumps when it is not. For order there are two families, and picking the wrong one is a classic bug. jl, jle, jg and jge treat the values as signed, so minus one counts as less than three. jb, jbe, ja and jae treat them as unsigned, where minus one is a very large number indeed.

A jump that is not taken does nothing at all, and the processor carries on with the next line. That fall-through is how an if … else is built: the true branch jumps past the false one, and the false one jumps past the rest.

Labels are just names for addresses. A label beginning with a dot is local to the previous ordinary label, so every function can have its own .write without colliding with another one.

A jump can also be unconditional: jmp .done goes to the label and asks no question. That is the instruction that closes an if-with-else, and it is also how a loop gets back to its test. It does not read the flags at all, so nothing between the comparison and the jmp can change where it goes.

The jumps worth knowing

_start:
  cmp eax, 60       ; the flags now describe eax - 60
  je  .equal        ; the zero flag: the two were equal
  jne .different    ; the zero flag is clear
  jl  .lower        ; signed: eax is below sixty
  jg  .higher       ; signed: eax is above sixty
  jb  .below        ; unsigned: a different question
  ja  .above        ; unsigned again

.equal:
  ; execution lands here only if the jump was taken

One comparison, two possible lines

The program compares a score with sixty and prints one of two messages. Run it, then change the score so the other branch runs and check that the output really does change.

Output
Click "Run" to execute your code

Change the seventy-five to forty-five and run it again, then change the jg to jl and leave the number alone. Two one-character edits, two different lines printed, and no other line in the program had to move.

Four flags, many questions

The flags are single bits beside the registers, and each one answers a different question about the last result. The zero flag says the result was zero, which for a comparison means the two values were equal. The sign flag copies the top bit of the result, and the carry flag records a borrow or an unsigned overflow.

A jump is just a test of one of those bits. je reads the zero flag. jb reads the carry flag, which is why it means below for unsigned values. jl reads a combination of the sign and overflow flags, which is why it means below for signed ones. The two families exist because one bit cannot answer both questions.

The important consequence is that the flags describe only the most recent flag-setting instruction. Arithmetic sets them too, so an add placed between a comparison and its jump quietly replaces the answer the jump was going to read.

The shape of an if and else

There is no block syntax. A branch is a label, a condition and a jump, and the false path is simply the code that falls through when the jump is not taken. For an if with no else, the jump skips over the body; for an if with an else, the true branch ends with a jump past the false branch.

That second jump is easy to forget. Without it, the true branch runs, prints its line, and then continues straight into the false branch as well. The output looks like both answers were correct, which is a confusing way to learn about fall-through.

Chained tests work the same way in sequence. Test the narrowest or highest band first, jump to its handler, and let the next comparison handle the rest. The order is part of the logic, not a detail.

Three branch mistakes. An instruction between cmp and the jump can overwrite the flags. Signed and unsigned jumps are not interchangeable once a value can be negative. And a true branch that does not jump past the false one runs both, so the output can look right for the wrong reason.

Try these next

  • Add a fourth band, or move the boundary from eighty to ninety and check which band seventy-two falls in.
  • Reverse a condition and swap the two labels so the output stays the same.
  • Compare two equal values and confirm the zero flag sends je down the true path.

Three bands from two comparisons

The score of seventy-two falls in the middle band. Run it, then try forty-five and ninety-one. The two comparisons are ordered from the highest band down, and the bottom band is whatever is left.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does cmp leave behind for the jump that follows it?