AArch64/ARM64 Tutorial

Chapter 10: Compares and Branches

Sometimes we may have some instructions in a program that we only want to be executed under certain conditions. Such as the condition of a register being equal to a certain value. What we essentially need to do is add in alternate "routes/paths" of the execution of the CPU within our program.

In order to create these alternative routes, we need two types of instructions. Compare instructions and Branch instructions. To start let's go over the most basic Branch instruction..


Unconditional Branch:
b label

label? What is that? Branch Instructions do indeed use Immediate Values, but these Values would need to be manually calculated by hand. So to get around this, we use labels. The Assembler will read the labels and automatically know what Immediate Value to apply to the branch instruction.

Example:
b jump_here

add w13, w14, w15

jump_here:
mov x1, #1

In the above example, when the branch instruction gets executed, the execution flow of the CPU will jump over (skip) the add instruction. It will "land" at the mov instruction and thus execute the mov.

As you can see with the use of the labels, we can name the labels pretty much whatever we want (with some restrictions). When you use a label in a branch instruction, its "landing spot" must contain the same exact label name but be appended with a colon.


Moving onto Conditional Branches... Conditional branches are branches that only execute base on an 'if'. For example let's look at the 'branch if equal' instruction...

Branch If Equal:
beq the_label

the_label will only be 'jumped' to if and only if the conditional branch is true. In order to set up this 'if' for a conditional branch, we need to make a comparison. There are only two different compare instructions for ARM64. Let's go over the most common of the two..


Compare:
cmp xD, aimm
cmp wD, aimm
cmp xD, xD
cmp wD, wD

Example compare and conditional branch code:
cmp x0, #0
beq jump_here
add w1, w2, w3
jump_here:
str x7, [x7]

What occurs if x0 is 0

  1. The compare instruction gets executed. Certain condition flags are set to let any future conditional branch know on how to proceed.
  2. Conditional Branch (beq) gets executed. It sees that x0 is indeed equal to 0.
  3. Since x0 = 0, the Conditional branch gets executed. 
  4. Execution of the CPU jumps (skips) over the add instruction and "lands" at the str instruction
  5. str instruction is now executed.

What occurs if x0 is *not* 0

  1. The compare instruction gets executed. Certain condition flags are set to let any future conditional branch know on how to proceed.
  2. Conditional Branch gets executed and sees the conditions (x0 = 0?) is *NOT* met. Therefore, nothing happens, there is no jump. Execution of the CPU continues directly downward.
  3. add instruction gets executed
  4. str instruction gets executed.

Here is a picture of the above example of code. The magenta arrows show the general execution flow of the CPU (downward). Once the conditional branch is reached, the execution of the CPU splits into two paths. The green path shows the path for when x0 = 0. The red path shows the path for when x0 =/= 0.

Let's look at another example of code that implements conditional branching:

