RTOSベース設計におけるスタックオーバーフロー(パート1)
著者:Jean J. Labrosse氏、RTOSエキスパート
RTOSベースのアプリケーションではすべてのタスクが固有のスタックを必要とし、そのサイズはタスクの状態(関数呼び出しのネスティング、関数への引数渡し、ローカル変数など)によって異なります。
スタックオーバーフローを回避するためには、スタック空間を多めに割り当てる必要がありますが、RAMの浪費を避けるために多すぎてもいけません。
スタックオーバーフローとは?
共通の認識を持てるよう、先ず初めにスタックオーバーフローとは何かについて説明します。ここでは、スタックが高位メモリから低位メモリに向かって積まれていくものと想定します。もちろん、スタックが逆方向に積まれる場合も同じ事態が発生します。図1をご覧ください。
図1 スタックオーバーフロー
(1) CPUのスタックポインタ(SP)レジスタは、タスクに割り当てられたスタック空間内の場所を指示します。このタスクは、以下に示すように関数foo()を呼び出そうとしています。
void foo (void)
{
int i;
int array[10];
:
:
// Code
}
(2) foo()が呼び出されると、CPUは呼び出し元のリターンアドレスをスタックに保存します。ただし、これはCPUやコンパイラでだいぶ異なります。
(3) 次に、コンパイラは、ローカル変数を保存するためにSPを調整します。しかし、この時点でスタックがオーバーフローしてしまい(SPはスタックに割り当てられたストレージ領域外を指示)、スタック・ベースを超えるデータがどのようなものであれ、foo()の処理はほとんどすべて破綻してしまいます。実際はコードフローにも依りますが、アレイ(array)がまったく使用されず問題が直ぐには現れないこともあります。しかし、そのような場合でも、foo()が別の関数を読み出した場合、スタック外に何らかの影響を及ぼす可能性は大いにあります。
(4) そこで、スタックポインタは、foo()がコードの実行を開始する際、foo()の呼び出し前の位置から48バイトのオフセットを設けます(スタックエントリは4バイト幅と仮定)。
(5) 通常、ここに何があるかは分かりません。別のタスクのスタックの場合もあれば、変数やデータ構造、あるいはアプリケーションが使用するアレイの場合もあります。ここにあるものが何であれ、上書きすることで予測不能な動作が引き起こされる可能性があります。たとえば、別のタスクで計算した値が期待値と異なり、それが原因でコードでの判断が誤ったパスをたどってしまう、あるいはシステムが通常条件で正常に動作したかと思うと次の瞬間には動作しなくなるということもあります。何が起きるかはまったく分からず、予測は極めて困難です。実際、コードを変更するたびに動作が異なることもあります。
アプリケーションの動作が「おかしい」と感じる場合、先ず思い浮かぶのは、スタックサイズの不足です。
スタックサイズの決定方法
タスクに必要なスタックのサイズはアプリケーションによって異なりますが、以下の項目を合算することで必要なスタック空間を手作業で見積もることができます。
1) すべての関数呼び出しのネスティングに必要なメモリ。関数呼び出しの階層レベルごとに下記が必要。
- CPUのアーキテクチャに応じて、1回の関数呼び出しのリターンアドレスに1つのポインタ。実際には、CPUによっては、リターンアドレスを専用レジスタ(通常リンクレジスタまたはLRと呼ばれます)に保存するものもあります。しかし、関数が別の関数を呼び出す場合、呼び出し元はLRを保存しておく必要があるため、いずれにせよLRはスタックに入れられると仮定するのが賢明です。
- これらの関数呼び出しに渡される引数に必要なメモリ。引数は、多くの場合CPUレジスタで渡されます。しかし、関数が別の関数を呼び出す場合は、やはり、レジスタの内容はスタックに保存されることになります。したがって、引数はスタックに渡されるものとして、タスクのスタックサイズを決定することを推奨します。
- これらの関数のローカル変数のストレージ
- 関数内での状態保存用の追加スタック空間
IARリンカには、各タスクに必要なスタックサイズを決めるのに最適な機能があります。
図2に示すように、リンカを設定する際にボックスにチェックマークを入れるだけで、ビルド時に、アプリケーションの関数ごとの呼び出しスタックの深さ(バイト単位)を表すリンクマップ(.MAPファイル)を出力することができます。
次に、それぞれのタスクの呼び出しスタックサイズに注目します。最大スタックサイズを決定するには、まだいくつかの数値を加算する必要があります。
図2 IARリンカの設定オプション
2) フルCPUのコンテキスト用ストレージ(CPUによって異なる)。必要な場合は、FPUレジスタ用ストレージ
3) ネストされた各ISR用の、別のフルCPUコンテキスト用ストレージ(CPUに別途ISR処理用スタックがない場合)
4) これらのISRが使用するローカル変数に必要なスタック空間
以下の簡単な式を用いて、各タスクに必要なスタックの合計サイズ(バイト単位)を計算できます。式では33%のマージンをとっています。
実際には、ほとんどの組込みアプリケーションで、実行時のスタック使用率を70%未満に抑えるのが望ましいとされています。条件に応じてより安全な値にすることもできます。
スタック使用率を計算する場合、コードの正確なパスが常に分かっていることが前提となりますが、これはいつも可能とは限りません。
特に、printf()のような関数を呼び出す場合、どれだけのスタック空間が必要になるかを推測することすら困難であり、ほぼ不可能と言えるでしょう。また、関数ポインタを用いた間接的な関数呼び出しも問題となる可能性があります。
最後になりますが、再帰的コードは書かないようにすべきです。この種のコードがあると、通常、スタックの使用量の決定が困難になるためです。
一般的には、かなり大きなスタック空間から始めて、やや厳しい条件でアプリケーションを実行し、実行時のスタック使用量をモニタします。
一部のRTOS(特にuC/OS-IIIおよびCs/OS3)では、実行時にスタック使用率をモニタして、これをIARのC-SPYに組込まれたRTOS認識機能を用いて表示することができます。
さらに詳しい情報
パート2の 「RTOSベースの設計におけるスタックオーバーフローの検出」、またはオンデマンドのウェビナーTips and hints for better debugging your RTOS-based application(RTOSベースアプリケーションのより効率的なデバッグのためのヒントとコツ) を参照してください。
著者について
本稿は、RTOSを使用したアプリケーション開発をテーマとしたシリーズの一部です。
Jean Labrosse(ジーン・ラブロス)氏は、Micriumの創設者であり、広く普及しているuC/OS-IIおよびuC/OS-IIIカーネルの作成者です。組込みソフトウェアのuC/ラインの発展のために積極的に取り組んでいます。
組込みシステム市場での豊富な経験を有し、市場を知り尽くしているLabrosse氏は、Weston Embedded Solutionsの主席アドバイザおよびコンサルタントとして、現行のRTOS製品の将来的な方向性の策定に尽力しています。Weston Embedded Solutionsは、Micriumのコードベースから生まれた信頼性の高いCesium RTOSファミリー製品のサポートと開発を専門としています。