Skip to content

Instantly share code, notes, and snippets.

@minghai
Last active January 4, 2018 15:52
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save minghai/5476522 to your computer and use it in GitHub Desktop.
Save minghai/5476522 to your computer and use it in GitHub Desktop.
StackOverflowにて行われた2006年の難読化されたC言語プログラミングコンテスト入選作、sykes2.cの解説の翻訳と訳者の追加情報

難読化Cコードコンテスト 2006のsykes2.cを説明して下さい。

Original posted at http://stackoverflow.com/questions/15393441/obfuscated-c-code-contest-2006-please-explain-sykes2-c

質問:このCのプログラムはどのように動くのか?

main(_){_^448&&main(-~_);putchar(--_%64?32|-~7[__TIME__-_/8%8][">'txiZ^(~z?"-48]>>";;;====~$::199"[_*2&8|_/64]/(_&2?1:8)%8&1:10);}

これはそのままコンパイルできる(gcc4.6.3でテストした)。 コンパイルされた時刻を表示する。私のシステムでは以下のように表示される。

!!  !!!!!!              !!  !!!!!!              !!  !!!!!! 
!!  !!  !!              !!      !!              !!  !!  !! 
!!  !!  !!              !!      !!              !!  !!  !! 
!!  !!!!!!    !!        !!      !!    !!        !!  !!!!!! 
!!      !!              !!      !!              !!  !!  !! 
!!      !!              !!      !!              !!  !!  !! 
!!  !!!!!!              !!      !!              !!  !!!!!!

Source: sykes2 - A clock in one line, sykes2 author hints

ヒント:デフォルトではコンパイル時に警告は無い。-Wallを付けてコンパイルすると次の警告が出される。

sykes2.c:1:1: warning: return type defaults to ‘int’ [-Wreturn-type]
sykes2.c: In function ‘main’:
sykes2.c:1:14: warning: value computed is not used [-Wunused-value]
sykes2.c:1:1: warning: implicit declaration of function ‘putchar’ [-Wimplicit-function-declaration]
sykes2.c:1:1: warning: suggest parentheses around arithmetic in operand of ‘|’ [-Wparentheses]
sykes2.c:1:1: warning: suggest parentheses around arithmetic in operand of ‘|’ [-Wparentheses]
sykes2.c:1:1: warning: control reaches end of non-void function [-Wreturn-type]

ベストアンサー

難読化を解いてみよう。

まずインデントを入れる。

main(_) {
    _^448 && main(-~_);
    putchar(--_%64
        ? 32 | -~7[__TIME__-_/8%8][">'txiZ^(~z?"-48] >> ";;;====~$::199"[_*2&8|_/64]/(_&2?1:8)%8&1
        : 10);
}

このゴチャゴチャをほぐすために変数を導入する

main(int i) {
    if(i^448)
        main(-~i);
    if(--i % 64) {
        char a = -~7[__TIME__-i/8%8][">'txiZ^(~z?"-48];
        char b = a >> ";;;====~$::199"[i*2&8|i/64]/(i&2?1:8)%8;
        putchar(32 | (b & 1));
    } else {
        putchar(10); // newline
    }
}

二の補数だから-~i == i+1なことに注意。従って

main(int i) {
    if(i != 448)
        main(i+1);
    i--;
    if(i % 64 == 0) {
        putchar('\n');
    } else {
        char a = -~7[__TIME__-i/8%8][">'txiZ^(~z?"-48];
        char b = a >> ";;;====~$::199"[i*2&8|i/64]/(i&2?1:8)%8;
        putchar(32 | (b & 1));
    }
}

訳者注:~はNOTで全ビットを反転する。二の補数で考えれば符号を逆転し-1した数になる。それを-で符号だけ戻せば+1になる。

さて、a[b]b[a]と全く同じ、そして-~ == 1+の変換を再び適用すると

main(int i) {
    if(i != 448)
        main(i+1);
    i--;
    if(i % 64 == 0) {
        putchar('\n');
    } else {
        char a = (">'txiZ^(~z?"-48)[(__TIME__-i/8%8)[7]] + 1;
        char b = a >> ";;;====~$::199"[(i*2&8)|i/64]/(i&2?1:8)%8;
        putchar(32 | (b & 1));
    }
}

訳者注: i^448はxorなのでiが448の時のみ0(偽)になる。引数無しで実行した時、第一引数はコマンド引数の数+1、つまり1になるためiは1になるので1から447までは常にi+1main関数を再帰呼び出しすることになる。iが448の時if文が偽になるので進み、直後でiは447に減らされた後、最初の実行1回目が始まる。実行が終了すると以前の呼び出しの直後から実行され再びiは1減少する。以降実行しては以前の呼出直後に戻るのでiが447から0までの繰り返し実行となる。

