Section 12.3 Binary Decisions
We now know how to implement two of the primary program flow constructs—sequence and repetition. We continue on with the third—binary decision. You know this construct from high-level languages as the if
or if-else
statement.
Subsection 12.3.1 Simple if-else
We start the discussion with a common example—a simple program that asks the user whether changes should be saved or not (Listing 12.3.1). This example program does not do anything, so there really is nothing to change, but you have certainly seen this construct. (As usual, this program is meant to illustrate concepts, not good C/C++ programming practices.)
Let's look at the flow of the program that the if-else
controls:
The Boolean expression
(response == 'y')
is evaluated.If the evaluation is true, the first block, the one that displays “Changes saved.”, is executed.
If the evaluation is false, the second block, the one that displays “Changes discarded.”, is executed.
In both cases the next statement to be executed is
return 0;
.
The way that the if-else
construct controls program flow can be seen from the flow chart in Figure 12.3.2.
This flow chart shows that we need the following assembly language tools to construct an if-else
:
Instruction(s) to evaluate Boolean expressions.
An instruction that conditionally transfers control (branches) to another location in the program. This is represented by the large diamond, which shows two possible paths.
An instruction that unconditionally transfers control to another location in the program. This is represented by the line that leads from “Execute ‘Then’ part” to the end of the
if-else
construct. Program control must branch around the “Else” part.
We already know the instructions that are needed to implement an if-else
construct. The important thing to note is that there must be an unconditional branch at the end of the “then” block to transfer program flow around the “else” block. The assembly language generated for this program is shown in Listing 12.3.3.
This program introduces a new instruction, ldrb
, which as you probably guess, loads a byte from memory. We also introduce the strb
instruction for completness.
LDRB
-
Loads a byte from memory into a register and zeroes the 24 high-order bits.
LDRB<c> <Rt>, <label> % Label LDRB<c> <Rt>, [<Rn>{, #+/-<imm>}] % Offset, immediate LDRB<c> <Rt>, [<Rn>, #+/-<imm>]! % Pre-indexed, immediate LDRB<c> <Rt>, [<Rn>], #+/-<imm> % Post-indexed, immediate LDRB<c> <Rt>, [<Rn>, +/-<Rm>{, <shift>}] % Offset, register LDRB<c> <Rt>, [<Rn>, +/-<Rm>{, <shift>}]! % Pre-indexed, register LDRB<c> <Rt>, [<Rn>], +/-<Rm>{, <shift>} % Post-indexed, register
<c>
is the condition code, Table 9.2.1.<Rt>
is the destination register,<Rn>
is the base register, and<Rm>
contains an offset value.<label>
is a labeled memory address.<imm>
is a signed integer in the range \(-2048 \ldots +2047\text{.}\)
The memory address to load the word from is determined the following way (further explained in Section 11.1):
The Label form uses the address corresponding to the
<label>
.In the Offset form, the signed integer is added to the value in the base register,
<Rn>
, the value at this address is loaded into<Rt>
, but the base register is not changed.In the Pre-indexed form, the signed integer is added to the value in the base register,
<Rn>
, the base register is updated to the new address, and then the value at this new address is loaded into<Rt>
.In the Post-indexed form, the signed integer is added to the value in the base register,
<Rn>
, the value at this new address is loaded into<Rt>
, and then the base register is updated to the new address.
STRB
-
Stores a word from a register into memory.
STRB<c> <Rt>, <label> % Label STRB<c> <Rt>, [<Rn>{, #+/-<imm>}] % Offset, immediate STRB<c> <Rt>, [<Rn>, #+/-<imm>]! % Pre-indexed, immediate STRB<c> <Rt>, [<Rn>], #+/-<imm> % Post-indexed, immediate STRB<c> <Rt>, [<Rn>, +/-<Rm>{, <shift>}] % Offset, register STRB<c> <Rt>, [<Rn>, +/-<Rm>{, <shift>}]! % Pre-indexed, register STRB<c> <Rt>, [<Rn>], +/-<Rm>{, <shift>} % Post-indexed, register
<c>
is the condition code, Table 9.2.1.<Rt>
is the source register, and<Rn>
is the base register.<label>
is a labeled memory address.<imm>
is a signed integer in the range \(-2048 \ldots +2047\text{.}\)
The memory address to store the word at is determined the following way (further explained in Section 11.1):
The Label form uses the address corresponding to the
<label>
.In the Offset form, the signed integer is added to the value in the base register,
<Rn>
, the value in<Rt>
is stored at this address, but the base register is not changed..In the Pre-indexed form, the signed integer is added to the value in the base register,
<Rn>
, the base register is updated to the new address, and then the value in<Rt>
is stored at this address.In the Post-indexed form, the signed integer is added to the value in the base register,
<Rn>
, the value in<Rt>
is stored at this address, and then the base register is updated to the new address.
Our assembly language version of the yes/no program in Listing 12.3.4 follows this general pattern. It, of course, uses more meaningful labels than what the compiler generated.
The branch out of the “then” block over the “then” block:
yesLoop: ldrb r3, [r4] @ get a char cmp r3, NUL @ end of string? beq endThen @ yes, end of then block mov r0, STDOUT @ no, write to screen mov r1, r4 @ address of current char mov r2, 1 @ write 1 byte bl write add r4, r4, 1 @ increment pointer var b yesLoop @ back to top endThen: b endElse @ branch over else block
could be done more efficiently with:
yesLoop: ldrb r3, [r4] @ get a char cmp r3, NUL @ end of string? beq endElse @ yes, end of if-else construct mov r0, STDOUT @ no, write to screen mov r1, r4 @ address of current char mov r2, 1 @ write 1 byte bl write add r4, r4, 1 @ increment pointer var b yesLoop @ back to top
But it would be very rare for this gain in efficiency to be noticeable. On the other hand, using the strict “if-then-else” structure makes the code more readable for anyone who modifies it. The trade-off here is an almost certainly unnoticeable performance gain versus a fair chance of introducing a bug into the code. (In case you have not learned this yet, it is much easier to make a coding error in code that is more difficult to read.)
The overall structure of an if-else
can be seen in Listing 12.3.5 follows this general pattern. It, of course, uses more meaningful labels than what the compiler generated.
Subsection 12.3.2 Range Checking
Another common programming problem is to check to see if a variable is within a certain range. This requires a compound Boolean expression, as shown in the C program in Listing 12.3.6.
We will move directly to my assembly language version of this range-checking program in Listing 12.3.7.
When a Boolean expression is used to control the flow of a program (for example, while
loop, if-else
) most language employ Short Circuit Evaluation of the expression. That is, as each term is evaluated, if it is determined that the entire expression evaluates to either “true” or “false” the appropriate action is taken, without evaluating the remaining terms in the expression. We can see how this is implemented by looking at the assembly language that implements the Boolean expression,
((response <= '9') && (response >= '0'))
in Listing 12.3.6.
From Listing 12.3.7 this is the code sequence:
ldrb r3, [fp, #-response] @ load response cmp r3, #'9 @ check high end bhi notNumeral @ >'9', other char cmp r3, #'0 @ check low end blo notNumeral @ <'0', other char
If it is determined that the user's response is higher than the character ‘9
’, there is no need to do further checking. The bhi notNumeral
instruction “short circuits” further evaluation of this Boolean expression.