Assembly course · Lesson 3

Jumps, flags and conditional jumps

At the end you can control the flow of a program: skip, compare, branch and build a loop. Prerequisite: lesson 2.

Architecture:
The video for this lesson is coming.

The idea: the instruction pointer moves

The processor executes instructions one after the other: the instruction pointer (eip)(rip)(pc) points to the next instruction and moves on after each one. A jump sets the instruction pointer to another address – usually that of a label. There are two kinds:

; Lesson 3: jmp jumps always. The line in between is never executed.

.format pe console
.arch i386

.code
start:
  mov eax, 1
  jmp skip                ; the next instruction to run is the one at "skip"
  mov eax, 99             ; skipped
skip:
  ret                     ; exit code 1
; Lesson 3: jmp jumps always. The line in between is never executed.

.format pe console
.arch x64

.code
start:
  mov eax, 1
  jmp skip                ; the next instruction to run is the one at "skip"
  mov eax, 99             ; skipped
skip:
  ret                     ; exit code 1
; Lesson 3: b (branch) jumps always. The line in between is never executed.

.format pe console
.arch thumb

.code
start:
  movs r0, #1
  b skip                  ; the next instruction to run is the one at "skip"
  movs r0, #99            ; skipped
skip:
  bx lr                   ; exit code 1
; Lesson 3: b (branch) jumps always. The line in between is never executed.

.format pe console
.arch arm64

.code
start:
  mov x0, #1
  b skip                  ; the next instruction to run is the one at "skip"
  mov x0, #99             ; skipped
skip:
  ret                     ; exit code 1

The program ends with the exit code 1: the instruction that would set the result register to 99 is never executed. In the debugger you see the instruction pointer jump over the line.

Flags: the memory of the CPU

How does a conditional jump know whether its condition is met? The CPU has single bits for that, the flags. Many instructions set them as a side effect when they calculate. The four most important:

FlagNameis 1 if the result …
ZFZerowas 0
SFSignwas negative (the top bit is 1)
CFCarryhad a carry: in a subtraction, when a larger number was subtracted from a smaller one (unsigned)
OFOverflowdid not fit the register as a signed number
FlagNameis 1 if the result …
ZZerowas 0
NNegativewas negative (the top bit is 1)
CCarryhad a carry in an addition; in a subtraction C is 1 if no borrow was needed (unlike the Intel CF)
VoVerflowdid not fit the register as a signed number

Not every instruction sets flags: add, sub, dec, cmp do, mov never does. (inc and dec leave CF unchanged.)

