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
- Linux: official release package (requires JDK); some distribution repositories have
- 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 binutilsor 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 useqemu-aarch64;-L <rootfs>to specify the dynamic library/linker source,-straceto 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 -Sconnect 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); Fedoradnf 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); Archpacman -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/Archgdb --version; after loading the bare-metal firmware,set architecture armv7-mthenfileto load,info registersconfirm - Real-hardware SWD/JTAG debugging goes through OpenOCD + gdb ([[re-hardware-io]]); for emulated targets use
qemu-system-arm -s -Sorqemu-arm -g <port>
Procedure
Execute in order, archive the result of each step; dynamic execution defaults to the sandbox.
-
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
- The high bits of e_flags directly identify the floating-point ABI:
-
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, Xtand 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
- Cortex-M vector table(default starts at 0x0, 4 bytes per entry, address = exception number × 4):
-
AArch32 Thumb/T32 function boundaries (Thumb/ARM switching):
- Thumb function address LSB=1: ELF symbols (
nm/readelf -sThumb 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);blximmediate 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 asldr 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/$dto determine the disassembly state; after the mapping symbol is lost, it will disassemble in the wrong state—usearm-none-eabi-objdump -d -M force-thumbto 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
- Thumb function address LSB=1: ELF symbols (
-
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
-
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}, tailpop {..., pc}(borrows LR while restoring PC); leaf functions (no nested calls) often do not push LR, directlybx 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 -ATag_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
-
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/ldraccess 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 pctrampolines 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}andbl, 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=softfpis "soft-float calling convention + VFP instructions allowed"— it still emitsvmov/vadd/vcmpand other floating-point instructions, yet parameters still go through integer registers (measured:vmov s0, r1moves 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