アサーション設計

組み込みでのassertは、開発中のデバッグや診断テストに便利な機能ですが、すべての不具合を検出できるわけではないことに注意が必要です。業務ロジックのチェックに活用すると、出荷時におこなう「NDEBUGを用いてassertを一括で無効にする処理」により、業務ロジックのチェックプロセスまで消してしまうおそれがあります。assertは、適切な箇所と用途を選んで用いることが重要です。

この記事では、次の7テーマで実装に役立つ設計の指示を示します。

  • C11規格N1570 §7.2のassert.h
  • NDEBUG
  • C11規格N1570 §6.7.10の静的アサート
  • JPCERT/CC EXP31-Cの副作用回避
  • 業務仕様上の恒常チェックとassertの違い
  • 独自ASSERT_XXマクロの設計
  • リリース時の残し方

assertの基本動作とC標準(assert.h)

assertは診断の第一歩として広く使われますが、適切に用いるためには機能や動作の内容を知ることが重要です。

C11 §7.2 assert.hの定義

assertは、C11規格N1570 §7.2「Diagnostics <assert.h>」に定義されるマクロです。assert(条件式)は、NDEBUGマクロが未定義であるときに条件式が偽なら診断情報を出力し、abortを呼ぶと定めています。

assertは、関数と挙動が異なる場合があることに注意が必要です。たとえば、戻り値を活用する目的では使えないことがあげられます。assertはマクロであり、void式に展開されます。NDEBUG定義時は引数式が評価されないため、副作用を含めてはなりません。

診断メッセージ出力とabort呼び出し

hosted環境では、診断情報を標準エラー出力に書き込み、その後abort()を呼び出します。診断の書式は処理系定義です。組み込みなど実行環境によっては、stderrが観測できない、abortの結果がリセットや停止になるといった違いがあります。運用上の見え方は環境依存のため、実行環境ごとの挙動はドキュメントでの確認が必要です。

NDEBUGマクロによるリリース時無効化

NDEBUGを定義すれば、assertは何も生成しない式に展開されます。この仕様により、注意すべき項目があります。

NDEBUG定義でassertが無効化される仕様

assert.hがインクルードされる前にNDEBUGマクロを定義することで、assertを無効にできます。assertを用いる際には、データの変更など副作用をもたらし得る操作を含めないことが重要です。JPCERT/CC EXP31-C「assert()の中で副作用を使用しない」では、NDEBUG定義時のassertマクロが何も生成しない仕様を根拠に、副作用を回避する必要性を解説しています。

なおassert.hは、インクルードのたびにその時点のNDEBUGの状態へ応じてassertを再定義します。ヘッダの再インクルードやインクルード順序で挙動が変わります。仕様上、翻訳単位内でNDEBUGをdefine/undefしてassert.hを再インクルードすれば、同一翻訳単位でもassertの有効/無効を混在させられます。ただし運用事故の元なので、原則はビルドフラグで全翻訳単位を一律に制御します。

NDEBUGはコンパイルオプション(例: -DNDEBUG)で全翻訳単位へ一律に付与し、ソース内の#define NDEBUGは避けます。定義位置とインクルード順序は、プロジェクトの規約として決めておきます。

ビルド設定でのReleaseビルドとDebugビルドの分離

統合開発環境やビルド構成生成ツールのReleaseビルドでは、既定フラグにNDEBUGの定義が含まれる構成が多く見られます。Makefileを手書きしている場合は、Releaseターゲットで意図的に定義しているかを見直したい部分です。ビルド設定と挙動の対応を明示しておけば、Releaseビルド時の想定違いを未然に防げます。

静的アサート(static_assert /_Static_assert)の使いどころ

実行時にしかチェックできないassertとは別に、コンパイル時に条件を検証する仕組みも用意されています。C11規格N1570 §6.7.10「Static assertions」が定義する_Static_assertです。

C11 §6.7.10 _Static_assertの使用例

C11規格N1570 §6.7.10では定数式を評価し、偽の場合はコンパイルが失敗すると定められています。コンパイル失敗時に、指定のメッセージを表示できます。

C11〜C17では_Static_assertがキーワードとして導入され、ヘッダのインクルードは不要です。多くの処理系では、assert.hがstatic_assertを_Static_assertの便宜マクロとして提供します。C23ではstatic_assertがキーワード化し、_Static_assertは互換目的で残るものの非推奨となりました。いずれも実行時のオーバーヘッドを伴わずにコンパイルの段階でチェックでき、設計上の誤りを事前に検出できます。

構造体サイズ・レジスタ配置の検証

