5. Assembly Programming Principles
The previous chapters have covered the ARM instruction set, and using the ARM assembler. Now we are in a position to start programming properly. Since we are assuming you can program in BASIC, most of this chapter can be viewed as a conversion course. It illustrates with examples how the programming techniques that you use when writing in a high-level language translate into assembler.
5.1 Control structures
Some theory
A program is made up of instructions which implement the solution to a problem. Any such solution, or algorithm, may be expressed in terms of a few fundamental concepts. Two of the most important are program decomposition and flow of control.
The composition of a program relates to how it is split into smaller units which solve a particular part of the problem. When combined, these units, or sub-programs, form a solution to the problem as a whole. In high-level languages such as BASIC and Pascal, the procedure mechanism allows the practical decomposition of programs into smaller, more manageable units. Down at the assembly language level, subroutines perform the same function.
Flow of control in a program is the order in which the instructions are executed. The three important types of control structure that have been identified are: the sequence, iteration, and decision.
An instruction sequence is simply the act of executing instructions one after another in the order in which they appear in the program. On the ARM, this action is a consequence of the PC being incremented after each instruction, unless it is changed explicitly.
The second type of control flow is decision: the ability to execute a sequence of instructions only if a certain condition holds (e.g. IF...THEN...). Extensions of this are the ability to take two separate, mutually exclusive paths (IF...THEN...ELSE...), and a multi-way decision based on some value (ON...PROC...). All of these structures are available to the assembly language programmer, but he has to be more explicit about his intentions.
Iteration means looping. Executing the same set of instructions over and over again is one of the computer's fortes. High-level languages provide constructs such as REPEAT..UNTIL and FOR...NEXT to implement iteration. Again, in assembler you have to spell out the desired action a little more explicitly, using backward (perhaps conditional) branches.
Some practice
Having talked about program structures in a fairly abstract way, we now look at some concrete examples. Because we are assuming you have some knowledge of BASIC, or similar high-level language, the structures found therein will be used as a starting point. We will present faithful copies of IF...THEN...ELSE,FOR...NEXT etc. using ARM assembler. However, one of the advantages of using assembly language is its versatility; you shouldn't restrict yourself to slavishly mimicking the techniques you use in BASIC, if some other more appropriate method suggests itself.
Position-independence
Some of the examples below (for example the ON...PROC implementation using a branch table) may seem slightly more complex than necessary. In particular, addressing of data and routines is performed not by loading addresses into registers, but by performing a calculation (usually 'hidden' in an ADR directive) to obtain the same address. This seemingly needless complexity is due to a desire to make the programs position-independent.
Position-independent code has the property that it will execute correctly no matter where in memory it is loaded. In order to possess this property, the code must contain no references to absolute objects. That is, any internal data or routines accessed must be referenced with respect to some fixed point in the program. As the offset from the required location to the fixed point remains constant, the address of the object may be calculated regardless of where the program was loaded. Usually, addresses are calculated with respect to the current instruction. You would often see instructions of the form:
.here ADD ptr, pc, #object-(here+8)
to obtain the address of object in the register ptr. The +8 part occurs because the PC is always two instructions (8 bytes) further on than the instruction which is executing, due to pipelining.
It is because of the frequency with which this calculation crops up that the ADR directive is provided. As we explained in Chapter Four, the line above could be written:
ADR ptr, object
There is no need for a label: BASIC performs the calculation using the current value of P%.
Instead of using PC offsets, a program can also access its data using base-relative addressing. In this scheme, a register is chosen to store the base address of the program's data. It is initialised in some position-independent way at the start of the program, then all data accesses are relative to this. The ARM's register-offset address mode in LDR and STR make this quite a straightforward way of accessing data.
Why strive for position-independence? In a typical ARM system, the programs you write will be loaded into RAM, and may have to share that RAM with other programs. The operating system will find a suitable location for the program and load it there. As 'there' might be anywhere in the available memory range, your program can make no assumptions about the location of its internal routines and data. Thus all references must be relative to the PC. It is for this reason that branches use offsets instead of absolute addresses, and that the assembler provides the
LDR <dest>,<expression>
form of LDR and STR to automatically form PC-relative addresses.
Many microprocessors (especially the older, eight-bit ones) make it impossible to write position-independent code because of unsuitable instructions and architectures. The ARM makes it relatively easy, and you should take advantage of this.
Of course, there are bound to be some absolute references in the program. You may have to call external subroutines in the operating system. The usual way of doing this is to use a SWI, which implicitly calls absolute address &0000008. Pointers handed to the program by memory-allocation routines will be absolute, but as they are external to the program, this doesn't matter. The thing to avoid is absolute references to internal objects.
Sequences
These barely warrant a mention. As we have already implied, ARM instructions execute sequentially unless the processor is instructed to do otherwise. Sequence of high-level assignments:
LET a = b+c
LET d = b-c
would be implemented by a similar sequence of ARM instructions:
ADD ra, rb, rc
SUB rd, rb, rc
IF-type conditions
Consider the BASIC statement:
IF a=b THEN count=count+1
This maps quite well into the following ARM sequence:
CMP ra, rb
ADDEQ count, count, #1
In this and other examples, we will assume operands are in registers to avoid lots of LDRs and STRs. In practice, you may find a certain amount of processor-to-memory transfer has to be made.
The ARM's ability to execute any instruction conditionally enables us to make a straightforward conversion from BASIC. Similarly, a simple IF..THEN...ELSE such as this one
IF val<0 THEN sign=-1 ELSE sign=1
leads to the ARM equivalent:
TEQ val, #0
MVNMI sign, #0
MOVPL sign, #1
The opposite conditions (MI and PL) on the two instructions make them mutually exclusive (i.e. one and only one of them will be executed after the TEQ), corresponding to the same property in the THEN and ELSE parts of the BASIC statement.
There is usually a practical limit to how many instructions may be executed conditionally in one sequence. For example, one of the conditional instructions may itself affect the flags, so the original condition no longer holds. A multi-word ADD will need to affect the carry flag, so this operation couldn't be performed using conditional execution. The solution (and the only method that most processors can use) is to conditionally branch over unwanted instructions.
Below is an example of a two-word add which executes only if R0=R1:
CMP R0, R1
BNE noAdd
ADDS lo1, lo1, lo2
ADC hi1, hi1, hi2
.noAdd ...
Notice that the condition used in the branch is the opposite to that under which the ADD is to be performed. Here is the general translation of the BASIC statements:
IF cond THEN sequence1 ELSE sequence2 statement
;'ARM' version
;Obtain <cond>
B<NOT cond> seq2 ;If <cond> fails then jump to ELSE
sequence1 ;Otherwise do the THEN part
...
BAL endSeq2 ;Skip over the ELSE part
.seq2
sequence2 ;This gets executed if <cond> fails
...
.endSeq2
statement ;The paths re-join here
At the end of the THEN sequence is an unconditional branch to skip the ELSE part. The two paths rejoin at endSeq2.
It is informative to consider the relative timings of skipped instructions and conditionally executed ones. Suppose the conditional sequence consists of X group one instructions. The table below gives the timings in cycles for the cases when they are executed and not executed, using each method:
Branch |
Conditional |
|
Executed |
s + Xs |