Be Aware of Your Memory Layout

Knowing how available memory is organised and knowing how much memory is used by a software program reserved can be key to prevent or diagnose memory corruption faults.

Typical RAM Layout, Pros & Cons

Typically, the stack region grows towards lower addresses; the heap region grows towards higher addresses. The stack starts at the "end-of-ram" address and the heap starts behind the .bss section, where the statically-allocated variables reside.

Pros

  • The end-of-RAM address (usually device specific) is the only information required to initialize the program.
  • On some platforms the end-of-RAM address can be determined by detecting the amount of available memory during software boot. [1]
  • Free RAM memory is located between stack and heap; the program can use as much of the free memory for stack and heap as required -- as long as they do not collide!

Cons

  • Stack region and heap region can collide!
  • Even if no memory is used for the heap allocations, the stack can collide with the .bss and .data sections. Therefore, outlawing the use of dynamic memory allocations does not necessarily prevent a stack overflow onto the .bss/.data sections
Typical memory layout, stack an heap can collide.

Typical memory layout, stack an heap can collide.

Memory corruptions are often difficult to diagnose because they might only occur under a specific worst-case condition, or because the corruption affects data belonging to a different module of the software.

Possible Symptoms of Memory Corruption

  • When the stack is corrupted the software can
    1. Return at the end of the current subroutine to the wrong address -- invalid instruction error, memory access error.
    2. Change stack variables when other variables are written -- can be diagnosed with a watchpoint on the stack variable.
  • Heap, .bss, or .data variables containing addresses to program code can point to a stack overflow into those regions -- this can be diagnosed with a watchpoint on the affected variable.
  • Writing to or reading from addresses larger than the end-of-RAM address or the address-space gap between ROM and RAM memory can
    1. Cause a hard-fault/trap or reset the processor
    2. Return invalid data, for example zeros.

Collecting Information

Some questions, to clarify the current situation:

  1. Is the program executed from ROM memory or RAM memory?
  2. How much RAM memory is available on the target platform(s)?
  3. How much RAM memory is needed for .data, .bss, stack, heap (by which modules)?
    1. Analyse the size of .data, .bss, .heap, .stack sections with $(target)-size, $(target)-nm -n, $(target)-readelf -S [2]
    2. Measure or estimate worst-case heap/stack size.
  4. How does the toolchain reserve/assign memory? Is it safe?
  5. In case of corruption: what are the consequences for the system?
  6. Should the memory layout be changed? Are there restrictions?
  7. Does the target platform(s) trap/hard-fault or return arbitrary data when invalid memory addresses are read/written? Consequences for the system?
  8. Measure or estimate the amount of memory that the software uses at runtime. a. Check if your tools/toolchain/software can measure memory usage.

In larger projects, the answers to the questions listed should be used to create and manage a memory budget throughout the software development cycle.

Using Another Memory Layout

The memory layout illustrated below can be employed to prevent heap/stack overflow corruptions. The decrementing stack is placed before the .data/.bss sections. Memory for the heap region is reserved near the end of the memory, after the .bss section.

The downside is that the free memory has to be split into two; one assigned to the stack, the other assigned to the heap.

Safer memory layout, avoids the stack/heap collision.

Safer memory layout, avoids the stack/heap collision.

With this layout the .bss/.data sections are quite safe from being corrupted by stack overflow or heap overflow.

A stack overflow will likely lead to a trap/hard-fault and can be diagnosed easily by looking at the stack pointer.

A heap overflow can be diagnosed by comparing the addresses returned by the memory allocator against the end-of-ram address. The returned address plus the size of the allocation must not exceed the end-of-ram address.

If no dynamic memory allocations from the heap are required the memory layout looks as pictured below. The entirety of the free memory is available for the stack region. The stack can't overflow into the .bss/.data sections.

Safer memory layout without heap, avoid stack/bss collision.

Safer memory layout without heap, avoid stack/bss collision.

Using the binutils linker 'ld', the memory layout can be changed by using a customised linker script. [3] The following listing shows a linker script, that I wrote for ARM Cortex-M microcontrollers. The linker script assigns heap and stack regions. 32Kbyte of RAM memory are reserved for the stack on line 77. Unfortunately, the only solution seems to declare the stack-size explicitly because the size of the .data and .bss regions is known only after they have been allocated.

memory-layout/cortex.lds (Source)

