2015年1月21日水曜日

C言語で処理系依存なコードを書く

プログラミングにおいては、相当な理由がない限り、わざわざ処理系依存なコードを書くことはありません。仕事なら「このコーディング規約に従うこと」「この静的解析ツールで警告をゼロにしろ」「MISRAガイドラインに従う」だの、厳しく言われるかもしれません。でも趣味の世界ならなんでもあり、やりたい放題できます!

ということで、処理系依存なコードを沢山書いてみましょう。と、突然言われてもなかなか処理系に依存するコードは出てきません。そこで、C言語の規格を参照して、「処理系依存」とされているものを順に書いてみましょう。そして、コンパイラによって動作が本当に違う!って確認していきたいと思います。

処理系依存なコードとは

2015年現在では、C言語の規格としてはC99(ISO/IEC 9899:1999、JIS X 3010:2003)やC11(ISO/IEC 9899:2011、対応するJIS規格は未発行)があります。 C99であればJISから日本語で規格を読むことができます(ただしWindows環境に限る。LinuxやMacでの閲覧は出来ませんでした)。 現在最新のC11ではJIS規格はまだありません。ISO規格の閲覧は有料となるため、C11では最終ドラフトのN1570で代替することにします。

それでは規格の「Annex J (informative) Portability issues .」 (JIS規格の場合は「附属書J.可搬性」)を参照して行きましょう。何ページにも渡って、下記の分類で「べからず集」が記載されています。

  • J.1 Unspecified behavior
  • J.2 Undefined behavior
  • J.3 Implementation-defined behavior
  • J.4 Locale-specific behavior
  • J.5 Common extensions

今回はこれから幾つかピックアップして実行してみます。使用するコンパイラはgccとclangです。バージョンはgcc 4.9.2とclang (Apple LLVM version 6.0 (clang-600.0.56) (based on LLVM 3.5svn))で実行しました。

J1: Unspecified behavior

— Whether two string literals result in distinct arrays (6.4.5). ...文字列リテラルが同じポインタになるかどうかは処理系依存


#include 

int main()
{
 const char* str1 = "hello, world";
 const char* str2 = "hello, world";
 printf("%s str1 = %p, str2 = %p\n",
  (str1==str2 ? "[Match]" : "[Different]"),
  str1, str2
  );
 return 0;
}

これを実行すると「Match」と表示されるか「Different」となるかは処理系次第のようです。GCCもClangも、デフォルトのままコンパイルすると「Match」となりました。文字列リテラルを最適化して配置するのがデフォルトなのですね。

— The order in which the function designator, arguments, and subexpressions within the arguments are evaluated in a function call (6.5.2.2). ... 入れ子になった関数の呼び出し順序は処理系依存です。


#include 

int f(int x, int y)
{
 printf("f(%d, %d)\n", x, y);
 return x+y;
}

int g(int n)
{
 printf("g(%d)\n", n);
 return n;
}

int main()
{
 int a = f(f(g(1),g(2)) , f(g(3),g(4)));
 printf("a = %d\n", a);
 return 0;
}

これはGCCとClangで実行順が異なりました。GCCでは一番右側から実行されていきます。 「 g(4) g(3) f(3, 4) g(2) g(1) f(1, 2) f(3, 7) a = 10
しかしClangでは、一番左側から実行されていくようです。 「 g(1) g(2) f(1, 2) g(3) g(4) f(3, 4) f(3, 7) a = 10 」 面白いですね。

— Whether or not a size expression is evaluated when it is part of the operand of a sizeof operator and changing the value of the size expression would not affect the result of the operator (6.7.6.2).


#include 

int main()
{
 int a = 1;
 size_t b = sizeof(++a);
 printf("a=%d. \n", a);

 a = 1;
 size_t c = sizeof(int [++a]);
 printf("a=%d. (will be 2)\n", a);

 a = 1;
 size_t d = sizeof(int [++a % 1 + 10]);
 printf("a=%d. (unspecified: 1 or 2)\n", a);
 printf("b=%zd, c=%zd, d=%zd\n", b,c,d);

 return 0;
}

sizeof演算子に式を与えた場合の動作で、可変長配列について規定したものです。++a % 1 + 10++aを実行してもしなくても結果は変わりません。ちなみにgccもclangもともに1,2,2という結果でした。

J2: Undefined behavior
J3: Implementation-defined behavior
J4: Locale-specific behavior
j5: Common extensions

続きはまた次回。

おわり

分量がおおいので、今回はここまででおしまいです。

(英語力の問題もあり、誤解をしていたらごめんなさい)

追記

追記:下記サイトにて未定義動作や処理系依存のコード例と、その回避方法が記載されていました。 https://www.securecoding.cert.org/confluence/display/seccode/DD.+Unspecified+Behavior;jsessionid=9AFACDECE35236C29B02BC507735CF34 https://www.securecoding.cert.org/confluence/display/seccode/CC.+Undefined+Behavior

0 件のコメント:

コメントを投稿