Code in ARM Assembly: Conditional loops

In the previous episode in this series, I started to look at how to control flow using an instruction which sets the NZCV flags, followed by a conditional branch to a label. The example given there is among the simplest, a series of if … else if … statements or a small switch statement. That can be summarised in the diagram:


This article moves on to look at the other main type of control, conditional loops.

Although conditional loops can be written in several different ways using high-level languages like Swift, there are two basic patterns, depending on whether the test is made in the head or tail of the loop.

Tail-tested conditional looping

In Swift, the most widely used form of loop which tests whether the condition is met in the tail is the for statement:
for index in 1...100 {

A typical implementation in ARM64 assembly language might start by setting up registers containing the start index value and the end index, then perform the code in the loop, incrementing the index value, and testing whether it remains less than or equal to the end index. If it does, then branch back to the loop to perform it again; if it doesn’t, then continue with the following operations.

Here’s an example in which the index is stored in register X5, and the maximum index in X4:
MOV X5, #1 // start index = 1
MOV X4, #100 // end index = 100
for_loop: // do what you need in the loop
// …
ADD X5, X5, #1 // increment the index by 1
CMP X5, X4 // check whether end index has been reached
B.LE for_loop // if index <= end index, loop back
// next code after loop


It’s simple to modify that for variants which count down, for example, by decrementing the index from an upper limit until it reaches the lower limit.

Head-tested conditional looping

In Swift, a common form of loop which performs testing at the head uses while:
while x < 100 {

A typical implementation in ARM64 assembly language might start by setting up registers containing the initial value of x and the end limit, then check whether x has reached that end value. If it has, then branch to the next code after the loop. Otherwise perform the loop code and increment the x value before performing an unconditional loop back to the starting test and passing through the loop again.

Here’s an example in which the x value is stored in register X5, and the end value in X4:
MOV X5, #1 // start value = 1
MOV X4, #100 // end value = 100
while_loop: CMP X5, X4 // check whether end value has been reached
B.GE while_done // if value > end value, end loop
// do what you need to in the loop
// …
ADD X5, X5, #1 // increment value (or whatever needed to progress towards termination)
B while_loop // unconditional loop back
while_done: // next code after loop


A classic example of that is iterating through each of the single-byte characters in a null-terminated ASCII string.

In Swift, this might be called as:
var output: [CChar] = Array(repeating: 0, count: 255)
let s: String = "Sample string"
ascii_copy(s, &output)

which calls the C header of:
extern int ascii_copy(const char *, char *);

Register usage follows:
// X1 - address of output string
// X0 - address of input string
// X4 - original output string for length calc.
// W5 - current character being processed

and the assembly code is:
_ascii_copy: MOV X4, X1
// loop until the byte pointed to by X1 is non-zero
loop: LDRB W5, [X0], #1 // load ASCII character and increment pointer
// do whatever you need to the character in W5
STRB W5, [X1], #1 // store character to output str
CMP W5, #0 // stop on hitting a null character
B.NE loop // loop if character isn't null
SUB X0, X1, X4 // get the length by subtracting the pointers
RET // Return to caller

Branching out of a loop

Control transfer statements such as continue, break, return and throw can be implemented using an instruction which sets the NZCV flags, then a conditional branch, such as
// do things
CMP X5, X4 // compare the two registers
B.GT break_loop // break out of loop
// do other things
B loop // repeat loop
break_loop: // break out of loop and continue with next code


All four of these idioms are collected together in today’s tear-out PDF: ARMcontrolflow1

In the next article, I’ll start looking at some common general-purpose register instructions.

The example ASCII string code above is based on Alexander von Below’s modified code to accompany Stephen Smith’s book.

Previous articles in this series:

1: Building an app to develop assembly routines, including an explanation of calling assembly language from Swift, with a complete Xcode project
2: Registers explained
3: Working with pointers
4: Controlling flow


ARM register summary
ARM operand architecture
Conditions and conditional branching instructions
AsmAttic, a complete Xcode project


Procedure Call Standard for the Arm 64-bit Architecture (ARM) from Github
Writing ARM64 Code for Apple Platforms (Apple)
Stephen Smith (2020) Programming with 64-Bit ARM Assembly Language, Apress, ISBN 978 1 4842 5880 4.
Daniel Kusswurm (2020) Modern Arm Assembly Language Programming, Apress, ISBN 978 1 4842 6266 5.
ARM64 Instruction Set Reference (ARM).