HardFault(ハードフォルト)解析手順

HardFaultは、不正なメモリアクセスなどをきっかけに、一部のマイコンアーキテクチャ(Arm Cortex-Mシリーズ※など)で発生する例外です。発生した瞬間にプログラムは本来の処理から切り離されるため、原因の特定にはスタックへ残された情報を読み解く作業が欠かせません。

この記事では、Arm Cortex-Mシリーズを例に、典型的な原因の整理から、スタックダンプとフォルトレジスタの読み方、発生箇所をソースコードに対応づける手順、解析の自動化と再発防止までを順に説明します。

なお、この記事のレジスタ名(CFSR/BFAR/MMFARなど)は、主にCortex-M3/M4/M7/M33など(SCBに当該レジスタを持つコア)を想定しています。Cortex-M0/M0+ではこれらのフォルト系レジスタを持たず、フォルトはHardFaultに一本化される簡易な例外モデルとなっています。

  • Arm、CortexはArm Limitedの商標または登録商標です

HardFaultが起きる典型的な原因

やみくもにコードを眺める前に、頻出パターンを押さえておくと見当を付けやすくなります。

メモリアクセス違反(NULL・ワイルドポインタ・配列越境)

頻出する原因はメモリアクセス違反です。未初期化のポインタや、解放済みの領域を指したままのポインタを参照すると、許可されていない領域への読み書きが発生してフォルトに至ります。配列の境界を越えた書き込みは、隣接する変数やスタック上のデータを破損させるため、破損箇所とは別の地点で症状が現れる点が厄介です。

なお、NULLポインタ経由のアクセスでは0付近のアドレスがフォルトアドレスとして記録されることがあり、後述するフォルトアドレスレジスタ(例:BFARやMMFAR)が有効であれば原因を推定する手掛かりになります。

スタックオーバーフロー・未定義命令・非アラインアクセス

スタックが不足すると、関数呼び出しやローカル変数の確保が他の領域を侵食してメモリ破壊を招き、結果としてフォルトに至ることがあります(MPUなどでスタック領域を保護している場合は比較的早期に検出されます)。深い再帰や大きなローカル配列が典型的な引き金です。

また、破損した関数ポインタなどによって不正な命令を実行すると、UsageFault(使用法フォルト。未定義命令や不正な状態遷移などが要因)などが発生し、設定によってはHardFaultへエスカレーションします。

ゼロ除算は、CCRのDIV_0_TRPを有効にした構成ではUsageFaultの対象になります。非アラインアクセスも、CCRのUNALIGN_TRPの設定や命令種別により、UsageFaultとなる場合があります。ただし、コア世代および命令によっては設定にかかわらず制約があるため、使用コアのリファレンスマニュアルを確認してください。

フォルト情報はどこに残るか

発生時の状況は、専用のレジスタ群と、スタックへ退避されるスタックフレームに記録されています。

例外発生時にスタックへ退避されるレジスタ

HardFault発生時にスタックへ退避されるレジスタの図。MCUでHardFaultが発生すると、R0、R1、R2、R3、R12、LR、PC、xPSRが、決まった順序でスタックに退避される。LRには呼び出し元へ戻るアドレスが、PCにはフォルト発生時の命令アドレスが入っていることを示している。

この記事で例示するアーキテクチャでは、例外に入る際にハードウェアがR0~R3、R12、LR(リンクレジスタ)、PC(プログラムカウンタ)、xPSR(プログラムステータスレジスタ)の8ワードを、使用中のスタックへ自動で退避します(基本のスタックフレーム)。フォルトの種類によっては、スタックフレームのPCからフォルトに関係する命令のアドレスを特定できます。スタックフレームには例外発生前のLRも保存されます。このLRの値から、例外発生前の関数呼び出し経路を追う手掛かりが得られます。

(注)FPUを使用する設定では、追加レジスタがスタックに退避され、スタックフレームが拡張される場合があります。また、SPの8バイトアライン調整のためにパディングが入ることがあります。

退避の順序は固定なので、フレームの先頭アドレスさえわかれば各値を機械的に取り出せます。例外解析では、スタック領域の内容を記録した「スタックダンプ」を取得すれば、例外時のスタックフレームを確認できます。

CFSR/HFSR/BFAR/MMFARの読み方

CFSRは、MemManage Fault、BusFault、UsageFaultの原因を示すレジスタです。メモリ保護違反、バスエラー、未定義命令やゼロ除算などの要因はそれぞれ別のビットに記録されるため、どのビットがセットされているかで種類を判別できます。

HardFaultの原因は、CFSRだけで判別できるわけではありません。そのため、まずはHFSR(HardFault Status Register)を確認します。HFSRのFORCEDがセットされている場合は、CFSR(MMFSR/BFSR/UFSR)を参照してエスカレーション元の要因を確認し、必要に応じてBFAR/MMFAR/AFSR(補助フォルトステータスレジスタ)も参照します。一方、HFSRのVECTTBLや、Cortex-M33のセキュリティ拡張に関係するフォルトでは、CFSR以外の状態レジスタも確認します。

BFAR(BusFault Address Register)とMMFAR(MemManage Fault Address Register)には失敗したアクセス先のアドレスが入り、これらの値が有効かどうかは、CFSR内のBFARVALID/MMARVALIDビットで判定します。有効であれば、どのアドレスへのアクセスでフォルトが発生したのかまでわかります。

アドレスが0付近の場合はNULLポインタの可能性があり、スタック領域から外れている場合はスタックポインタの破損やスタックオーバーフローの可能性があるなど、原因の推定につながります。

MSP/PSPの判別とスタック位置の特定

スタックフレームがメインスタック(MSP)とプロセススタック(PSP)のどちらに積まれたかは、例外エントリ時にLRへ書き込まれるEXC_RETURN(例外復帰時にプロセッサが参照する特殊値)の値で判別します。

EXC_RETURNのbit2は、例外発生前にどちらのスタックを使用していたか(=復帰時にどちらのスタックを使用するか)を示し、一般的には「例外発生前にThreadモードが使用していたSP(MSP/PSP)」の判別に使えます(0=MSP、1=PSP)。

RTOS(リアルタイムOS)環境ではタスクごとにPSPを使う構成が広く採用されるため、判別を誤ると無関係な領域を読み込むおそれがあります。使用スタックの判別には、HardFaultハンドラへ入った直後のLRが保持するEXC_RETURNを使用します。これに対し、スタックフレームのオフセット5に保存されるLRは、例外発生前の実行コンテキストのLRです。ハンドラ内でC関数を呼び出す前に、アセンブラ部またはnaked関数などでSPとLR(EXC_RETURN)を退避・引き渡す実装が必要です。

使用スタックを特定したら、先頭から8ワードを読み出し、オフセット5(LR)とオフセット6(PC)の値を取り出します。

発生箇所をコードに対応づける手順

フォルトレジスタとスタックフレームから取り出せるのは、あくまでアドレスという数値です。原因を修正するには、この数値をソースコード上の関数や行番号へ変換する作業が必要になります。デバッガを接続できる開発中の環境と、ログしか残らない市場不具合の解析とでは使える手段が異なるため、ここでは両方の手順を説明します。

PC/LRから逆アセンブルで命令を突き止める

CFSRにPRECISERR、DACCVIOL、IACCVIOLなどが記録されている場合、スタック済みPCは原則としてフォルトに関連する命令を指します。この場合は、取り出したPCの値をデバッガの逆アセンブル表示に入力し、該当箇所のコードを確認します。ただし、STKERR、UNSTKERR、MSTKERR、MUNSTKERRがセットされている場合は、スタックフレーム自体が破損または不完全である可能性があるため、PCや退避レジスタを無条件に信用してはいけません。

BFSRのIMPRECISERRがセットされている場合、スタック済みPCは原因命令を指しません。また、BFARは有効でない場合があります。BFARVALIDを必ず確認した上で、書き込みバッファ、DMA、周辺回路、直前の書き込み処理、周辺コードフローなどを含めて原因を調査します。

命令がどのレジスタを参照していたかを見れば、退避済みのR0~R3の値と突き合わせて、不正なアドレスの出どころを追跡できます。

LRの値からは呼び出し元の関数がわかるため、命令単位の情報と呼び出し経路の両面から絞り込みが進みます。

mapファイル・addr2lineで関数と行に変換する

デバッガを接続できない環境では、リンカが出力するmapファイルでアドレスと関数の対応を調べます。デバッグ情報付きのバイナリがあれば、addr2lineなどアドレスをソースファイル名と行番号に変換できるツールを使います(利用可能なツールは開発環境により異なります)。

市場で発生した不具合のように再現が難しいケースでも、PCやLR、フォルトレジスタなどをログに残しておけば、机上で原因コードの候補を特定する手掛かりが得られます。

解析の自動化と再発防止

毎回手作業でスタックを読むのは現実的ではないため、フォルトハンドラに解析処理を組み込んでおきます。具体的には次のような仕組みが定石です。

  • フォルトハンドラ内でEXC_RETURNから使用スタックを判別し、まず最小限のフォルト情報(退避レジスタとCFSRの内容)をRAM上の専用領域または不揮発性メモリへ保存する
  • シリアル出力は、通信機能が使用可能であり、タイムアウトや再入性を考慮できる場合に限定する
  • スタック使用量を監視し、スタックオーバーフローの兆候を早期に検知する
  • ウォッチドッグでハングを検知し、保存後はソフトウェアリセットを含めて復旧させる設計を検討する

HardFaultは再現が難しい場合がありますが、フォルト発生時のスタックフレーム、フォルトレジスタ、使用中のSP、ファームウェア識別情報を自動保存する仕組みを実装しておけば、再現を待たずに原因を調査しやすくなります。

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