機能安全

機能安全を脅かすC言語に潜む危険への対処

<span id="hs_cos_wrapper_name" class="hs_cos_wrapper hs_cos_wrapper_meta_field hs_cos_wrapper_type_text" style="" data-hs-cos-general-type="meta_field" data-hs-cos-type="text" >機能安全を脅かすC言語に潜む危険への対処</span>
 

本稿では、セーフティクリティカルな機能を持つシステムの開発にC言語を使用する際に生じる問題について検討します。機能安全を実装する際に必須の知識です。

C言語は、未定義動作やハードウェア依存性といった落とし穴を多数抱えているにも関わらず、幅広く使用されており、セーフティクリティカルな開発分野では好んで用いられている言語です。問題となりえる点をあらかじめ考慮して利点に変えることが可能です。

 

C言語はどのような言語か

1991年、『Developers Insight』誌に「How to Shoot Yourself In the Foot(自分の足を撃ち抜く方法)」という面白い記事が掲載されました。記事は次のように始まります。「昨今、新たなプログラム言語が急増しています(そのいずれもが無数の機能を互いに盗みあっているように見えます)が、そのために、ユーザは自分が今使っている言語が何なのかさえも覚えていられなくなってきています。このガイドは、そのようなジレンマに陥っているプログラマを助けるサービスとして提供されるものです。」

言語のリストはCから始まり、そこには一言こう書かれています。

  • C:自分の足を撃ち抜く言語

辛辣な表現ですが、この言葉には一片の真実以上のものが含まれています。一方、C以外のプログラミング言語は、型安全性や未定義動作などの問題は、はるかに少ないとはいえ、多くの場合、ハードウェアに近いプログラミング機能が欠けています。今後もCを使い続けていくのであれば、うまくバランスを取る必要があります。つまり、わかり易い落とし穴とわかり難い落とし穴の間をうまく通り抜けながら、その機能を最大限に活用する必要があります。セーフティクリティカルな機能の開発では、C言語を次の2つの視点から見ることができます。

  • プログラミング言語の選択に関し、セーフティクリティカルなプロジェクトには、どのような外部条件があるのか?
  • Cにおいてより明確な問題を軽減するには、どうすればよいか?また、レガシーコードを使用する場合はどうすればよいか?

スタンダードな回答

開発している製品が、自動車、産業用制御、医療機器、鉄道などの分野で使用される場合、その製品は高い確率で正式な機能安全要件の対象となります。それらの要件とは、製品の許容される故障率あるいは製品の特定機能の許容される故障率に関する要件であることもあれば、IEC61508(電気/電子プログラマブルデバイス)、ISO26262(自動車)、EN 50126x(鉄道)などの特定の機能安全規格に従って開発される製品の一般的な要件であることもあります。少なくともこの10年、安全機能を実装することは、純粋な機械制御またはPLC制御の自動化から離れてマイクロコントローラの領域に移りつつあり、そのため各種要件もソフトウェアの領域に入り込んできている、という明確な傾向があります。

ソフトウェア要件の求めるところは、各種規格で似通っているため、ここではIEC61508を例にとります。この規格は分野固有の多くの規格でベースとなるものです。つまり、IEC61508で有効な要件の大部分は、ISO26262などでも有効です。

これらの規格は、要件の抽出から、顧客サイトでの製品展開・廃止までの立案に至るまで、あらゆる作業方法や作業の文書化に大きな影響を与えます。選択した規格に提示された目的を達成できたかどうかを判定するのは、自分自身やプロジェクトの利害関係者だけではありません。公認団体の第三者評価機関をはじめ、自身の組織の担当者も納得させる必要があります。

これらの規格のほとんどは、安全度水準の概念をさまざまに変更して使用されています。したがって、製品の分類によって規格の適用方法にもばらつきがあります。

機能安全の規格を適用すべきか?

以上のこととプログラミング言語の選択とは、いったいどのような関係があるのでしょうか。この興味深い質問について検討してみましょう。下表は、使用しているアプリケーションや安全機能の安全度水準に応じて、最適なプログラミング言語を選択する際の参考になります。

HRは強く推奨(Highly Recommended)することを意味しており、規格の用語では、実際にこの推奨に従うことを極めて強く指示するものです。これに従わない場合はそれだけの十分に強い理由がある、つまり100パーセント自信をもってその根拠を示す必要があります。

Table_from_IEC61508

表1 : IEC61508 part 3 appendix Aにある表

もう少しわかりやすく

表からわかるように、適切なプログラミング言語を使用することが強く推奨されていますが、推奨の範囲であれば、実際はさほど意味がないのではないでしょうか?しかし、表で参照されている附属書Cでは、適切なプログラミング言語を次のように定義しています。

言語は完全かつ曖昧さがないように定義することが望ましい。言語はプロセッサおよびプラットフォームマシン志向であるよりも、ユーザ志向または問題志向であることが望ましい。特殊目的言語よりも、広く用いられている言語またはこれらのサブセットの方が望ましい。言語は、次の事項を奨励するものであることが望ましい。小さく、管理可能なソフトウェアモジュールの使用、特定のソフトウェアモジュール内のデータへのアクセス制限、変数サブレンジの定義、その他のエラー抑制構成要素の使用

