【組み込み】スタックとヒープの違いとは?マイコンでメモリ不足が起きる原因を解説

  • URLをコピーしました!

今回は、組み込み開発でよく登場するスタックヒープの違いについて解説します。
C言語やC++でマイコン向けのプログラムを書いていると、動作不良やリセット、原因不明の暴走が起きることがあります。その原因のひとつがメモリ不足です。

特に、PCアプリケーションの感覚で実装していると、マイコンでは想像以上に早くメモリが足りなくなります。
この記事では、スタックとヒープの基本的な役割、メモリ不足が起きる典型的な原因、実務で意識したい対策までを整理して紹介します。

目次

スタックとヒープとは

組み込みソフトで使うRAMは、用途ごとにいくつかの領域に分かれます。
その中でも、特に理解しておきたいのがスタックヒープです。

スタックとは

スタックは、関数を呼び出したときに一時的に使われるメモリ領域です。
主に以下のような情報が置かれます。

  • 関数内のローカル変数
  • 関数の引数
  • 戻り先アドレス
  • 一時的な作業データ

関数が終了すると、その関数で使ったスタック領域は基本的に解放されます。
そのため、短期間だけ必要なデータを扱うのに向いています。

たとえば、次のような配列はスタックに確保されることが多いです。

void func(void)
{
    char buffer[256];
}

このbufferは関数の実行中だけ存在します。
便利ですが、サイズが大きすぎるとスタックを圧迫します。

ヒープとは

ヒープは、必要なときに動的に確保して使うメモリ領域です。
C言語ではmalloc()calloc()free()などで操作します。

char *buffer = malloc(256);
if (buffer != NULL) {
    // 使用処理
    free(buffer);
}

ヒープは実行中にサイズを変えやすい反面、管理が複雑になります。
解放忘れや断片化が起きると、空きメモリがあるように見えても必要なサイズを確保できないことがあります。

スタックとヒープの違い

両者の違いを簡単に整理すると、次の通りです。

使い方の違い

  • スタック: 関数呼び出しに応じて自動で確保・解放される
  • ヒープ: プログラムが明示的に確保・解放する

管理のしやすさの違い

  • スタック: 自動管理のため扱いやすい
  • ヒープ: 柔軟だが、解放漏れや断片化に注意が必要

組み込みでの注意点

マイコンではRAM容量が非常に小さいことがあります。
たとえば数KB〜数百KB程度しかない環境では、スタックとヒープの使い方がそのまま安定性に直結します。

マイコンでメモリ不足が起きる主な原因

ここからは、実際にメモリ不足を引き起こしやすい原因を見ていきます。

大きなローカル変数を使っている

最もよくあるのが、関数内で大きな配列や構造体を宣言してしまうケースです。

void task(void)
{
    uint8_t rx_buf[1024];
    uint8_t tx_buf[1024];
}

このようなコードは、1回の関数呼び出しだけで2KB以上のスタックを消費します。
RTOS環境でタスクごとにスタックサイズを小さく設定している場合、これだけでスタックオーバーフローになることもあります。

関数の呼び出しが深い

再帰処理や、関数を何段階も呼び出す設計もスタックを消費します。
1つ1つの関数で使うメモリは小さくても、呼び出しの深さが増えると合計使用量が大きくなります。

組み込みでは再帰を避ける方針が取られることが多いのは、このためです。

malloc/freeの使いすぎ

ヒープを頻繁に使う設計も危険です。
たとえば通信データを受けるたびにmalloc()して、処理後にfree()する実装は、一見すると無駄が少なく見えます。

しかし実際には、以下の問題が起きやすくなります。

  • 解放忘れによるメモリリーク
  • 繰り返し確保・解放による断片化
  • 確保失敗時の例外処理漏れ

長時間連続動作する機器では、こうした問題が数時間後や数日後に不具合として現れることがあります。

タスク数や割り込み処理が多い

RTOSを使う場合、各タスクに個別のスタック領域が必要です。
タスクを増やしすぎたり、余裕を見て大きめのスタックを割り当てすぎたりすると、それだけでRAMを圧迫します。

また、割り込み処理でもスタックは消費されます。
ネストした割り込みや、重い処理を割り込み内で行う設計は注意が必要です。

実務で意識したい対策

メモリ不足を防ぐには、単に「RAMを増やす」以外にもできることがあります。

大きなバッファは配置場所を見直す

一時変数として大きな配列をローカルに置くのではなく、必要に応じて静的領域に移すことを検討します。

static uint8_t work_buf[1024];

ただし、静的領域に置けば万能というわけではありません。
同時に複数箇所から使わないか、再入性に問題がないかは確認が必要です。

動的確保を減らす

組み込みでは、ヒープを使わず固定長バッファで設計する方が安定しやすいです。
必要な最大サイズが見積もれるなら、あらかじめ確保して使い回す構成の方がトラブルを減らせます。

スタック使用量を見積もる

「たぶん足りる」ではなく、関数ごとのローカル変数量タスクごとのスタック使用量を確認することが重要です。
コンパイラやリンカのマップファイル、RTOSのスタック監視機能が使えるなら活用したいところです。

サイズ感を数値で確認する

構造体や配列のサイズは、感覚ではなくsizeofで確認するのが確実です。

printf("packet size = %u\n", (unsigned int)sizeof(packet_t));

特に構造体は、アライメントやパディングの影響で想定より大きくなることがあります。
通信バッファやログ用ワーク領域がどれだけRAMを使っているかは、数値で把握しておくと安心です。

まとめ

スタックとヒープは、どちらもRAMを使う重要な領域ですが、役割と扱い方が異なります。

  • スタックは関数呼び出し時の一時領域
  • ヒープは動的に確保して使う領域
  • マイコンではどちらも容量が限られる
  • 大きなローカル変数、深い呼び出し、動的確保の多用がメモリ不足の原因になりやすい

組み込み開発では、機能追加より先にメモリの使い方を把握することが安定動作への近道です。
不具合が出てから調べるのではなく、設計段階でスタックとヒープの使い分けを意識しておくと、後からの手戻りをかなり減らせます。

よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

エンジニア。20代。組み込みエンジニアとして働き始めるも、働き方や業務内容に限界を感じ、 AI,Web3エンジニアを目指して勉強中。 エンジニアとして思うことや、学んだことを発信します。

目次