Wiki / Hardware

ARM Architecture

The instruction set behind almost every phone, router and microcontroller you will reverse. Fixed-width instructions, a visible program counter, and two encodings you must not confuse.

The three Cortex families
ARM Cortex
Cortex-A
MMU, Linux: routers, cameras, phones
Cortex-R
real-time: storage, automotive
Cortex-M
no MMU, XIP from flash, Thumb only
AArch64 (ARMv8-A, 64-bit) is a distinct instruction set, not an extension of the 32-bit one - a 64-bit target is a different disassembly problem. Cortex-M is the bare-metal firmware-RE case; Cortex-A is the Linux router/camera case.

ARM is the instruction set architecture behind the overwhelming majority of embedded and mobile targets: phones, routers, cameras, and the Cortex-M microcontrollers inside countless products. If you reverse firmware, you read ARM.

The families

  • Cortex-A, application processors. Run Linux, have an MMU, are what a router or camera SoC is built on.
  • Cortex-R, real-time. Storage controllers, automotive.
  • Cortex-M, microcontrollers. No MMU, execute in place from flash, bare-metal or an RTOS. This is the firmware-reverse-engineering case.

AArch64 (ARMv8-A, 64-bit) is a distinct instruction set from the 32-bit one, not an extension of it. A 64-bit target is a different disassembly problem.

Registers and calling convention

32-bit ARM has sixteen visible registers:

r0-r3    arguments and return values (r0 is the return value)
r4-r11   callee-saved locals
r12      scratch (intra-procedure call)
r13 sp   stack pointer
r14 lr   link register: the return address
r15 pc   program counter, directly readable and writable

Two consequences matter for reversing. The return address starts in a register, not on the stack, so a leaf function need never touch memory to return, and a classic stack-smash needs the function to have spilled lr first. And the program counter is a general register, so mov pc, r3 and pop {r4, pc} are ordinary control flow, which makes function ends easy to spot and indirect jumps common.

The AAPCS (the ARM procedure call standard, historically called the EABI) is what fixes all of this: which registers pass arguments, which are preserved, how the stack is aligned, how structs are returned. It is why compiled ARM code from different toolchains interoperates, and why a disassembler can label arguments at all.

ARM and Thumb

This is the single most common way to disassemble ARM incorrectly.

  • ARM state: 32-bit instructions, 4-byte aligned.
  • Thumb / Thumb-2: 16-bit instructions with some 32-bit ones, 2-byte aligned. Much denser, and what Cortex-M runs exclusively.

The processor switches between them, and bit 0 of a branch target address selects the state: an odd address means Thumb. Addresses are still even; the low bit is a mode flag, not part of the address. That is why a Cortex-M vector table is full of odd-looking values, and why those odd values are the clue that identifies the table.

Disassemble Thumb code as ARM and you get valid-looking, entirely wrong instructions. If nothing makes sense, this is the first thing to check.

The vector table

A Cortex-M image begins with its exception vector table:

0x00  initial stack pointer      (a RAM address)
0x04  reset vector               (entry point, odd for Thumb)
0x08  NMI handler
0x0C  HardFault handler
...   IRQ handlers, one per peripheral interrupt

Recognising it gives you three things at once: the load address (from the common high bits of the handler addresses), the entry point, and the list of peripherals the firmware actually uses.

Other things worth knowing

  • Conditional execution. In ARM state, most instructions can be predicated (addeq, movne), so branchless code is common and control flow graphs are flatter than expected.
  • PC-relative literal pools. Constants too large to encode are stored near the code and loaded with ldr r0, [pc, #offset]. A disassembler that misreads them as instructions produces nonsense; a good one folds them into the operand.
  • Endianness. ARM is usually little-endian, but big-endian deployments exist, particularly in networking.
  • System calls on Linux use svc, with the number in r7 (32-bit) or x8 (AArch64). Recognising the pair is how you find syscalls in a static binary with no symbols.

What it pairs with

ghidra and radare2 disassemble it, qemu runs it on your own machine, elf-format describes the container it usually arrives in, and firmware-reverse-engineering covers the case where there is no container at all.

Further reading