C90, C99, C11, C++98, C++11で異なる動作をするコード

(訳注:2016/9/28、頂きましたフィードバックを元に記事を修正いたしました。)

C言語の規格のリビジョン間には微妙な違いがありますが、このことを利用して「C90、C99、C11のどれとしてコンパイルされたかどうかにより、違う挙動をする」というプログラムを作ることが可能です。同様に、C++はほぼC言語の上位互換ですが、C言語とC++で違った結果を生み出すプログラムも存在します。

これは2015年のInternational Obfuscated C Code Contest(難読Cコード・国際コンテスト)へのDon Yangの投稿において、
C89、C99、C11、C++98、C11のどれとしてコンパイルされるかによって異なる出力を生成するプログラムを作成するのに使われています。C90の場合は、以下のような星形を出力します。

*********************************************              ***               **
***********************     *****************             ******             **
*********************        ****************           **********           **
********************         ****************         **************         **
******        ******         *****************     ****************************
******           **          **************************************************
******                      *************************(O************************
*******                     *     *********************************************
*********                              ****************************************
***********                             ***************************************
************                            ***************************************
*********                             *****************************************
*******                     ***************************************************
******                       ****************)d));o((d=************************
******           **          **************************************************
******         *****         *********************      ***********************
********************(         O******************        **********************
**********************       *******************          *********************
************************     *******************          **************)p)-p);
o(d-(p=*****************************************          *********************

C99ではこの星に目がついたものが出力され、C++11では丸が出力される、といったものになっています。(仕掛けはさらにあります。このプログラムは標準入力のテキストを読み込み、難読化されたC90のソースコードを生成するのですが、その生成されたソースは生成時に入力された文字列を出力するのです―コード内の*は全てポインタのデリファレンスなのです!)

このプログラムのソースコードは、読むのが少し難しいものになっています。

                                           #define r(R) R"()"
                          /*[*/#include  /**/<stdio.h>
                      #include<math.h>/*!![crc=0f527cd2]*/
                   float I,bu,k,i,F,u,U,K,O;char o[5200];int
              #define R(U) (sizeof('U')==1||sizeof(U"1"[0])==1)
            h=0,t=-1,m=80,n=26,d,g,p=0,q=0,v=0,y=112,x=40;  float
           N(float/*x*/_){g=1<<30;d=-~d*1103515245&--g;return  d*_
          /g;}void/**/w(int/**/_){if(t<0){for(g=0;g<5200;o[g++   ]=
          0);for(;g;o[g+79]=10)g-=80;for(t=37;g<62;o[80+g++]=32)   ;
         }if(m&&o[h*80+m-1]==10){for(g=0;g<79;o[t*80+g++]=0){}o[t
         ++*80+g]=10;t%=64;n+=2;I=N(70)+5;if(n>30&&(I-x)*(I-x)+n*
        n>1600&&R()){O=0;F=(x=0x1!=sizeof(' '))?k=1+N(2),i=12-k+N(
        8),N(4):(k=17+N(5),i=0,r()[0]?O=.1:  0);for(u=U=-.05;u<32;
        U=k+i+i*.5*sin((u+=.05)+F))for( K=0   ;K< U;K+=.1)if((bu=K*
       sin(u/5),g=I+cos( u/5) *K)>=0&&g  <     79  )o[g+(int)(t+44+
       bu*(.5-(bu>0?3*O:  O)   ) )%64*  80      ]  =32;x*=02//* */2
      -1;n=O+x?n=I+(x?0   :N     (k)-   k           /2),g=(t+42  )%
      64,m=-~g%64,x?g=m          =-~        m%64:0  ,n>5?o[g*80   +
     n-3]=o[m*80+n-3]=       0:   0              ,n <75?o[g*80+n
     +2]=o[m*80+n+2]=0   :0:0;                      x=I;}h=-~h%64
    ;m=0;}putchar((g=o [h*                          80+m++])?g:_);
   if(g){w(_);}}void W                               (const char*_
  ){for(;*_;w(*_++));}                               int main(int a
  ,char**_){while(a--)d              +=_[a          ]-(char*)0;W( \
 "#include<stdio.h>typed"             "e"         "f\40int\40O;v"
 "oid o(O _){putchar(_);}O"                    "\40main(){O"  ""
"*_[512],**p=_,**d,b,q;for(b=0;b"        "++<512;p=_+q)_[q"    \
"=(p-_+1)*9%512]=(O*)p;") ;      for(;(g= getchar())-EOF;p=
q){q=p;for(v=512;p-q-g&&q-p-              g;  v--)q=-~q*9%512
;W("o(");if(p>q)w(y),w(45);w(                      40);w(y^=20
);w(075);for(a=0;a<v;a++)w(42);                      for(W("(O**"
 );a--;w(42)){}w(41);w(y^024);w(                      41);if(p<=q)w(
   45),w(y^20);W(");");}for(a=7;a-6                      ;W(a<6?"{;}":""
      ))for(a  =0;a  <6 &&   !o[h*80+m                       +a];a++){}W("r"
         "etu"  /*J   */       "rn+0;}\n"                             );return
             /*                      "#*/0                                   ;}

しかし、私が見てわかる限り、どのC/C++の方言が使われているかを検知するために3つのトリックを使っているようです。

  • // コメント
    C90には//コメントがありません。そのため、

    int i = 2 //**/2
        ;
    

    という文は、C90と他のC/C++のリビジョンを区別するのに使われています。C90ではこれを以下のようにコンパイルします。

    int i = 2 //**/2
        ;
    

    一方、C++やC言語のより新しいリビジョンでは、以下のようにコンパイルされます。

    int i = 2 //**/2
        ;
    
  • 文字定数の型
    'a'などの文字定数は、C言語ではint型になりますが、C++ではchar型になります。つまり、sizeof('a')はC言語とC++で違う値として評価されます。

  • ワイド文字リテラル
    C11とC++11にはワイド文字リテラルがあり、例えばU"hello!"char32_t型の文字からなる文字列です。これを以下のマクロと組み合わせます。

    #define R(U) sizeof(U"a"[0])
    

    このマクロはプログラム中でR("")という形で用いられています。C11とC++では、これは以下のように展開されます。

    sizeof(U"a"[0])
    

    これは4として評価されます。一方、古いリビジョンの規格では、U"a"は2つのトークンとして扱われるので、このマクロは以下のように展開されます。

    sizeof("""a"[0])
    

    これは1として評価されます。