Not every instruction sets flags. cmp always does; arithmetic instructions only when there is an s in the name (adds, subs). In Thumb-2 the short forms of mov, add and sub have this s anyway (hence movs r0, #1 in lesson 1).

cmp: compare without changing

cmp a, b calculates a − b, throws the result away and keeps only the flags. All statements about "greater" and "smaller" can be read from them. For 7 − 12 = −5:

The result is not 0 (ZF=0), negative (SF=1), borrowed in an unsigned subtraction (CF=1) and does not overflow (OF=0).

The result is not 0 (Z=0), negative (N=1), needed a borrow (C=0) and does not overflow (V=0).

Conditional jumps

A conditional jump usually follows a cmp and asks the flags. Because cmp does not know whether you mean your numbers with or without a sign, there are two families for "smaller" and "greater": signed (with sign, −1 is smaller than 1) and unsigned (without sign, the number made of all ones is the largest).

Jumpjumps if (after cmp a, b)Flags
je / jza = bZF=1
jne / jnza ≠ bZF=0
jla < b, signed (less)SF≠OF
jlea ≤ b, signedZF=1 or SF≠OF
jga > b, signed (greater)ZF=0 and SF=OF
jgea ≥ b, signedSF=OF
jba < b, unsigned (below)CF=1
jbea ≤ b, unsignedCF=1 or ZF=1
jaa > b, unsigned (above)CF=0 and ZF=0
jaea ≥ b, unsignedCF=0
Jumpjumps if (after cmp a, b)Flags
beqb.eqa = b (equal)Z=1
bneb.nea ≠ bZ=0
bltb.lta < b, signed (less than)N≠V
bleb.lea ≤ b, signedZ=1 or N≠V
bgtb.gta > b, signed (greater than)Z=0 and N=V
bgeb.gea ≥ b, signedN=V
blob.loa < b, unsigned (lower)C=0
blsb.lsa ≤ b, unsigned (lower or same)C=0 or Z=1
bhib.hia > b, unsigned (higher)C=1 and Z=0
bhsb.hsa ≥ b, unsigned (higher or same)C=1

You do not have to know the flags by heart; the names of the jumps say what they mean. All that matters: with every comparison, ask yourself whether your numbers have a sign.

The larger of two numbers

; Lesson 3: cmp sets the flags, a conditional jump looks at them. Prints the larger of two numbers.

.format pe console
.arch i386
.import msvcrt.dll printf

.code
start:
  mov eax, 7
  mov ecx, 12
  cmp eax, ecx            ; computes eax - ecx = -5, throws the result away, keeps the flags
  jge keep                ; jump if greater or equal (signed): here not, 7 < 12
  mov eax, ecx            ; so the second number is the larger one
keep:
  push eax                ; the larger number
  push dword fmt
  call [printf]
  add esp, 8
  xor eax, eax
  ret

.data
fmt db 'The larger number is %d', 10, 0
; Lesson 3: cmp sets the flags, a conditional jump looks at them. Prints the larger of two numbers.

.format pe console
.arch x64
.import msvcrt.dll printf

.code
start:
  sub rsp, 40
  mov edx, 7
  mov ecx, 12
  cmp edx, ecx            ; computes edx - ecx = -5, throws the result away, keeps the flags
  jge keep                ; jump if greater or equal (signed): here not, 7 < 12
  mov edx, ecx            ; so the second number is the larger one
keep:
  lea rcx, [fmt]          ; the format (first argument), the number is already in edx (second)
  call [printf]
  xor eax, eax
  add rsp, 40
  ret

.data
fmt db 'The larger number is %d', 10, 0
; Lesson 3: cmp sets the flags, a conditional branch looks at them. Prints the larger of two numbers.

.format pe console
.arch thumb
.import msvcrt.dll printf

.code
start:
  push {r4, lr}
  movs r1, #7
  movs r2, #12
  cmp r1, r2              ; computes r1 - r2 = -5, throws the result away, keeps the flags
  bge keep                ; branch if greater or equal (signed): here not, 7 < 12
  mov r1, r2              ; so the second number is the larger one
keep:
  adr r0, fmt             ; the format (r0); the number is already in r1
  ldr r3, printf
  blx r3
  movs r0, #0
  pop {r4, pc}

.data
align 4
fmt db 'The larger number is %d', 10, 0
; Lesson 3: cmp sets the flags, a conditional branch looks at them. Prints the larger of two numbers.

.format pe console
.arch arm64
.import msvcrt.dll printf

.code
start:
  stp x29, x30, [sp, #-16]!
  mov w1, #7
  mov w2, #12
  cmp w1, w2              ; computes w1 - w2 = -5, throws the result away, keeps the flags
  b.ge keep               ; branch if greater or equal (signed): here not, 7 < 12
  mov w1, w2              ; so the second number is the larger one
keep:
  adrp x0, fmt
  add x0, x0, :lo12:fmt   ; the format (x0); the number is already in w1
  adrp x16, printf
  ldr x16, [x16, :lo12:printf]
  blr x16
  mov x0, #0
  ldp x29, x30, [sp], #16
  ret

.data
fmt db 'The larger number is %d', 10, 0

The program compares 7 and 12. The jump jge jumps only if the first number is greater than or equal to the secondThe jump bge jumps only if the first number is greater than or equal to the secondThe jump b.ge jumps only if the first number is greater than or equal to the second – not here. Then the next instruction runs and copies the second number into the register that is printed. If the jump is taken, this instruction is skipped and the first number stays. Either way the program goes on behind the label keep: a branch that joins up again.

The same in the notation of Asmonite

Asmonite has a shorter notation for such branches: if … endif. The assembler turns it into exactly the cmp and the conditional jump you have just written by hand: if signed a < b becomes cmp a, b and the jump with the opposite condition around the block (jgebgeb.ge for "greater or equal"). So the block runs when the condition is true. The word signed says that the numbers have a sign; with unsigned you take the other family.

; Lesson 3: the same program in the notation of Asmonite: if ... endif instead of cmp and the jump.
; "if signed a < b" is cmp a, b and the jump with the opposite condition around the block (jge).

.format pe console
.arch i386
.import msvcrt.dll printf

.code
start:
  mov eax, 7
  mov ecx, 12
  if signed eax < ecx     ; cmp eax, ecx / jge: the block runs when eax < ecx
    mov eax, ecx          ; so the second number is the larger one
  endif
  push eax                ; the larger number
  push dword fmt
  call [printf]
  add esp, 8
  xor eax, eax
  ret

.data
fmt db 'The larger number is %d', 10, 0
; Lesson 3: the same program in the notation of Asmonite: if ... endif instead of cmp and the jump.
; "if signed a < b" is cmp a, b and the jump with the opposite condition around the block (jge).

.format pe console
.arch x64
.import msvcrt.dll printf

.code
start:
  sub rsp, 40
  mov edx, 7
  mov ecx, 12
  if signed edx < ecx     ; cmp edx, ecx / jge: the block runs when edx < ecx
    mov edx, ecx          ; so the second number is the larger one
  endif
  lea rcx, [fmt]          ; the format (first argument), the number is already in edx (second)
  call [printf]
  xor eax, eax
  add rsp, 40
  ret

.data
fmt db 'The larger number is %d', 10, 0
; Lesson 3: the same program in the notation of Asmonite: if ... endif instead of cmp and the jump.
; "if signed a < b" is cmp a, b and the jump with the opposite condition around the block (jge).

.format pe console
.arch thumb
.import msvcrt.dll printf

.code
start:
  push {r4, lr}
  movs r1, #7
  movs r2, #12
  if signed r1 < r2       ; cmp r1, r2 / bge: the block runs when r1 < r2
    mov r1, r2            ; so the second number is the larger one
  endif
  adr r0, fmt             ; the format (r0); the number is already in r1
  ldr r3, printf
  blx r3
  movs r0, #0
  pop {r4, pc}

.data
align 4
fmt db 'The larger number is %d', 10, 0
; Lesson 3: the same program in the notation of Asmonite: if ... endif instead of cmp and the jump.
; "if signed a < b" is cmp a, b and the jump with the opposite condition around the block (jge).

.format pe console
.arch arm64
.import msvcrt.dll printf

.code
start:
  stp x29, x30, [sp, #-16]!
  mov w1, #7
  mov w2, #12
  if signed w1 < w2       ; cmp w1, w2 / b.ge: the block runs when w1 < w2
    mov w1, w2            ; so the second number is the larger one
  endif
  adrp x0, fmt
  add x0, x0, :lo12:fmt   ; the format (x0); the number is already in w1
  adrp x16, printf
  ldr x16, [x16, :lo12:printf]
  blr x16
  mov x0, #0
  ldp x29, x30, [sp], #16
  ret

.data
fmt db 'The larger number is %d', 10, 0

The program does the same as the one above. You should still know the raw form, because in the debugger, in disassemblers and in other people's code you only see that. More about the notation (else, if zero and others) is on the page if, else, do … while.

Try it in Asmonite

  1. Assemble the program with Ctrl+F9 and go with F8 to just after the cmp.
  2. Look at the flags on the right: ZF=0, SF=1, CF=1, OF=0 – as calculated above for 7 − 12. Changed values are red.
  3. Look at the flags on the right: N=1, Z=0, C=0, V=0 – as calculated above for 7 − 12. Changed values are red.
  4. Go on with F8: the jump is not taken, the next marked line is the mov.
  5. Experiment: run the program again to just after the cmp. Now toggle the flag OF with a double click.Now toggle the flag V with a double click. With F8 the program now jumps over the mov: jge only asks whether SF equals OFge only asks whether N equals V, and you have changed the answer. So it really is only the flag that decides.

A loop

If a conditional jump goes back to a label, you get a loop: the instructions in between run as long as the condition holds. This program counts down from 5 to 1 and prints each number.

; Lesson 3: a loop. dec sets the zero flag when the result is 0, jnz jumps back while it is not.

.format pe console
.arch i386
.import msvcrt.dll printf

.code
start:
  mov ebx, 5              ; ebx survives the call of printf
again:
  push ebx
  push dword fmt
  call [printf]
  add esp, 8
  dec ebx                 ; ebx = ebx - 1; ZF = 1 when it becomes 0
  jnz again               ; jump if not zero: back to "again"
  xor eax, eax
  ret

.data
fmt db '%d', 10, 0
; Lesson 3: a loop. dec sets the zero flag when the result is 0, jnz jumps back while it is not.

.format pe console
.arch x64
.import msvcrt.dll printf

.code
start:
  push rbx                ; rbx must come back unchanged: we save it ...
  sub rsp, 32             ; shadow space (push made the stack aligned)
  mov ebx, 5              ; ... and may use it: it survives the call of printf
again:
  lea rcx, [fmt]
  mov edx, ebx
  call [printf]
  dec ebx                 ; ebx = ebx - 1; ZF = 1 when it becomes 0
  jnz again               ; jump if not zero: back to "again"
  xor eax, eax
  add rsp, 32
  pop rbx
  ret

.data
fmt db '%d', 10, 0
; Lesson 3: a loop. subs sets the zero flag when the result is 0, bne branches back while it is not.

.format pe console
.arch thumb
.import msvcrt.dll printf

.code
start:
  push {r4, r5, r6, lr}   ; r4 to r6 survive a call and must come back unchanged
  movs r4, #5             ; the counter
  ldr r5, printf
again:
  adr r0, fmt
  mov r1, r4
  blx r5
  subs r4, r4, #1         ; r4 = r4 - 1; the "s" sets the flags: Z = 1 when it becomes 0
  bne again               ; branch if not equal (Z = 0): back to "again"
  movs r0, #0
  pop {r4, r5, r6, pc}

.data
align 4
fmt db '%d', 10, 0
; Lesson 3: a loop. subs sets the zero flag when the result is 0, b.ne branches back while it is not.

.format pe console
.arch arm64
.import msvcrt.dll printf

.code
start:
  stp x29, x30, [sp, #-32]!
  stp x19, x20, [sp, #16]       ; x19, x20 survive a call and must come back unchanged
  mov w19, #5                   ; the counter
  adrp x20, printf
  ldr x20, [x20, :lo12:printf]
again:
  adrp x0, fmt
  add x0, x0, :lo12:fmt
  mov w1, w19
  blr x20
  subs w19, w19, #1             ; w19 = w19 - 1; the "s" sets the flags: Z = 1 when it becomes 0
  b.ne again                    ; branch if not equal (Z = 0): back to "again"
  mov x0, #0
  ldp x19, x20, [sp, #16]
  ldp x29, x30, [sp], #32
  ret

.data
fmt db '%d', 10, 0

dec ebx subtracts 1 and sets the flags while doing so: if the result is 0, ZF=1. jnz again (jump if not zero) jumps back as long as ZF=0. No extra cmp is needed because dec has already set the flags. The counter register ebx must survive the call of printf; functions must return ebx unchanged, which is why the counter lives there and not in eax.

subs r4, r4, #1 subtracts 1 and sets the flags while doing so (the s): if the result is 0, Z=1. bne again jumps back as long as Z=0. No extra cmp is needed. The counter lives in r4 because printf must return r4 to r11 unchanged; it may change r0 to r3. For the same reason the program saves r4 to r6 at the start and restores them at the end.

subs w19, w19, #1 subtracts 1 and sets the flags while doing so (the s): if the result is 0, Z=1. b.ne again jumps back as long as Z=0. No extra cmp is needed. The counter lives in w19 because printf must return x19 to x28 unchanged; it may change x0 to x17. For the same reason the program saves x19 and x20 at the start and restores them at the end.

What you see

The larger number is 12
5
4
3
2
1

With a breakpoint on the jump at the end of the loop and F9 you see the counter register one smaller at each stop. In the last pass it becomes 0, the zero flag goes to 1, and the jump is not taken.

Exercises

1. The smaller of two numbers

Change the program so that it prints the smaller number. You only have to change one instruction.

Solution

Instead of jge keep write jle keep: if the first number is less than or equal to the second, it stays.

Instead of bge keep write ble keep: if the first number is less than or equal to the second, it stays.

Instead of b.ge keep write b.le keep: if the first number is less than or equal to the second, it stays.

2. Counting up

Make the loop count up from 1 to 5. You now need a cmp, because the counting does not end at 0.

Solution
; Lesson 3: exercise 2: counting up. cmp compares with 6, jne jumps back while the counter is not 6.

.format pe console
.arch i386
.import msvcrt.dll printf

.code
start:
  mov ebx, 1              ; ebx survives the call of printf
again:
  push ebx
  push dword fmt
  call [printf]
  add esp, 8
  inc ebx
  cmp ebx, 6              ; zero flag = 1 when ebx is 6
  jne again               ; jump if not equal: back to "again"
  xor eax, eax
  ret

.data
fmt db '%d', 10, 0
; Lesson 3: exercise 2: counting up. cmp compares with 6, jne jumps back while the counter is not 6.

.format pe console
.arch x64
.import msvcrt.dll printf

.code
start:
  push rbx                ; rbx must come back unchanged: we save it ...
  sub rsp, 32             ; shadow space (push made the stack aligned)
  mov ebx, 1              ; ... and may use it: it survives the call of printf
again:
  lea rcx, [fmt]
  mov edx, ebx
  call [printf]
  inc ebx
  cmp ebx, 6              ; zero flag = 1 when ebx is 6
  jne again               ; jump if not equal: back to "again"
  xor eax, eax
  add rsp, 32
  pop rbx
  ret

.data
fmt db '%d', 10, 0
; Lesson 3: exercise 2: counting up. cmp compares with 6, bne branches back while the counter is not 6.

.format pe console
.arch thumb
.import msvcrt.dll printf

.code
start:
  push {r4, r5, r6, lr}   ; r4 to r6 survive a call and must come back unchanged
  movs r4, #1             ; the counter
  ldr r5, printf
again:
  adr r0, fmt
  mov r1, r4
  blx r5
  adds r4, r4, #1
  cmp r4, #6              ; Z = 1 when r4 is 6
  bne again               ; branch if not equal (Z = 0): back to "again"
  movs r0, #0
  pop {r4, r5, r6, pc}

.data
align 4
fmt db '%d', 10, 0
; Lesson 3: exercise 2: counting up. cmp compares with 6, b.ne branches back while the counter is not 6.

.format pe console
.arch arm64
.import msvcrt.dll printf

.code
start:
  stp x29, x30, [sp, #-32]!
  stp x19, x20, [sp, #16]       ; x19, x20 survive a call and must come back unchanged
  mov w19, #1                   ; the counter
  adrp x20, printf
  ldr x20, [x20, :lo12:printf]
again:
  adrp x0, fmt
  add x0, x0, :lo12:fmt
  mov w1, w19
  blr x20
  add w19, w19, #1
  cmp w19, #6                   ; Z = 1 when w19 is 6
  b.ne again                    ; branch if not equal (Z = 0): back to "again"
  mov x0, #0
  ldp x19, x20, [sp, #16]
  ldp x29, x30, [sp], #32
  ret

.data
fmt db '%d', 10, 0

The counter starts at 1, is increased by 1 and compared with 6. As long as it is not 6, the program goes back. So the loop runs for 1, 2, 3, 4 and 5.

3. Brain teaser: signed or unsigned

In the compare program, set the first number to −1 and the second to 1. What does the program print with the jump for numbers with a sign (jgebgeb.ge), and what if you use the jump for numbers without a sign (jaebhsb.hs)? Predict it first and then check it in the debugger.

Solution

With a sign, −1 is smaller than 1: the jump is not taken, and the program prints 1. Without a sign, −1 is the same bit pattern as 4294967295, and that is larger than 1: the jump is taken, and the program prints −1 (%d reads the same bits with a sign again). The same bits, two meanings – which one applies is decided solely by the jump you choose. In the notation of Asmonite it is just another word: if signed … against if unsigned ….

; Lesson 3, exercise 3: the same with -1 and 1, but an unsigned comparison.

.format pe console
.arch i386
.import msvcrt.dll printf

.code
start:
  mov eax, -1             ; 0xFFFFFFFF: -1 as a signed number, 4294967295 as an unsigned one
  mov ecx, 1
  cmp eax, ecx
  jae keep                ; jump if above or equal (unsigned): 4294967295 >= 1, so it jumps
  mov eax, ecx
keep:
  push eax                ; the larger number
  push dword fmt
  call [printf]
  add esp, 8
  xor eax, eax
  ret

.data
fmt db 'The larger number is %d', 10, 0
; Lesson 3, exercise 3: the same with -1 and 1, but an unsigned comparison.

.format pe console
.arch x64
.import msvcrt.dll printf

.code
start:
  sub rsp, 40
  mov edx, -1             ; 0xFFFFFFFF: -1 as a signed number, 4294967295 as an unsigned one
  mov ecx, 1
  cmp edx, ecx
  jae keep                ; jump if above or equal (unsigned): 4294967295 >= 1, so it jumps
  mov edx, ecx
keep:
  lea rcx, [fmt]          ; the format (first argument), the number is already in edx (second)
  call [printf]
  xor eax, eax
  add rsp, 40
  ret

.data
fmt db 'The larger number is %d', 10, 0
; Lesson 3, exercise 3: the same with -1 and 1, but an unsigned comparison.

.format pe console
.arch thumb
.import msvcrt.dll printf

.code
start:
  push {r4, lr}
  movs r1, #-1            ; 0xFFFFFFFF: -1 as a signed number, 4294967295 as an unsigned one
  movs r2, #1
  cmp r1, r2
  bhs keep                ; branch if higher or same (unsigned): 4294967295 >= 1, so it branches
  mov r1, r2
keep:
  adr r0, fmt             ; the format (r0); the number is already in r1
  ldr r3, printf
  blx r3
  movs r0, #0
  pop {r4, pc}

.data
align 4
fmt db 'The larger number is %d', 10, 0
; Lesson 3, exercise 3: the same with -1 and 1, but an unsigned comparison.

.format pe console
.arch arm64
.import msvcrt.dll printf

.code
start:
  stp x29, x30, [sp, #-16]!
  mov w1, #-1             ; 0xFFFFFFFF: -1 as a signed number, 4294967295 as an unsigned one
  mov w2, #1
  cmp w1, w2
  b.hs keep               ; branch if higher or same (unsigned): 4294967295 >= 1, so it branches
  mov w1, w2
keep:
  adrp x0, fmt
  add x0, x0, :lo12:fmt   ; the format (x0); the number is already in w1
  adrp x16, printf
  ldr x16, [x16, :lo12:printf]
  blr x16
  mov x0, #0
  ldp x29, x30, [sp], #16
  ret

.data
fmt db 'The larger number is %d', 10, 0

Remember