armasm, or to give it its current full name, the ARM/Thumb Macro Assembler, is the official assembler for writing assembly language code on the ARM microprocessor. The most recent version is bundled with version 1.2 of the Arm Developer Suite, available for Windows, Solaris, Red Hat Linux and HP/UX.

You may wonder why anyone in modern software development needs an assembler at all especially with the efficiency of the ARM C and C++ compilers, but there are three main reasons for writing in assembler on the ARM. The first is to implement interrupt handlers, particularly for FIQs (fast interrupt requests). The second is to produce highly optimised code for processing-intensive tasks; the ARM is increasingly being used for digital signal processing tasks (DSP) which are highly time-critical, and the DSP extensions in the recent ARM architecture V5E need some use of assembler to use all the features. And finally, assembler is commonly used for boot code, loaders, and related low-level system tasks, where access to all the ARM's registers and system instructions is required.

armasm is a powerful macro assembler which is used to convert text assembler source code files into binary object files. It follows the instruction syntax of ARM's definition of the instruction set, so consult your reference materials for full details and addressing modes of B, AND, EOR, SUB, RSB, ADD, ADC, SBC, RSC, TST, TEQ, CMP, CMN, ORR, MOV, BIC, MVN, MUL, MLA, SMLAL, SMULL, UMLAL, UMULL, MRS, MSR, LDR, STR, LDM, STM, SWP, SWI, CDP, MRC, MCR, LDC, STC, as well as everything in the architectures later than 4. (And remember: destination register comes first! Almost always!) But as well as performing the translation from instruction mnemonic into binary opcode, armasm has a number of other features to help the user deal with the quirks of the ARM architecture, and produce code more quickly.

