Step 1 of 5 · Reading · ~4 min
Learn
Control Flow and Memory
Comparisons, Jumps, Loops
Assembly has no if, else, while or for. By the end of this lesson you can build all of them from two pieces, a comparison that sets flags and a jump that reads them, including a loop nested inside another loop.
Flags and cmp
The CPU keeps a few one-bit flags that arithmetic sets as a side effect: ZF (result was zero), SF (result was negative), CF (an unsigned borrow or carry happened) and OF (a signed overflow happened). cmp a, b computes a - b, throws the result away, and keeps only the flags.
For cmp rax, rbx with rax = 5 and rbx = 10, the difference is -5: SF = 1, CF = 1 (5 is below 10 as unsigned numbers too), ZF = 0, OF = 0. A conditional jump then reads those flags and either jumps to a label or falls through to the next line.
The jump family
| Question | Signed | Unsigned |
|---|---|---|
| equal / not equal | je, jne | je, jne |
| less / greater | jl, jg | jb (below), ja (above) |
| or equal | jle, jge | jbe, jae |
jmp label always jumps. The signed and unsigned rows read different flags from the same cmp. With rax = -1, cmp rax, 0 makes jl taken (-1 < 0) but jb not taken, because as an unsigned number -1 is 0xFFFFFFFFFFFFFFFF. Counters and lengths are usually small and positive, so either works for them; pick signed unless you are comparing addresses or sizes.
test a, b ANDs its operands and keeps only the flags. test rax, rax sets ZF exactly when rax is 0, and test rax, 1 sets ZF exactly when the lowest bit is 0 (an even number).
A loop is a label and a backward jump
Put the check at the top, the body in the middle, and jmp back to the top. Here is a loop that sums 1 through 6 in a register:
xor rax, rax ; total
mov rbx, 1 ; i
.add:
cmp rbx, 6
jg .end ; i > 6: leave
add rax, rbx
inc rbx
jmp .add
.end: ; rax = 21
| pass | i at the check | total after the body |
|---|---|---|
| 1 | 1 | 1 |
| 2 | 2 | 3 |
| 3 | 3 | 6 |
| 4 | 4 | 10 |
| 5 | 5 | 15 |
| 6 | 6 | 21 |
| exit | 7 | 21 |
Labels that start with a dot are local: .add belongs to the nearest global label above it, so several functions can each have a .end.
A loop inside a loop
The inner loop's counter must be set up again every time the outer loop comes around, so the setup goes inside the outer body. This counts the pairs (i, j) with 1 <= i < j <= 4, where the inner loop starts one past i:
xor rax, rax ; pair count
mov r8, 1 ; i
.outer:
cmp r8, 4
jg .done
lea r9, [r8 + 1] ; j starts at i + 1
.inner:
cmp r9, 4
jg .next
inc rax
inc r9
jmp .inner
.next:
inc r8
jmp .outer
.done: ; rax = 6
lea r9, [r8 + 1] computes r8 + 1 into r9 without touching the flags or r8. If the r9 setup sat above .outer, it would run once, and after the first pass the inner loop would be finished for good.
The counter that a syscall eats
syscall overwrites rcx and r11. A loop that keeps its counter in rcx and calls write inside the body loses the counter on every pass: rcx now holds the return address, a number around four million, so dec rcx / jnz repeats millions of times and floods the output. The x86 loop instruction has the same problem because it counts in rcx. Keep counters in rbx or r12 to r15, which every syscall preserves.
Common mistakes
- Counter in
rcxorr11across a syscall. The loop runs far too long or ends early. jgewherejgwas meant.cmp i, 6thenjge .endstops before handling 6.- Resetting nothing. An inner counter set up outside the outer loop leaves every row after the first empty.
- Forgetting
inc. The loop never ends and the run is killed for exceeding its time limit.
Your exercise
The starter reads a number into r12 and defines a star byte and a newline byte. Draw the shape the task describes with two loops. Choose registers that survive a syscall, and decide carefully what the inner loop compares against.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…