HOME会社概況業務内容開発分野開発事例CANモジュールソフテックだよりお問い合わせ
HOME > ソフテックだより > 第47号(2007年8月1日発行) 技術レポート「MISRA-Cルールに基づいたプログラミング」

「ソフテックだより」では、ソフトウェア開発に関する情報や開発現場における社員の取り組みなどを定期的にお知らせしています。
さまざまなテーマを取り上げていますので、他のソフテックだよりも、ぜひご覧下さい。

ソフテックだより(発行日順)のページへ
ソフテックだより(技術分野別)のページへ
ソフテックだより(シーン別)のページへ


ソフテックだより 第47号(2007年8月1日発行)

技術レポート

「MISRA-Cルールに基づいたプログラミング」

1.はじめに

以前、ソフテックだより第19号(2006年6月7日発行)「C言語のコーディング規約による保守性の向上」として、当社でプログラミング時に注意している内容をご紹介しましたが、今回は、現在私の携わっている開発で新たに導入したMISRA-Cルールについてご紹介します。

2.MISRA-Cの導入

当社の開発の一つに組み込みソフトウェア開発があります。
開発言語に使用しているC言語は、自由度が高い反面、コンパイルによるエラーチェックが弱いという弱点があります。そのため、C言語としては間違いでは無い(コンパイルは通る)が、作成者の意図した動作をしない場合があり、そうなると原因を突き止めるまでにかなりの時間がかかってしまうこともよくあります。

また、一度製品化したプログラムは、バージョンアップやハードウェア部品の製造中止に伴い、変更を行うことが多くあります。
その時にも、わかりづらい書き方をしているプログラムは、変更はおろか、どう動いているのか解析するだけでも一苦労するものです。さらにそういったプログラムは、ちょっと変更を加えただけで、思わぬところで弊害が出てしまいがちです。
また、コンパイラに依存してしまうような書き方をすると、CPU変更をした時に「前までは問題無く動いていたのに、急に動かなくなった」という、思わぬ弊害が出てしまうこともあります。
実際当社でもCPU変更がたびたび行われますが、CPUが16ビットから32ビットに変わったことで、意図しない動作をしてしまったことがありました。
これらの問題を防ぐためには、信頼性、保守性、移植性に注意してプログラミングする必要があり、そのため今回新たにMISRA-Cを導入しました。

3.MISRA-Cとは?

MISRA-Cとは、MISRA(Motor Industry Software Reliability Association:英国の自動車関連ソフトウェアの業界団体)が発表した安全性の高いC言語プログラミングを行うためのガイドラインです。1998年に、”Guidelines For The Use Of The C Language In Vehicle Based Software”が発表され、このガイドラインがMISRA-Cと呼ばれるようになりました。
その後2004年には第2版である”Guidelines for the use of the C language in critical systems”が発表され、この第2版をMISRA-C:2004、第1版はMISRA-C:1998と呼ばれるようになりました。
名前からもわかるとおり、第1版を発表した1998年には車載用ソフトウェアを対象としたプログラミングガイドラインでしたが、第2版を発表した2004年には対象を重要システムへと拡大しています。

MISRA-Cでは、信頼性、保守性、移植性の向上を図り、C言語でプログラミングする際の注意事項をまとめています。MISRA-C:1998では、127のルールがあり、第2版のMISRA-C:2004では、第1版からルールの追加、削除が行われ141のルールが存在しています。

これらのルールに対してMISRA-Cでは、絶対に守る必要は無く、ルールを守ると逆に品質が落ちてしまう場合には、手順を踏むことで、ルールを逸脱することを認めることも明記されています。

4.MISRA-Cルールに基づいたプログラミング

実際に当社で採用したルールのうちいくつかをご紹介します。

目次

(1)
ルール5.2 識別子の隠蔽
(2)
ルール8.2 オブジェクト・関数の宣言・定義
(3)
ルール9.1 自動変数
(4)
ルール12.2 式の評価
(5)
ルール12.4 論理演算子「&&」・「||」の右側オペランド
(6)
ルール12.7 ビット単位の演算子
(7)
ルール14.4 goto文
(8)
ルール19.10 関数形式マクロの仮引数
(9)
ルール14.7 関数の出口
(1) ルール5.2 識別子の隠蔽

【ルール】

外部スコープの識別子が隠蔽されることになるため、内部スコープの識別子には、外部スコープの識別子と同じ名前を用いてはならない。

【解説】

外部スコープを持つ変数と、内部スコープを持つ変数の名前が一緒だった場合、内部スコープを持つ変数が有効である間、外部スコープを持つ変数が見えなくなってしまうため、同じ名前は禁止としています。

【サンプル1】

int nVal1_1;    // A
void Func1_1(void)
{
  int nVal1_1;  // B
  nVal1_1 = 10; // 非適合:外部のnVal1_1は不可視となってしまう
}

【サンプル2】

int g_nVal1_2;
void Func1_2(void)
{
  int nVal1_2;
  nVal1_2 = 10;  // 内部スコープを持つ変数への代入とわかりやすい
}

【補足】

サンプル1では、nVal1_1という変数名が2つ使われています。(便宜上外部スコープの変数をA、内部スコープの変数をBとします)
関数内でnVal1_1に10を代入していますが、値はBに入ります。
それを知らずに、Aに入れようとして記述してしまうと、意図しない動作をしてしまいます。
サンプル2のように、変数名を変えることで、誤った解釈がしにくくなり、信頼性、保守性を高めることができます。
(サンプル2では、グローバル変数を表す"g_"を付けています)

(2) ルール8.2 オブジェクト・関数の宣言・定義

【ルール】

オブジェクト又は関数を宣言又は定義するときは、常にその型を明記しなければならない。

【解説】

宣言や定義をする際に型を省略した場合、int型として扱われますが、誤解される可能性があるため省略せずに必ず型を指定することにしています。

【サンプル】

static s_nVal2_1;    // 非適合:型が誤解される可能性があるため禁止
static int s_nVal2_1;  // 適合

【補足】

上記のs_nValはどちらもint型として扱われます。
しかし、1つ目は型が明示されていないため、誤解される可能性があります。
本ルールを用いることで、保守性を高めることができます。

(3) ルール9.1 自動変数

【ルール】

全ての自動変数は、用いる前に値を代入しなければならない。

【解説】

自動変数は初期化しない場合には値が不定であり、初期化せずに使用すると予期しない動作をしてしまう可能性があるため、自動変数は必ず初期化することにしています。
※なお、外部スコープを持つ変数は処理系によっては0クリアしてくれるものもありますが、安全のため必ず初期化するようにしています。

【サンプル1】

void Func3_1(void)
{
  int nVal3_1;
  int nVal3_2;
  int nVal3_3;

  nVal3_1 = 0;
  nVal3_3 = 0;

  nVal3_3 = nVal3_1;  // 適合
  nVal3_3 = nVal3_2;  // 非適合:値が不定のため禁止
}

【サンプル2】


int Func3_2(int nPrm)
{
  int nVal3_4;

  if(nPrm > 0)
  {
    nVal3_4 = 1;
  }

  if(nPrm < 0)
  {
    nVal3_4 = 2;
  }

  return nVal3_4;   // 非適合:値が不定の可能性がある
}

【サンプル3】

int Func3_3(int nPrm)
{
  int nVal3_5;

  nVal3_5 = 0;
  if(nPrm > 0)
  {
    nVal3_5 = 1;
  }

  if(nPrm < 0)
  {
    nVal3_5 = 2;
  }

  return nVal3_5;
}

【補足】

サンプル1では、初期化していないnVal3_2をnVal3_3に代入しています。
自動変数のnVal3_2はこの時には値が不定となるため、nVal3_3も不定になってしまいます。
この状態でnVal3_2またはnVal3_3を使い続けると、間違った判断、メモリ破壊などにつながってしまい大変危険です。

またサンプル2では、引数のnPrmに応じてnVal3_4に値を入れて、戻り値としています。
もし引数のnPrmが0だったら、nVal3_4は不定のままですので戻り値も不定値になってしまいます。

この場合、サンプル3のようにnVal3_5をクリアしておくことで、信頼性を高めることができます。

【苦い経験】

恥ずかしながら私も、自動変数を初期化していないために動作が不安定になってしまった経験があります。
C言語を使用し始めて約1年が経過したころのお話です。

その時には、画面表示処理を作成していましたがその中で、いくつか条件を用意してその中で自動変数を更新し、最後にそれを参照するような処理を記述していました。(サンプル2に近いです)
もちろん自動変数の初期化はしていません。

