ARM64 Branches — Conditional Execution and Control Flow
In this tutorial, you will learn about ARM64 Branches. We cover key concepts, practical examples, and best practices to help you master this topic.
ARM64 branches use b/bl/br instructions with condition codes (eq, ne, lt, gt) and conditional select instructions for branchless control flow optimization.
What You'll Learn
- Unconditional and conditional branches
- Compare and branch instructions
- Branch with link (function calls)
- Conditional select (branchless)
- Table branches
Why It Matters
Branches affect performance through branch prediction. ARM64 offers conditional select for branchless code. Durga Antivirus Pro uses branchless techniques in ARM64 signature scanning for consistent performance.
Real-World Use
Control flow in any program, branchless cryptography, switch statements, function dispatch, and loop constructs.
flowchart LR
A["ARM64 Branches"] --> B["Unconditional"]
A --> C["Conditional"]
A --> D["Compare + Branch"]
A --> E["Conditional Select"]
B --> F["b, bl, br"]
C --> G["b.eq, b.ne"]
D --> H["cbz, cbnz"]
E --> I["csel, csinc"]
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#dbeafe,stroke:#2563eb,color:#1e40af
Unconditional Branches
// Simple branch (up to +/-128MB)
b label // branch to label
b label2 // branch backwards also possible
// Branch with link (call)
bl function // save return address in X30, branch
ret // return (branch to X30)
// Branch to register
br x0 // branch to address in x0 (indirect)
blr x1 // call through x1 (indirect call with link)
// Return
ret // alias: br x30
ret x1 // return to address in x1
Condition Codes
| Code | Condition | Flags | Meaning |
|---|---|---|---|
| eq | Equal | Z=1 | == |
| ne | Not equal | Z=0 | != |
| hs/cs | Higher/same | C=1 | Unsigned >= |
| lo/cc | Lower | C=0 | Unsigned < |
| mi | Minus/negative | N=1 | < 0 |
| pl | Plus/positive | N=0 | >= 0 |
| vs | Overflow | V=1 | Overflow |
| vc | No overflow | V=0 | No overflow |
| hi | Higher | C=1 & Z=0 | Unsigned > |
| ls | Lower/same | C=0 or Z=1 | Unsigned <= |
| ge | Greater/equal | N==V | Signed >= |
| lt | Less than | N!=V | Signed < |
| gt | Greater than | Z=0 & N==V | Signed > |
| le | Less/equal | Z=1 or N!=V | Signed <= |
Conditional Branches
cmp x0, x1
b.eq equal_label // branch if x0 == x1
b.ne not_equal // branch if x0 != x1
b.lt less_than // branch if signed x0 < x1
b.le less_equal // branch if signed x0 <= x1
b.gt greater_than // branch if signed x0 > x1
b.ge greater_equal // branch if signed x0 >= x1
// Unsigned comparisons
cmp x0, x1
b.hi higher // branch if unsigned x0 > x1
b.ls lower_same // branch if unsigned x0 <= x1
// Single flag tests
tst x0, #1
b.eq even // branch if x0 is even
Compare and Branch
// Combine compare and branch (zero/non-zero)
cbz x0, zero_label // branch if x0 == 0
cbnz x0, non_zero // branch if x0 != 0
// Combined compare and branch (32-bit)
cbz w0, zero_label
cbnz w0, non_zero
// These save one instruction vs cmp + b.eq
Test and Branch
// Test bit and branch
tbz x0, #3, bit_clear // branch if bit 3 of x0 is 0
tbnz x0, #3, bit_set // branch if bit 3 of x0 is 1
// Useful for testing flags packed in a register
// Without needing to shift/mask first
Loop Constructs
// while (x0 > 0) { ... x0--; }
while_loop:
cmp x0, #0
b.le while_done
// loop body
sub x0, x0, #1
b while_loop
while_done:
// for (i = 0; i < n; i++)
mov x1, #0 // i = 0
for_loop:
cmp x1, x0 // compare i with n
b.ge for_done
// loop body using x1 as index
add x1, x1, #1 // i++
b for_loop
for_done:
Switch Statements
// Simple jump table
switch:
cmp x0, #3
b.hi default_case // if x0 > 3, go to default
adr x1, jump_table
ldr x2, [x1, x0, lsl #3] // load target address
br x2
jump_table:
.quad case_0
.quad case_1
.quad case_2
.quad case_3
case_0: // ...
case_1: // ...
case_2: // ...
case_3: // ...
default_case: // ...
Branchless Code with csel
// Branchless absolute value
// int abs(int x) { return x < 0 ? -x : x; }
abs:
cmp x0, #0
cneg x0, x0, lt // if (lt) x0 = -x0
ret
// Branchless min/max
min:
cmp x0, x1
csel x0, x0, x1, le // if (le) x0=x0 else x0=x1
ret
max:
cmp x0, x1
csel x0, x0, x1, ge // if (ge) x0=x0 else x0=x1
ret
Common Mistakes
1. Using signed conditions for unsigned values
After cmp, signed conditions (lt, gt) interpret values as signed. Use hi, ls for unsigned.
2. Forgetting the condition suffix
b eq is correct. beq is a different encoding (same function, both work in ARM64).
3. Branch range limits
Conditional branches have +/- 1MB range. Unconditional branches have +/- 128MB. Use br for indirect jumps.
4. cbz vs cmp/b.eq performance
cbz is preferred for zero-tests — it decodes in a single instruction. For other conditions, use cmp + b.cond.
5. Not using conditional select for simple branches
csel avoids branch misprediction penalties. Use it for simple 2-way selections.
Practice Questions
1. What register does bl store the return address in?
X30 (Link Register). ret branches to X30.
2. How do you branch if x0 is zero in ARM64?
cbz x0, label (single instruction) or cmp x0, #0; b.eq label (two instructions).
3. What is the difference between b.lt and b.lo?
b.lt is signed less-than. b.lo is unsigned lower (C=0). They use different flag conditions.
4. How do you implement max(a, b) without a branch?
cmp x0, x1; csel x0, x0, x1, ge — selects x0 if x0 >= x1, else x1.
Challenge: Write a branchless function that clamps a value between 0 and 100.
Solution
// int clamp(int x) { return x < 0 ? 0 : (x > 100 ? 100 : x); }
clamp:
cmp x0, #0
csel x0, xzr, x0, lt // if (lt) x0 = 0
cmp x0, #100
csel x0, x0, #100, le // if (le) keep x0 else x0 = 100
ret
FAQ
{{< faq question="Does ARM64 have a conditional branch on carry?" >}}
Yes. b.hs (higher/same = carry set) and b.lo (lower = carry clear) test the carry flag.
{{< /faq >}}
{{< faq question="What is the branch penalty on ARM64?" >}} ARM CPUs have shorter pipelines than x86. A mispredicted branch costs ~10 cycles (vs ~15-20 on x86). {{< /faq >}}
{{< faq question="Can I branch to a 32-bit address in ARM64?" >}}
Yes, use br with a register holding the target address. b and bl use PC-relative offsets.
{{< /faq >}}
{{< faq question="What is a tail call in ARM64?" >}}
A function call made as the last operation, reusing the caller's return address: b function instead of bl function; ret.
{{< /faq >}}
{{< faq question="How do I branch to a computed address?" >}
Load the address into a register and use br x0. Common for virtual method dispatch and switch tables.
{{< /faq >}}
Mini Project
Write a binary search implementation using ARM64 conditional branches.
// int binary_search(int* arr, int len, int target)
// x0 = arr, x1 = len, x2 = target
binary_search:
mov x3, #0 // low = 0
sub x4, x1, #1 // high = len - 1
loop:
cmp x3, x4
b.gt not_found // low > high -> not found
add x5, x3, x4
lsr x5, x5, #1 // mid = (low + high) >> 1
ldr w6, [x0, x5, lsl #2] // arr[mid]
cmp w6, w2
b.eq found
b.lt go_right // arr[mid] < target
sub x4, x5, #1 // high = mid - 1
b loop
go_right:
add x3, x5, #1 // low = mid + 1
b loop
found:
mov x0, x5 // return mid
ret
not_found:
mov x0, #-1
ret
What's Next
Now that you understand ARM64 branches, proceed to ARM64 functions.
| Topic | Description | Link |
|---|---|---|
| ARM functions | Functions and ABI | {{< ref "25-arm-functions" >}} |
| ARM NEON | SIMD processing | {{< ref "26-arm-neon" >}} |
| ARM syscalls | Linux syscalls | {{< ref "27-arm-syscalls" >}} |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro