Project

General

Profile

Actions

DetectStackOverflow » History » Revision 10

« Previous | Revision 10/12 (diff) | Next »
mole lord, 09/27/2020 05:47 AM


DetectStackOverflow

FreeRTOSスケジューラ起動前のスタックオーバフロー検出

STM32F4には Core Coupled Memory (以降CCM) が 0x1000_0000 番地にあります。(リファレンスマニュアルを参照)
リンカスクリプトをいじってCCMの先頭部分にスタック領域を配置すれば、スタックオーバフローがRAMが存在しない領域へのWriteとなり、 Hard Fault を起こします。

乱暴ですが、Hard Faultをもってスタックオーバフロー検出ができます。
(グローバル変数を破壊して摩訶不思議な挙動をされるよりはよいです)

CCMRAMにスタックを置くデメリットは、スタック上の値をDMACが扱えなくなることですが、ふつうそんなことはしないので問題ないでしょう。

具体的には、 STM32F429ZITx_FLASH.ld を開いて、

/* Highest address of the user mode stack */
_estack = 0x20030000;    /* end of RAM */
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x0;      /* required amount of heap  */
_Min_Stack_Size = 0x400; /* required amount of stack */

を以下のように書き換えます。

/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x0;      /* required amount of heap  */
_Min_Stack_Size = 0x400; /* required amount of stack */
/* Highest address of the user mode stack */
_estack = 0x10000000 + _Min_Stack_Size;

また、._user_heap_stackの部分を削除し、

  /* User_heap_stack section, used to check that there is enough RAM left */
  ._user_heap_stack :
  {
    . = ALIGN(8);
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
  } >RAM

その代わりに >CCMRAM に挿入します。

  .ccmram :
  {
    . = ALIGN(4);
    _sccmram = .;       /* create a global symbol at ccmram start */
    . = ALIGN(8);
    _user_heap_stack = .;
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
    *(.ccmram)
    *(.ccmram*)

    . = ALIGN(4);
    _eccmram = .;       /* create a global symbol at ccmram end */
  } >CCMRAM

動作確認は、無限再帰の関数を作ってそれをmain()から呼び出してやることで行えます。

static int32_t
recurse(int32_t x)
{
    return recurse(x + 1);
}

int main()
{
    ...
    int32_t x = recurse(0);
    int32_t i;
    for (i = 0; i < x; i++) ;
    ...

FreeRTOSのタスクのスタックオーバフロー検出

FreeRTOS自体にもスタックオーバフロー検出の仕組みが備わってはいます。しかし、いまいち信用ならないので、Memory Protection Unit(以降MPU)を使って書き込み禁止領域を作ることで検出を行います。

デメリットは、スタック領域1つごとに32バイトの使用不可領域ができることと、スタックの開始アドレスを32バイト境界にしか配置できないことです。
それと引き換えに、RTOSを使うときに一番厄介な「なんだかわからないがソフトを修正したらいきなり挙動がおかしくなった、変更されないはずのグローバル変数が書き換わった」などの、スタックオーバフローによる不思議な動作に悩まされなくなります。

リポジトリの SHA-1: d4c1e95 の common/molelord/MoleMem.c に実装があります。

HAL_MPU_Disable();
MPU_Region_InitTypeDef init = {0};
init.Enable           = MPU_REGION_ENABLE;
init.BaseAddress      = プロテクトしたいアドレス;
init.Size             = MPU_REGION_SIZE_32B;
init.AccessPermission = MPU_REGION_PRIV_RO_URO;
init.IsBufferable     = MPU_ACCESS_BUFFERABLE;
init.IsCacheable      = MPU_ACCESS_CACHEABLE;
init.IsShareable      = MPU_ACCESS_SHAREABLE;
init.Number           = MPU_REGION_NUMBER0 + リージョン番号(07);
init.TypeExtField     = MPU_TEX_LEVEL0;
init.SubRegionDisable = 0x00;
init.DisableExec      = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&init);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);

MPUの使い方は AN4838 Managing memory protection unit in STM32 MCU の p.14 Setting the MPU with cube HAL にあります。
AccessPermission に MPU_REGION_PRIV_RO_URO を設定しているので、そこへのライトアクセスが発生すると
MemManage_Handler()が呼び出されます。

プロテクトできる領域は8つまでなので、タスクが9つ以上ある場合は取捨選択が必要です。
検証の終わったタスクのプロテクトは外してほかに回すなどの工夫で対処できるでしょう。

注意

FreeRTOS自体のスタックオーバフロー検出機能と併用すると、FreeRTOSはスタック領域をマジックナンバーで埋めようとするので毎回MemManage_Handlerに飛んできます。
排他的に使用してください。

また、たとえばchar buf[64]をスタック上に確保するけれどほぼ先頭10バイトしか書き込まない、といった場合には、MPUで保護した32バイトがbuf[32~63]になるために保護をスルーしてさらに若いアドレスにスタックオーバフローする、という可能性もあります。
作った人間としてとても残念ですが、万能ではないようです。


累計表示回数:89

Updated by mole lord 23 days ago · 10 revisions