/* Copyright (C) 2018 Oliver Kleinke
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

OUTPUT_FORMAT("elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(main)

MEMORY {
    flash (RX) : ORIGIN = 0x00000000, LENGTH = 256K
    sram  (WX) : ORIGIN = 0x20000000, LENGTH = 64K
}

REGION_ALIAS("REGION_TEXT", flash);
REGION_ALIAS("REGION_ARM_EXIDX", flash);
REGION_ALIAS("REGION_ARM_EXTAB", flash);
REGION_ALIAS("REGION_STACK", sram);
REGION_ALIAS("REGION_DATA", sram);
REGION_ALIAS("REGION_BSS", sram);
REGION_ALIAS("REGION_HEAP", sram);


SECTIONS {
    .text : {
        __text_begin__ = .;
        . = ALIGN(0x4);

        KEEP(*(.vectors))

        KEEP(*(.text))
        *(.text.*)

    } > REGION_TEXT
    __text_size__ = SIZEOF(.text);

    .rodata : {
        __rodata_begin__ = .;

        KEEP(*(.rodata))
        *(.rodata.*)

    } > REGION_TEXT
    __rodata_size__ = SIZEOF(.rodata);

    # ARM-specific regions
    .ARM.exidx : {
        __exidx_begin__ = .;

        *(.ARM.exidx* .gnu.linkonce.armexidx.*)

    } > REGION_ARM_EXIDX
    __exidx_size__ = SIZEOF(.ARM.exidx);

    .ARM.extab : {
        __extab_begin__ = .;

        *(.ARM.extab* .gnu.linkonce.armextab.*)

    } > REGION_ARM_EXTAB
    __extab_size__ = SIZEOF(.ARM.extab);

    .stack (NOLOAD) : ALIGN(0x10) {
        __stack_begin__ = .;

        /* Reserve 32K sram for .stack */
        . += 32K;
        . = ALIGN(0x10);

        __stack_init__ = .;

        . = ALIGN(0x20);

    } > REGION_STACK
    __stack_size__ = SIZEOF(.stack);

    .data : {
        __data_lma__ = LOADADDR(.data);
        __data_begin__ = .;

        . = ALIGN(0x10);

        KEEP(*(.data))
        *(.data.*)

        . = ALIGN(0x20);

    } > REGION_DATA AT> REGION_TEXT
    __data_size__ = SIZEOF(.data);

    .bss (NOLOAD) : {
        __bss_begin__ = .;

        . = ALIGN(0x10);

        KEEP(*(.bss))
        KEEP(*(COMMON))
        *(.bss.*)

        . = ALIGN(0x20);

    } > REGION_BSS
    __bss_size__ = SIZEOF(.bss);

    .heap (NOLOAD) : {
        __heap_begin__ = .;

        /* Remaining sram memory is assigned to .heap */
        . = ORIGIN(REGION_HEAP) + LENGTH(REGION_HEAP);

    } > REGION_HEAP
    __heap_size__ = SIZEOF(.heap);
}

The following header file can be included to use the linker-defined symbols in program code. [4]

memory-layout/linker.h (Source)

/* Copyright (C) 2018 Oliver Kleinke
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#pragma once

#include "std.h"

// Declare linker-defined symbols

extern const u8 __text_begin__[];
extern const u32 __text_size__[];

extern u8 const * __rodata_begin__[];
extern const u32 __rodata_size__[];

extern u8 const * __exidx_begin__[];
extern const u32 __exidx_size__[];

extern u8 const * __extab_begin__[];
extern const u32 __extab_size__[];

extern u8 const * __stack_begin__[];
extern const u32 __stack_size__[];
extern u8 const * __stack_init__[];

extern u8 const * __data_lma__[];
extern u8 const * __data_begin__[];
extern const u32 __data_size__[];

extern u8 const * __bss_begin__[];
extern const u32 __bss_size__[];

extern u8 const * __heap_begin__[];
extern const u32 __heap_size__[];

Below is the output of arm-none-eabi-readelf -S, for a small toy-program that was linked using the linker script.

There are 11 section headers, starting at offset 0x18438:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 010000 00001c 00  AX  0   0  4
  [ 2] .stack            NOBITS          20000000 020000 008000 00  WA  0   0 16
  [ 3] .data             PROGBITS        20008000 018000 000020 00  WA  0   0  4
  [ 4] .bss              NOBITS          20008020 018020 000020 00  WA  0   0  4
  [ 5] .heap             NOBITS          20008040 018040 007fc0 00  WA  0   0  1
  [ 6] .comment          PROGBITS        00000000 018020 000057 01  MS  0   0  1
  [ 7] .ARM.attributes   ARM_ATTRIBUTES  00000000 018077 000030 00      0   0  1
  [ 8] .symtab           SYMTAB          00000000 0180a8 000200 10      9  11  4
  [ 9] .strtab           STRTAB          00000000 0182a8 00013c 00      0   0  1
  [10] .shstrtab         STRTAB          00000000 0183e4 000052 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  y (purecode), p (processor specific)