First and most shockingly for most users is that in the ARM assembler, in common with such fine languages as COBOL and Python, initial whitespace in a line is significant. All normal instructions must be indented (the amount isn't significant), while labels must be flush to the left (and not marked with periods or colons), for example:
functionentry           ; label
    STMFD sp!,{r4}      ; instruction
    MOV r0, r5
(Note that comments are introduced with a semi-colon.)

File format
Each block of code is introduced by an AREA directive, which specifies whether it is code or data, read-only or read-write, etc. A block is ended by a new AREA directive. However each file must end with an END directive, to stop the assembler falling off the end of the file and hurting itself. AREA and END, like all armasm assembler directives, must be indented like regular instructions.

armasm has a number of pseudo-instructions (instructions which look like assembler mnemonics but are not), which are used to simplify the problems of handling 32-bit values in 32-bit opcodes. The problem is that all data for an instruction must be encoded, along with the instruction itself, in a 32-bit word. An arbitrary 32-bit word cannot be thus encoded. The MOV instruction can load certain constant values into memory (see your assembler manual for details), but other constants must be loaded from a region of memory known as a literal pool, which can store any 32-bit value.

When you are changing magic numbers in a program you won't want to be wondering how they can be encoded; however the pseudo-instruction "LDR rn,=value" will load an arbitrary value into a register in the most efficient way possible: a simple value like 0xff will be assembled with a move instruction (like MOV rn,#0xff which takes 1 cycle on a standard ARM7), while a more complex instruction will be converted into a load from a program counter-relative address (such as LDR rn,[pc,#OFFSET] which takes 3 cycles on an ARM7). This seems complicated, but the beauty of LDR is that you don't need to worry about it: just use LDR rn,=CONST. However, there is a little confusion created because LDR is not only a pseudo-instruction, but depending on the context an actual ARM assembler opcode mnemonic (LoaD Register).

The pseudo-instruction ADR can be used to load the address of a data value into a register if the address is nearby (see the manual for details, or watch for error messages). "ADR rn,label" will assemble to "ADD rn,pc,#offset" (one cycle on an ARM7) if the offset is small enough, or give an error message otherwise. This saves manually calculating offsets by subtracting labels from the PC. (You can, if you want, access the current PC value with "{PC}" or "." should you need to construct more complex expressions, or you can perform arithmetic on the actual value in the PC register.)

As already mentioned, labels are defined by the absence of whitespace at the start of the line. If you aren't familiar with assembler programming, labels are used to mark instructions for branches, and to identify data, kind of like line numbers in old BASIC programs or, well, labels in C and Perl. But there are a couple of clever features. Firstly, say you want to use more than the traditional alphanumeric characters in your label name. No problem, just surround it with pipe characters "|". For instance "|I'm dreaming of writing CaML!!!|" is a valid label. You can also have relative local labels which are given numbers, which need not be unique, and are referenced by "%f1" which means the next instance of the label "1", or "%b2" which means the last label "2".

ARM/Thumb code
ARM processors from the ARM7 upwards have supported 2 instructions sets, the standard ARM instructions, which are all encoded as 32-bit words, and the Thumb instructions, which has a smaller set of instructions encoded in 16 bits. armasm can produce either, and the instruction set to be used is specified on the command line, or by using CODE32 and CODE16 directives in assembly language files.

Register names
Like other assemblers, armasm lets you rename registers using the EQU directive (which defines constants). However, there is also a special directive "RN" which assigns a name to a register, for instance "counter RN r0" (if left-aligned) allows you to refer to register r0 as "counter" in subsequent code. This may appear to be nothing but syntactic sugar and it does nothing to prevent registers being referred to by more than one name, but it is handy for treating registers as if they were variables. The RLIST directive can be used in a similar way to define a register list to be used for multiple load/store instructions (LDM , and STM ,).

While we are on the subject, it's worth noting that the ARM assembler defines a number of alternative names for the sixteen main registers r0-r15. a1-a4 correspond to r0-r3; v1-v8 correspond to r4-r11; sb (static base) is r9; sl (stack limit) is r10; fp (frame pointer) is r11; ip (intra-procedure-call scratch register) is r12; sp (stack pointer) is r13; lr (link register) is r14; and pc (program counter) is r15. Some of these reflect the hardware (where r14 and r15 have special functions), while others are conventions for register usage defined in the ARM/Thumb Procedure Call Standard (ATPCS, formerly the thumbless APCS). These default names can create collisions with other names (however this more of a problem when you use inline assembler within your C, and call variables with the same names as registers), and it is a question of style how many are used. However "sp", "lr" and "pc" are most common.

armasm declares that it is a "macro assembler", but what does that mean? In fact, it allows for some very powerful code generation features. A macro in assembler is similar to a macro with parameters in C, such as "#define SQR(X) ((X)*(X))", which replaces e.g. the code "SQR(14)" with "((14)*(14))" at every occurence. However the syntax of macros in armasm is far different; this is however a good thing, because unlike in most (all?) C compilers where macros are converted in the preprocessor without any syntax checking, the use of macros is an integral part of the assembler, and if you make a mistake mid-way through a multi-line macro it will tell you where it is (not like in a C compiler which will generally not even know a macro had been used).

Here is a simple example of a macro. I say simple, but only if you understand the ARM instruction set. So I will try to explain. All (or, in some versions of the ARM processor, almost all) instructions are conditional, which means that depending on the values of certain status flags, they may not be executed, and the programmer decides which condition to apply to each instruction.

Conditions are specified by appending a condition code to the end of an assembler mnemonic, so that "B" means branch always, while "BLE" means branch if less than or equal and "BCC" means branch if the carry flag is clear, etc. These can be applied to (almost) any instruction, for instance a multiply "MUL rd, rm, rs", which calculates rm * rs and writes the result to rd.

Based on this, I shall show you a macro that allows you to square a value conditionally: "COND_SQ cond, rd, rs" will if condition "cond" is true, square the value in register "rs" and store it in "rd"; if the condition is false it will do nothing. First, we specify that we want to define a macro and identify the parameters:

    COND_SQ $cond, $rd, $rs
All parameters to the macro are preceded by dollar signs (just like variables in Perl!). This seems silly, but not so silly when we come to write our macro code:
    MUL$cond $rd, $rs, $rs
Using the "$" in "$cond" makes it simple to join the string $cond onto the opcode without a space intruding, as it would be hard to tell in MULcond where the string "MUL" ended and the variable "cond" began. A similar rationale exists in Perl. (Of course, you can do the same thing in C using the concatenate operator, but the ARM approach is arguably more elegant and certainly clearer to follow.)

Finally, we finish our macro with a "MEND" statement. Giving us:

    COND_SQ $cond, $rd, $rs
    MUL$cond $rd, $rs, $rs
To call this just insert in the body of your code:
    COND_SQ LT, r4, r3
This will convert to:
    MULLT r4, r3, r3

Other features
The ARM assembler does all the usual things other assemblers do. You can use "EQU" to define constants:
And use DCD to define constant data:
    DCD 1,2,3,4,5
You can also use the MAP command to define data structures. If you define a C structure called "DATA" as:
struct DATA {
    int a;
    long long b;
    int c;
then to access the members of the structure from assembler, you need to know the offsets of each member from the start of the structure. You can calculate each one manually, or to slightly simplify things, you can use the MAP command (here we assume "int" has length 32 bits and "long long" 64 bits):
    MAP   0
a   FIELD 4
b   FIELD 8
c   FIELD 4
end_of_struct FIELD 0
This will define "a" to be 4, "b" to be "a+4", "c" to be "b+8" and "end_of_struct" to be "c+4", which is slightly less error prone than the traditional:
a EQU 0
b EQU a+4
c EQU b+8
end_of_struct EQU c+4

armasm is a powerful assembler which makes writing assembly code on the ARM simple. Its quirks soon become second nature. It also integrates well with the IDE (integrated development environment) of CodeWarrior which ARM Ltd supplies as part of the Arm Developer Suite. Symbols used in the assembler show up in the debuggers, and you can step through assembler source files in the visual debugger AXD. If you need to write low-level code on the ARM, it's hard to imagine a better set of tools.

Happy programming, but remember, real programmers don't use assemblers. They memorise numerical opcodes and poke them directly into memory.

Sources: ARM training course, and ARM documentation, particulary the ARM Developer Suite Version 1.2 Assembler Guide.

If I've not explained something well, or you want to know more, or you have any comments, don't hesitate to msg me.

Log in or register to write something here or to contact authors.