ARM Thumb-2 Assembly Basics
Reading assembly is an essential debugging skill even if you rarely write it. Learn the Cortex-M register file (R0–R15, xPSR), the load/store architecture, key Thumb-2 instructions (MOV, LDR/STR, ADD/SUB, CMP, B/BL/BX, PUSH/POP), condition flags N Z C V, the AAPCS calling convention, inline assembly and intrinsics, and how C constructs compile to instructions.
When a HardFault stops your program at address 0x08001A3C, the debugger shows you assembly. When an ISR is too slow, the disassembly shows why. When you write a context switch or startup code, some assembly is unavoidable.
ARM Cortex-M cores execute the Thumb-2 instruction set: a mix of 16- and 32-bit instructions with high code density. The architecture is load/store — arithmetic only works on registers, and memory is accessed only through LDR/STR.
An analogy
Registers are the CPU's hands, memory is the warehouse shelves. The CPU can only work on what it holds in its hands (registers); LDR fetches from a shelf and STR puts it back.
Registers and the load/store model
Cortex-M has 16 visible 32-bit registers. R0–R12 hold data, SP points to the stack, LR holds the return address after BL, and PC is the current instruction address. To add two variables from memory: LDR both into registers, ADD, then STR the result back.
Flags and conditional execution
Instructions ending in S (ADDS, SUBS) and CMP/TST update N (result negative), Z (result zero), C (unsigned carry/no-borrow) and V (signed overflow). Conditional branches test combinations: BEQ (Z=1), BHI (unsigned >, C=1 & Z=0), BGT (signed >). Thumb-2 IT blocks make up to four following instructions conditional without branching.
- Unsigned compare → BHI/BLO/BHS/BLS
- Signed compare → BGT/BLT/BGE/BLE
Calling convention (AAPCS)
Functions receive their first four arguments in R0–R3 (more on the stack) and return in R0. A called function may freely destroy R0–R3 and R12 but must preserve R4–R11 (pushing them if used). The stack must be 8-byte aligned at public interfaces. Knowing this lets you read a crash backtrace and write assembly callable from C.
When to use assembly
Write assembly only for context switching, startup before C runtime exists, special instructions, or proven hot spots. Otherwise let the compiler optimise and use intrinsics. DSP extensions on Cortex-M4/M7 (SMLAD, QADD, SIMD) are accessed via CMSIS-DSP. Always inspect disassembly (objdump -d, the IDE disassembly view) when tuning performance — the compiler's output is the truth.
Formulas
- Load / store:
LDR R0, [R1, #4] → R0 = mem[R1 + 4]; STR R0, [R1] → mem[R1] = R0 - Arithmetic with flags:
ADDS R0, R0, R1; SUBS R2, R2, #1; CMP R0, #10 (SUBS without result) - Branches:
B label; BEQ/BNE/BLT/BGT label; BL func (LR = return); BX LR (return) - Stack:
PUSH {R4-R6, LR} … POP {R4-R6, PC} - AAPCS:
Args R0–R3, return R0(–R1); R4–R11 callee-saved; R0–R3, R12 caller-saved - Signed overflow flag:
V = 1 when two operands of the same sign give a result of the opposite sign
Worked example: Signed overflow
Given: R0 = 0x7FFFFFFF, R1 = 1; ADDS R0, R0, R1
Find: Result and flags.
- Sum = 0x80000000 (bit 31 set) → N = 1
- Positive + positive gave negative → V = 1
- No unsigned carry out → C = 0; Z = 0
Answer: R0 = 0x80000000, N=1 Z=0 C=0 V=1
Worked example: Reading a HardFault location
Given: Stacked PC = 0x08001A3C; Disassembly: ldr r3, [r2, #8] with r2 = 0x00000000
Find: Cause.
- Load from address 0x00000008 via a NULL struct pointer
- Reading vector-table area is legal on some parts but writes fault
Answer: NULL pointer dereference of a struct member at offset 8
Worked example: Loop timing
Given: 4-instruction loop, ~5 cycles/iteration; 1000 elements at 168 MHz
Find: Execution time.
- 5000 cycles / 168 MHz
Answer: ≈ 30 µs
C function and its Thumb-2 translation
/* C source */
uint32_t sum(const uint32_t *p, uint32_t n)
{
uint32_t s = 0;
while (n--) s += *p++;
return s;
}
@ Typical -O2 output (arm-none-eabi-gcc, Cortex-M4)
sum:
movs r3, #0 @ s = 0
cbz r1, .done @ if n == 0 return
.loop:
ldr r2, [r0], #4 @ r2 = *p; p += 4 (post-increment)
subs r1, r1, #1 @ n--, sets Z flag
add r3, r3, r2 @ s += r2
bne .loop @ repeat while n != 0
.done:
mov r0, r3 @ return value in R0
bx lr @ return to callerArguments arrive in R0 (pointer) and R1 (count), the result returns in R0. The loop is 4 instructions — you can estimate timing by counting (LDR ≈ 2 cycles, others 1, taken branch ≈ 2–4).
Intrinsics and inline assembly
#include "cmsis_gcc.h"
void critical_update(volatile uint32_t *shared, uint32_t v)
{
uint32_t primask = __get_PRIMASK(); /* save interrupt state */
__disable_irq(); /* CPSID i */
*shared = v; /* protected write */
__set_PRIMASK(primask); /* restore (not blindly enable) */
}
static inline uint32_t reverse_bits(uint32_t x)
{
uint32_t r;
__asm volatile ("rbit %0, %1" : "=r"(r) : "r"(x)); /* one instruction */
return r;
}CMSIS intrinsics (__disable_irq, __WFI, __REV, __CLZ) give access to special instructions without writing assembly files. Saving and restoring PRIMASK makes critical sections nest safely.
Common mistakes
- Wrong: Using signed branches for unsigned comparisons. — Right: BHI/BLO for unsigned, BGT/BLT for signed.
- Wrong: Forgetting to preserve R4–R11 in assembly functions. — Right: PUSH/POP callee-saved registers you use.
- Wrong: Blindly re-enabling interrupts after a critical section. — Right: Save and restore PRIMASK so nested sections work.
Applications
- Crash analysis: Map faulting PC and registers to source lines.
- RTOS ports: Context switch (PendSV) written in assembly.
- Performance tuning: Verify the compiler vectorised or unrolled a hot loop.
- Security: Reviewing code for timing side channels at instruction level.
Key takeaways
- R0–R3 args, R0 return, LR return address, SP stack.
- Load/store: compute only in registers.
- Flags N Z C V drive conditional branches.
- Read disassembly to debug and optimise; use intrinsics before assembly.
Going further
- Pipeline & cycle counting: Cortex-M3/M4 3-stage pipeline; DWT->CYCCNT measures cycles.
- Exclusive access: LDREX/STREX implement lock-free atomics.
- Helium (M-profile Vector Extension): SIMD on Cortex-M55/M85 for DSP and ML.