Skip to content

Flags, Branches and Loops

A branch is a decision over a precisely defined condition, not an implicit C comparison. Build a bounded sum and show why signed and unsigned tests differ.

Before you start

Complete registers and integer operations. You know the leaf-function contract and can link a C driver. Run this lesson in its own practice directory on the Pi.

Outcome: trace a loop, handle an empty input, and distinguish b.lt from b.lo.

Flags and branch conditions

cmp x0, x1 computes subtraction flags without retaining the arithmetic result. N, Z, C and V describe its negative/zero/carry/overflow properties. Signed decisions combine N and V; unsigned decisions use carry and zero. The same bit pattern can therefore produce different decisions.

Decision after CMP Branch
Equal / not equal b.eq / b.ne
Signed less / greater or equal b.lt / b.ge
Unsigned lower / higher or same b.lo / b.hs
Unsigned higher / lower or same b.hi / b.ls

cbz and cbnz test a register directly. subs changes flags; plain sub does not. Put a conditional branch after the flag-producing instruction it depends on, not after an intervening call.

Trace a finite loop

sum_to_n(n) adds n down to 1. Its practical input contract is n <= 1000; the driver tests only within that bound. It is not a constant-time algorithm for arbitrary 64-bit input.

// file: branches.S
.text
.global sum_to_n
.type sum_to_n, %function
sum_to_n:
    mov x1, #0
    cbz x0, .Ldone
.Lloop:
    add x1, x1, x0
    subs x0, x0, #1
    b.ne .Lloop
.Ldone:
    mov x0, x1
    ret
.size sum_to_n, .-sum_to_n

.global signed_less
.type signed_less, %function
signed_less:
    cmp x0, x1
    cset w0, lt
    ret
.size signed_less, .-signed_less

.global unsigned_less
.type unsigned_less, %function
unsigned_less:
    cmp x0, x1
    cset w0, lo
    ret
.size unsigned_less, .-unsigned_less
.section .note.GNU-stack,"",%progbits

cset materializes a condition as 0 or 1; it does not introduce a new comparison. For n=3, the (remaining, accumulator) pairs after the additions are (3,3), (2,5), (1,6).

/* file: branches.c */
#include <stdint.h>
#include <stdio.h>
extern uint64_t sum_to_n(uint64_t);
extern int signed_less(int64_t, int64_t);
extern int unsigned_less(uint64_t, uint64_t);
int main(void)
{
    const uint64_t inputs[] = {0, 1, 3, 10, 1000};
    for (unsigned i = 0; i < sizeof inputs / sizeof inputs[0]; ++i) {
        uint64_t n = inputs[i];
        if (sum_to_n(n) != n * (n + 1) / 2) return 1;
    }
    if (signed_less(-1, 1) != 1 ||
        unsigned_less(UINT64_MAX, 1) != 0 ||
        signed_less(7, 7) != 0 || unsigned_less(7, 7) != 0) return 1;
    puts("PASS branches: empty, bounded loop, signed and unsigned");
    return 0;
}
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -g -O0 branches.c branches.S -o branches
./branches
PASS branches: empty, bounded loop, signed and unsigned

Failure and recovery

If zero hangs, check the guard before the loop: decrementing zero wraps to a huge count. Interrupt an unintended run with Ctrl+C and fix the guard; do not wait for wraparound. If signed cases fail, inspect the condition mnemonic rather than changing the C prototype. Returning 0 from main is success; a function returning comparison 0 means false.

Part 1 checkpoint

Explain process entry versus function return, draw the x/w alias relationship and trace n=0 and n=3. Add an n=2 test (expected sum 3) and comparison cases where the operands are equal. Predict each result before running.

Check your understanding

Why does UINT64_MAX compare above 1 while the signed value −1 compares below 1?

Compare your reasoning

Both can use all-one bits, but the comparison interpretation differs. Unsigned lower uses carry rules; signed less uses N and V. Equality is independent of that choice.

Continue the course

Previous: Registers and Integer Operations · Course roadmap · Next: Memory, Arrays and Loads

Donate