Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 5

ARM Assembly Fundamentals: Data Movement & Arithmetic

Recommended resources

Hello! Welcome back to your interview preparation course.

In the last lesson, we covered the ARM Procedure Call Standard (AAPCS), which defines the "rules of the road" for how functions interact. We learned how arguments are passed, values are returned, and who is responsible for saving registers.

Now that we understand the rules, it's time to learn the language itself. This lesson will equip you with the fundamental vocabulary of ARM assembly: the instructions for moving data and performing basic calculations. Mastering these is the first and most crucial step toward writing and understanding any low-level code on an ARM processor.

1. The Foundation: A Load-Store Architecture

Before we dive into specific instructions, we must understand a core design principle of ARM processors: they have a load-store architecture. This might be different from other architectures you've encountered.

What does this mean?

  • Data processing instructions (like ADD, SUB, AND, etc.) can only operate on values held in registers or immediate (constant) values embedded directly in the instruction. They cannot directly access memory.
  • Memory access is restricted to a specific set of instructions, primarily LDR (Load Register) and STR (Store Register), which move data between memory and the processor's registers.

Think of it this way: to perform an operation on data that resides in memory, you must first explicitly load it into a register, perform the operation using that register, and then, if needed, explicitly store the result back to memory. This is a key principle of RISC (Reduced Instruction Set Computer) design, aiming for simpler, faster instructions.

For a brief overview of this concept, let's start with a short video clip.

ARM Assembly: Lesson 3 (LDR, STR)

The video 'ARM Assembly: Lesson 3 (LDR, STR)' by LaurieWired begins by clearly explaining the load-store architecture concept.

Please watch the first minute of the video (00:00 - 00:59).


2. Data Movement Instructions

Data movement instructions are the most frequently used in any program. They move data between registers, load immediate values, and transfer data to and from memory.

2.1 Moving Immediate Values into Registers: MOV

The MOV (Move) instruction is the simplest way to get a value into a register. It's typically used to load a constant or copy a value from one register to another.

The basic syntax is:
MOV <DestinationRegister>, <Source>

The source can be an immediate value (a constant) or another register.

  • MOV R0, #42: Loads the decimal value 42 into register R0. The # symbol signifies an immediate value.
  • MOV R1, R0: Copies the content of register R0 into register R1.

Let's see this in action.

ARM Assembly: Lesson 1 (MOV, Exit Syscall)

The video 'ARM Assembly: Lesson 1 (MOV, Exit Syscall)' by LaurieWired provides a clear, practical demonstration of the MOV instruction.

Please watch from 10:50 to 12:35 to see the syntax for moving an immediate value, and then from 15:59 to 16:47 to see how the register values change in the debugger.

2.2 Moving Data To and From Memory: LDR and STR

As we established, LDR and STR are the gateways to memory.

  • LDR (Load Register): Reads data from a memory location into a register.
  • STR (Store Register): Writes data from a register to a memory location.

The most common way to specify the memory address is register indirect addressing, where a register holds the memory address you want to access. The syntax uses square brackets [] to denote that you are accessing the memory at the address held in the register.

  • LDR R1, [R0]: Reads a 32-bit word from the memory address stored in R0 and places it into R1.
  • STR R1, [R0]: Writes the 32-bit value from R1 to the memory address stored in R0.

Given your C programming background, you can think of [R0] as dereferencing a pointer stored in R0.

To really understand LDR and STR, we need to see them in a complete context, which includes defining data in memory, getting its address, and then operating on it. The following resource walks through this entire process.

ARM Assembly: Lesson 3 (LDR, STR)

Let's return to 'ARM Assembly: Lesson 3 (LDR, STR)' to see a full demonstration of memory access.

Please watch from 01:21 to 14:23. This is a crucial section that covers: Defining data in memory using .data and .word directives. Loading an address: Using LDR R0, =var1 to get the memory address of var1 into R0. Note the = sign; this is a pseudo-instruction that the assembler uses to load an address. Loading a value: Using LDR R1, [R0] to dereference the address in R0 and load the actual value from memory into R1. Storing a value: Using STR R2, [R3] to store a new value back into a memory location.

Test your understanding!

Consider the following assembly snippet:

.data
my_var: .word 100

.text
.global _start
_start:
    LDR R0, =my_var  ; Instruction 1
    LDR R1, [R0]     ; Instruction 2

After these two instructions execute, what are the values in registers R0 and R1?

