ルモーリン

スタックオーバーフローを防止

投稿:2012-03-09、更新:2012-09-09

どうも世の中の普通の処理系(コンパイラ)ではスタック不足の時に自動追加できないようです。 何を言っているのかわかるのはAmigaのプログラマ位でしょうか? 商用コンパイラSAS/Cには自動追加があるのでデバイスドライバのようなシビアなプログラム以外で普通に自動追加させていました。 なのでスタック消費量など気にしないでバンバン自動変数(関数内で修飾なしで確保する変数)を使っていても、スタック不足になりません。 自動追加は、スタックがモノシリック(1個)じゃなくて、既設のスタックに後付けのスタックをチェイン付けできる機構です。 コンパイルオプション「-S」をつけてアセンブラ出力を見ると、 既設の処理系に後付けで自動追加を入れるのは尋常じゃない程の工夫が要りそうなのであきらめます。
2個のコンパイルオプションを追加します。 -finstrument-functionsで関数のプロローグとエピローグに関数呼び出し(名称固定)を追加、 -mpoke-function-nameで関数の入口前方に関数名の文字列と識別フラグ付きバイト数を埋め込みます。 このオプションはクリチカル(クリティカルとも言う)な処理に使えませんので フォルダFreeRTOS_portableごとオプションから-finstrument-functionsを削除します。 それと名称固定の関数名は__cyg_profile_func_enter()と__cyg_profile_func_exit()で、 これらの関数に属性no_instrument_functionを付けておき、自分を呼び出して無限再帰するのを防止します。
__attribute__ ((no_instrument_function)) void __cyg_profile_func_enter(void *this_fn, void *call_site)
__attribute__ ((no_instrument_function)) void __cyg_profile_func_exit(void *this_fn, void *call_site)
これらの関数が呼ばれた時にスタックポインタがどこまで移動してるかを把握して最大移動時の移動量と呼び出し関数をタスク毎に記録します。
// スタック使用状況を把握

//スケジューラ起動前(main関数)のスタック末尾は、この関数の先頭アドレスです。
extern void _vStackTop(void);
//ヒープの先頭は、この関数の先頭アドレスです。
extern void _pvHeapStart(void);

typedef struct {
	xTaskHandle xTask;
	uint8_t taskname[12];
	uint32_t *pStartFrame;
	uint32_t *pEndFrame;
	uint32_t *pPeakFrame;
	uint32_t uDepth;
	uint32_t *pCaller;
	uint8_t functionname[28];	// 全体を64バイトに
} t_StackTable;
static t_StackTable StackTable[16];

//スケジューラ起動前のシングルタスク中に関数入口から呼ばれた場合の代替TCBです。
//TCBの構成は、FreeRTOSConfig.hにあるシンボル定義で変わります。
//本物のTCBはtasks.cローカルの構造体で参照不可ですから代用品を用意しました。

typedef struct {
	uint32_t *pStackStart;	//最初に使うスタック位置です。
	uint32_t *(pDummy[11]);	//ダミー配列です。
	uint32_t *pStackEnd;	//スタックの限界です。
	uint8_t	uTaskName[12];	//タスク名称です。
} t_TCB;

static t_TCB singleTCB = {
	(uint32_t *)&_vStackTop - 16,	//[0]リンカがスタックとして設定する名称です(RAMの末尾)。
	{},
	(uint32_t *)&_pvHeapStart,	//[12]スタックの向かい側にヒープがあって、その先頭をスタックの限界とします。
	"single task"	//[13]タスク名称です。
};