再帰をループに変換してもう少し簡単にしてみよう

// コマンドライン引数は与えないで下さい
main() {
    int i;
    for(i=447; i>=0; i--) {
        if(i % 64 == 0) {
            putchar('\n');
        } else {
            char t = __TIME__[7 - i/8%8];
            char a = ">'txiZ^(~z?"[t - 48] + 1;
            int shift = ";;;====~$::199"[(i*2&8) | (i/64)];
            if((i & 2) == 0)
                shift /= 8;
            shift = shift % 8;
            char b = a >> shift;
            putchar(32 | (b & 1));
        }
    }
}

これは1回の繰り返しで1つの文字を出力する。64文字毎に改行を行う。そうでなければ1つのデータテーブルのペアを用いて何を出力するのか決定し、文字コード32(スペース)か文字コード33(!)を出力する。最初のテーブル(">'txiZ^(~z?")は10個のビットマップの集合で各文字の表現を記述している。2つ目のテーブル(";;;====~$::199")はビットマップから表示するための適切なビットを選択する。

2つ目のテーブル

2つ目のテーブルを調べることから始めよう。

int shift = ";;;====~$::199"[(i*2&8) | (i/64)];

i/64は行番号(6から0)でi*2&8はもしiを8で割った剰余が4、5、6、7の場合、その場合のみ8になる。

訳者注:それ以外では0になる。

訳者注:i*2は1ビット左へシフトになり、8(1000b)と論理積を取れば元の数の3ビット目以外は0になる。これはつまりiの3ビット目が1の場合のみ8、そうでなければ0になる。i/64(0から6)との論理和を取れば値は0から6と8から14になる。この値がインデックスの値になるのでshiftの値はインデックスの場所にある文字のコードになる。8文字目(インデックスが7の時)の文字~は使用されていない。

訳者注:i/64は行番号(6から0)なので64ループ毎に1回変わるのみ。(i*2&8)は第3ビットを見ている、かつiの初期値447は64*8-1なのでi%8は7から1づつ減っていく8回毎のループなので(i*2&8)i%8が7から4までは8で3から0までは0になる。従って(i*2&8) | (i/64)の数列は14を4回、6を4回を8回繰り返し、以降1減じて13を4回、5を4回を8回繰り返し、以下8を4回、0を4回を8回繰り返すまで減少していく。

if((i & 2) == 0) shift /= 8; shift = shift % 8はテーブルの文字コード値から(i%8 = 0,1,4,5)の時8進数2桁で上位1桁の値、(i%8 = 2,3,6,7)の時、下位の1桁の値になる。

訳者注:8で割るということは8=2^3なので3ビット右へシフトすることを意味する。つまりshiftを8進数と考えた時に下位1桁を消し2桁目の数が1桁目となる。iの2ビット目が0なら2桁目、そうでなければshiftはそのままで1桁目がshift = shift % 8で得られることになる。

シフトテーブルは以下のようになる。 まず以下の前提で

char10進数16進数8進数
;593b73
=613d75
$362444
:583a72
1493161
9573971
~12673156未使用
\0000C言語の終端文字(14文字目)

この時、以下となる

訳者注:colcolumnで列の意だが、ここではバナー1文字表示中における列の値となる。

row = i/64colval
66-70
64-50
63-25
61-07
56-71
54-57
53-25
51-07
46-71
44-57
43-25
41-07
36-71
34-56
33-25
31-07
26-72
24-57
23-23
21-07
16-72
14-57
13-23
11-07
06-74
04-54
03-23
01-07

または表形式では

00005577
11775577
11775577
11665577
22773377
22773377
44443377

作者は文字列の終端文字を最初の2つのテーブル要素に(隠れて!)用いていることに注意すること

訳者注:C言語の文字列には必ず最後に終端文字として\0が追加されている。このプログラムではその数値、0も利用されている。

これは7つのセグメントによる表示を設計している。7が空白である。従って1つ目のテーブルは点灯すべき区画を定義しているに違いない。

最初のテーブル

__TIME__は特別なマクロでプリプロセッサにより定義される。これはプリプロセッサが実行された時刻を含む文字列に"HH:MM:SS"の形式にて展開される。文字列がまさに8文字になることに気づくだろう。文字0から9のASCIIコード値は48から57になり:は58になることに注意せよ。出力は1行当たり64文字になるので__TIME__の1文字当たり8文字に当たる。

7 - i/8%8は従って__TIME__に対する現在出力している文字のインデックスである。7-iを降順に回しているために必要である。従ってt__TIME__の出力される文字である。

訳者注:i/8は8文字がバナー表示の1文字にあたるので8文字を1セットとして数えた場合に現在何セット目を処理しているかになる。さらに8セットが1行であるからその%8を取れば8文字である__TIME__の何文字目を処理しているかが得られる。iが447から0へ降順でループしているのでi/8%8も7から0へと減っていく。このため7から引くことで0から7へと変換している。i/8%8の数列は7が8回、6が8回と続き0が8回まで行った後、7が8回に戻る。

