SuperAPI AI Directory

re-arm

Coding & development

View repository

Updated 10-15 21:34

About this skill

ARM architecture reverse engineering (Cortex-M/A, Thumb, AAPCS)

When to use / when not to use

  • Use: ARM in non-Android scenarios — embedded bare-metal/firmware (Cortex-M0/M3/M4/M7, Cortex-A)
  • Use: Thumb/ARM instruction set switching, AAPCS calling convention identification, M-series vector table entry point location, position-dependent code relocation
  • Use: MMIO peripheral register cross-analysis (0x4000xxxx peripheral region, LDR literal pool)
  • Do not use: Android .so(go to [[re-android-native]])
  • Do not use: general AArch64 Linux user-mode programs (go to the [[re-binary-core]] general foundation; this skill only supplements ARM-specific semantics)
  • Do not use: only need to unpack firmware to see contents ([[re-fw-extract]]) / analyze filesystem configuration ([[re-fw-rootfs]]) / boot firmware as a whole ([[re-fw-emulate]])
  • Do not use: other architectures such as RISC-V / x86 (go to [[re-binary-core]])

Tool preparation

Verify all tools before use. Static analysis can be done without a sandbox; qemu dynamic execution defaults to sandbox + network isolation ([[re-analyze/platform-tips]] highest principle).

Decompiler (choose either Ghidra / IDA)

  • Ghidra (built-in ARM processor module; for Cortex-M0/M0+/M3/M4/M7 select the Cortex variant (v8-m only M23/M33/M55), for Cortex-A select v7, all little-endian LE):
    • Linux: official release package (requires JDK); some distribution repositories have apt install ghidra / pacman -S ghidra
    • macOS: brew install --cask ghidra; Windows: official zip
    • Verify: analyzeHeadless -help(headless mode) or GUI import ARM ELF
    • Ghidra automatically switches Thumb/ARM disassembly based on the Thumb function bit 0 (TMode); verify the processor variant and endianness when importing
  • IDA: commercial version includes ARM32/ARM64 + Thumb modules; for the Freeware version's architecture support scope, refer to the official page
  • Confirm architecture (ARM vs AArch64) and variant when importing — Cortex-M0/M0+ implements Armv6-M: mainly 16-bit Thumb encoding, but includes a small number of legal 32-bit encodings (BL, DMB/DSB/ISB, MRS/MSR); you must select the v6-M variant, and only the presence of encodings outside Armv6-M (MOVW/MOVT, CLZ, hardware divide/modulo) indicates the wrong variant was selected

readelf / file — architecture and byte order confirmation (binutils)

  • Linux: bundled with binutils (apt install binutils , etc.); macOS: brew install binutils or LLVM-based readelf; Windows: inside WSL
  • Verify: readelf --version

binwalk — firmware and embedded file scanning

  • For installation channels and version distinction (v3 mainline / v2 legacy), see the binwalk/unblob installation matrix in [[re-fw-extract]]
  • Verify: binwalk --version、unblob --version

qemu-arm / qemu-aarch64 — ARM user-mode emulation (dynamic verification)

  • Linux: apt install qemu-user / dnf install qemu-user / pacman -S qemu-user
  • macOS: brew install qemu(including user mode); Windows/WSL: Linux version inside WSL
  • For 32-bit use qemu-arm, for 64-bit use qemu-aarch64;-L <rootfs> to specify the dynamic library/linker source,-strace to trace system calls
  • Verify: qemu-arm --version、qemu-aarch64 --version
  • Bare-metal Cortex-M (no OS boot, vector table starting at 0x0) is not applicable to qemu-user — use qemu-system-arm -machine mps2-an385(Cortex-M3) and other board-level models (-s -S connect to gdb), or [[re-emulation]] using Unicorn instruction by instruction

