Sitemap

ARM Binary Analysis — Part9

6 min readJun 5, 2025

--

Understanding ARM Memory Layout: A Practical Guide to Stack, Heap, and Memory Mappings

Press enter or click to view image in full size

When working with ARM binaries, understanding the memory layout is crucial for writing efficient code, debugging, and reverse engineering. ARM processors, powering everything from smartphones to embedded systems, rely on a structured memory model that includes the stack, heap, and other memory mappings, all underpinned by virtual memory concepts. In this guide, we’ll explore the memory layout of ARM binaries, focusing on practical examples to illustrate how the stack, heap, and memory mappings work in ARMv7-A (A32 instruction set) programs.

Why Understand ARM Memory Layout?

The memory layout dictates how a program organizes its data, code, and resources during execution. For developers, this knowledge is essential for managing function calls (stack), dynamic memory allocation (heap), and understanding how the operating system maps memory. For reverse engineers, it’s key to analyzing binaries and identifying vulnerabilities. We’ll use a sample ARM program to demonstrate these concepts and make them actionable.

Overview of ARM Memory Layout

An ARM binary’s memory layout typically includes:

  • Code (.text): Contains the program’s executable instructions.
  • Data (.data, .bss): Stores initialized and uninitialized global/static variables.
  • Stack: Used for function call management, local variables, and temporary storage.
  • Heap: Used for dynamic memory allocation (e.g., via malloc).
  • Memory Mappings: Managed by the operating system, mapping virtual addresses to physical memory.

Virtual memory, a key concept, allows each process to operate in its own isolated address space, with the operating system handling translations to physical memory. In ARM, virtual memory is managed by the Memory Management Unit (MMU).

Setting Up Your Environment

To follow along, install:

  • ARM Toolchain: gcc-arm-none-eabi for compiling ARM binaries (sudo apt-get install gcc-arm-none-eabi).
  • QEMU: For running ARM binaries (sudo apt-get install qemu-user).
  • GDB: Use gdb-multiarch for debugging (sudo apt-get install gdb-multiarch).
  • Ghidra: For static analysis (download from ghidra-sre.org).

We’ll create a sample program to explore memory layout concepts.

Sample Program: Stack and Heap in Action

Let’s write an ARM assembly program that uses the stack for function calls and simulates heap-like behavior by reserving memory. Since malloc requires a C runtime, we’ll focus on stack usage and a static buffer to mimic heap allocation. Save this as memory.s:

.section .data
buffer: .space 16 // Simulate heap with 16-byte static buffer
.section .text
.global _start
_start:
// Initialize stack with some values
mov r0, #10 // First argument
mov r1, #20 // Second argument
bl process // Call process function
b . // Infinite loop
process:
push {r4, r5, lr} // Save r4, r5, and link register (stack)
mov r4, r0 // Save first argument
mov r5, r1 // Save second argument
ldr r2, =buffer // Load address of buffer (simulated heap)
str r4, [r2] // Store r4 in buffer[0]
str r5, [r2, #4] // Store r5 in buffer[4]
add r0, r4, r5 // Compute sum (return value)
pop {r4, r5, lr} // Restore registers
bx lr // Return

Compile and link:

arm-none-eabi-as -o memory.o memory.s
arm-none-eabi-ld -o memory memory.o

This program:

  • Uses the stack to save registers in process.
  • Uses a static buffer (buffer) in the .data section to mimic heap storage.
  • Computes the sum of two numbers (10 + 20 = 30).

Let’s analyze how this fits into the memory layout.

Exploring the Memory Layout

1. Code Section (.text)

The .text section contains the program’s instructions, loaded at a virtual address (e.g., 0x10000 in QEMU). Disassemble with objdump:

arm-none-eabi-objdump -d memory

Output (simplified):

00010000 <_start>:
10000: e3a0000a mov r0, #10
10004: e3a01014 mov r1, #20
10008: eb000002 bl 10018 <process>
1000c: eafffffe b 1000c <_start+12>
00010018 <process>:
10018: e92d4030 push {r4, r5, lr}
1001c: e1a04000 mov r4, r0
10020: e1a05001 mov r5, r1
10024: e59f2010 ldr r2, [pc, #16]
10028: e5824000 str r4, [r2]
1002c: e5825004 str r5, [r2, #4]
10030: e0840005 add r0, r4, r5
10034: e8bd4030 pop {r4, r5, lr}
10038: e12fff1e bx lr

The .text section is mapped read-only and executable, typically at a low virtual address.

2. Data Section (.data, .bss)

The .data section holds initialized data, like our buffer. Check the memory layout with objdump -h:

arm-none-eabi-objdump -h memory

Output (simplified):

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000003c 00010000 00010000 00001000 2**2
1 .data 00000010 00020000 00020000 00002000 2**2

The buffer is in .data at 0x20000, with 16 bytes allocated. The .bss section (not used here) would hold uninitialized data, mapped read-write but not stored in the binary.

3. Stack

The stack is used for function calls, local variables, and saving registers. In our program, the process function pushes R4, R5, and LR onto the stack:

push {r4, r5, lr}  // Decrements SP by 12 bytes, stores r4, r5, lr

The stack grows downward (lower addresses) and is aligned to 4 bytes per the ARM Procedure Call Standard (APCS). Let’s debug to see it in action:

Get Mohamad Aerabi’s stories in your inbox

Join Medium for free to get updates from this writer.

Run QEMU in debug mode:

qemu-arm -g 1234 ./memory &

Start GDB:

gdb-multiarch ./memory
(gdb) target remote localhost:1234
(gdb) break *0x10018 // Break at process
(gdb) continue

Check the stack pointer (SP) and stack contents:

(gdb) info registers sp
sp 0x7ffffffc 0x7ffffffc
(gdb) x/3xw $sp
0x7ffffffc: 0x0000000a 0x00000014 0x0001000c

The stack contains:

  • 0x0000000a (R4, value 10).
  • 0x00000014 (R5, value 20).
  • 0x0001000c (LR, return address).

The stack is mapped read-write, typically at a high virtual address (e.g., 0x7ffffffc).

4. Heap (Simulated)

In a bare-metal environment, there’s no malloc, so we used a static buffer in .data to simulate heap-like storage. In a real system with an OS (e.g., Linux), the heap is a read-write memory region allocated dynamically via system calls like brk or mmap. Our program stores values in buffer:

ldr r2, =buffer    // Load buffer address (0x20000)
str r4, [r2] // Store 10 at buffer[0]
str r5, [r2, #4] // Store 20 at buffer[4]

Verify in GDB:

(gdb) x/2xw 0x20000
0x20000: 0x0000000a 0x00000014

In a full OS, the heap would be mapped dynamically, growing as needed.

5. Virtual Memory Concepts

ARM binaries rely on virtual memory to isolate processes and manage memory efficiently. Key concepts:

  • Virtual vs. Physical Addresses: Each process sees a virtual address space (e.g., 0x10000 for .text, 0x20000 for .data). The MMU translates these to physical addresses.
  • Memory Mappings: The OS maps sections like .text (read/execute), .data (read/write), and stack/heap (read/write) to virtual addresses.
  • Page Tables: The MMU uses page tables to map virtual pages (typically 4KB in ARM) to physical memory, enforcing permissions (e.g., no writes to .text).
  • Address Space Layout: In Linux, a typical ARM process layout is:
  • Low addresses: .text, .data, .bss.
  • High addresses: Stack (grows down).
  • Middle: Heap (grows up), shared libraries.

To inspect mappings in a Linux environment, you’d use /proc/<pid>/maps. In our bare-metal QEMU setup, mappings are simpler but follow similar principles.

Practical Example: Analyzing Memory in GDB

Let’s debug to confirm the memory layout. Set breakpoints to inspect the stack and buffer:

(gdb) break *0x10028  // Before storing to buffer
(gdb) continue
(gdb) x/2xw 0x20000
0x20000: 0x00000000 0x00000000 // Buffer before stores
(gdb) continue
(gdb) x/2xw 0x20000
0x20000: 0x0000000a 0x00000014 // Buffer after stores

Check the stack again at the epilogue (0x10034):

(gdb) break *0x10034
(gdb) continue
(gdb) x/3xw $sp
0x7ffffffc: 0x0000000a 0x00000014 0x0001000c

This confirms the stack and buffer usage, aligning with APCS and our memory layout.

Practical Tips for Working with ARM Memory Layout

  1. Use GDB for Dynamic Analysis: Inspect SP, memory, and registers to verify stack and heap usage.
  2. Check Section Headers: Use objdump -h to see where .text, .data, and .bss are mapped.
  3. Ghidra for Static Analysis: Visualize memory layout and data references in Ghidra’s listing view.
  4. Stack Alignment: Ensure SP is 4-byte aligned, per APCS, to avoid crashes.
  5. Virtual Memory Debugging: In Linux, use cat /proc/self/maps in a running ARM process to see mappings.
  6. Heap Analysis: For real heap usage, analyze malloc calls or system calls like mmap in a full OS.

Common Pitfalls and Solutions

Stack Overflow: Pushing too many values without popping can overwrite critical data. Monitor SP in GDB.

Invalid Memory Access: Ensure addresses (e.g., buffer) are valid. Use x in GDB to check memory.

Missing Runtime: Bare-metal binaries lack heap management. Use static buffers or link a C library for malloc.

Virtual Memory Misunderstanding: Study the MMU and page tables to understand address translation.

Conclusion

Understanding the memory layout of ARM binaries — code, data, stack, heap, and virtual memory mappings — unlocks the ability to write, debug, and analyze low-level code effectively. Our example showed how the stack manages function calls, how static buffers simulate heap storage, and how virtual memory organizes the address space. By using tools like GDB, objdump, and Ghidra, you can explore these concepts hands-on.

Practice with simple binaries, trace memory accesses in GDB, and study real-world ARM programs (e.g., Linux ELF binaries) to deepen your skills. With this knowledge, you’ll be equipped to tackle embedded development, reverse engineering, and performance optimization on ARM systems.

Happy exploring!

See you in Part10

--

--