Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 27

Memory Fragmentation and MISRA: Why Dynamic Allocation is Restricted

Recommended resources

Hello! Welcome back.

In our last lesson, we dissected the "big three" dynamic memory bugs: memory leaks, use-after-free, and double-free errors. We established that these bugs introduce severe, unpredictable behavior, which is why robust prevention strategies are critical.

Today, we'll explore another fundamental problem with dynamic memory allocation that is just as critical, especially in your field: memory fragmentation. This lesson will explain why even perfectly bug-free allocation code can lead to system failure, and it will provide the core rationale for why automotive standards like MISRA C, which you're familiar with through your work with ISO 26262 and ASPICE, place strict prohibitions on using functions like malloc.

Our learning outcome is to: Explain memory fragmentation and why automotive standards like MISRA often forbid or restrict dynamic allocation.

This is a very common and important topic in interviews for embedded systems roles, as it demonstrates an understanding of system-level reliability beyond just writing correct C syntax.

What is Memory Fragmentation?

Memory fragmentation is a phenomenon that occurs in a dynamic memory system (the heap) where free memory is broken up into many small, non-contiguous blocks, or "fragments." Over time, as your program allocates and frees blocks of various sizes, the heap can become a checkerboard of used and unused memory.

The critical issue is that even if the total amount of free memory is large, you may not be able to find a single contiguous block large enough to satisfy a new allocation request. This can cause malloc to return NULL, leading to a runtime failure, even when there's technically plenty of memory available.

There are two distinct types of fragmentation.

Internal and External Fragmentation

To understand the problem fully, we need to distinguish between its two forms.

  1. External Fragmentation: This is the problem described above. It occurs when free memory is scattered between allocated blocks. The memory is "external" to the allocated chunks. This is the most common and intuitive form of fragmentation.

  2. Internal Fragmentation: This occurs when memory is wasted inside an allocated block. This happens because memory allocators often allocate more memory than was requested. For example, to maintain alignment or for bookkeeping purposes, an allocator might serve requests in chunks of 8 or 16 bytes. If you request 9 bytes, you might be given 16 bytes, and the 7 unused bytes within that block constitute internal fragmentation. They are allocated but unused.

This image provides a clear visual distinction between the two.

Memory Fragmentation: Internal vs External
In Internal Fragmentation (left), wasted space exists *within* the allocated blocks. In External Fragmentation (right), the free space exists *between* the allocated blocks, preventing a large allocation even if the total free space is sufficient.

The Real-World Impact on Embedded Systems

For a desktop application, a malloc failure might be an inconvenience. For the ADAS and gateway systems you develop, it can be a catastrophic failure. The core problems that fragmentation introduces into safety-critical systems are non-determinism and unpredictability.

  1. Unpredictable Memory Availability: A system can run flawlessly for hours, days, or even weeks. But the specific sequence of allocations and deallocations during that time can slowly fragment the heap. Eventually, a critical function—perhaps one that only runs in an emergency maneuver—might need to allocate a buffer and suddenly fail because no single block is large enough. This makes it nearly impossible to prove that the system is safe over its entire operational lifetime, a key requirement of standards like ISO 26262.

  2. Unpredictable Timing (Non-Real-Time Behavior): In a hard real-time system, tasks must complete within a strict deadline. The time it takes for malloc to execute is not constant. When the heap is heavily fragmented, the allocator might have to search a long list of free blocks to find one that fits. This search can take an unpredictably long time, causing a task to miss its deadline. This violates real-time constraints and is unacceptable in components like RADAR or camera processing stacks where deterministic latency is essential.


Why MISRA C Forbids Dynamic Allocation

Your experience with automotive software makes you well-aware of the strict coding standards in place. The problems of fragmentation and the bugs we discussed previously are the primary drivers behind MISRA's stance on dynamic memory.

Let's dive into the official rationale.

A Rationale-Based Classification of MISRA C Guidelines

The paper 'A Rationale-Based Classification of MISRA C Guidelines' offers a direct explanation for why MISRA takes such a hard line. It's an excellent resource for understanding the 'why' behind the rules you encounter in your work.

Please read the section 'G. HTDR: Hard To Do Right', focusing on the discussion around 'Dir 4.12: Dynamic memory allocation shall not be used.' Pay close attention to the points under '1) Out-of-storage Run-Time Failures' and '2) Timing Considerations'. Also, note the mention of 'Rule 21.3', which specifically targets the standard library functions.