だいたいの場合には条件のどれかに入るため問題無く動作するのですが、「まれに」どの条件にも入らない時があり、画面におかしな文字が表示される時がありました。
この「まれに」が曲者で、いざデバッグした時には正しい動きをしてしまいます。こうなるとデバッグをあきらめて悪くなりそうなところを探す、プログラムとのにらめっこが始まります。
幸いその時には、自動変数が初期化されていない!ということに早めに気付くことができましたが、それ以来、自動変数は確実に初期化するようになりました。

(4) ルール12.2 式の評価

【ルール】

式の値は、規格が認めるどのような順序で評価されようとも、同じで無ければならない。

【解説】

評価順序に依存した記述をすると、処理系によって意図しない動作をする場合があるため、禁止としています。

【サンプル1】


int nVal4_1_Ary[10];
int nVal4_1_Idx;
int Func4_1(int nPrm)
{
  int nVal4_1;
  nVal4_1 = nVal4_1_Ary[nVal4_1_Idx] + nVal4_1_Idx++;
  // 非適合:評価順序は規定されていない
}

【サンプル2】

int nVal4_2_Ary[10];
int nVal4_2_Idx;
int Func4_2(int nPrm)
{
  int nVal4_2;
  nVal4_2 = nVal4_2_Ary[nVal4_2_Idx] + nVal4_2_Idx;
  nVal4_2_Idx++;
  // 適合:
}

【補足】

サンプル1では、nVal4_1_IdxがnVal4_1_Ary配列の添え字と、加算の右辺として使われています。
また、加算の右辺として使われているほうに後置インクリメントが使われています。

後置インクリメントであるため、加算の右辺はインクリメント前のnVal4_1_Idxが使われますが、nVal4_1_Ary[nVal4_1_Idx]の中のnVal4_1_Idxは、インクリメント前のnVal4_1_Idxが使われるのか、後のnVal4_1_Idxが使われるのかは保証されていません。
サンプル2のように、処理系に依存しないように分けて記述することで、信頼性、移植性を高めることができます。

(5) ルール12.4 論理演算子「&&」・「||」の右側オペランド

【ルール】

論理演算子&&又は||の右側のオペランドには、副作用があってはならない。

【解説】

&&演算子、||演算子では、第1項目で結果が決まってしまうと、それ以降の評価を行いません。
そのため、第2項目以降に、副作用(値の変更など)が行われる処理を記述してしまうと、処理される場合とされない場合があります。
誤解を与える可能性があるため、第2項移行に副作用のある処理を記述することを禁止しています。

【サンプル1】

int nVal5_1;
int Func5_1(void)
{
  nVal5_1++;        // 値が変わる
  return nVal5_1;
}

int Func5_2(void)
{
  int nVal5_2;

  if((nVal5_2 == 0)
  || (Func5_1() == 10))  // 非適合:Func5_1関数が実行されない場合がある
  {
  }

  return 0;
}

【サンプル2】

int nVal5_3;
int Func5_3(void)
{
  nVal5_3++;        // 値が変わる
  return nVal5_3;
}