gdb-multiarch — Cross-debugging

  • Linux: Debian/Ubuntu apt install gdb-multiarch(this package is in the official repositories of Debian/Ubuntu and their derivatives); Fedora dnf install gdb(the official gdb supports multiple targets and can debug bare-metal ARM; arm-none-eabi-gdb is not in Fedora's official repositories—if you need an embedded-specific toolchain, use the official precompiled ARM toolchain); Arch pacman -S gdb(the official gdb already has multiarch built in)
  • macOS: brew install gdb(requires Developer Tools authorization, see the macOS branch of [[re-analyze/platform-tips]]) or the Linux version inside WSL; Windows/WSL: the Linux version inside WSL
  • Verify: Debian/Ubuntu gdb-multiarch --version;Fedora/Arch gdb --version; after loading the bare-metal firmware, set architecture armv7-m then file to load,info registers confirm
  • Real-hardware SWD/JTAG debugging goes through OpenOCD + gdb ([[re-hardware-io]]); for emulated targets use qemu-system-arm -s -S or qemu-arm -g <port>

Procedure

Execute in order, archive the result of each step; dynamic execution defaults to the sandbox.

  1. Architecture and endianness confirmation:

    file target.bin                 # "ARM"=32 位(含 Thumb)、"AArch64"=64 位;MSB/LSB 端序提示
    file target.elf
    readelf -h target.elf           # Machine=ARM / AArch64;Data=little-endian / big-endian
    readelf -A target.elf | head    # Tag_ABI_VFP_args: 1=硬浮点(HFABI)、0=软浮点;EABI 版本
    
    • The high bits of e_flags directly identify the floating-point ABI:EF_ARM_ABI_FLOAT_HARD(0x400) hard-float / EF_ARM_ABI_FLOAT_SOFT(0x200) soft-float (corresponds to pitfall 5)
    • For a bare-metal .bin (no ELF header), determine the architecture, endianness and Cortex-M variant from the vector table/reset vector in step 2
  2. Entry-point location:

    • Cortex-M vector table(default starts at 0x0, 4 bytes per entry, address = exception number × 4):
      • Entry 1 (offset 0x0): initial MSP value—points into the RAM region (around 0x20000000), which can corroborate the load base address in reverse
      • Entry 2 (offset 0x4): Reset handler (the first instruction executed after reset)
      • After that, NMI (0x8), HardFault (0xC)… the LSB of each entry's address must be 1 (Thumb); LSB=0 triggers INVSTATE and enters HardFault
      • VTOR (0xE000ED08) can relocate the vector table; firmware may place the table in RAM or another flash address. Try 0x0 first; if that doesn't hold, search for MSP/Reset combinations
    • Cortex-A startup code—first distinguish AArch32 / AArch64; the two exception models differ:
      • AArch32 (classic Armv7-A model): reset vector 0x00000000 (or high vectors 0xFFFF0000, depending on the architecture version's control bits), exception vector table with 8 entries (Reset/Undef/SWI/Prefetch Abort/Data Abort/Reserved/IRQ/FIQ)
      • AArch64 (Armv8-A) does not have this fixed 8-entry table: the exception vector base address comes from each EL's VBAR_EL1/EL2/EL3, each table has 16 slots, spaced 0x80 apart(covering +0x000..+0x780), classified by current EL / lower EL × SP_EL0 / SP_ELx × AArch64 / AArch32 source × Synchronous/IRQ/FIQ/SError; VBAR_ELx is undefined after reset and must be explicitly configured by the startup code—do not use the 0x00000000 / 0xFFFF0000 low/high vectors rule to find the AArch64 exception table
      • When reverse engineering bare-metal Armv8-A firmware, the reliable clue for locating the exception table is msr vbar_el1/el2/el3, Xt and its address-construction sequence;the reset entry itself is determined by the implementation/SoC boot configuration(the platform may provide a core reset vector base configuration); it cannot be assumed to be fixed at 0
      • The startup flow is generally: set the stack pointer → copy .data → zero .bss → configure MMU/clock → jump to main; for those with a boot chain (boot ROM → BL1/BL2 → u-boot/EL3 firmware), the stages connect one level at a time along the chain [[re-fw-rootfs]]
    • After locating the reset vector, mark the entry point in the decompiler and expand the main logic along the call chain
  3. AArch32 Thumb/T32 function boundaries (Thumb/ARM switching):

    • Thumb function address LSB=1: ELF symbols (nm / readelf -s Thumb functions have odd addresses), BL targets, vector table entries, and vtable function pointer entries all carry the Thumb bit
    • bx rN / blx rN: switch state according to bit 0 of the target register (1=Thumb);blx immediate form switches directly to the ARM-state target
    • BL immediate distance limit: ±32MB in ARM state, ±16MB in Thumb state (BL is a 32-bit encoding; although Armv6-M lacks full Thumb-2, it still supports BL; the actual reachable range is determined by the specific encoding/relocation such as R_ARM_THM_CALL , and out-of-range cases are handled by the linker inserting a veneer); when out of range, the linker inserts a veneer trampoline (form such as ldr pc, =addr、movw+movt+bx), and a patch-like sequence of trampolines should not be mistaken for business logic; veneers are also used for ARM↔Thumb state switching
    • For binaries with symbols stripped: binutils relies on the mapping symbol $a/$t/$d to determine the disassembly state; after the mapping symbol is lost, it will disassemble in the wrong state—use arm-none-eabi-objdump -d -M force-thumb to force Thumb, or hand it to Ghidra to switch heuristically based on TMode
    • Use the immediate pool and string references from step 6 to cross-validate function boundaries
  4. Base address / relocation (load address vs link address):

    • The firmware link base address and the actual load location of the extracted image are often inconsistent (example: a flash firmware linked at 0x08000000 is extracted from an external storage address; the RAM range pointed to by the first vector table entry MSP can corroborate this)
    • Determine the link base address (infer from the reset vector / string references in the immediate pool / jump table cross-check) → calculate the offset → use Ghidra "Memory Map → Set Image Base" or IDA "Edit → Segments → Rebase program" to correct globally → after correction, strings, immediate pools, and jump tables all align
    • When an ELF is available, compare LMA/VMA (readelf -l): VMA is the link address; the load address is LMA / p_paddr
    • Correct first, then decompile; otherwise many references will be misaligned and the decompilation will be unrecognizable
  5. AAPCS calling convention identification:

    • 32-bit (AAPCS32): r0-r3 pass the first 4 arguments (extra arguments are passed on the stack), return value in r0, 64-bit arguments occupy even pairs such as r0:r1; r4-r8/r10-r11 are callee-saved, r9 is platform-dependent; r12(IP) is intra-call temporary, r13(SP), r14(LR) return address, r15(PC); public interfaces are 8-byte stack aligned
    • Function prologue characteristics:push {r4-r11, lr}, tail pop {..., pc}(borrows LR while restoring PC); leaf functions (no nested calls) often do not push LR, directly bx lr
    • HFABI (hard float): floating-point arguments go through VFP registers s0-s15 / d0-d7 (double precision), result in s0/d0; under the soft-float ABI floating-point arguments go through r0-r3;readelf -A Tag_ABI_VFP_args distinguishes them (most error-prone when mixed, see pitfall 5)
    • 64-bit (AAPCS64, AArch64): x0-x7 pass arguments, x8 indirect result register, x19-x28 callee-saved, x29 frame pointer, x30(LR), SP 16-byte aligned; prologue characteristics stp x29, x30, [sp, #-16]!
    • Cross-reference for recovering C++ artifacts on ARM (RTTI/vtables/exceptions) [[re-cpp-abi]]; function pointer entries in AArch32 vtables that point to Thumb/T32 functions likewise carry the Thumb state bit (bit0=1; clear bit0 when resolving the actual address); AArch64 uses A64 and has no Thumb state, so this rule does not apply
  6. Peripheral register cross-reference (MMIO xref):

    • Cortex-M peripheral region 0x40000000-0x5FFFFFFF (specific peripheral base addresses such as 0x4000xxxx and 0x48000000 vary by device); from 0xE0000000 onward is the PPB (NVIC/SCB/SysTick); verify against the SoC datasheet's memory map
    • Typical access form:ldr rN, [pc, #imm] Load address from the literal pool at the end of the function → str/ldr access the peripheral; when manually computing the pool address, note that in Thumb state PC=current+4 and in ARM state +8, and it is 4-byte aligned
    • Mark the peripheral region as a register region (not a data segment) in the decompiler—to prevent misjudgment (see pitfall 3); infer hardware behavior from the register semantics of UART/GPIO/timer, etc.; a loop polling a status/flag bit is the handshake/wait logic interacting with the peripheral
    • Cross-validation: check peripheral address constants + initialization sequences (clock enable, GPIO configuration) against the vendor's reference code patterns

Cross-domain combination

  • [[re-binary-core]]: general ARM ELF initial survey/decompilation/debugging foundation ([[re-ghidra]], [[re-ida]], [[re-gdb]], [[re-radare2]] are used as usual)
  • [[re-firmware]]: firmware-class sample gateway path (re-firmware → re-fw-extract → re-fw-rootfs → this skill)
  • [[re-fw-extract]] / [[re-fw-rootfs]]: prerequisites for firmware unpacking and filesystem/configuration analysis
  • [[re-fw-emulate]]: boot the whole firmware or run an ARM program in user mode (qemu series)
  • [[re-rtos]]: after locating the RTOS (FreeRTOS, etc.) task table/TCB in MCU firmware, decompile by task decomposition
  • [[re-hardware-io]]: JTAG/SWD on-device debugging and flash reading (OpenOCD + gdb-multiarch)
  • [[re-emulation]]: use Unicorn to emulate execution in scenarios without qemu (instruction-by-instruction verification of bare-metal Cortex-M)
  • [[re-cpp-abi]]: recovery of C++ artifacts RTTI/exceptions/vtables on ARM
  • [[re-variant]]: comparison of multiple firmware versions and patch diff
  • [[re-sandbox]]: mandatory prerequisite for all dynamic execution (default sandbox principle of [[re-analyze/platform-tips]])
  • Companion: [[re-patching]] (Thumb byte patching), [[re-android-native]] (Android .so scenarios)

Common pitfalls and traps

  • Misjudging Thumb odd addresses (function start offset by 1 byte): Symptom—when locating a function by symbol or BL target address it starts halfway through an instruction and the disassembly is completely garbled; Cause—the LSB=1 in a Thumb function address is a mode bit, not a code byte, and using it directly as an absolute address shifts everything out of alignment; Countermeasure—clear bit 0 (&~1) before entering a function/resolving a jump target; use this as the basis when manually computing addresses (Ghidra/IDA already handle this automatically)
  • Veneer obfuscation beyond the BL/BLX distance limit: Symptom—patches of movw/movt + bx、ldr pc trampolines appear in the call chain and are analyzed as business logic; Cause—the BL immediate can only cover ARM ±32MB / Thumb ±16MB, and the linker inserts veneers when exceeded; Countermeasure—recognize the short trampoline form (ends with bx/ldr pc and the target is a far address), skip it, and continue tracing the final target; cross-state (Thumb↔ARM) calls also go through veneers
  • Treating the M-series peripheral-mapped region as a data segment: Symptom — the 0x4000xxxx region is treated as "data" with random contents, and peripheral reads/writes are treated as ordinary memory accesses whose semantics cannot be analyzed; Cause — the MMIO region is registers, not storage: reads can change state and writes have side effects; Countermeasure — annotate it as a register region in the decompiler, recover semantics bit by bit according to the datasheet memory map + register table, and identify handshake/wait loops by polling status bits
  • R14(LR) dual-purpose decompilation distortion: Symptom — some functions "do not save the return address" yet call subfunctions, so the call graph is broken; Cause — LR is a register, not a dedicated return stack; leaf functions can use it as an ordinary temporary register, and the LR at a Cortex-M exception entry may also be the special EXC_RETURN value; Countermeasure — according to push {..., lr} and bl , reconstruct call relationships from the LR assignment points before and after; the exception entry context restoration sequence (push 8 words r0-r3/r12/LR/PC/xPSR, with PC at [sp,#0x18]) is used to confirm exception handler functions
  • Incorrect parameter interpretation when HFABI and soft-float are mixed: Symptom — the first parameter of a function is float, but the decompiler takes it from r0, so all values are wrong; Cause — ABI determination is wrong (treating softfp as hard, or vice versa); Countermeasure —readelf -A 's Tag_ABI_VFP_args and the 0x400/0x200 bits of e_flags determine the ABI,then see which register group the parameters actually land in: the first parameter is received from s0-s15/d0-d7 = hard float, received from r0-r3 = soft float.Key:-mfloat-abi=softfp is "soft-float calling convention + VFP instructions allowed"— it still emits vmov/vadd/vcmp and other floating-point instructions, yet parameters still go through integer registers (measured:vmov s0, r1 moves r1 into s0), sothe frequency of floating-point instructions cannot prove float ABI, it can only prove "the code uses VFP"
  • Wrong Cortex-M0 variant selected (misjudged by instruction width): Symptom — disassembling M0 firmware as v7/v8-M produces a large number of 32-bit instructions outside Armv6-M; Cause — ARMv6-M (M0/M0+) is mainly 16-bit Thumb encoding, but it is not "only 16-bit": BL, DMB/DSB/ISB, MRS/MSR are legal 32-bit encodings; judging only by instruction width will treat legal encodings as hallucinations and also miss real out-of-range instructions; Countermeasure — confirm the variant on import (v6M vs v7/v8M), and judge by whether the specific opcode belongs to Armv6-M — only encodings outside Armv6-M such as MOVW/MOVT, CLZ, and hardware divide/modulo indicate that the wrong variant was selected