How to Ensure Embedded Systems Do Not Crash? Safely Use Pointers in C and C++!

As someone who has worked in the embedded systems industry for over twenty years, I have witnessed tremendous advancements in technology—from 8-bit microcontrollers to today’s complex multi-core systems.

However, one thing has remained constant: pointers in C and C++. They are a double-edged sword, providing incredible flexibility in memory management, but mismanagement can lead to serious damage.

Recently, an incident where a NULL pointer caused a system crash clearly reminded us how important it is to safely use pointers in our code.

In this article, we will explore best practices for safely using pointers in C and C++, ensuring your embedded systems run smoothly without unexpected crashes.

Understanding Pointers

Pointers are essentially variables that store the memory addresses of other variables. Pointers can enable efficient memory operations and dynamic memory allocation, but they also come with risks, the most obvious being that dereferencing a NULL or uninitialized pointer can lead to catastrophic failures. They can also lead to security vulnerabilities, overwrite unintended locations, and other issues, so understanding how pointers work is the first step to safely using them.

Declaring Pointers

To declare a pointer, you can use the * operator:

int *ptr; // A pointer to an integer

This declaration does not allocate memory for an integer; it merely creates a pointer that can point to an integer memory location. It is crucial to initialize the pointer before using it, as using an uninitialized pointer may lead to undefined behavior. (Your compiler might initialize it to 0 or NULL, or it might just hold the value of the memory before allocation).

Initializing Pointers

You can initialize pointers in several ways:

1. Assigning the address of a variable:

int var = 42;

int *ptr = &var; // ptr now points to var

2. Using dynamic memory allocation:

int *ptr = (int *)malloc(sizeof(int)); // Allocating memory for one integer

if (ptr == NULL) {

// Handle memory allocation failure

}

*ptr = 42; // Assign a value to the allocated memory

Using dynamic memory allocation requires caution, especially in embedded systems where memory is typically limited. Since malloc is also non-deterministic, it cannot guarantee execution at runtime, making dynamic memory allocation questionable in embedded systems.
If you choose to use dynamic memory allocation, always check if the pointer is NULL to verify that memory allocation was successful. The malloc function will return a pointer to the allocated block of memory. If it is NULL, the operation has failed!
(I once tried to prove my colleagues wrong by heavily using malloc in my software. I succeeded, but later I regretted my arrogance!).

Best Practices for Safe Pointer Usage

Best Practice 1—Always Initialize Pointers

When creating a pointer, it is best to assign it to a useful object immediately. Uninitialized pointers may point to random memory locations, leading to undefined behavior. You should always initialize pointers, even if just to initialize them to a NULL pointer:

int *ptr = NULL; // Initialize to NULL

A pointer pointing to NULL is better than one pointing to a random location.

Best Practice #2—Check for NULL Before Dereferencing

The advantage of initializing a pointer to NULL is that we can check it before dereferencing to ensure it has been initialized. If the value is 0x08FF001234, I might assume this pointer has been initialized to the correct location. (Assumptions are bad! We might use MPU and other linker tricks to verify it points to the correct area).

Before dereferencing a pointer, ensure it is not NULL. This simple check can prevent crashes:

if (ptr != NULL) {

// Dereference and do useful work!

} else {

// Pointer is NULL, cannot dereference!

// Exception handling!

}

When I have an array of function pointers, I often use this trick. I might have a table like this:

typedef void (*LedCommand_t)(void);

LedCommand_t LedCommands[] = {

turnLedOn,

turnLedOff,

NULL

}

The last item in the table is NULL, making it easy to check. I can loop through the table and generate a line of code to call that function (as long as it is not NULL)!

Best Practice 3—Use Smart Pointers in C++

If you are using C++, consider using smart pointers like std::unique_ptr and std::shared_ptr, which provide automatic memory management:

#include

std::unique_ptr ptr(new int(42)); // Automatically deallocates memory

Smart pointers help manage memory automatically, reducing the chances of memory leaks and dangling pointers.

Best Practice #4—Use Pointer Arithmetic Cautiously

Pointer arithmetic is very powerful. I once wrote an application that used it to move memory and store data in a buffer. It is an efficient solution, but it can also be dangerous if misused. To ensure these pointers stay within the correct memory boundaries, some debugging and extensive testing are absolutely necessary.
Ensure you stay within the allocated memory range:

int arr[5] = {1, 2, 3, 4, 5};