上記定義の各部分をCと比べてみましょう。

  • 言語は完全かつ曖昧さがないように定義することが望ましい:数え方にも依りますが、C99には少なくとも190個の未定義動作があります。
  • 言語はプロセッサやプラットフォームマシン志向であるよりも、ユーザ志向または問題志向であることが望ましい:Cは元々PDP-11アーキテクチャのためのシステム開発言語となるよう作成されたものであること、また、特定のターゲットに対する特定のCの実装は別のターゲットへの実装とは異なるものになり、時には同じターゲットへの代替の実装と異なることさえあることを考えると、Cはこの部分の定義に準拠しているとは言い難いものがあります。
  • 特殊目的言語よりも、広く用いられている言語またはこれらのサブセットの方が望ましい:ようやくCに当てはまると言えるものが出てきました。CあるいはC++などCから派生した言語については、非常に多くの開発者が周知のことです。
  • 言語は、次の事項を奨励するものであることが望ましい : 小さく、管理可能なソフトウェアモジュールの使用、特定のソフトウェアモジュール内のデータへのアクセス制限、変数サブレンジの定義、その他のエラー抑制構成要素の使用:Cはこれらの概念をサポートする抽象化の作成を明確に禁止しているわけではありませんが、この言語自体によって上記をサポートすることは一切ありません。実際、上記とは正反対であると言っても差し支えないでしょう。

Cは規格の求めるところに全く応えていません。どうすればよいでしょうか?

実際、単に規格を読む限り、その答えは極めて簡単です。読み進めると、具体的な言語に関する判定を行う表があり、Cについて次のように記載されています。

Table_from_IEC61508

表2: Table from IEC61508, part 7, appendix C

Cは推奨言語ではないものの、コーディング標準と静的解析ツールを使用した場合は、適切にサブセットを使用する前提でCが強く推奨されています。しかし、サブセットおよびコーディング標準とは、このコンテキストにおいてどのような意味を持つのでしょうか。

サブ規格

このコンテキストでの言語サブセットの目的は、プログラミングエラーの発生確率を減らし、何らかの方法でコードベースに入り込んだエラーを発見し易くすることです。つまりCでは、未定義動作や処理系定義の動作の使用をできる限り削減することを意味します。そのような言語サブセットで使用可能なものは多数ありますが、最も広く知られているのはMISRA-Cでしょう。MISRA-Cのルールセットの取り組みは、英国のMotor Industry Software Reliability Associationによって始まりました。MISRA-Cルールは、当初は自動車向けソフトウェアのみを対象としていましたが、年を追うごとに他の産業分野を巻き込み世界中へと広がり、今日、組込み業界で最も広く用いられているCサブセットとなっています。

IEC61508では、コーディング標準に関する問題にも非常に多くの言及があります。MISRA-Cルール以外で考慮すべきトピックの例を以下に示します。

  • グローバル変数などの共有リソースへのアクセスを防ぐ方法
  • オブジェクト割当てのためのスタックメモリとヒープメモリの使用
  • 再帰呼び出しの許可/不許可
  • 複雑さの制限。例えば、関数の許容されるサイクロマチック複雑度に対する制限など
  • 特定のコンテキストで適用できないMISRA-Cルールなどを放棄する方法
  • 組込み関数や言語拡張など、コンパイラ固有の機能の使用方法
  • 境界値チェック、アサーション、事前条件、事後条件などのエラーを捕捉するための構成要素の使用方法
  • モジュール間のインタフェース構成とアクセス
  • 文書化要件

本質的に、コーディング標準では、コード品質と完全性に影響するにも関わらず、言語やサブセットでは明示されていない問題への対処法についてヒントが示されています。

習うより慣れよ

ここからは、上記セクションで生じたいくつかの論点に触れ、プロジェクトにおいてそれらの論点にどのようにアプローチするかを検討します。

MISRA-C

MISRA-Cを使用する場合、白紙の状態から開始するか、これまでのコードを再利用するかで、アプローチが若干異なることがあります。新たにコードを開発する場合は、以下の点を考慮してください。

  • あらゆるルールをやみくもに守ろうとしないでください。コードの中には、ルールに準拠できない部分が当然あります。特にハードウェアにインタフェースするコードの場合に、これが当てはまります。代わりにルールから逸れる場合は、十分な情報に基づいて判断を行い、その判断を文書化してください。すべてのルールに対応することを目指さなければならないのか、プロジェクトレベルでなら無視できるルールがあるのか、プロジェクト関係者や外部評価者と事前に話し合ってください。
  • 基本型および基本型の算術とキャストに関するルールは常に準拠するよう努めてください。この領域は落とし穴だらけで、あるプラットフォームでは完璧に機能するように見えるコードも、別のプラットフォームでは破綻する可能性があります。
  • 同様のコードに対し同じルールからの逸脱手順を何度も使用するような状況に陥ったら、それは警告信号と捉える必要があります。
    • ルールを正しく解釈していますか?
    • そのコードパターンは本当に必要ですか?もし必要なら、問題となるコードを、隔離された関数または関数のセットとして除外することを検討してください。
    • 開発中に、ルール準拠をインタラクティブにチェックできる静的チェッカを使用してください。

 MISRA-Cのルールセットをレガシーコードに適用する場合は、以下をおすすめします。

  • 一度に1ルールずつ適用します。
  • まず、簡単なルールから適用します。例えば、条件が成立したときに実行する本体を構成する箇所は、単文でも{}で囲むルール(MISRA-C:2004ルール14.8)などから。
  • shortintcharなどの普通の型は使用できないと述べているルールに注意を払い、uint16_tなどの大きさを明示した型を使用して一度に1モジュールずつ変更します。
  • いくつか簡単なモジュールで練習した後、バグが多いまたは維持が困難と思われるモジュールに取り掛かります。

