GAIO CLUB

2022年12月19日

【第2回】理解しづらいコーディングガイドラインとは? ~ビット操作~

静的解析/コンパイラ技術
いまさら聞けない 静的解析/コンパイラ技術
前回は、C言語の文法を守って書いても、思った通りに動かないことがあるというお話をしました。

勘違いしそうな記述例を挙げたのですが、文法が正しくても間違いを犯しそうな例は他にもあります。そして、記述ミスによる不具合を防止するためのコーディングガイドラインというものもあります。
コーディングガイドラインはC言語の文法よりも厳しいルールなので、このガイドラインを守ることにより「思った通りに動かなくなる」可能性が低くなります。

C言語コーディングガイドラインには、MISRA C・CERT C・ESCR(IPA/SEC) などがあります。
ほとんどのルールは解りやすいのですが、中には難解なルールもありますので、いくつかの理解し難いルールとそれに関わるお話をしたいと思います。

ビット操作に関するもの

ガイドラインそのままの文ではありませんが、このようなルールがあります。

"ビット演算子 ~ 及び << が、潜在型のunsigned charまたは unsigned shortであるオペランドに適用される場合、その結果は、そのオペランドの潜在型へ直ちにキャストする"

難解ですね。何を言っているのでしょう。~式の例を書いてみます。
#include <stdio.h>
   unsigned char UC;
   unsigned int  UI_1,UI_2;
   
   void test1(void)
   {
     UI_1 = ~UC;                  /* ルール違反     */
     UI_2 = (unsigned char)~UC;   /* 直ちにキャスト */
   }
   void test2(void)
   {
     UI_1 = (~UC) >> 2;                  /* ルール違反     */
     UI_2 = ((unsigned char)~UC) >> 2;   /* 直ちにキャスト */
   }
   
   int main(void)
   {
     UC = 0xF0;
     test1();
     printf("test1\n");
     printf("  UI_1 = ~UC -------------------------> 0x%8.8X\n",UI_1);
     printf("  UI_2 = (unsigned char)~UC ----------> 0x%8.8X\n",UI_2);
     test2();
     printf("test2\n");
     printf("  UI_1 = (~UC) >> 2 ------------------> 0x%8.8X\n",UI_1);
     printf("  UI_2 = ((unsigned char)~UC) >> 2 ---> 0x%8.8X\n",UI_2);
     return 0;
   }
潜在型へキャストするというのは、もとの型に戻すということですが、キャストの有無で全く結果が異なります。
このプログラムをgcc(x86)でコンパイルした動作結果はこのようになりました。
test1
   UI_1 = ~UC -------------------------> 0xFFFFFF0F
   UI_2 = (unsigned char)~UC ----------> 0x0000000F
test2
   UI_1 = (~UC) >> 2 ------------------> 0xFFFFFFC3
   UI_2 = ((unsigned char)~UC) >> 2 ---> 0x00000003
演算は基本的にint型で行われます(longが含まれる場合はlong型で行われます)。
例えばgccの場合、intは4バイトなので変数UC は4バイトに拡張した後でビット反転します。ここで、上位の24ビットが1になり、さらに右シフトを行う例ではこの上位ビットが下位8ビットに落ちてきます。右シフト以外でも、上位24ビットが演算結果に影響を与えることがあります。
潜在型にキャストすることで~UCの上位24ビットが0になり、ビット反転で変化した上位ビットの影響を受けなくなります。
ルールが指摘するもう1つの演算子 << も ~演算子と同じように上位24ビットの一部が1になってしまう恐れがあります。

ビット関連の仕様は不明確

C言語のビットに関わる仕様はあまり明確ではありません。ビット演算だけでなく、ビットフィールドを使用する時も注意が必要です。

処理系依存という言葉がありますが、C言語の仕様が明確でない部分はCコンパイラの実装に依存します。つまり、コンパイラが異なると動作などが異なる可能性があります。

例を挙げます。
struct bit 
   {
     unsigned char b1:1;
     unsigned char b2:2;
     unsigned char b3:3;
   } bit_fields;
このビットフィールド全体のサイズは処理系依存です。1バイトかもしれないし、int サイズかもしれません。

3つのメンバ変数b1b2b3がMSBから順に割り当てられるか、逆のLSBから順に割り当てられるかも、処理系依存です。さらに、余りのビットがMSB側に取られるか、LSB側に取られるかも処理系依存です。

全体サイズが1バイトであると仮定して図にすると以下のようにいろいろな割り当て方法が考えられます。
ビットフィールド
このようなことから、ガイドラインではビットフィールドの使用を禁止したり、どのような仕様を前提として使用しているか明文化せよと要求しています。

指定ビットだけ操作するとは限らない

ビットフィールドのアクセス方法もコンパイラによって違いがあります。

正確に言えば、コンパイラの違いというより、CPU命令(機械語)の違いに依存します。ビットにアクセスするための機械語が無ければ、ビットだけを読み書きすることはできません。当たり前ですね。
そのような場合には、charやshort、long サイズでのアクセスになってしまいます。

例えば下のような単純な代入式であっても、charデータを読み出して、その中の1ビットだけを変更し、charデータを書き戻すというような複数の機械語命令が動作します。
bit_fields.b1 = 0;
  
機械語例
      mov.b	_bit_fields,r0	 ; 1byteリード
      and	#7F,r0	         ; b1ビットを0に
      mov.b	r0,_bit_fields	 ; 1byte ライト
このように簡単な代入式でも複数の機械語によるリード/ライトを行うことがあるので、割り込み処理の影響を受けないよう考慮しなければならないということは、よく言われることです。

筆者紹介

浅野 昌尚(あさの まさなお)

ガイオ・テクノロジー株式会社

開発1部 QTXグループ

1980年代から30年以上にわたり汎用構造のCコンパイラ開発に従事し、その間に8ビットマイコンからRISC・VLIW・画像処理プロセッサまで、さまざまなCPU向けのクロスCコンパイラを開発。

人気のコラム

最新のコラム