int *ptr = arr; // Pointer to the first element

for (int i = 0; i < 5; i++) {

printf(“%d\n”, *(ptr + i)); // Accessing elements using pointer arithmetic

}

From the example above, you can see that while pointers may stay within memory boundaries, they are filled with magical numbers, making this code dangerous! Changing the number of elements in the array from 5 to 4 would require modifying at least three places in the code, so if you modify the code, the chance of reading data outside the buffer is very high!

Best Practice #5—Use Toolchain Files for Memory Management

Efficient memory management is crucial in embedded systems. Toolchain files can help define memory layouts and ensure pointers are correctly aligned. When using toolchains like CMake, you can specify memory settings to ensure pointers point to the correct locations:

set(MEMORY_START 0x20000000)

set(MEMORY_END 0x2001FFFF)

By defining memory regions, you can manage pointers more effectively, ensuring they point to valid memory addresses. You can also use these memory regions to check the integrity of pointer values! Just assigning a value to a pointer does not mean the pointer value is correct.

Safely Handling Dynamic Memory

Dynamic memory allocation is a common source of pointer-related issues, which is why we often avoid using it in resource-constrained systems. Using dynamic memory before entering the main loop may be safe, as it makes the allocation appear static for the application.
However, sometimes dynamic allocation is the best tool for the job. In such cases, here are some strategies for safely using it:

Strategy 1–Always Free Allocated Memory

Whenever you allocate memory, ensure you free it when it is no longer needed to prevent memory leaks:

int *ptr = (int *)malloc(sizeof(int));

if (ptr != NULL) {

*ptr = 42;

free(ptr); // Free the allocated memory

}

Failing to free allocated memory can lead to memory exhaustion, especially in long-running embedded systems.

Strategy #2—Set Pointer to NULL After Freeing

After freeing a pointer, set it to NULL to avoid dangling pointers:

free(ptr);

ptr = NULL; // Prevents accidental dereference

This simple step can prevent potential crashes caused by dereferencing freed memory.

Strategy 3—Use RAII

Resource Acquisition Is Initialization (RAII) is a common principle in C++ and object-oriented programming languages. It encapsulates dynamic memory allocation within a class. The class constructor allocates memory, and the destructor frees memory, ensuring resources are managed properly.

class Resource {

public:

Resource() {

data = new int[10]; // Allocate memory

}

~Resource() {

delete[] data; // Deallocate memory

}

private:

int* data;

};

Avoid Common Pointer Traps

Trap #1—Pointer Casting

In C, casting pointers can hide problems and lead to undefined behavior. Instead of doing this, let the compiler handle type conversions:

int *ptr = (int *)malloc(sizeof(int)); // Avoid casting; it’s unnecessary in C

Letting the compiler do its job can help uncover potential issues during the compilation process.

Note: You must be careful with this issue. Some camps strongly advocate explicit programming, and you must explicitly perform the conversion.

Trap #2—Multiple Pointers Pointing to the Same Memory

When multiple pointers point to the same memory location, changing the value through one pointer can lead to confusion and errors:

int *ptr1 = (int *)malloc(sizeof(int));

int *ptr2 = ptr1;

*ptr1 = 10; // Both ptr1 and ptr2 point to the same memory

printf(“Value through ptr2: %d\n”, *ptr2); // Outputs 10

If you are not careful about who owns the memory, this situation can lead to unexpected behavior.

Conclusion

Pointers are a powerful feature in C and C++, but they also come with risks that can lead to serious issues like NULL pointer crashes. By following these best practices (initializing pointers, checking for NULL before dereferencing, using smart pointers in C++, and cautiously managing dynamic memory), you can safely navigate the complexities of pointers.
Moreover, by leveraging toolchain files to manage memory layouts, you can ensure pointers are always valid and perform well in embedded systems. As you develop your skills in using pointers, remember that safety and caution should always be your guiding principles.
How to Ensure Embedded Systems Do Not Crash? Safely Use Pointers in C and C++!

END

Author: Jacob
Source: EDN Electronic Technology Design
Copyright belongs to the original author, if there is infringement, please contact to delete.
Recommended Reading
Why Are Major Companies Using Yocto?
How Difficult Is It to Cultivate an Excellent Embedded Engineer?
C/C++ Deadline Approaches, the US Firmly Requires Full Elimination by 2026!
→ Follow to Stay Updated ←

Leave a Comment

×