TruePERSPECTIVES_logo.png

How to instrument the memory manager of your embedded system

Posted by Magnus Unemyr on Nov 3, 2016 8:30:00 AM

Many embedded developers do not use dynamic memory allocation; i.e. malloc() and free(). While there might be many reasons to use only statically allocated data structures - or your own memory manager, for that matter - there are still embedded developers using dynamic memory management. Some runtime library functions, like the full-blown printf(), use dynamic memory allocation internally too.

Part of the problem is malloc() and free() are pretty much black boxes – you don’t really have a lot of insights into how the memory is being used. A small hack can improve the situation. In this blog post, you will learn how to get analytics data out of the dynamic memory manager that comes with the GNU C/C++ compiler of Atollic TrueSTUDIO.

memory_manager.png

malloc() and free() use a memory pool, which is located in RAM. When you allocate dynamic memory using malloc(), it looks into the memory pool and tries to re-use a memory block that has previously been handed back using free(). If it can’t re-use a free slot in the memory pool, it grows the memory pool using a call to the _sbrk() function.

The _sbrk() function is located in the syscalls.c file, which is auto-generated by the Atollic TrueSTUDIO project wizard.

If we want to know what the memory manager does, we can instrument _sbrk() to emit information on how the memory pool is growing. The code example below shows how you can do this (by editing the _sbrk function in syscalls.c):

void * _sbrk(int32_t incr)
{
    extern char   end; /* Set by linker.  */
    static char * heap_end;
    char *        prev_heap_end;
    if (heap_end == 0) {
        heap_end = & end;
    }

    prev_heap_end = heap_end;
    heap_end += incr;

    printf("_sbrk: Growing memory pool by %d bytes. New span is 0x%X-0x%X (%d bytes)\n",
        incr,
        (uint32_t)(& end),
        (uint32_t)(heap_end),
        (uint32_t)(heap_end) - (uint32_t)(& end));

    return (void *) prev_heap_end;
}

With this instrumentation, we can understand how the dynamic memory management system uses the memory in your target system. Whenever _sbrk() is called, it prints out:

  • The number of new bytes added to the memory pool
  • The address range occupied by the memory pool
  • The size of the memory pool

To test this concept, we can create a simple main() application that allocates some memory regions using malloc(). It can be pointed out that this small example program does not free some of the allocated memory, which is bad programming practice – but it doesn’t affect the principles of the example and it keeps the important concepts more clear.

Additionally, this example uses an Atollic-specific “tiny” printf() version that do not use malloc() internally. This ensures the test harness do not affect the test results.

Also note the function print_mallinfo() before main(). It prints out the internal current state of memory allocation. The print_mallinfo() function uses a mallinfo struct that is returned by the C runtime library function mallinfo().

The following fields are defined in the mallinfo struct:

  • arena is the total amount of space in the heap
  • ordblks is the number of chunks which are not in use
  • uordblks is the total amount of space allocated by malloc
  • fordblks is the total amount of space not in use
  • keepcost is the size of the top most memory block

Here is the code (main.c):

 

#include <malloc.h>
#include <stdint.h>
#include <stdio.h>

void print_mallinfo() {
        struct mallinfo info = mallinfo();
        printf("mallinfo: \n\
                        arena:\t\t%d\t(total space in heap)\n\
                        ordblks:\t%d\t(number of chunks not in use)\n\
                        uordblks:\t%d\t(total amount of space allocated by malloc)\n\
                        fordblks:\t%d\t(total amount of space not in use)\n\
                        keepcost:\t%d\t(size of topmost memory block)\n",
                        info.arena,
                        info.ordblks,
                        info.uordblks,
                        info.fordblks,
                        info.keepcost);
}

int main(void) {
        print_mallinfo();

        printf("\nAllocating 1 byte\n");
        uint8_t *bytes1 = (uint8_t*)malloc(1);
        print_mallinfo();

        printf("\nAllocating 16 bytes\n");
        uint8_t *bytes2 = (uint8_t*)malloc(16);
        print_mallinfo();

        printf("\nAllocating 64 bytes\n");
        uint8_t *bytes3 = (uint8_t*)malloc(64);
        print_mallinfo();

        printf("\nFreeing second allocation of 16 bytes\n");
        free(bytes2);
        print_mallinfo();

        printf("\nAllocating 16 bytes again\n");
        bytes2 = (uint8_t*)malloc(16);
        print_mallinfo();

        printf("\nFreeing 16 byte allocation again\n");
        free(bytes2);
        print_mallinfo();

        printf("\nAllocating 64 bytes\n");
        bytes3 = (uint8_t*)malloc(64);
        print_mallinfo();

        printf("\nFreeing first 1 byte allocation\n");
        free(bytes1);
        print_mallinfo();

        while (1) {
                ;
        }
}

 

If we run this application, we will get this output:

mallinfo:

arena:      0           (total space in heap)

ordblks:    0           (number of chunks not in use)

uordblks:   0           (total amount of space allocated by malloc)

fordblks:   0           (total amount of space not in use)

keepcost:   0           (size of topmost memory block)

 

Allocating 1 byte

_sbrk: Growing memory pool by 0 bytes. New span is 0x20000144-0x20000144 (0 bytes)

_sbrk: Growing memory pool by 12 bytes. New span is 0x20000144-0x20000150 (12 bytes)

_sbrk: Growing memory pool by 0 bytes. New span is 0x20000144-0x20000150 (12 bytes)

mallinfo:

arena:      12          (total space in heap)

ordblks:    0           (number of chunks not in use)

uordblks:   12          (total amount of space allocated by malloc)

fordblks:   0           (total amount of space not in use)

keepcost:   0           (size of topmost memory block)

 

Allocating 16 bytes

_sbrk: Growing memory pool by 24 bytes. New span is 0x20000144-0x20000168 (36 bytes)

_sbrk: Growing memory pool by 0 bytes. New span is 0x20000144-0x20000168 (36 bytes)

mallinfo:

arena:      36          (total space in heap)

ordblks:    0           (number of chunks not in use)

uordblks:   36          (total amount of space allocated by malloc)

fordblks:   0           (total amount of space not in use)

keepcost:   0           (size of topmost memory block)

 

Allocating 64 bytes

_sbrk: Growing memory pool by 72 bytes. New span is 0x20000144-0x200001B0 (108 bytes)

_sbrk: Growing memory pool by 0 bytes. New span is 0x20000144-0x200001B0 (108 bytes)

mallinfo:

arena:      108         (total space in heap)

ordblks:    0           (number of chunks not in use)

uordblks:   108         (total amount of space allocated by malloc)

fordblks:   0           (total amount of space not in use)

keepcost:   0           (size of topmost memory block)

 

Freeing second allocation of 16 bytes

_sbrk: Growing memory pool by 0 bytes. New span is 0x20000144-0x200001B0 (108 bytes)

mallinfo:

arena:      108         (total space in heap)

ordblks:    0           (number of chunks not in use)

uordblks:   84          (total amount of space allocated by malloc)

fordblks:   24          (total amount of space not in use)

keepcost:   0           (size of topmost memory block)

 

Allocating 16 bytes again

_sbrk: Growing memory pool by 0 bytes. New span is 0x20000144-0x200001B0 (108 bytes)

mallinfo:

arena:      108         (total space in heap)

ordblks:    0           (number of chunks not in use)

uordblks:   108         (total amount of space allocated by malloc)

fordblks:   0           (total amount of space not in use)

keepcost:   0           (size of topmost memory block)

 

Freeing 16 byte allocation again

_sbrk: Growing memory pool by 0 bytes. New span is 0x20000144-0x200001B0 (108 bytes)

mallinfo:

arena:      108         (total space in heap)

ordblks:    0           (number of chunks not in use)

uordblks:   84          (total amount of space allocated by malloc)

fordblks:   24          (total amount of space not in use)

keepcost:   0           (size of topmost memory block)

 

Allocating 64 bytes

_sbrk: Growing memory pool by 72 bytes. New span is 0x20000144-0x200001F8 (180 bytes)

_sbrk: Growing memory pool by 0 bytes. New span is 0x20000144-0x200001F8 (180 bytes)

mallinfo:

arena:      180         (total space in heap)

ordblks:    0           (number of chunks not in use)

uordblks:   156         (total amount of space allocated by malloc)

fordblks:   24          (total amount of space not in use)

keepcost:   0           (size of topmost memory block)

 

Freeing first 1 byte allocation

_sbrk: Growing memory pool by 0 bytes. New span is 0x20000144-0x200001F8 (180 bytes)

mallinfo:

arena:      180         (total space in heap)

ordblks:    0           (number of chunks not in use)

uordblks:   144         (total amount of space allocated by malloc)

fordblks:   36          (total amount of space not in use)

keepcost:   0           (size of topmost memory block)

 

As we can see, a simple instrumentation of _sbrk() and an extra utility function gave us information on how the memory is being used by malloc() and free() – at least on an overview level. In particular, we can see how large the memory pool is at different times of execution.

This is very important information, as the availability of enough memory must be ensured at all times to guarantee the embedded system behaves as expected.

It is also possible to see how much free memory there is in the memory pool. When you call free() to return memory, this memory range becomes free and can be re-used by future malloc() calls. The fordblks field in the mallinfo struct contains the number of bytes in the memory pool that is free.

 This memory can however be split into various-sized areas across the whole memory pool, in effect creating a fragmented memory area with “holes” of different sizes – that may or may not be large enough to re-use by a future malloc() call.

If you use dynamically allocated memory in your own embedded projects, you can use this little trick to get a better understanding of how your application use memory.

 

Do you want to learn more on ARM Cortex-M development and debugging? Read this whitepaper:

Read our ARM development  whitepaper!

 

Topics: GNU tools (GCC/GDB)