APCS

(ARM Procedure Call Standard)

 

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.
Lost?
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 subdirectory called 's'.

 

 

Registers

Registers R0 to R3 are destructible. All the rest 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 preprocessor, 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 objasm, though other assemblers (such as Nick Roberts' ASM) may define them for you.
To define a register name, you use the RN directive, at the very start of your program:

a1     RN      0
a2     RN      1
a3     RN      2
    ...etc...

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)
You can set up a header file, if you don't want to do 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.

 

 

Areas

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:
        ENTRY
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.

 

 

Code

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
        ENTRY

        ADR    r0, title
        SWI    &02         ; OS_Write0

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


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

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

*cat
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.

I have tried the program on several popular assemblers, and have noted the versions that I have used...

 

AAsm 2.00 (Acorn) v2.00
This is a simple assembler that doesn't support areas, and it outputs an Absolute file (there is no linker stage).
Removing the AREA and ENTRY lines will allow the file to compile correctly.
The output is 60 bytes long, as there is no AOF header.

 

ARMmaker 0.52 (Steven Haslam)
I cannot get this to output AOF files. I define ENTRY start and AREA main, CODE, READONLY at the start of the program, and ARMmaker replies "Entry point "start" not defined".
I put this down to my unfamiliarity with the program. Asking ARMmaker to output an absolute file worked, creating a 60 byte program (identical to AAsm's output).

The code used for ARMmaker was:

r0 BINDREG R0

        OUTPUT ABSOLUTE

start
        ADR    r0, title
        SWI    "OS_Write0"

        SWI    "OS_GetEnv"
        SWI    "OS_Write0"
        SWI    "OS_NewLine"
        SWI    "OS_Exit"


title
        DCS    "This program was called with:\n\r   "
        DCB    0
        ALIGN
Like Nick Roberts' ASM, this assembler can convert SWI names for you.

This guide was written in France. When I return to England, I shall contact the author of ARMmaker to clarify how AOF files are created, and will update this when I find out.

 

AS 1.21 (Niklas Röjemo)
AS was quite happy with the original objasm input.

As a bonus, AS can also cope with SWI name conversions.
AS is not a 'better' version of objasm as it lacks the macro facilities and the more exotic features of objasm. However, on the plus side it is supplied with source so you can add your own custom bits.

 

ASM 3.1 (Nick Roberts)
STOP PRESS - click for latest info on ASM
Trying to assemble the code was met with the error "Internal error in translate_name : Bad file type character".
A rather unfriendly error, what it means is that ASM seems to prefer its code to be in a subdirectory called 'a' ('a' for assembler, more logical than 's' I guess...).
To be fair to Nick, this is one of the first things mentioned in the documentation - so sometimes it pays to RTFM. However, 's' is the convention, and that message could have been more helpful...
[editors note: fixed, see note below]

One of the more complex of the free assemblers, ASM supports IF...THEN, macros, different processors. All in all, this may rate as my favourite non-Acorn assembler. But note that, unlike Steven Haslam's ARMmaker, ASM requires a linker. However, linkers are available - such as DrLink.

ASM failed to recognise the RN directive. This isn't a big issue, as the standard (r#) and APCS names are understood by the assembler.
ASM requires that labels are suffixed with a colon and, like ARMmaker, it wasn't happy with the string syntax so the more traditional EQUS/EQUB has been used.
END was not required, and as you can see ASM is capable of converting SWI names on the fly.
I am describing the differences in ASM so verbosely not because I want to pick on ASM, but because I feel that Nick's minor changes (such as the colon) and the SWI conversion all make for much tidier looking code. I also like the EQUx directives, I use them in BASIC and feel happier with them than with DCx or stuff like '='.

The code used for ASM was:

        AREA main, CODE, READONLY
        ENTRY start

start:
        ADR    r0, title
        SWI    "OS_Write0"

        SWI    "OS_GetEnv"
        SWI    "OS_Write0"
        SWI    "OS_NewLine"
        SWI    "OS_Exit"


title:
        EQUS   "This program was called with:\n\r   "
        EQUB   0
        ALIGN

 

STOP PRESS!
ASM is now up to version 4.09. It supports instructions for all processors ARM2 to StrongARM, with target settings so you can't compile invalid code for a given processor. It also features built-in assembly-time mathematical functions (sin, cos, tan, acs, etc etc), floating point code, NOPs, macros, full conditional assembly, structures (!!!), limited-scope labels, pragmas, and redefinable constants.
ASM now accepts code in 's' or 'a', or anywhere else you wish to put your code.
As always, a descriptive manual is supplied in Impression or text format.
I've not had time to review or use this version of ASM, but I get the feeling that it has left all of the other assemblers behind.

Download ASM

 

objasm 2.00 (Acorn)
This was used as the benchmark, so was obviously happy with the input.

 

TLA 0.1f (G.F.A.@ Lancaster)
TLA is a very peculiar assembler. It appears to have a central core which is machine independent along with various opcodes for the ARM processor.
It is a peculiar assembler (in my opinion), and I wasn't able to get an ADR style instruction assembled. To be precise, the OS_Write0 example (in the documentation) crashed with the same error (branch through zero at <somewhere in ROM>).
But, for interests sake, I have recoded the program to work with TLA, though I would recommend an a different assembler - something that is closer to the DDE style. The code used for TLA was:

        .ENTRY start
        .CODEAREA
        .PROC main

start
        SWI    OS_WriteS
        .ASCII "This program was called with:"
        SWI    OS_NewLine
        SWI    OS_WriteS
        .ASCII "   "

        SWI    OS_GetEnv
        SWI    OS_Write0
        SWI    OS_NewLine
        SWI    OS_Exit

        .END

 

 

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.

 

 

More...

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 probabaly out-optimise many of you. 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.
Return to assembler index
Copyright © 2000 Richard Murray