//オプション「-finstrument-functions」は関数入口で__cyg_profile_func_enterを呼びます。
//属性「no_instrument_function」の関数は呼びません。
//関数「__cyg_profile_func_enter」は自分を呼ばないよう設定しておきます。
//この関数のスタック消費量は40バイトです。
__attribute__ ((no_instrument_function)) void __cyg_profile_func_enter(void *this_fn, void *call_site)
{
	t_TCB *pTCB = (t_TCB *)xTaskGetCurrentTaskHandle();
	if (!pTCB)
	{
		pTCB = &singleTCB;
	}
	uint32_t *pFrame = __builtin_frame_address(0);

	uint32_t i;
	for (i = 0; i < sizeof(StackTable) / sizeof(StackTable[0]); i++)
	{
		if (!StackTable[i].xTask)
		{
			StackTable[i].xTask = pTCB;
			memcpy(StackTable[i].taskname, pTCB->uTaskName, sizeof(StackTable[i].taskname));
			StackTable[i].pStartFrame = pTCB->pStackStart + 16;
			StackTable[i].pEndFrame = pTCB->pStackEnd;
			StackTable[i].pPeakFrame = StackTable[i].pStartFrame;	//スタック開始位置から使用
		}

		if (StackTable[i].xTask == pTCB)
		{
			if (pFrame < StackTable[i].pPeakFrame)
			{
				StackTable[i].pPeakFrame = pFrame;
				StackTable[i].uDepth = StackTable[i].pStartFrame - StackTable[i].pPeakFrame;
				StackTable[i].pCaller = __builtin_return_address(0);

				// オプション「-mpoke-function-name」でコンパイラが関数入口の直前に文字列とフラグ付き長さを埋め込みます。
				//this_fnは関数入口のアドレスで、最右ビットはフラグで1です。
				//Cortex-M3はバイトオーダーがデフォルトでリトルエンディアンです。
				uint8_t *pFunctionName = (uint8_t *)this_fn;
				pFunctionName -= ((uint32_t)this_fn) % 2;
				pFunctionName -= 4;
				if (0xFF == pFunctionName[3] && 0x00 == pFunctionName[2] && 0x00 == pFunctionName[1])
				{
					pFunctionName -= pFunctionName[0];
					StackTable[i].functionname[sizeof(StackTable[i].functionname) - 1] = 0x00;
					strncpy((char *)StackTable[i].functionname, (char *)pFunctionName, sizeof(StackTable[i].functionname) - 1);
				}
			}
			break;
		}
	}
}

//オプション「-finstrument-functions」は関数出口で__cyg_profile_func_exitを呼びます
//属性「no_instrument_function」の関数は呼びません。
//関数「__cyg_profile_func_exit」は自分を呼ばないよう設定しておきます。
__attribute__ ((no_instrument_function)) void __cyg_profile_func_exit(void *this_fn, void *call_site)
{

}
これらの仕掛けを用意した上でRed Suite3(NXP)から実機のデバッグを開始してmain()の先頭で自動的に一時停止します。 その時にテーブルStackTableの内容表示を設定して実行再開します。 ひと揃いの機能を実行したあと(自動的に全機能を実行するならそれを待つ)プログラムを一時停止させてStackTableを表示するとこんな感じ。 表示画面では改行されないのですけれどホームページでは見にくいのでタスク毎に改行してあります。
{
{xTask = 0x10000184, taskname = "single task", pStartFrame = 0x10008000, pEndFrame = 0x10005514, pPeakFrame = 0x10007e98, uDepth = 90, pCaller = 0x6dd5, functionname = "tag_find", '\000' <repeats 19 times>}, 
{xTask = 0x100003e0, taskname = "StartUpSequ", pStartFrame = 0x10000c30, pEndFrame = 0x10000438, pPeakFrame = 0x10000910, uDepth = 200, pCaller = 0x769, functionname = "vListInsert", '\000' <repeats 16 times>}, 
{xTask = 0x10000c48, taskname = "IDLE\000\000\000\000\000\000\000", pStartFrame = 0x10000dd8, pEndFrame = 0x10000ca0, pPeakFrame = 0x10000d30, uDepth = 42, pCaller = 0x1611, functionname = "xTaskResumeAll", '\000' <repeats 13 times>}, 
{xTask = 0x10000f58, taskname = "silDriver\000\000", pStartFrame = 0x100013a8, pEndFrame = 0x10000fb0, pPeakFrame = 0x10001220, uDepth = 98, pCaller = 0x769, functionname = "vListInsert", '\000' <repeats 16 times>}, 
{xTask = 0x100013c0, taskname = "TakoRuka1\000\000", pStartFrame = 0x10001510, pEndFrame = 0x10001418, pPeakFrame = 0x10001438, uDepth = 54, pCaller = 0x6eb, functionname = "vListInsertEnd", '\000' <repeats 13 times>}, 
{xTask = 0x10001528, taskname = "TakoRuka3\000\000", pStartFrame = 0x10001678, pEndFrame = 0x10001580, pPeakFrame = 0x100015c8, uDepth = 44, pCaller = 0xd97, functionname = "prvCopyDataToQueue\000\000\000\000\000\000\000\000\000"}, 
{xTask = 0x10001690, taskname = "TakoRuka4\000\000", pStartFrame = 0x100017e0, pEndFrame = 0x100016e8, pPeakFrame = 0x10001730, uDepth = 44, pCaller = 0xd97, functionname = "prvCopyDataToQueue\000\000\000\000\000\000\000\000\000"}, 
{xTask = 0x100017f8, taskname = "clock\000\000\000\000\000\000", pStartFrame = 0x10001a48, pEndFrame = 0x10001850, pPeakFrame = 0x10001900, uDepth = 82, pCaller = 0x769, functionname = "vListInsert", '\000' <repeats 16 times>}, 
(以下省略)
}
1行目のタスク名single taskはmain()からの呼び出しでの名前です。 タスクからではなく当然TCBもなくシングルタスク用のスタックを参照するよう工夫してあります。 タスク毎に記録してありますからタスク生成で指定するスタック量をこれらの記録を参考に指定すると良い訳です。 例えば4行目を見ますと、LCD表示タスク(silDriver)/最大スタック使用量(98)/その時の関数名(vListInsert)といった具合です。
記録用の関数がスタックを40バイト使いますが記録にはカウントされませんので、この分と将来の修正に備えて余裕を持って指定しておきます。 uDepthの数値はvoid *のサイズ分(4バイト単位)なので40バイトは10単位に相当しますから10足して、あとは2の累乗でキリがよい数字にします。 経過は「98→108→128」となって128を指定すると大丈夫な訳です。
(調整前)
xTaskCreate(sil_driver, (const signed char * const)"silDriver", 256, NULL, tskIDLE_PRIORITY, NULL);
(調整後)
xTaskCreate(sil_driver, (const signed char * const)"silDriver", 128, NULL, tskIDLE_PRIORITY, NULL);
ARM公式サイトでドキュメントを一所懸命読んで「これは使える」と思ったコンパイルオプションを試してみると 「そんなオプション知りません。」と言われてしまう。 よくよく調べてみるとRed Suite3(NXP)のコンパイラはARM純正のコンパイラではなくGCCだから、 そもそも見るドキュメントが間違っていたという。 純正コンパイラは有償ライセンス(推測)だけあって、そりゃあもう使いたいオプションがゴロゴロ入っています。 GCCにはないんだこれが。 一番強力で使いたかったのが「所要スタック量を推定するオプション」というそのまんまのがありました。 プロの開発にあって当然の機能ですし、なければ代わりにスタックを自動追加できる位の処理系でないとね。 ヨソのコンパイラに触れる度にSAS/Cの化け物具合がよく分かりますが、無い物ねだりになりがちです(涙)。

まあ、どうせ話が通じないと思うので、あとはソース見てください(^^;
yrntrlmnmnt20120309.zip
2012.09.09 RedSuite3(NXP)に残りスタック量を見る画面がありました。ちゃんちゃん。 タスクテーブル