Common Mistakes in Embedded C Development - Modern C and Stack Overflows

Ilya KatlinskiIlya Katlinski
4 min read

Article photo by Jorge Ramirez on Unsplash

📘 Introduction

C is everywhere. From tiny temperature sensors to aerospace control systems, C has been the undisputed king of embedded development for decades. According to the JetBrains Developer Ecosystem 2023 report, C remains one of the most-used languages in embedded software development. The Eclipse IoT Developer Survey 2024 echoes this, showing C at the top of the list for IoT firmware with a 55% adoption rate.

But with that long reign comes a dark side: memory bugs, hard-to-debug crashes, and subtle issues that can bring down a system hours or even weeks after deployment.

If you’ve ever spent a sleepless night tracking down a rogue pointer or a mysterious reboot, you’re not alone.

In recent years, languages like Rust have gained attention for promising “memory safety” out of the box. And while the Rust community isn’t wrong about C’s historical pitfalls, what is often overlooked is that many of those issues stem from outdated patterns, not from the language itself.

Modern C, when used properly, can be much safer than most people think.

This article kicks off a larger series on common mistakes in Embedded C, and we’re starting with memory management. The topic is so foundational and widespread, we’re breaking it into four focused parts:

  1. Stack Overflows on Small Systems

  2. Unsafe Use of Pointers

  3. Using malloc/free in Real-Time Code

  4. Buffer Overflows and Memory Leaks

🚀
Action Point: If you’re writing C the same way it was done in the 90s, it’s time for a rethink. Read on to sharpen your skills and write safer, more reliable embedded code.

What Do We Mean by Modern C?

When we talk about modern C, we’re not referring to a completely new language or writing C like it’s C++. We’re talking about using newer C standards (like C99, C11, and C23) along with better habits and safer coding techniques.

Modern C means:

  • Using standard types like uint8_t, int32_t, instead of unsigned char or long makes your code more portable and predictable.

  • Use safe functions like snprintf() instead of risky ones like sprintf() to avoid buffer overflows.

  • Keeping memory usage under control, for example, avoiding unnecessary malloc() calls, especially in real-time or ISR code.

  • Turning on compiler warnings and fixing them instead of ignoring them.

  • Utilising static analysis tools (such as cppcheck or compiler sanitisers) helps catch bugs before they reach the hardware.

In short, modern C is simply writing C more safely, utilising the tools and features available today, rather than the way it was done 30 years ago.


🧠 Stack Overflows on Small Systems

🐞 The Problem

Imagine you’re building an IoT temperature logger on an ESP32. Every minute, it collects sensor data and sends it as a JSON payload to the cloud. Seems simple enough:

void logAndSend() {
    char json[2048]; // ⛔️ Dangerous: 2 KB on the stack!
    snprintf(json, sizeof(json), "{ \"temp\": %.2f }", readTemp());
    mqttSend("iot/topic", json);
}

Why it’s a problem:

  • Most embedded systems, like ESP32 or STM32, have small default stack sizes, especially in RTOS tasks (e.g. 4–8 KB).

  • Allocating large arrays on the stack can quickly overflow the task or main thread stack.

  • The system might reboot randomly, corrupt memory silently, or just stop working under load.

✅ Solution 1: Use Static Buffers (Persistent, Safe Memory)

static char json[2048]; // ✅ Safe: Allocated in .bss, not stack

void logAndSend() {
    snprintf(json, sizeof(json), "{ \"temp\": %.2f }", readTemp());
    mqttSend("iot/topic", json);
}
Warning: Static memory is safe in simple, single-threaded systems. In concurrent environments, always protect shared static data with synchronisation to avoid race conditions or corruption.

📌 Use this when:

  • The buffer is reused or shared across multiple function calls.

  • You know the required size at compile time.

✅ Solution 2: Use Heap Carefully (If Appropriate)

void logAndSend() {
    char* json = malloc(2048);
    if (json) {
        snprintf(json, 2048, "{ \"temp\": %.2f }", readTemp());
        mqttSend("iot/topic", json);
        free(json);
    } else {
        log_error("Failed to allocate memory!");
    }
}

📌 Use this only when:

  • You need to allocate buffers dynamically (e.g. variable size from cloud config).

  • You are not in an ISR or a real-time critical path.

  • You handle errors and free memory reliably.

✅ Solution 3: Stream or Chunk Data to Avoid Large Buffers

Instead of building a full 2 KB JSON string in memory, stream the data or send it in small parts.

void logAndSend() {
    for (int i = 0; i < 10; i++) {
        char chunk[128]; // ✅ Small buffer, safe on stack
        float temp = readTemp();
        snprintf(chunk, sizeof(chunk), "{ \"sample\": %d, \"temp\": %.2f }", i, temp);
        mqttSend("iot/topic", chunk);
    }
}

💡 Takeaway

Never assume you have “enough” stack. In embedded systems, the stack is a tight resource, often the first thing to fail silently.

  • Avoid large local arrays

  • Prefer static memory for fixed buffers

  • Use dynamic memory only with care


At Itransition, we build IoT solutions with all these challenges in mind, ensuring our clients receive reliable, scalable systems with minimal maintenance overhead. Learn more about our approach at https://www.itransition.com/iot.

0
Subscribe to my newsletter

Read articles from Ilya Katlinski directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ilya Katlinski
Ilya Katlinski