Writing an APCS program


The APCS, or ARM Procedure Call Standard, provides a mechanism for writing tightly defined routines in assembler. This may seem possibly wasteful when you consider the case of writing your own project entirely in assembler. However, modern applications tend to be of a hybrid fashion - the base written in a high level language, with speed-critical parts written in assembler. Then, APCS comes into its own.



In a DDE application, several directories are used:

As c is used for C code, it stands to reason that s is used for assembler.
Me too. I suspect 's' means source, from the days when real programmers shunned high level languages.
Whatever the reason, by convention the assembler code goes in a sub-directory called 's'.




Registers R0 to R3 are destructible. All the rest (save R14 (lr)) must be properly preserved, if they are in any way altered by your code.

APCS defines the registers using different names to our usual R0 to R14. With the power of the assembler pre-processor, you can define R0 etc, but it is just as well to learn the APCS names in case you are modifying code written by others.

Register names
Reg #  APCS   Meaning
R0 a1 Working registers
R1 a2 "
R2 a3 "
R3 a4 "
R4 v1 Must be preserved
R5 v2 "
R6 v3 "
R7 v4 "
R8 v5 "
R9 v6 "
R10 sl Stack Limit
R11 fp Frame Pointer
R12 ip  
R13 sp Stack Pointer
R14 lr Link Register
R15 pc Program Counter


These names are not defined by standard in Acorn's older objasm, though other assemblers (such as Nick Roberts' ASM, and the later (32 bit compatible) objasm) should define them for you.
To define a register name in objasm, you use the RN directive, at the very start of your program:

a1     RN      0
a2     RN      1
a3     RN      2

r13    RN      13
sp     RN      13
r14    RN      14
lr     RN      r14
pc     RN      15
That example shows us two important things:
  1. That registers can be multiply defined - you can have both 'r13' and 'sp'.
  2. That registers can be defined from previously defined registers - 'lr' was defined from the setting of 'r14'.
    (this is correct for objasm, other assemblers may not do this)
Other assemblers may differ, like using a command such as "BINDREG", refer to the instructions for your particular example.

You can set up a header file, if you don't want to do all of this in every module you write. Then, for objasm, you simply:

        GET    h.regnames
For ASM, you would use the INCLUDE directive. Other assemblers may vary.




The next thing that you must do is to define your area. This may be CODE or DATA, and can optionally be READONLY.
Other areas exist, but are outside the scope of this document.
For our purposes, we shall be writing a program or function, so our area will be a READONLY CODE area.

We define this with:

        AREA   |main|, CODE, READONLY
This sets up an area called "main". The area name must be enclosed in vertical bars, and it may be any valid identifier.

If we were linking our code with C, then we would call the area "|C$$code|". This is described in mode detail in example 6. You cannot call a function "main" when interworking with C, as by convention the initial function of a C program is main(). Likewise, you cannot call your function the same as an existing one.



Entry point

For a stand-alone program, your next directive would be:
to tell the assembler that this is where your program wishes to be entered. You can only have one entry point per program.

When interworking with a high level language, you do not have an entry point. Instead, you have a collection of routines that are exported. Refer to example 6 for more information.
Please note, there is quite a lot of stuff that should be set up for a stand-alone assembler program, such as reading command line parameters and stack management. It gets even hairier if you wish to write a multitasking application entirely in assembler. Therefore, it is recommended that you write the basis of your program in C, and use assembler for the parts that require the speed benefit. However, example 5 will describe a simple utility written entirely in assembler.




Your code follows.

Now is the time to explain why the previous examples were indented eight spaces, except for the register definitions.

In assembler code, there is a very simple syntax. Things on the left are counted as identifiers, while things that are indented are either instructions or directives.
Any line beginning with a semicolon is a comment, and a semicolon in a line of code means that the rest of the line is a comment.
You do not start labels with a period. Unlike BASIC, a colon does not start a new instruction. You can only have one instruction per line.
For example:

r0 RN  0

        AREA |main|, CODE, READONLY

        ADR    r0, title
        SWI    &02         ; OS_Write0

        SWI    &10         ; OS_GetEnv
        SWI    &02         ; OS_Write0
        SWI    &03         ; OS_NewLine
        SWI    &11         ; OS_Exit

        =      "This program was called with:", 10, 13, "   ", 0

When run, the program outputs a short title, followed by the command line that started the program. For example:
TaskWindow Server v0.01

Dir. IDEFS::Willow.$.Coding.Projects.Assembler.apcstest Option 00 (Off) 
CSD  IDEFS::Willow.$.Coding.Projects.Assembler.apcstest
Lib. IDEFS::Buffy.$.library
URD  IDEFS::Stephanie.$
o            D/      s            D/     test         WR/
*test -this -is a -test
This program was called with:
   test -this -is a -test



But... that doesn't work!

If you are not using objasm, then chances are it won't work correctly. Each assembler available does the basic instruction set, and also some of the BASIC commands, such as ALIGN and DCD. However, specifics and directives are pretty much up to the author of the assembler.
As an example, many of the assemblers will do SWI name expansion, where SWI "OS_Exit" will read the SWI name and calculate the SWI number for you; and ARMmaker will go as far as to allow you to specify output types (ABSOLUTE, MODULE, AOF, etc) which is very useful if you don't have a linker.

You may also like to refer to my opinions on various assemblers.



The examples

The examples have been written for objasm.

Example 5:
A simple example written entirely in assembler.
Example 6:
An example of combining assembler and C.




A full explanation of APCS and assemblers is out of the scope of this guide. Assuming that you are familiar with assembler, the instructions for ASM and ARMmaker are both interesting reads. These two documents should provide you with sufficient information to obtain good results from your assembler, whichever you choose, and help you write productive code.
However, for complete instructions (should your require them), you would need to read the PRMs and the Acorn Assembler documentation.



And finally...

You will (by the time you've read the details for example 6), realise that I am overstressing the following point:
Don't recode everything in assembler because you can.
Modern compilers aren't stupid. The Norcroft v4.00 compiler, apparently, doesn't generate totally optimised code [source: lots of arguments on comp.sys.acorn.programmer over the years] but it does perform some optimisations. And, instruction for instruction, a compiler can probably out-optimise many of us! I, for one, wouldn't want to take on a compiler against my code.

But don't be discouraged. Assembler has its uses. Here are some examples:

I'm sure you can think of more examples that might benefit from being coded in assembler.

Please be sure to read 'Don't be over zealous'.

Return to assembler index
Copyright © 2004 Richard Murray