静的アサートは、sizeof(PERIPH_RegMap_t)が想定と一致するか、配列長がハードウェア仕様とそろうかといった検証に向きます。パディングやアライメント差によりメモリレイアウトが仕様と一致しない事態を、コンパイルの時点でチェックできる利点は大きいです。

引数の副作用を避ける

assertに副作用のある式を渡す書き方は、リリース後にトラブルを起こす原因です。規約でも明確に注意喚起されています。

JPCERT/CC EXP31-Cの要点

JPCERT/CC EXP31-CおよびJPCERT/CC PRE31-Cでは、副作用のあるassertの使い方を例示しています。インクリメントやデクリメント、代入など、値を変更する処理をassert文の中で用いることは副作用の一例です。

副作用付き記述で起きる典型的な障害

assert(index++ > 0)と記述した場合、NDEBUGの定義によりindexのインクリメントが実行されなくなります。「Debugビルドでは通るコードが、Releaseビルド後に動作しなくなる」事例の一つです。assert内には参照だけを記述するルールの徹底が、実務的な予防策になります。

業務仕様上の恒常チェックとassertの違い

「起こり得ない状態」の検出と、「起こり得る異常な入力値」のチェックは、目的が異なります。異常な入力値のチェックにassertを用いることは、実運用での不具合につながります。

入力範囲チェック・タイムアウトの扱い

外部入力の妥当性検証やタイムアウト監視は、業務ロジックの本体です。JPCERT/CC MSC11-C「診断テストはアサートを使って組み込む」は、アサートを誤った想定の予防に使うものとしています。その上で、実行時のエラー検査に使用してはならないと明記しています。NDEBUGで消えてはならない項目のチェックであるためです。if文とエラー処理の組み合わせは、代替となる方法の一例です。

防御的プログラミングとの区別

防御的プログラミングは不正な値が入力されても問題が起こらないようにする手法で、「仕様上あり得る異常」に備えるものです。一方で、assertは、「実装が正しければ到達しないはずの条件」を確認するものです。両者を適切に使い分けることは、正しく動作するシステムづくりにつながります。

独自ASSERT_XXマクロの設計

C標準のassertだけでは組み込みの現場要件を満たしにくいため、ログや失敗時の挙動に関して独自マクロを設計することがあります。独自マクロでも、引数の式を二重に評価しない形にすることが重要です。ASSERT(x++)のように副作用のある式を渡すと、マクロ内で条件式を2回展開した場合にxが2回進みます。条件式は一時変数で受けてから評価し、複数の文をまとめる場合はdo { ... } while (0)で包みます。

ログ出力方針

Releaseビルドでも一部の異常はリングバッファに記録し、後から取り出す実装例もあります。文字列出力のROMコストを避けたい場合は、__FILE__/__LINE__のかわりに事前登録したイベントIDを使う設計も一つの方法です。

失敗時のリセット・セーフモード・デバッガブレーク

失敗した際にはウォッチドッグタイマ(WDT)によるリセット、フェールセーフモードへ遷移、開発時のみブレークというように、動作を切り換える方法を活用できます。マクロ内で条件コンパイルすれば、DebugビルドとReleaseビルドで使い分けをおこなえます。

リリース時に何を残し何を削るか

仕様検証・実装バグ検出・デバッグ支援の3層を、Debug・Release・量産の各ビルドで残すか削るかに整理した表。仕様検証(外部入力・タイムアウト/通常のif+エラー処理)はDebug・Release・量産のいずれでも残す。実装バグ検出(不変条件/assert)はDebugでは残し、Release・量産では削る。デバッグ支援(ログ・トレース/独自マクロ)も同じくDebugでは残し、Release・量産では削る。

assertは開発中の網。
業務仕様のチェックを兼ねさせない。

最後にエラーチェック全般の観点から、リリース版で何を残すか、何を削るかを整理します。

仕様検証・実装不具合検出・デバッグ支援の3層

値やコードをチェックする目的は、3層に分けると分類しやすくなります。仕様検証(外部入力、タイムアウト)は業務ロジックとして常時残し、if文とエラー処理を組み合わせて実装します。実装不具合検出(不変条件)は、assert文を用いて開発中のみ使用します。デバッグ支援(ログ・トレース)のために独自のマクロを組み、Debugビルド限定で用いるのも一例です。

量産時のバイナリサイズと安全性の両立

assertの__FILE__/__LINE__はROMを消費します。量産では独自マクロに置き換え、必要な情報だけをコンパクトに残す運用は一例です。安全性の確保も踏まえて、「assertによる副作用の防止」「業務チェックとの分離」「条件コンパイルの活用」の3点を運用ルールに定めることが有効です。

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