同期させよ

ここで、C言語でさらに広く誤解されている部分の1つ、volatileキーワードに焦点を移します。このキーワードの誤用は、誰に尋ねても、組込みシステムの大失敗につながる原因リストの最上位に挙がるでしょう。

オブジェクトをvolatileと宣言する主な理由は、オブジェクトの値がコンパイラにとって未知の方法で変化する可能性があるため、オブジェクトへのすべてのアクセスは保持されなくてはならないことをコンパイラに伝えることです。volatileオブジェクトが必要となる代表的な状況は次の3つです。

  • 共有アクセス:オブジェクトが、マルチタスク環境の複数のタスクで共有されている、または、1つの実行スレッドおよび1つ以上の割込みサービスルーチンの両方からアクセスされる場合。
  • トリガーアクセス:アクセスの発生がデバイスに影響を及ぼすような、メモリマッピングされたハードウェアデバイスに関するもの。
  • 未知のアクセス:オブジェクトの内容がコンパイラにとって未知の方法で変化する可能性がある場合。

それでは、volatileキーワードをオブジェクト宣言に適用した場合、コンパイラからはどのような保証が受けられるでしょうか?実質的には、すべての読書きアクセスが保持されるというだけです。

ターゲットアーキテクチャによっては、所定の順序で、また、可能な場合はアトミックに実行されることもあるかもしれません。

Compilation_of_volatile

図1: ARM/THUMBをターゲットとした場合のvolatileのコンパイル

図1のコードは、volatileオブジェクトが別の実行コンテキストからアクセス可能であることを考えた場合、スレッドや割込みに対して安全でしょうか?vol値のメモリからのロードとメモリへのストアは、このコードが32ビットのロード/ストアアーキテクチャ向けであるため、どちらもアトミックです。しかし、ソースはアトミックではありません。vol++を構成する3つの命令のどこかで、相変わらずコンテキストスイッチや割込みに見舞われる可能性があります。

対処方法

  • 特定のメモリアクセスを除き、volatileがアトミックを意味するとは決して想定すべきではありません。
  • オブジェクトが別の実行コンテキストからアクセス可能な場合、volatileオブジェクトに対し単なるアトミックな読出しや書込み以上のことを行うコードは、排他制御などの適切なシリアル化プリミティブによって、あるいは割込みの無効化によって、確実に保護してください。
  • お使いのコード標準のvolatileキーワードとシリアル化プリミティブの正しい使用方法を守ってください。
  • 既存コードのファイルスコープを持つ静的オブジェクトなど、すべてのグローバル変数の使用法を確認してください。

スタックを増やす?

スタックのサイズは適切ですか?これは開発者にとって永遠の難問です。スタックが必要以上に大きければ、多くのオンボードRAMを搭載したデバイスが必要となり、コストが増加します。スタックが小さすぎれば、開発は完全な失敗となるでしょう。セーフティクリティカルな要件をもつ製品にとっては、控えめに言っても「話になりません」。以下に、スタックサイズを決める際に検討すべき対策のチェックリストを示します。

  • デバッガの実行時スタックチェック機能を使用できる場合は、それを使用します。
  • 専用チェックルーチンで実行時に定期的にチェックされるマジックナンバーによって、メモリのスタック領域の前後を満たします。民生用のIEC 60730規格の要件に適合するように、MCUサプライヤから、このような機能や、特定目的のライブラリで使用できる他のMCUセルフチェック機能が既に提供されているかも知れません。
  • コールツリー解析を行い、最も厳しい条件でのスタック深度(割込みハンドラが使用するスタックも含む)を判定します。コードの見直しやリンカマップファイルの検査を手動で行う場合、最適化によってスタックの使用率が影響を受けることを忘れないでください。ツールサポートを購入・開発することもできます。あるいは、使っているビルドツールチェーンが、コールツリー解析とスタック深度解析の支援が可能なものであることを確認してください。

始めましょう

重要ポイントを以下にまとめます。

  • 着手する前に、適用可能な規格によって定義されたソフトウェア開発要件を理解しておく。
  • MISRA-Cをコーディング標準のベースとして使用する。
  • volatileの使用法を見直す。
  • スタック割当てのためにテストおよび解析を実施する。