As you've just read, MISRA's reasoning aligns perfectly with the issues we've discussed:

  • Directive 4.12: Dynamic memory allocation shall not be used: This is a "required" directive because using dynamic memory correctly in a critical system requires immense expertise and proof, which is often infeasible.
  • Out-of-storage failures: The paper highlights that fragmentation makes it "very often difficult" to prove that memory will be sufficient, especially for long-running systems. This is the core reliability issue.
  • Timing considerations: It explicitly states that malloc and free have variable and possibly long latency, which is "unacceptable for systems that have to meet hard real-time constraints."
  • Rule 21.3: The memory allocation and deallocation functions of <stdlib.h> shall not be used: This rule reinforces the directive by targeting the standard library functions specifically because they have unknown fragmentation and timing properties. You cannot build a deterministic system on a non-deterministic foundation.

So, What's the Alternative?

In an interview, a great follow-up question is, "If malloc is forbidden, how do you handle memory for objects with variable lifetimes or complex data structures?" Your answer here can showcase deep practical knowledge.

1. Static Allocation

The simplest and safest approach is to allocate all memory at compile time. This means using global or static variables and arrays with fixed sizes determined before the program runs. The total memory usage is known, fixed, and can be verified by the linker. This is the most common approach in deeply embedded systems.

In practice, this is often enforced at the build system level. For instance, a linker script can be configured to allocate zero bytes for the heap, making it impossible to even link code that uses malloc.

Linker Script for Heap and Stack Allocation
A typical linker script snippet for an embedded system. Note how `HeapSize` is explicitly set to `0x0`, effectively disabling the heap and preventing the use of standard dynamic memory allocation.

2. Memory Pools (Object Pools)

When some level of dynamic behavior is truly needed, the standard practice is to use a memory pool. This is a disciplined, controlled, and deterministic form of dynamic allocation.

The concept is simple:

  1. A large, single block of memory is allocated statically at compile-time (the "pool").
  2. This pool is divided into a number of fixed-size smaller blocks.
  3. Custom pool_alloc() and pool_free() functions are written to manage these blocks. pool_alloc() takes a block from a list of free blocks, and pool_free() returns it to the list.

This approach solves the key problems of standard malloc:

  • Deterministic Timing: Allocation and deallocation are typically very fast, constant-time operations (e.g., just updating a pointer in a linked list).
  • No External Fragmentation: Since all blocks in the pool are the same size, there are no "holes" created. Any freed block is identical to any other and can satisfy the next allocation request.
  • Bounded Memory Usage: The maximum memory usage is fixed at compile time (the size of the pool).

Memory allocation using Pool - Embedded Code Patterns

To see how this pattern is implemented, let's look at the 'Memory allocation using Pool' article from Embedded Code Patterns.

Please read the sections 'Pool pattern' and 'Conclusion'. Focus on the description of what a pool is and the benefits it provides, especially the statement that it 'results in zero memory leakage, reasonable memory waste and no fragmentation.'

By using static allocation and memory pools, automotive systems achieve the necessary determinism and safety while still allowing for flexible software designs where required.


Conclusion

This lesson connected the theoretical concept of fragmentation to the practical realities of safety-critical embedded development. You should now be very comfortable explaining this topic in an interview setting.

Key Takeaways:

  • Memory Fragmentation comes in two forms: internal (wasted space within allocated blocks) and external (free space is broken into non-contiguous chunks).
  • The primary consequences of fragmentation in embedded systems are unpredictable memory availability and non-deterministic timing, both of which are unacceptable in safety-critical applications.
  • MISRA C forbids standard dynamic allocation (Dir 4.12, Rule 21.3) precisely because its behavior regarding fragmentation and timing is unspecified and non-deterministic, making it impossible to prove system safety and reliability.
  • The industry-standard alternatives are static allocation (all memory sized at compile-time) and memory pools (a controlled, deterministic allocation scheme that avoids fragmentation and has constant-time performance).

In our next lesson, we will shift from memory management to data layout. We will explore how compilers arrange struct members in memory, introducing the crucial concepts of data alignment and padding. This is another low-level topic essential for performance optimization and hardware interaction.