cmp x0, #0
beq condition_met
mov x1, #1
b the_end
condition_met:
strh w1, [x2]
the_end:
str x3, [x4, #0x40]

In the above code we can see a conditional branch present and an unconditional branch present. Why is there an unconditional branch? Well we want the strh instruction to only execute when x0 = 0. Not only that, we want the mov instruction to only execute when x0 =/= 0. We want the str instruction to be executed last in both scenarios. Therefore to satisfy those requirements, an unconditional branch had to be placed in to make sure both paths don't collide with each other.

Let's follow each path.....

What occurs when x0 = 0

  1. Compare instruction gets executed.
  2. beq instruction is executed. The conditional branch will be taken since x0 does indeed equal 0
  3. Execution the CPU hops over the mov and "b the_end" instructions to "land" at the strh instruction
  4. strh gets executed
  5. Execution of CPU continues in normal downward fashion to then execute the final (str) instruction

What occurs when x0 =/= 0

  1. Compare instruction gets executed
  2. beq gets executed but the branch is *NOT* taken since x0 =/0. Therefore beq acts like a nop (do nothing).
  3. Execution of CPU continues downward, the mov instruction gets executed.
  4. Unconditional Branch (b the_end) gets executed, execution of CPU hops over the strh instruction to skip it and "lands" at the final (str) instruction.
  5. Final (str) instruction gets executed.

Here is a picture of the above snippet of code. The magenta arrows show the general execution flow of the CPU (downward). Once the conditional branch is reached, the execution of the CPU splits into two paths. The green path shows the path for when x0 = 0. The red path shows the path for when x0 =/= 0.



Signed vs Unsigned Treatment of Values:
Back in Chapter 6, I've mentioned about a point where compares+branches determine if a value in a Register is to be treated as Signed, or treated as Unsigned. What I was referring to is enforcing Signed vs Unsigned based on what kind of conditional branch is used after the compare. This will allow you to choose how to treat Register and/or Immediate Values in the Compare instruction.

Example:
bhs label // Branch if Greater than or Equal to (Unsigned).

Here are all the conditional branch instruction options~


So for example. Let's say we have two registers. w7 and w19. Pretend that...

Let's preform a compare on the two values with the following instruction...

cmp w2, w19

Now remember we can determine how the values are treated via what kind of conditional branch we are using. Let's say we want the values to be treated as Signed. We will also pretend, we want the execution of this code to take a branch if and only if w2 is greater than w19. Since we want the values treated as Signed and we want a Branch for when w2 > w19, we have the following two instructions

cmp w2, w19 //Compare w2 vs w19
bgt somewhere //If w2 > w19, take the branch

Now in the above code, the conditional branch (bgt), will **NOT** be taken. w2 contains the value of -1 because 0xFFFFFFFF is being treated as Signed. w19 contains the value of 160 (0xA0). -1 is **NOT** greater than 160. Thus, the conditional branch is skipped.

What if we used a bhi (Unsigned Greater Than) conditional Branch?

cmp w2, w19 //Compare w2 vs w19
bhi somewhere //If w2 > w19, as unsigned, take the branch

Would the branch be taken? YES, it would. Confused? Let's go over it. The bhi will "treat" the values in w2 and w19 as Unsigned. This will cause w2 to be the value of 4,294,967,295 instead of having -1. w19 is still 160.


Okay, you've now learned about compares and branches. Let's do a quick exercise using your new skills. We want to write a Source of code that does the following...

Let's pretend the word value in question is located at the address in x12. We will use w6 & w7 for our multiplication operation. Now we need to start off with a ldr instruction ofc. Let's write it out...

ldr w6, [x12] //Load our word value from memory into w6

Our word value is now in w6. Remember we can only perform the multiplication if it's 0 or positive. Well we know we need a comparison instruction + conditional branch instruction. We should be using a compare instruction against the value of 0. Why? Well since the multiplication can only be allowed to perform if the loaded value is 0 or positive, it would make sense to use 0 as the "reference point" or "middle ground" for our compare instruction.

What kind of conditional branch though? Well the phrase "0 or positive" is just an alternative saying for "greater than equal to 0". This implies that negative values are indeed possible. Since that is the case, we should be treating the value in w6 as Signed.

Therefore, we should be using the blt branch. As an fyi, the blo branch is the Unsigned version. Let's apply the blt branch with the attached label name "done" and see what our code looks like now.

ldr w6, [x12] //Load our word value from memory into w6
cmp w6, #0 //Compare value in w6 against Zero
blt done //If w6 is less than Zero (signed), branch to the "done:" label

Now the question is, where do we make the conditional branch land at? Well that's simple, we will have it at the very end of our Source. To do that, the "done:" label will be at the very bottom of our Source. Before we place that in and view our current progress, let's cover the multiplication operation.

We will use the standard/basic mul instruction. Remember that Immediate Value usage is prohibited in this instruction. Our multiple (13) will need to be placed into a different register beforehand. We will use w7 for that.

mov w7, #13 //Place the multiple into w7

Now we can perform the multiplication.

mul w6, w6, w7 //w6 x w7 = w6

In the mul instruction, we've set w6 as the Destination Register to overwrite the old w6 value. You could of course use a different register as a scratch/temp register, but there's no need to waste an unnecessary use of a another register.

Okay we got our multiplication completed. We need to store it back to where we've loaded it from.

str w6, [x12] //Store new value back to where we've loaded it from

Alright, remember that "done:" label that we need? Well since it has to be the last item of our Source, and the str instruction is our last ARM64 instruction, the label must be underneath the str instruction. With all of that being stated, here is our complete source...

ldr w6, [x12] //Load our value from memory into w6
cmp w6, #0 //Compare value in w6 against Zero
blt done //If w6 is less than Zero (signed), branch to the "done:" label
mov w7, #13 //Place the multiple into w7
mul w6, w6, w7 //w6 x w7 = w6
str w6, [x12] //Store new value back to where we've loaded it from
done:

Sweet. We did it. Our Source is complete.


Final Notes:
It's important to understand the conditional branch instruction "analyzes" the results from the MOST RECENT compare instruction. Also "official" ARM format of conditional branch instructions requires a dot (.) to be placed within the instruction name, after the "b" (i.e. b.ne). This is absolutely silly in my opinion. Lucky for us, every ARM64 Assembler that I know of allows you to omit this silly requirement. Debugging tools, such as the GNU Debugger, will include this dot when displaying conditional branches in programs.


Next Chapter

Tutorial Index