Show answer
  • R0 will contain the memory address of my_var.
  • R1 will contain the value 100.

Instruction 1 is a pseudo-instruction that loads the address of the label my_var into R0. Instruction 2 then uses that address to dereference memory and load the 32-bit word stored at that location (which is 100) into R1.


3. Arithmetic and Logical Instructions

These are the core data-processing instructions. They follow a very regular format, typically taking two source operands and placing the result in a destination register.

For a comprehensive reference on the syntax and semantics of these instructions, the following document is very useful.

ARMR Assembly Language

The document 'ARMR Assembly Language' provides concise tables summarizing the basic data processing instructions.

Please read section 4.2.1, 'Simple Data Processing Instructions' (pages 140-142). Focus on the tables for 'Register Transfer Instructions', 'Arithmetic Instructions', and 'Logical Instructions'. This will formally introduce MOV/MVN, ADD/SUB, and AND/EOR/ORR/BIC.

Let's highlight the most common ones you'll use.

3.1 Arithmetic Operations: ADD and SUB

  • ADD Rd, Rn, Rm: Rd = Rn + Rm
  • ADD Rd, Rn, #imm: Rd = Rn + immediate_value
  • SUB Rd, Rn, Rm: Rd = Rn - Rm
  • SUB Rd, Rn, #imm: Rd = Rn - immediate_value

Example:
To implement x = a + b where a is in R1 and b is in R2:
ADD R0, R1, R2 ; Result x is placed in R0

3.2 Logical Operations: AND and ORR

These bitwise operations are fundamental for low-level driver development, as they are used to manipulate individual bits in hardware control registers.

  • AND Rd, Rn, Rm: Performs a bitwise AND between Rn and Rm, stores result in Rd.
  • ORR Rd, Rn, Rm: Performs a bitwise OR between Rn and Rm, stores result in Rd.

As with arithmetic instructions, the second source operand can also be an immediate value.

Practical Use Case: Register Modification

Your work in driver development frequently involves read-modify-write cycles on peripheral registers. AND and ORR are the tools for this at the assembly level.

  • To clear a bit (masking): Use AND. To clear bit 4 of a register R1, you would AND it with a mask where bit 4 is 0 and all other bits are 1.
    AND R1, R1, #0xFFFFFFEF ; Clears bit 4 of R1

  • To set a bit: Use ORR. To set bit 4 of R1, you would ORR it with a mask where bit 4 is 1 and all other bits are 0.
    ORR R1, R1, #0x00000010 ; Sets bit 4 of R1


4. Putting It All Together: A Simple Function

Let's combine what we've learned with the AAPCS rules from the previous lesson to write a complete assembly function.

C Prototype: int calc(int a, int b);
Functionality: return (a + 5) - b;

Here's the ARM assembly implementation:

.global calc       // Export the 'calc' symbol to be visible to the linker

calc:
    // Function Prologue - not strictly needed here as we don't call
    // other functions and only use caller-saved registers, but good practice.
    
    // Function Body
    ADD R0, R0, #5     // R0 = a + 5. Per AAPCS, 'a' is in R0.
    SUB R0, R0, R1     // R0 = (a + 5) - b. Per AAPCS, 'b' is in R1.
    
    // Function Epilogue
    BX LR              // Return to caller. The result is already in R0,
                       // as required by the AAPCS.

This simple function demonstrates:

  1. Following the AAPCS for input arguments (R0, R1) and the return value (R0).
  2. Using an ADD instruction with an immediate value.
  3. Using a SUB instruction with a register operand.
  4. Correctly returning using BX LR.

Conclusion

In this lesson, we've covered the essential building blocks of ARM assembly programming. You now have the vocabulary to move data and perform fundamental computations.

Key Takeaways:

  • ARM is a load-store architecture: operations happen on registers, and LDR/STR are used for memory access.
  • MOV is for loading immediate values or copying between registers.
  • LDR loads from memory into a register; STR stores from a register into memory. The [Rx] syntax is key for dereferencing.
  • ADD and SUB perform basic arithmetic.
  • AND and ORR are crucial for bitwise manipulation, a cornerstone of driver development.

Next Lesson Preview:

A program isn't just a linear sequence of calculations. It needs to make decisions and repeat actions. In our next lesson, we will explore control flow instructions. We will learn how to use CMP (Compare) and TST (Test) to set processor flags, and then use conditional branch instructions (B<cond>, BL<cond>) to create loops and if-then-else structures.