ルネサスRX向けアプリケーションのスタック使用量の解析方法
IAR Embedded Workbench for RXのバージョン2.60でスタック使用量の解析を導入しました。本稿では、解析機能に関連して、使い方と最大スタック使用量の計算方法、および実行時のスタックポインタが示す兆候について、ご紹介します。
背景
スタックはメモリ上の連続した領域で、開発者が静的に確保する必要があります。以下のローカルなデータを保持します。
- レジスタに保持されないローカル変数
- レジスタに保持されない関数の実引数
- 計算の途中結果
- 関数の戻り値(レジスタに渡されない場合)
- 割り込みのコンテキスト
- 関数のリターン直前に保持すべきレジスタの値
スタックは2種類にわけることができます。1つは、関数のためにあらかじめ確保する領域、もう1つは実行時に動的に確保する領域です。両者の境は、スタックの頂点つまりスタックポインタが指すアドレスです。スタックポインタは通常、プロセッサの専用レジスタです。関数のために確保する領域は、スタックポインタを実行時に動かすことで実現します。そして、関数からリターンする時点で、解放します。このため、関数の終了後も使用されるデータを保持することはできません。
スタックの主な利点は、異なる関数が同じメモリ領域を共用できることです。ヒープと異なり、スタックは断片化したり、メモリリークに苦しむということはありません。
適切なスタック領域の確保はシステムの安定性・信頼性の面で重要です。スタックサイズを小さく設定しすぎると、スタックポインタはスタック領域外を指す状況となり、オーバーフローが発生します。この場合、アプリケーションはスタック外に書き込みを行い、変数の上書き・ワイルドポインタ・戻りアドレスの破壊など深刻なエラーを引き起こします。一方で、スタックサイズを大きく設定しすぎると、リソースが限られるMCU向けの組込みシステムにおいて、無駄となります。
RX向けスタック使用量の解析方法
適切な環境下で、リンカは正確に、各コールグラフ(他の関数から呼び出されない関数を起点とする関数呼び出しの有向グラフ)の最大スタック使用量を計算することができます。スタック使用量の章が、リンカが出力するマップファイルに加えられます。マップファイルは各コールグラフの最も深い呼び出し関係を列挙します。この計算は各関数の正確なスタック使用量が明らかな場のみ有効です。
大抵、コンパイラは各関数の使用量の情報を出力します。しかし、いくつかの場合、開発者が明示的に補足的な情報をコンパイラに渡す必要があります。例えば、間接関数呼び出し、再帰呼び出しの最大の回数などです。このためにプラグマディレクティブを使用したり、スタック使用解析制御ファイルを使用したりします。
スタック使用解析を有効にする
リンカオプションのアドバンストタブを選択し、スタックの使用量解析を有効化にチェックをする。
またリンカからマップファイルの出力をするように設定してください。マップファイルにスタック使用量の解析結果が出力されます。リンカオプションのリストタブから設定します。
単純なプリケーションに対して、スタック使用量の解析結果は簡素で理解しやすいものです。大抵の場合、スタートアップルーチンと割り込みハンドラは他からの呼び出し関係がないため、コールグラフのルートと認識されます。下記の例では、最大のスタック使用量はスタートアップルーチン(__iar_program_start)を起点とするグラフで288byte、割り込みのコールグラフは計120 byte(__interrupt_170および _default_handler)です。
*************************************************************************
*** STACK USAGE
***
Call Graph Root Category Max Use Total Use
------------------------ ------- ---------
interrupt 120 120
Program entry 288 288
Program entry
"__iar_program_start": 0xffffb14c
Maximum call chain 288 bytes
"__iar_program_start" 4
"_main" 8
"_printf" 8
"__PrintfFullNoMb" 152
"__LdtobFullNoMb" in xprintffull_nomb.o [4] 80
"__GenldFullNoMb" in xprintffull_nomb.o [4] 36
interrupt
"__interrupt_170": 0xffffaa22
Maximum call chain 52 bytes
"__interrupt_170" 52
interrupt
"_default_handler": 0xffff98cb
Maximum call chain 68 bytes
"_default_handler" 52
"_abort" 4
"__exit" 12
間接関数呼び出しの指定
間接関数呼び出しとは、関数ポインタを使用した関数呼び出しのことです。呼び出し対象の関数はビルド時には不明なため、リンカは間接関数呼び出しに対してスタックの使用量を自動的に計算することはできません。 そのためリンカから警告メッセージが出力されます。
Warning[Lo009]: [stack usage analysis] the program contains at least one
indirect call. Example: from "_BSP_IntHandler" in bsp_int.o [1]. A
complete list of such functions is in the map file.
マップファイルのSTACK USAGE の章に説明があります。
The following functions perform unknown indirect calls:
"_BSP_IntHandler" in bsp_int.o [1]: 0xffffabd4
注意:ルネサスRXマイコンのABIに従い、コンパイラは関数名にアンダーバーを付加したラベルを生成します。このため“_BSP_IntHandler”は関数BSP_IntHandlerを指します。
この問題を解決するため、開発者は#pragma callsディレクティブを使用すべきです。このディレクティブでは、呼び出される可能性がある関数を列挙することができます。また、ディレクティブの記述する箇所は、間接関数呼び出しの直前にすべきです。例えば、以下のコード片では、関数UartRxHandler、UartTxHandler、UartFaultHandlerが関数ポインタisrによって呼び出されることを明示しています。
void BSP_IntHandler (int int_id) {
void (*isr)(void);
……
if (int_id < BSP_INT_SRC_NBR) {
isr = BSP_IntVectTbl[int_id];
#pragma calls=UartRxHandler,UartTxHandler,UartFaultHandler
isr();
}
……
}
コールグラフの情報をリンカに提供する
RTOSを使用したマルチタスクの環境では、各タスクのルートとなる関数がコールグラフのルートとなります。時々、それらの関数が自動的に識別できないことがあります。その場合、リンカは警告メッセージを出力します。
Warning[Lo008]: [stack usage analysis] at least one function appears
to be uncalled. Example: "_App_TaskJoy" in app.o [1].
A complete list of uncalled functions is in the map file.
マップファイルの STACK USAGE の章に以下のような説明があります。
Uncalled function
"_App_TaskJoy" in app.o [1]: 0xffff992c
……
Uncalled function
"_App_TaskLCD" in app.o [1]: 0xffff9988
……
Uncalled function
"_App_TaskButton" in app.o [1]: 0xffff99f6
……
この問題を解決するため、開発者は、特定の関数がコールグラフのルートであることを明示するために#pragma call_graph_rootディレクティブを使用すべきです。
#pragma call_graph_root="task" // task category
static void App_TaskJoy (void *p_arg)
{ …… }
#pragma call_graph_root="task" // task category
static void App_TaskLCD (void *p_arg)
{ …… }
#pragma call_graph_root="task" // task category
static void App_TaskButton (void *p_arg)
{ …… }
#pragma call_graph_root="interrupt" // interrupt category
void OS_CPU_SysTickHandler (void)
{ …… }
#pragma call_graph_root="task" // task category
static void App_TaskJoy (void *p_arg)
{ …… }
#pragma call_graph_root="task" // task category
static void App_TaskLCD (void *p_arg)
{ …… }
#pragma call_graph_root="task" // task category
static void App_TaskButton (void *p_arg)
{ …… }
#pragma call_graph_root="interrupt" // interrupt category
void OS_CPU_SysTickHandler (void)
{ …… }
実際には、コールグラフの名前として、taskやinterrupt以外の任意の文字列を使用することが可能です。コンパイラは、割り込み関数やタスク関数にコールグラフのカテゴリを自動的に割り当てます。
スタック使用解析制御
プラグマディレクティブは基本的にソースコードに記述しなければなりませんが、許容されない場合もあります。ソースコードを変更することなく、同等の情報をリンカに渡す方法として、スタック使用解析制御ファイルを使用することがあります。
スタック使用解析制御ファイルは、テキストファイルで、.sucという拡張子を持ちます。ファイルのパスは、リンカオプションのアドバンストタブから設定可能です。
スタック使用解析制御ファイルにはいくつかのディレクティブが使用可能で、例えば、function、exclude、possible calls、call graph root、max recursion depth、no calls fromなどです。possible calls ディレクティブは#pragma callsと同様に間接関数呼び出しの呼び出し対象の関数を明示します。call graph root ディレクティブは#pragram call_graph_rootディレクティブと同等で、コールグラフのルートを明示するのに使用します。
先述の#pragmaに代わるスタック使用解析制御ファイルの内容を以下に示します。注意:ラベルにはアンダーバーが必要です。
call graph root [task] : _App_TaskJoy [app.o];
call graph root [task] : _App_TaskLCD [app.o];
call graph root [task] : _App_TaskButton [app.o];
call graph root [interrupt] : _OS_CPU_SysTickHandler;
possible calls _BSP_IntHandler : _UartRxHandler, _UartTxHandler,
_UartFaultHandler;
再帰呼び出しの呼び出し回数の明示
再帰呼び出しは明示的もしくは間接的に自身を呼び出すことです。各呼び出しは自身のデータをスタックに保持します。設計を適切に行わないと、スタックオーバーフローのリスクが高まります。
ビルド時には実際に何度の再帰呼び出しが発生するか不明なため、リンカは再帰関数呼び出しに対するスタック使用量を計算することができません。そこで以下のような警告メッセージが出力されます。
Warning[Lo010]: [stack usage analysis] the program contains at least
one instance of recursion for which stack usage analysis has not been
able to calculate a maximum stack depth. One function involved is
"_GLCD_SendCmd. A complete list of all recursion nests is in the map file.
マップファイルのSTACK USAGE の章には以下の説明があります。
The following functions make up recursion nest 0, which has no
maximum recursion depth specified:
"_GLCD_SendCmd": 0xffff8aac
この問題を解決するため、開発者は、max recursion depthディレクティブを使用するべきです。スタック使用量の解析は、最大の再帰呼び出しの深さに基づいて計算を行います。下記の例は、GLCD_SendCmd関数が3回再帰呼び出しを行うことを明示します。
max recursion depth _GLCD_SendCmd : 3;
実行時のスタック使用量の計測
静的なスタック使用量の解析では、ビルド時に論理的にスタック使用量を計算します。しかし、実際のスタック使用量は実行時に様々な値を取りえます。 IAR Embedded Workbench for RXは、C-SPYで実装した、もう1つの方法を提供します。C-SPYはスタック領域を全てマジックナンバで初期化することができます。例えば0xCDなどです。アプリケーションの実行開始前に、この値で初期化を行います。アプリケーションがある程度の間、実行を行った後、スタック領域を端から確認していくことで、0xCD以外のパターンを見つけ、そこまでがスタックポインタが到達したと推定できます。スタック領域のうち、まだ0xCDが残っているのなら、その分はスタック領域から安全に削減することが可能と考えられます。もちろん、少し余裕を取って削減する方がより安全です。
スタック使用量の計測のために、スタックオプションから設定を行ってください。
スタックウィンドウは表示メニューから利用可能です。どこで実行を停止しても、C-SPYはスタック使用量のグラフィック表示をすることができます。
スタックウィンドウの左端はスタックの最下位アドレスを表します。(スタックが空の場合のスタックポインタの値)右端は、スタック領域の限界を表します。濃い灰色の領域が使用中のスタックで、 薄い灰色の領域は未使用のスタックです。スタックポインタの位置を表すバーは、指定した閾値以上になると赤色になります。
注意:この機能は、スタックオーバーフローを検出するものではありません。その兆候を可視化するものです。実際に動かしながら計測するので、信頼性のある方法ですが、スタックオーバーフローが起こらなくなるという保証はありません。例えば、スタックが領域外に伸長しつつも、領域の境を書き換えないということが起こりうるからです。