int Func5_4(void)
{
  int nVal5_4;

  if((nVal5_4 == 0)
  {
    if(Func5_3() == 10) // 適合
    {
    }
  }

  return 0;
}

【補足】

サンプル1では、nVal5_2の値が0の場合にはFunc5_1関数が実行されますが、nVal5_2が0ではない場合にはFunc5_1関数は実行されません。
そのため、常にFunc5_1関数が実行されると勘違いして記述してしまうと、意図した動作をしなくなってしまいます。
また、意図して記述していたとしても、実行されるのかされないのかが一目でわかりづらくなってしまいます。
サンプル2のように記述することで、nVal5_4が0の時のみ、Func5_3関数が実行されることがわかりやすくなり、保守性を高めることができます。

(6) ルール12.7 ビット単位の演算子

【ルール】

ビット単位の演算子は、符号付のオペランドに対して適用してはならない。

【解説】

ビット列の並びを操作する演算子を符号付変数に用いた結果は、処理系依存となるため禁止しています。

【サンプル】

int nVal6_1 = -1;
int nVal6_2;
nVal6_2 = nVal6_1 >> 2;   // 非適合:結果は処理系依存となるため禁止

【補足】

nVal1は符号付の整数型であり、演算結果は処理系に依存してしまいます。
多くの処理系では、右シフトして空いた上位は1で埋めると思いますが、厳密には処理系に依存します。
そのため、サンプルの書き方は移植性に影響を及ぼします。
本ルールを用いることで、移植性を高めることができます。

(7) ルール14.4 goto文

【ルール】

goto文を用いてはならない。

【解説】

goto文は保守性を著しく損なうため、使用禁止としています。

【補足】

最近goto文を使用する人はほとんどいないと思いますが、goto文は処理の流れを強引に変えてしまうことが可能であり、一気に読みにくいプログラムになってしまいます。
goto文を禁止することで、保守性を高めることができます。

(8) ルール19.10 関数形式マクロの仮引数

【ルール】

#または##のオペランドとして用いられている場合を除き、関数形式マクロの定義では、仮引数のそれぞれのインスタンスを括弧で囲まなければならない。

【解説】

マクロを括弧で囲まない場合、演算順序が意図しないものとなる可能性があるため、必ず括弧で囲むこととしています。

【サンプル】

#define MACRO1(prm1, prm2)   (prm1 * prm2)   // 非適合
#define MACRO2(prm1, prm2)   ((prm1) * (prm2)) // 適合

void Func8_1(void)
{
  int nVal8_1;

  nVal8_1 = MACRO1(2, 2+2); // nVal8_1は6となる
  nVal8_1 = MACRO2(2, 2+2); // nVal8_1は8となる
}

【補足】

サンプルではマクロ2つを実行していますが、中身はそれぞれ次のようになります。

MACRO1(2, 2+2)⇒(2 * 2 + 2)
MACRO2(2, 2+2)⇒((2) * (2 + 2))

演算子の優先度は、加算より掛け算の方が高いため、MACRO1とMACRO2とで結果に違いが出てしまいます。
それぞれを括弧で囲むことで、信頼性を高めることができます。

以上、当社で採用したルールのうちいくつかをご紹介しましたが、今回の開発で採用しなかった(逸脱を許可した)ルールもあります。
今回は、プロジェクトであらかじめルールに対して「採用する」か「逸脱を許可する」か、を決めておきました。
その中で、逸脱を許可したルールを1つご紹介します。

(9) ルール14.7 関数の出口

【ルール】

関数では、最後に唯一の出口が無くてはならない。

【解説・逸脱の理由】

関数内の制御の流れを単純化し、可読性を向上させるためのルールですが、例えば引数が意図しない値の場合などには、関数の先頭でチェックを行い抜けてしまったほうが、可読性が良い場合が多くあります。
そのため、エラーチェックにより処理を抜けてしまう場合には、複数の出口を許可しています。

【サンプル】

ルールに準拠した場合

int Func9_1(int nPrm1, int nPrm2)
{
  int nRet = 0;

  if(nPrm1 != 0)
  {
    if(nPrm2 != 0)
    {
      nRet = 1;  // ネストが深くなってしまう
    }
  }

  return nRet;
}

ルール逸脱を許可した場合

int Func9_2(int nPrm1, int nPrm2)
{
  int nRet = 0;

  if(nPrm1 == 0)
    return nRet;

  if(nPrm2 == 0)
    return nRet;

  nRet = 1;

  return nRet;
}

【補足】

サンプルでは、引数nPrm1、nPrm2共に0以外の時に処理をしたい場合を例にしていますが、ルールを採用するとネストが深くなってしまいます。
そのため、エラーチェックで先に抜けてしまう場合には、逸脱を許可しています。

5.おわりに

いかがだったでしょうか?

現在の開発では、上にあげたようなMISRA-Cルールを導入し、品質向上に努めていますが、目に見えるほどの効果はまだありません。
しかしルールの中には、無意識のうちに注意していることや、普段あまり気にしていなかったが、実は注意するべきだとわかったことがありました。
特に、無意識のうちに注意していることは人に伝えるのが難しく、自分だけが注意しているという状況になりやすいものです。
そういったことがルール化されることで、注意すべき内容を多人数で共有でき、品質を向上させることが可能だと考えています。

ただ、MISRA-Cルールを守っていればそれで良いかと言うと、もちろんそれだけでは不十分です。MISRA-Cはあくまで書き方のルールであり、このチェックを完璧にしたら、全ての問題が解決するというわけではありません。
ですが、意図しない動作をしているバグは調査に多くの時間がかかることも珍しくなく、こういった努力の積み重ねで品質は着実に向上していくものと信じています。

また、現在MISRA-C導入をご検討されている方々の参考になれば幸いです。

(T.O.)

[参考文献]

「組込み開発者におくるMISRA-C:2004」

編著者:MISRA-C研究会
出版年:2006/10/11
出版社:財団法人 日本規格協会

関連ページへのリンク

関連するソフテックだより