組込みのスタック設計入門

組込み開発でスタック起因の不具合に悩まされた経験を持つ方は少なくないでしょう。再現条件が特定のコールパスに偏り、通常のテストで検出しにくい点が厄介な性質です。本記事では、スタックサイズの見積もりから実機での実測、スタックオーバーフロー検出の実装パターン、運用フェーズの見直しまで、設計から保守までの流れを整理していきます。

なぜスタック設計が組込みで重要か

組込みシステムでは、スタック設計を後回しにすると不具合の温床になりがちです。CPUに割り当てる領域は数百バイトから数KB程度にとどまることが多く、関数のネストや大きなローカル変数、割り込み処理が重なると確保したメモリの範囲を超える事態が起こり得ます。

ひとたびスタックオーバーフローが起きると、隣接メモリを破壊して戻り先アドレスを書き換え、文字化けやシステムのハングアップ、無限ループとして表面化します。特定の呼び出しパスでしか再現せず、通常のテストでは検出されにくい点も厄介です。特定の呼び出しパスや、周辺からの負荷の状態によって、不具合が起きるか起きないかが変わり、不具合の再現条件を見つけるのが大変です。また、修正をしたとしても、十分に修正されているかを確認し、品質がOKだと判断することも難しい不具合になります。長時間連続稼働が前提となる組込みシステムでは、設計段階で扱うべき品質課題といえるでしょう。

スタックサイズの見積もり手順(実装前)

スタックサイズの見積もりは、関数呼び出しの最深経路、割り込み処理の消費分、再帰や可変長配列の影響を積算するのが基本です。

各関数のスタック使用量は、コンパイラが出力するレポートから把握できます。マイコンメーカや、デバッガを供給している各社のデバッグ用ツールでは、スタックサイズを確認する方法 が公開されています。一部のマイコンメーカーやツールベンダーでは、スタックサイズを確認するためのツールや手法が公開されています。ArmのCortex-Mでは例外発生時にレジスタ群が自動退避されるため、FPU利用の有無を含めた割り込み分も加算が必要です。

再帰や可変長配列は静的解析が難しく、設計段階で制限するのが定石となります。IPAのコーディング作法ガイドやJPCERT/CCのMEM05-Cも同様の方針を示しており、見積もり値には2〜3割の余裕を持たせるのが実務上の目安です。

スタック使用量を実測する(実装時)

見積もり値が妥当かどうかは、実機テストで裏取りします。スタック領域全体を0xA5などの既知パターン値で初期化し、負荷をかけた後にどこまで上書きされたかを見れば、最大使用量つまりスタック使用率を逆算できます。FreeRTOSではuxTaskGetStackHighWaterMark関数がタスクごとの最小残量を返すため、稼働中のシステムに組み込めるのも便利でしょう。

設計時の理論値だけでは拾えない動的な振る舞いを可視化できる点が強みです。実機での消費量が確保領域の7割程度に収まれば、突発的な負荷にも余裕があると判断できます。

スタックオーバーフロー検出の実装パターン

実測で余裕を確認しても、稼働中に想定外の経路で限界に達する可能性は残ります。スタックオーバーフロー検出は、単一の手法ではなく複数の方式を組み合わせるのが現実的です。軽量で実装しやすい方式と、ハードウェアの支援を前提とした厳密な方式では、適用条件と検出タイミングが異なるからです。ここでは「スタックガード方式」「RTOS機能の活用」「MPUによる保護」について、順に整理していきます。

ガード値監視(スタックガード方式)

スタックガード方式では、スタックガード領域に「カナリア値」と呼ばれる専用の値を置き、変化していないかを定期的に確認する仕組みが採用されています。FreeRTOSのMethod 2では、スタック末尾の領域に固定値を置き、コンテキストスイッチごとに照合します。GCCの-fstack-protectorが採用するスタックカナリアも同じ発想にもとづいており、実装しやすくハードウェアに依存しない選択肢といえるでしょう。

RTOSのスタックチェック機能とフック活用

RTOS使用時は、ビルトインのスタックチェック機能とオーバーフローフックが活用できます。FreeRTOSのconfigCHECK_FOR_STACK_OVERFLOWを有効にすると、コンテキストスイッチ時にポインタ境界とカナリア値の照合が走り、検出時にvApplicationStackOverflowHookが呼ばれる仕組みです。Zephyrのスタック保護機能やTOPPERS/ASP3にも同等の機能があり、ログ記録や安全停止処理と組みあわせて運用できます。

MPU/保護機構が使える場合の検出強化

MPU、メモリ保護ユニットを搭載したマイコンなら、スタック領域の直下をアクセス不可属性に設定することで、境界違反時にMemManageFault例外として捕捉できます。Cortex-M33以降のArmv8-Mに搭載されたMSPLIMやPSPLIMという専用レジスタを使えば、書き込み前の段階で違反の検出も可能です。

スタックガード方式と異なり、破壊が起きる前に検知できる点が強みです。ただしMPU非搭載機では選択できないため、対象機種に応じてスタックガード方式と組み合わせる判断が現実的でしょう。

スタック不足を見分けるには

スタック不足が疑われる場合は、複数の観点から原因を切り分けます。検出が予防に相当するのに対し、こちらは事後の確認手順といえるでしょう。

ハードフォルトが起きたら、まずスタックポインタが想定範囲内かを確認します。次に、ガード値が書き換わっていないか、Watermarkがスタック上限に達していないかを順に見ていきます。スタックダンプから戻り先アドレスやレジスタ退避値の異常を照合する方法も有効でしょう。複数の兆候が当てはまれば、スタック不足の可能性が高いと考えられます。

ただし、これらの確認手段は、ガード値の埋め込みやWatermarkの初期化、スタックダンプ取得の仕組みを設計や実装の段階で仕込んでおくことではじめて機能します。準備が抜けたまま不具合に直面すると、原因解析へ大きな時間を費やすことになるでしょう。

スタック設計を見なおすポイント

組込み製品は長期にわたり保守と機能追加が続くため、スタック設計は一度きりでなく継続的な管理対象として扱います。改修のたびに呼び出しチェーンや割り込み構成が変わり、ライブラリ更新で関数構造が変わることもあるためです。

機能追加時にスタック使用量レポートを必須化し、CI(継続的インテグレーション)におけるコードの静的解析でしきい値超過時に警告を出す運用が効果的でしょう。定期的な見直しが品質と信頼性を支える基盤となります。

組み込みソフトの世界 トップへ戻る