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
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
- Return at the end of the current subroutine to the wrong address -- invalid instruction error, memory access error.
- 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
- Cause a hard-fault/trap or reset the processor
- Return invalid data, for example zeros.
Collecting Information
Some questions, to clarify the current situation:
- Is the program executed from ROM memory or RAM memory?
- How much RAM memory is available on the target platform(s)?
- How much RAM memory is needed for .data, .bss, stack, heap (by which modules)?
- Analyse the size of .data, .bss, .heap, .stack sections with $(target)-size, $(target)-nm -n, $(target)-readelf -S [2]
- Measure or estimate worst-case heap/stack size.
- How does the toolchain reserve/assign memory? Is it safe?
- In case of corruption: what are the consequences for the system?
- Should the memory layout be changed? Are there restrictions?
- Does the target platform(s) trap/hard-fault or return arbitrary data when invalid memory addresses are read/written? Consequences for the system?
- 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.
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.
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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
/* 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)
Below is the output of arm-none-eabi-readelf -S, for a small toy-program that was linked using the linker script.
References & Further Reading
More memory protection techniques:
- https://en.wikipedia.org/wiki/Stack_buffer_overflow#Protection_schemes
- https://barrgroup.com/Embedded-Systems/How-To/Prevent-Detect-Stack-Overflow
- https://barrgroup.com/Embedded-Systems/How-To/Malloc-Free-Dynamic-Memory-Allocation
Using avr-libc? https://www.nongnu.org/avr-libc/user-manual/malloc.html
[1] | https://wiki.osdev.org/Detecting_Memory_(x86) |
[2] | https://sourceware.org/binutils/docs/binutils/index.html |
[3] | https://sourceware.org/binutils/docs/ld/Scripts.html |
[4] | https://sourceware.org/binutils/docs/ld/Source-Code-Reference.html |