aは入力tに従い次の2進数に等しくなる

char t = __TIME__[7 - i/8%8];
char a = ">'txiZ^(~z?"[t - 48] + 1;

"0" = 0x30 = 48
"9" = 0x39 = 57
":" = 0x3a = 58
tcharcodea = code + 1
0>0x3e00111111
1'0x2700101000
2t0x7401110101
3x0x7701111001
4i0x6901101010
5Z0x5a01011011
6^0x5e01011111
7(0x2800101001
8~0x7e01111111
9z0x7a01111011
:?0x3f01000000

各数値はセグメントを現わすビットマップであり7セグメントディスプレイにて点灯される位置を示す。文字は全て7ビットのASCIIであるので最高位のビットは常に0である。従ってセグメントテーブルの7は常にブランクとして表示される。2つ目のテーブルを7をブランクにすると次のようになる。

000055  
11  55  
11  55  
116655  
22  33  
22  33  
444433  

従って例えば4は01101010(ビットの1、3、5、6がONなので)以下のように表示される。

    !!
!!  !!
!!  !!
!!!!!!
    !!
    !!
    !!

本当にコードを理解したのか示すために出力をこのテーブルに調整してみよう

  00  
11  55
11  55
  66  
22  33
22  33
  44

これは"?;;?==? '::799\x07"のようにエンコードされる。美術的観点から文字列の内いくつかに64を足してみよう(下位6ビットのみが利用されるのだからこれは出力に影響を与えない)。これにより"?{{?}}?gg::799G"となる(8番目の文字は未使用なので実際に何にしても良いことに注意)。新しいテーブルをオリジナルのコードに入れてみよう。

main(_){_^448&&main(-~_);putchar(--_%64?32|-~7[__TIME__-_/8%8][">'txiZ^(~z?"-48]>>"?{{?}}?gg::799G"[_*2&8|_/64]/(_&2?1:8)%8&1:10);}

次の結果を得る

      !!              !!                              !!   
!!  !!              !!  !!  !!  !!              !!  !!  !! 
!!  !!              !!  !!  !!  !!              !!  !!  !! 
      !!      !!              !!      !!                   
!!  !!  !!          !!  !!      !!              !!  !!  !! 
!!  !!  !!          !!  !!      !!              !!  !!  !! 
      !!              !!                              !!   

期待どおりの結果だ。オリジナルのようなソリッドな表現ではない。このことがなぜ原作者が彼のテーブルを選んだのかを説明できるだろう。

(了)

以下、訳者

訳者が正しく理解できたかを試すために頭からわかりやすく書き直してみた。

#include <stdio.h>

void main() {
  for (int y = 0; y < 7; y++) {
    for (int x = 0; x < 64; x++) {
      // 時刻上で何文字目であるか?
      int index = x / 8;
      char c = __TIME__[index];
      // cに対応するビットマップを取り出す
      char a = ">'txiZ^(~z?"[c - 48] + 1;
      // 字中の相対位置からセグメント番号へマッピングする
      // 表示する時刻の数値が変わってもマッピングは変わらないことに注意
      // i2が奇数の場合-1した位置と同じセグメントに属する
      // セグメントの種類は0-7で8種類 (7は常に消灯なので7セグメント)
      // バナー1文字が8x7の二次元テーブルで奇数は偶数と同じなので4x7
      // 1箇所に0-7で3ビットで済むので2箇所で6bitで1byteに収まる
      // 従って2byteで一行分に相当 2bytex7行で14byte(文字)になる
      // 分かりやすさのため8進数を止めて16進数にしている
      char maptab[] = {0x00,0x57,0x17,0x57,0x17,0x57,0x16,0x57,0x27,0x37,
                       0x27,0x37,0x44,0x37};
      int i2 = x % 8; // バナー1文字の中での位置
      char shift = maptab[i2 / (2 * 2) + y * 2];
      shift = ((i2 & 2) == 0) ? shift >> 4 : shift & 0xF;
      char b = a >> shift;
      putchar(32 | (b & 1));
    }
    putchar('\n');
  }
}

問題無く動作する。

!!  !!!!!!          !!!!!!  !!!!                !!  !!!!!!
!!  !!  !!              !!  !!              !!  !!  !!  !!
!!  !!  !!              !!  !!              !!  !!  !!  !!
!!  !!!!!!    !!      !!!!  !!!!      !!    !!!!!!  !!!!!!
!!      !!          !!      !!  !!              !!      !!
!!      !!          !!      !!  !!              !!      !!
!!  !!!!!!          !!!!    !!!!!!              !!  !!!!!!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment