메아리

Tour de IOCCC: 2000/dhyang

                    #define/**/X
                  char*d="X0[!4cM,!"
               "4cK`*!4cJc(!4cHg&!4c$j"
             "8f'!&~]9e)!'|:d+!)rAc-!*m*"
           ":d/!4c(b4e0!1r2e2!/t0e4!-y-c6!"
          "+|,c6!)f$b(h*c6!(d'b(i)d5!(b*a'`&c"
          ")c5!'b+`&b'c)c4!&b-_$c'd*c3!&a.h'd+"
         "d1!%a/g'e+e0!%b-g(d.d/!&c*h'd1d-!(d%g)"
        "d4d+!*l,d7d)!,h-d;c'!.b0c>d%!A`Dc$![7)35E"
       "!'1cA,,!2kE`*!-s@d(!(k(f//g&!)f.e5'f(!+a+)"
       "f%2g*!?f5f,!=f-*e/!<d6e1!9e0'f3!6f)-g5!4d*b"
       "+e6!0f%k)d7!+~^'c7!)z/d-+!'n%a0(d5!%c1a+/d4"
       "!2)c9e2!9b;e1!8b>e/!     7cAd-!5fAe+!7fBe(!"
      "8hBd&!:iAd$![7S,Q0!1     bF 7!1b?'_6!1c,8b4"
      "!2b*a,*d3!2n4f2!${4    f.      '!%y4e5!&f%"
     "d-^-d7!4c+b)d9!4c-a    'd        :!/i('`&d"
     ";!+l'a+d<!)l*b(d=!'   m-        a  &d>!&d'"
    "`0_&c?!$dAc@!$cBc@!$   b         <    ^&d$`"
    ":!$d9_&l++^$!%f3a'    n1        _       $ !&"
   "f/c(o/_%!(f+c)q*c     %!         *       f &d+"
   "f$s&!-n,d)n(!0i-     c-         k)       !  3d"
   "/b0h*!H`7a,![7*     i]          5        4   71"
  "[=ohr&o*t*q*`*d      *v         *r         ;  02"
  "7*~=h./}tcrsth      &t          :          r   9b"
 "].,b-725-.t--//      #r         [           <   t8-"
 "752793?  <.~;b      ].t--+r     /           #    53"
 "7-r[/9~X  .v90      <6/<.v;-52/={            k   goh"
 "./}q;   u  vto     hr  `.i*$engt$            $    ,b"
 ";$/     =t ;v;     6     =`it.`;7=`          :    ,b-"
 "725    = / o`.    .d       ;b]`--[/+       55/     }o"
 "`.d   :   - ?5    /           }o`.'     v/i]q      - "
 "-[;   5  2  =`  it            .        o;53-       . "
 "v96   <7 /      =o            :            d        =o"
 "--/i  ]q--      [;           h.            /        = "
 "i]q--[  ;v      9h           ./            <        - "
 "52={cj   u      c&`          i   t       . o        ; "
 "?4=o:d=         o--          /  i        ]q         - "
 "-[;54={  cj     uc&          i]q          -          -"
 "[;76=i]q[;6     =vsr        u.i           /          ={"
 "=),BihY_gha     ,)\0        "             ,          o [
  3217];int i,   r,w,f        ,              b        ,x ,
  p;n(){return   r  <X        X               X       X  X
  768?d[X(143+   X  r++       +               *d      )  %
   768]:r>2659   ?  59:       (                x      =  d
   [(r++-768)%   X  947      +             768]       ) ?
   x^(p?6:0):(p  =   34      X            X           X )
   ;}s(){for(x=  n   ();     (           x^           ( p
  ?6:0))==32;x=  n    ()     )   ;return x            ; }
  void/**/main X      ()     {           r           =  p
  =0;w=sprintf  (X     X     X         X X           X o
  ,"char*d=");  for          (    f=1;f <            * d
  +143;)if(33-(  b=d         [      f++ X           ]  )
  ){if(b<93){if   X(!        p          )             o
   [w++]=34;for    X(i       =         35             +
    (p?0:1);i<b;    i++      )         o
    [w++]=s();o[     w++               ]
     =p?s():34;}     else              X
       {for(i=92;     i<b;            i
        ++)o[w++]=     32;}           }
             else o     [w++          ]
                         =10;o        [
                           w]=0      ;
                            puts(o);}

사이토 하지메다! 라고 외치신 분은 바람의 검심을 너무 많이 보셨습니다.

뭘 하는 프로그램인가?

그냥 컴파일해서 실행해 봅시다.

$ gcc dhyang.c -o dhyang
$ ./dhyang
char*d=")35E!'1cA,,!""2kE`*!-s@d(!(k(f//g&!)f.e5'f(!+a+)f%2g*!?f5f,!=f-*e/!<d"
"6e1!9e0'f3!6f)-g"       "5!4d*b+e6!0f%k)d7!+~^'c7!)z/d-+!'n%a0(d5!""%c1a+/d4"
"!2)c9e2!9b;e1!8b"       ">e/!7cAd-!5fAe+!7fBe(!8hBd&!:iAd$![7S,Q"    "0!1bF7"
"!1b?'_6!1c,8b4!2"       "b*a,*d3!2n4f2!${4f.'!%y4e5!&f%d-^-d7!4"       "c+b)"
"d9!4c-a'd:!/i('`"       "&d;!+l'a+d<!)l*b(d=!'m-a&d>!&d'`0_&c"           "?!"
"$dAc@!$cBc@!$b<^"       ""              "&d$`:!$d9_&l++^$!%f3"          "a'n"
"1_"                                   "$!&f/c(o/_%!(f+c)q*c%"         "!*f&d"
"+f$"                                "s&!-n,d)n(!0i-c-k)!3d/"        "b0h*!H`"
"7a,!["                      "7X0[!4cM,!4cK`*!4cJc(!4cHg&!4"       "c$j8f'!&~"
"]9e)!'"                 "|:d+!)""rAc-!*m*:d/!4c(b4e0!1r"        "2e2!/t0e4!-"
"y-c6!+|,c6!)f$b("       "h*c6"      "!(d'b(i)d5!(b*a'"         "`&c)c5!'b+`&"
"b'c)c4!&b-_$c"                      "'d*c3!&a.h'd+d"         "1!%a/g'e+e0!%b"
"-g(d.d/!&c*"                        "h'd1d-!(d%g)"         "d4d+!*l,d7d)!,h-"
"d;c'!.b0c"                             ">d%!A`Dc$"       "![7*i]5471[=ohr&o*"
"t*q*`*d"                                "*v*r;027"       "*~=h./}tcrsth&t:r9"
"b].,b"          ""      "-725"            "-.t--/"       "/#r[<t8-752793?<.~"
";b]."        "t--"      "+r/#"             "537-r"        "[/9~X.v90<6/<.v;-"
"52/="      "{kgoh."     "/}q"    ";u"       "vtohr"       "`.i*$engt$$,b;$/="
"t;v"      ";6=`it."    "`;"      "7=`"       ":,b-7"       "25=/o`..d;b]`--["
"/+"      "55/}o`.d:"   ""       "-?5"        "/}o`.'"       "v/i]q--[;52=`it"
".o"     ";53-.v96<7"            "/=o"        ":d=o--/"        "i]q--[;h./=i]"
"q"     "--[;v9h./<-"           "52="         "{cjuc&`"         "it.o;?4=o:d="
"o"      "--/i]q--["           ";54="        "{cjuc&i]q-"        "-[;76=i]q[;"
"6="       "vsru.i"            "/={"        "=),BihY_gha,)"        "",o[3217];
int i,        r,w           ,f,b,p,        t=641,x;n(){return         r<t?d[(*
d+143+(r                ++))%t]:r>        +1341?59:(x=d[(r++-t)        %351+t]
)?x^(p?6:0            ):(p=+34);}        main(){w=sprintf(o,"char"       "*d="
);r=p=0;for(      f=1;f<*d+143;)       if((b=d[f++])-33){if(b<+93){        if(
!p)o[w++]=34;for(i=35+(p?0:1);i    <b;i++)o[w++]=n();o[w++]=p?n():+34       ;}
else for(i=92;i<b;i++)o[w++]=32;}else o[w++]=10;o[w]=0;puts(o);};/*Don_Yang*/;

...어?!

"あく"라는 히라가나가 나왔습니다. 게다가 이것도 코드군요. 한 번 다시 컴파일해서 실행해 봅시다.

$ ./dhyang > dhyang1.c
$ gcc dhyang1.c -o dhyang1
$ ./dhyang1
char*d="S,Q0!1bF7!1b?'_""6!1c,8b4!2b*a,*d3""!2n4f2!${4f.'!%y4e5!&f%d-^-d7!4c+"
"b)d""9!4c-a'd:!/i("       "'`&d;!+l'a+d<!)l*b(d=!'m-a&d>""!&d'`0_&""c?!$dAc@"
"!$cBc@!$b<^&d$"               "`:!$d9_&l++^$!%f3a'n1_$!&f/c(o/_%"    "!(f+c)"
"q*c%!*f&d"                       "+f$s&!-n,d)n(!0i-c-k)!3d/b0h"        "*!H`"
"7a,!"               "[7X0"          "[!4cM,!4cK`""*!4cJc(!4cH"           "g&"
"!4c$j"          "8f'!&~]9e)"         "!'|:d+!)rAc-!*m*:""d/!"          "4c(b"
"4e0!1r2"     "e2!/t0e""4!-y-"          "c""6!+|,c6!)f$b(h"           "*c6!(d"
"'b(i)d5!(b*a'`&c)c5!'b+`&b'"          "c)c4!&b-_$c'd*c3!"          "&a.h'd+d"
"1!%a/g'e+e0!%b-g(d.d/!&c*"          "h'd1d-!(d""%g)d4d"         "+!*l,d7d)!,"
"h-d;c'!.b0c>d%!A`Dc$![7)"        "35E!'1cA,,!2kE`*!-"         "s@d(!(k(f//g&"
"!)f.e5'f(!+a+)f%2g*!?"         "f5f,!=f-*e/!""<d6"          "e1!9e0'f3!6f)-g"
"5!4d*b+e6!0f%k)d7!"          "+~^'c""7!)z/d-+!"           "'n%a0(d5!%c1a+/d4"
"!2)c9e2!9b;e1!8b"        ">e/!7c"      "Ad-!5fA"         "e+!7fBe(!8hBd&!:iA"
"d$![7*i]5471"          "["               "=ohr&"        "o*t*q*`*d*v*r;027*~"
"=h./}tc"                                    "rst"       "h&t:r9b].,b-725-.t-"
"-//#r"                              "[<t8-752793"        "?<.~;b].t""--+r/#5"
"37-"                  "r"     "[/9~X.v90<6/""<.v;"        "-52/={kgoh./}q;uv"
"t"       "ohr`.i*$engt$"     "$,b;$/=""t;v;6=`it.`"        ";7=`:,b-725=/o`."
".d;b]`--[/+55/""}o`.d"       ":-?5/}o`.'v/i]q--[;52"         "=`it.o;53-.v96"
"<7/=o:d=o--/i]q--[;h."      "/=i]q--[;v9h./<-52={cju"         "c&`it.o;?4=o:"
"d=o--/i]q--[;54={cju"      "c&i]q--[;76=i]q[;6=vsru.i/"         "={=),BihY_g"
"ha,)",o[3217];int i,       r,w,f,b,p,t=641,x;n(){return r<        t?d[(*d+143
+(r++))%t]:r>+1341?          59:(x=d[(r++-t)%351+t])?x^(p?6:         0):(p=+34
);}main(){w=sprintf(o          ,"char""*d=");r=p=0;for(f=1;f<*d         +143;)
if((b=d[f++])-33){if(b            <+93){if(!p)o[w++]=34;for(i=35+(        p?0:
1);i<b;i++)o[w++]=n();o[             w++]=p?n():+34;}else for(i=92;i        <b
;i++)o[w++]=32;}else o[w++]=10;o[w]=0;puts(o);};/*Don_Yang*/;;;;;;;;;;;;;;;;;;

...어어어어?!?!!!!

이번에는 "そく"라는 히라가나가 나왔군요. 무슨 의미일까요? 계속 가 봅시다.

$ ./dhyang1 > dhyang2.c
$ gcc dhyang2.c -o dhyang2
$ ./dhyang2
char*d="X0[!4cM,""!4cK`*!4cJc(!4cHg&!4c$j8f'!&~]9e)!'|:d+!)rAc-""!*m*:d/!4c(b"
"4e0!1r2e2!/t0"      "e4!-y-c6!+|,c6!)f$b(h*c6!(d'b(i)d5""!(b*a'`&c)c5!'b+`&b"
"'c)c4!&b-_$c'"      "d*c3!&a.h'd+d1!%a/g'e+e0!%b""-g("   "d.d/!&c*h'd1d-!(d%"
"g)d4d+!*l,d7d"       ")!,h-d;c""'!.b0c>d%!A`Dc$![7)3"      "5E!'1cA,,!2kE`*!"
"-s@d(!(k(f//g&"      "!)f.e5"     "'f(!+a+)""f%2g*!"        "?f5f,!=f-*e/!<d"
"6e1!9e0'f3!6f)"                  "-g5!4d*b+e6!0f%k"          ")d7!+~^'c7!)z/"
""                               "d-+!'n%a0(d5!%c1"          "a+/d4!2)c9""e2!"
"9"                             "b;e1!8b>e/!7cAd-"         "!5fAe+!7fBe(!8hBd"
"&!"          ":"        "iAd$![7S,"  "Q0!1bF7!1"        "b?'_6!1c,8b4!2b*a,*"
"d3!2n4f2!${4f.'!"       "%y4e5!&"      "f%d-^"        "-d7!4c+b)d9!4c-a'd:!/"
"i('`&d;!+l'a+d<!"       ")l*b(d=!'"     "m-a"        "&d>!&d'`0_&c?!$dAc@!$c"
"Bc@!$b<^&d$"             "`:!$""d9_"    "&l"        "++^$!%f3a'n1_$!&f/c(o/_"
"%!(f+c)"                "q*c"     "%!*f&d+"        "f$s&!-n,d)n(!0i-c-k)!3d/"
"b0h*!"                "H`7a,!"      "[7*i"        "]5471[=ohr&o*t*q*`*d*v*r;"
"027"                 "*~=h./}tc"     "rs"        "th&t:r9b].,b-725-.t--//#r["
"<t"        "8-7"    "52793?<.~;b]"   ".t"       "--+r/#537-r[/9~X.v90<6/<.v;"
""        "-52/={kgoh./}q;uvtohr`.i*$eng"       "t$$,b;$/=t;v;6=`it.`;7=`:,b-"
""       "725=/o`..d;b]`--[/+55/}o`.d:-?"       "5/}o`.'v/i]q--[;52=`it.o;53-"
""      ".v96<7/=o:d=o--/i]q--[;h"  "./"        ""    "=i]q--[;v9h./<-52={cju"
""        "c&`it.o;?4=o:d=o--/i]"   "q-"                "-[;54={""cjuc&i]"  ""
"q"          "--[;76=i]q[;6=v"     "sru"                  ".i/={=),BihY_"   ""
"gh"          "a,)",o[3217]       ;int i                   ,r,w,f,b,p,t=   641
,x;n()          {return r       <t?d[(*                     d+143+(r       ++)
)%t]:r>+          1341        ?59:(x=d[          (r                       ++-t
)%351+t])?x                  ^(p?6:0):(        p=+34);                  }main(
){w=sprintf(o,             "char""*d="       );r=p=0;for               (f=1;f<
*d+143;)if((b=d[f        ++])-33){if(b      <+93){if(!p)o[            w++]=34;
for(i=35+(p?0:1);i<b;i++)o[w++]=n();o[    w++]=p?n():+34;}else      for(i=92;i
<b;i++)o[w++]=32;}else o[w++]=10;o[w]=0;puts(o);};/*Don_Yang*/;;;;;;;;;;;;;;;;

세번째 코드에는 "ざん"이라고 쓰여 있습니다. IOCCC 힌트에 심사위원들이 "보통 프로그램들은 흥미로운 레이아웃이 하나뿐인데 이건 네 개나 된다"라고 말한 이유를 알 만 하네요. (사이토 하지메 레이아웃도 하나로 칩니다.) 마지막으로 이 코드를 컴파일해 봅시다.

$ ./dhyang2 > dhyang3.c
$ gcc dhyang3.c -o dhyang3
$ ./dhyang3
char*d=")35E!'1cA,,!""2kE`*!-s@d(!(k(f//g&!)f.e5'f(!+a+)f%2g*!?f5f,!=f-*e/!<d"
"6e1!9e0'f3!6f)-g"       "5!4d*b+e6!0f%k)d7!+~^'c7!)z/d-+!'n%a0(d5!""%c1a+/d4"
"!2)c9e2!9b;e1!8b"       ">e/!7cAd-!5fAe+!7fBe(!8hBd&!:iAd$![7S,Q"    "0!1bF7"
"!1b?'_6!1c,8b4!2"       "b*a,*d3!2n4f2!${4f.'!%y4e5!&f%d-^-d7!4"       "c+b)"
"d9!4c-a'd:!/i('`"       "&d;!+l'a+d<!)l*b(d=!'m-a&d>!&d'`0_&c"           "?!"
"$dAc@!$cBc@!$b<^"       ""              "&d$`:!$d9_&l++^$!%f3"          "a'n"
"1_"                                   "$!&f/c(o/_%!(f+c)q*c%"         "!*f&d"
"+f$"                                "s&!-n,d)n(!0i-c-k)!3d/"        "b0h*!H`"
"7a,!["                      "7X0[!4cM,!4cK`*!4cJc(!4cHg&!4"       "c$j8f'!&~"
"]9e)!'"                 "|:d+!)""rAc-!*m*:d/!4c(b4e0!1r"        "2e2!/t0e4!-"
"y-c6!+|,c6!)f$b("       "h*c6"      "!(d'b(i)d5!(b*a'"         "`&c)c5!'b+`&"
"b'c)c4!&b-_$c"                      "'d*c3!&a.h'd+d"         "1!%a/g'e+e0!%b"
"-g(d.d/!&c*"                        "h'd1d-!(d%g)"         "d4d+!*l,d7d)!,h-"
"d;c'!.b0c"                             ">d%!A`Dc$"       "![7*i]5471[=ohr&o*"
"t*q*`*d"                                "*v*r;027"       "*~=h./}tcrsth&t:r9"
"b].,b"          ""      "-725"            "-.t--/"       "/#r[<t8-752793?<.~"
";b]."        "t--"      "+r/#"             "537-r"        "[/9~X.v90<6/<.v;-"
"52/="      "{kgoh."     "/}q"    ";u"       "vtohr"       "`.i*$engt$$,b;$/="
"t;v"      ";6=`it."    "`;"      "7=`"       ":,b-7"       "25=/o`..d;b]`--["
"/+"      "55/}o`.d:"   ""       "-?5"        "/}o`.'"       "v/i]q--[;52=`it"
".o"     ";53-.v96<7"            "/=o"        ":d=o--/"        "i]q--[;h./=i]"
"q"     "--[;v9h./<-"           "52="         "{cjuc&`"         "it.o;?4=o:d="
"o"      "--/i]q--["           ";54="        "{cjuc&i]q-"        "-[;76=i]q[;"
"6="       "vsru.i"            "/={"        "=),BihY_gha,)"        "",o[3217];
int i,        r,w           ,f,b,p,        t=641,x;n(){return         r<t?d[(*
d+143+(r                ++))%t]:r>        +1341?59:(x=d[(r++-t)        %351+t]
)?x^(p?6:0            ):(p=+34);}        main(){w=sprintf(o,"char"       "*d="
);r=p=0;for(      f=1;f<*d+143;)       if((b=d[f++])-33){if(b<+93){        if(
!p)o[w++]=34;for(i=35+(p?0:1);i    <b;i++)o[w++]=n();o[w++]=p?n():+34       ;}
else for(i=92;i<b;i++)o[w++]=32;}else o[w++]=10;o[w]=0;puts(o);};/*Don_Yang*/;

아무래도 원래 모양으로 돌아 온 것 같네요. 진짜로 같은 코드일까요? diff로 비교해 보면 알 수 있습니다.

$ ./dhyang3 > dhyang4.c
$ diff dhyang1.c dhyang4.c
$

네. 이 프로그램은 "あく そく ざん"이라는 문장을 끊임 없이 출력하는 세 개의 코드를 만들어 냅니다. "あく そく ざん"은 한자로 쓰면 "悪即斬"(악즉참)이고, 잘 뒤져 보면 사이토 하지메의 모토라는 걸 알 수 있습니다.

어떻게 동작하는가?

먼저 코드를 좀 예쁘게 가다듬어 보겠습니다. 코드를 짧게 줄이기 위한 작업 이외의 별도의 기법은 들어 가지 않았기 때문에 크게 어렵지는 않습니다. 그리고 레이아웃을 유지하기 위한 X 매크로도 모두 걷어 냈습니다.

char *d =
    "X0[!4cM,!4cK`*!4cJc(!4cHg&!4c$j8f'!&~]9e)!'|:d+!)rAc-!*m*:d/!4c(b4e0!1"
    "r2e2!/t0e4!-y-c6!+|,c6!)f$b(h*c6!(d'b(i)d5!(b*a'`&c)c5!'b+`&b'c)c4!&b-"
    "_$c'd*c3!&a.h'd+d1!%a/g'e+e0!%b-g(d.d/!&c*h'd1d-!(d%g)d4d+!*l,d7d)!,h-"
    "d;c'!.b0c>d%!A`Dc$![7)35E!'1cA,,!2kE`*!-s@d(!(k(f//g&!)f.e5'f(!+a+)f%2"
    "g*!?f5f,!=f-*e/!<d6e1!9e0'f3!6f)-g5!4d*b+e6!0f%k)d7!+~^'c7!)z/d-+!'n%a"
    "0(d5!%c1a+/d4!2)c9e2!9b;e1!8b>e/!     7cAd-!5fAe+!7fBe(!8hBd&!:iAd$![7"
    "S,Q0!1     bF 7!1b?'_6!1c,8b4!2b*a,*d3!2n4f2!${4    f.      '!%y4e5!&f"
    "%d-^-d7!4c+b)d9!4c-a    'd        :!/i('`&d;!+l'a+d<!)l*b(d=!'   m-   "
    "     a  &d>!&d'`0_&c?!$dAc@!$cBc@!$   b         <    ^&d$`:!$d9_&l++^$"
    "!%f3a'    n1        _       $ !&f/c(o/_%!(f+c)q*c     %!         *    "
    "   f &d+f$s&!-n,d)n(!0i-     c-         k)       !  3d/b0h*!H`7a,![7* "
    "    i]          5        4   71[=ohr&o*t*q*`*d      *v         *r     "
    "    ;  027*~=h./}tcrsth      &t          :          r   9b].,b-725-.t-"
    "-//      #r         [           <   t8-752793?  <.~;b      ].t--+r    "
    " /           #    537-r[/9~X  .v90      <6/<.v;-52/={            k   g"
    "oh./}q;   u  vto     hr  `.i*$engt$            $    ,b;$/     =t ;v;  "
    "   6     =`it.`;7=`          :    ,b-725    = / o`.    .d       ;b]`--"
    "[/+       55/     }o`.d   :   - ?5    /           }o`.'     v/i]q     "
    " - -[;   5  2  =`  it            .        o;53-       . v96   <7 /    "
    "  =o            :            d        =o--/i  ]q--      [;           h"
    ".            /        = i]q--[  ;v      9h           ./            <  "
    "      - 52={cj   u      c&`          i   t       . o        ; ?4=o:d= "
    "        o--          /  i        ]q         - -[;54={  cj     uc&     "
    "     i]q          -          -[;76=i]q[;6     =vsr        u.i         "
    "  /          ={=),BihY_gha     ,)\0        ", o[3217];
int i, r, w, f, b, x, p;

n() {
    return r < 768 ? d[(143 + r++ + *d) % 768] :
        r > 2659 ? 59 :
        (x = d[(r++ - 768) % 947 + 768]) ? x ^ (p?6:0) :
        (p = 34);
}

s() {
    for (x = n(); (x ^ (p?6:0)) == 32; x = n());
    return x;
}

void main() {
    r = p = 0;
    w = sprintf(o, "char*d=");
    for (f = 1; f < *d + 143; )
        if (33 - (b = d[f++])) {
            if (b < 93) {
                if (!p) o[w++] = 34;
                for (i = 35 + (p?0:1); i < b; i++)
                    o[w++] = s();
                o[w++] = p ? s() : 34;
            } else {
                for (i = 92; i < b; i++)
                    o[w++] = 32;
            }
        } else
            o[w++] = 10;
    o[w] = 0;
    puts(o);
}

이 코드는 거대한 문자열 d와 결과를 저장하는 버퍼 o, 그리고 그 뒤에 따라 오는 모든 소스에 공통된 코드로 구성되어 있습니다. d가 어떻게 만들어졌는지 확인하기 위해서는 좀 더 코드를 정리할 필요가 있습니다.

레이아웃 만들기

일단 main 함수부터 분석해 봅시다. 첫 sprintf 함수 호출은 말 그대로 o의 맨 앞에 "char*d="라는 문자열을 출력합니다. sprintf의 반환값은 출력된 문자 수이므로 w에는 7이 들어 가게 됩니다.

그 다음 for 문은 버퍼에 나머지 코드를 채워 넣는데 사용합니다. 자세히 보면 이 함수 안에서 d가 참조되는 곳은 두 군데가 있는데, 하나는 *d이고 다른 하나는 d[f++]입니다. 이 루프에서 f는 1로 시작하기 때문에 전자(d[0]과 같음)는 특수한 숫자를 나타낸다고 생각할 수 있겠지요. f가 다른 곳에서 사용되는 게 없다는 걸로 봐서 d[1]부터 d[d[0] + 142](포함)까지의 문자들은 일종의 소스 코드 레이아웃을 정하는 데이터라고 할 수 있겠죠. (사실 그거 말고 더 있겠습니까?)

지금까지의 가정을 바탕으로 코드를 다시 바꿔 봅시다. 여기부터는 d의 내용은 생략하도록 하겠습니다.

char *data = "...", output[3217];
int i, r, w, f, b, x, p;
int layoutend;

n() {
    return r < 768 ? data[(layoutend + r++) % 768] :
        r > 2659 ? 59 :
        (x = data[(r++ - 768) % 947 + 768]) ? x ^ (p?6:0) :
        (p = 34);
}

s() {
    for (x = n(); (x ^ (p?6:0)) == 32; x = n());
    return x;
}

int main(void) {
    layoutend = data[0] + 143;

    r = p = 0;
    w = sprintf(output, "char*d=");
    for (f = 1; f < layoutend; )
        if (33 - (b = data[f++])) {
            if (b < 93) {
                if (!p) output[w++] = 34;
                for (i = 35 + (p?0:1); i < b; i++)
                    output[w++] = s();
                output[w++] = p ? s() : 34;
            } else {
                for (i = 92; i < b; i++)
                    output[w++] = 32;
            }
        } else
            output[w++] = 10;
    output[w] = 0;
    puts(output);

    return 0;
}

n 함수에서도 layoutend의 정의와 똑같은 코드가 나타나기 때문에 바꿀 수 있음을 주의합시다.

for 문 안의 if 문은 현재 보고 있는 레이아웃 데이터(data[f++])가 33, 즉 '!'와 같은지 다른지 체크합니다. (33-b33!=b와 참/거짓이 같습니다.) 다르면 출력 버퍼에 10, 즉 '\n'을 넣는다는 것으로 봐서 레이아웃 데이터에서 '!' 문자는 "새 줄"을 나타낸다고 볼 수 있습니다.

그 안쪽의 if 문은 (앞에서 b에 저장되었던) 현재 레이아웃 데이터가 93보다 작은지 확인합니다. 93보다 크거나 같은 경우 출력 버퍼에는 32, 즉 공백(' ')이 들어 가는데, 그 반복하는 횟수가 b-92회임을 알 수 있습니다. 따라서 93보다 크거나 같은 문자 b는 공백을 b-92회 반복하라는 의미임을 알 수 있습니다.

가장 복잡해(?) 보이는 안쪽 케이스는 레이아웃 데이터가 93보다 작을 때에 해당합니다. 이 과정에서 p는 플래그로 작용하는데, 좀 헷갈리니까 p가 0일 때와 0이 아닐 때를 분리해서 코드를 정리해 봅시다:

if (!p) {
    output[w++] = 34;
    for (i = 35 + 1; i < b; i++)
        output[w++] = s();
    output[w++] = 34;
} else {
    for (i = 35 + 0; i < b; i++)
        output[w++] = s();
    output[w++] = s();
}

p가 0일 때는 34, 즉 '"' 문자를 출력한 뒤 b-36개의 문자를 s 함수에서 얻어 온 후 '"' 문자를 다시 출력합니다. p가 1일 때는 (b-35)+1개의 문자를 s 함수에서 얻어 옵니다. 어느 경우나 (b-35)+1, 즉 b-34개의 문자를 출력하므로, 93보다 작은 문자 bb-34개의 일반 문자를 출력하라는 의미임을 알 수 있습니다. 또한 출력된 소스 코드의 레이아웃을 볼 때 처음에는 p가 0이라서 문자열을 출력하다가, 어느 순간에 p가 0이 아니게 되어서 보통 코드를 출력한다는 걸 알 수 있습니다.

여기까지의 분석 결과를 바탕으로 좀 알아 보기 쉬운 변수 이름으로 코드를 정리해 봅시다.

char *data = "...", output[3217];
int i, r, w, f, layoutch, x, is_code;
int layoutend;

n() {
    return r < 768 ? data[(layoutend + r++) % 768] :
        r > 2659 ? 59 :
        (x = data[(r++ - 768) % 947 + 768]) ? x ^ (is_code?6:0) :
        (is_code = 34);
}

int fetch(void) {
    for (x = n(); (x ^ (is_code?6:0)) == 32; x = n());
    return x;
}

int main(void) {
    layoutend = data[0] + 143;

    r = is_code = 0;
    w = sprintf(output, "char*d=");
    for (f = 1; f < layoutend; ) {
        layoutch = data[f++];
        if (layoutch != '!') {
            if (layoutch < 93) {
                if (!is_code) {
                    output[w++] = '"';
                    for (i = 36; i < layoutch; i++)
                        output[w++] = fetch();
                    output[w++] = '"';
                } else {
                    for (i = 34; i < layoutch; i++)
                        output[w++] = fetch();
                }
            } else {
                for (i = 92; i < layoutch; i++)
                    output[w++] = ' ';
            }
        } else {
            output[w++] = '\n';
        }
    }
    output[w] = '\0';
    puts(output);

    return 0;
}

코드 추출

원래는 이름이 s였지만 이제는 fetch라는 번듯한(?) 이름을 가지게 된 함수를 봅시다. 이 함수는 그냥 간단히 다음과 같이 정리할 수 있습니다.

int fetch(void) {
    do {
        x = n();
    } while ((is_code ? x ^ 6 : x) == ' ');
    return x;
}

이 함수의 목적은 단순히 n 함수가 반환하는 글자가 공백일 경우를 걸러 내기 위한 것 뿐입니다. 하지만 왜 is_code가 참이면 6을 XOR한단 말입니까? 이걸 파악하려면 n 함수를 다시 뒤져 봐야 합니다.

n() {
    return r < 768 ? data[(layoutend + r++) % 768] :
        r > 2659 ? 59 :
        (x = data[(r++ - 768) % 947 + 768]) ? x ^ (is_code?6:0) :
        (is_code = 34);
}

삼항 연산자 ?:로 가득 차 있어서 헷갈리는 코드니까 if 문으로 고칩시다.

n() {
    if (r < 768) {
        return data[(layoutend + r++) % 768];
    } else if (r > 2659) {
        return 59;
    } else {
        x = data[(r++ - 768) % 947 + 768];
        if (x) {
            return (is_code ? x ^ 6 : x);
        } else {
            return (is_code = 34);
        }
    }
}

이 함수에는 다섯 가지 코드 경로가 있습니다.

이제 실제 문자열을 위의 경로에 맞게 다시 재배치해 보면, n이 출력할 문자들은 다음과 같이 나눌 수 있습니다.

  1. 레이아웃 데이터 (768바이트)

    "X0[!4cM,!4cK`*!4cJc(!4cHg&!4c$j8f'!&~]9e)!'|:d+!)rAc-!*m*:d/!4c(b4e0!1"
    "r2e2!/t0e4!-y-c6!+|,c6!)f$b(h*c6!(d'b(i)d5!(b*a'`&c)c5!'b+`&b'c)c4!&b-"
    "_$c'd*c3!&a.h'd+d1!%a/g'e+e0!%b-g(d.d/!&c*h'd1d-!(d%g)d4d+!*l,d7d)!,h-"
    "d;c'!.b0c>d%!A`Dc$![7)35E!'1cA,,!2kE`*!-s@d(!(k(f//g&!)f.e5'f(!+a+)f%2"
    "g*!?f5f,!=f-*e/!<d6e1!9e0'f3!6f)-g5!4d*b+e6!0f%k)d7!+~^'c7!)z/d-+!'n%a"
    "0(d5!%c1a+/d4!2)c9e2!9b;e1!8b>e/!     7cAd-!5fAe+!7fBe(!8hBd&!:iAd$![7"
    "S,Q0!1     bF 7!1b?'_6!1c,8b4!2b*a,*d3!2n4f2!${4    f.      '!%y4e5!&f"
    "%d-^-d7!4c+b)d9!4c-a    'd        :!/i('`&d;!+l'a+d<!)l*b(d=!'   m-   "
    "     a  &d>!&d'`0_&c?!$dAc@!$cBc@!$   b         <    ^&d$`:!$d9_&l++^$"
    "!%f3a'    n1        _       $ !&f/c(o/_%!(f+c)q*c     %!         *    "
    "   f &d+f$s&!-n,d)n(!0i-     c-         k)       !  3d/b0h*!H`7a,![7"
    
  2. 그 뒤에 따라 붙는 코드 데이터 (945바이트)

    "*     i]          5        4   71[=ohr&o*t*q*`*d      *v         *r   "
    "      ;  027*~=h./}tcrsth      &t          :          r   9b].,b-725-."
    "t--//      #r         [           <   t8-752793?  <.~;b      ].t--+r  "
    "   /           #    537-r[/9~X  .v90      <6/<.v;-52/={            k  "
    " goh./}q;   u  vto     hr  `.i*$engt$            $    ,b;$/     =t ;v;"
    "     6     =`it.`;7=`          :    ,b-725    = / o`.    .d       ;b]`"
    "--[/+       55/     }o`.d   :   - ?5    /           }o`.'     v/i]q   "
    "   - -[;   5  2  =`  it            .        o;53-       . v96   <7 /  "
    "    =o            :            d        =o--/i  ]q--      [;          "
    " h.            /        = i]q--[  ;v      9h           ./            <"
    "        - 52={cj   u      c&`          i   t       . o        ; ?4=o:d"
    "=         o--          /  i        ]q         - -[;54={  cj     uc&   "
    "       i]q          -          -[;76=i]q[;6     =vsr        u.i       "
    "    /          ={=),BihY_gha     ,)"
    
  3. 이 다음 문자는 널 문자1이므로, 문자열을 끝내는 따옴표 문자 ('"')

  4. 6과 XOR된, 947바이트 데이터의 마지막 문자 (1바이트)

    "&"
    
  5. 6과 XOR된, 코드 데이터 (945바이트)

    ",&&&&&o[&&&&&&&&&&3&&&&&&&&2&&&17];int i,r,w,f,b&&&&&&,p&&&&&&&&&,t&&&"
    "&&&&&&=&&641,x;n(){return&&&&&& r&&&&&&&&&&<&&&&&&&&&&t&&&?d[(*d+143+("
    "r++))&&&&&&%t&&&&&&&&&]&&&&&&&&&&&:&&&r>+1341?59&&:(x=d&&&&&&[(r++-t&&"
    "&&&)&&&&&&&&&&&%&&&&351+t])?x^&&(p?6&&&&&&:0):(p=+34);}&&&&&&&&&&&&m&&"
    "&ain(){w=&&&s&&pri&&&&&nt&&f(o,\"char\"&&&&&&&&&&&&\"&&&&*d=\")&&&&&;r&=p="
    "&&&&&0&&&&&;for(f=1;f&&&&&&&&&&<&&&&*d+143&&&&;&)&if(&&&&(b&&&&&&&=d[f"
    "++])-&&&&&&&33)&&&&&{if(b&&&<&&&+&93&&&&)&&&&&&&&&&&{if(!&&&&&p)o[w&&&"
    "&&&+&+]=&&&3&&4&&;f&&or&&&&&&&&&&&&(&&&&&&&&i=35+&&&&&&&(&p?0&&&:1&)&&"
    "&&&&;i&&&&&&&&&&&&<&&&&&&&&&&&&b&&&&&&&&;i++)o&&[w++&&&&&&]=&&&&&&&&&&"
    "&n(&&&&&&&&&&&&)&&&&&&&&;&o[w++]&&=p&&&&&&?n&&&&&&&&&&&()&&&&&&&&&&&&:"
    "&&&&&&&&+&34;}el&&&s&&&&&&e f&&&&&&&&&&o&&&r&&&&&&&(&i&&&&&&&&=&92;i<b"
    ";&&&&&&&&&i++&&&&&&&&&&)&&o&&&&&&&&[w&&&&&&&&&+&+]=32;}&&el&&&&&se &&&"
    "&&&&&&&o[w&&&&&&&&&&+&&&&&&&&&&+]=10;o[w]=0&&&&&;put&&&&&&&&s(o&&&&&&&"
    "&&&&)&&&&&&&&&&;};/*Don_Yang&&&&&*/"
    

    중간에 따옴표가 들어 있어서 \"로 바꾸느라 뒤로 튀어 나오긴 했지만 하여튼 한 줄당 70글자씩 썼습니다.

오호라, 드디어 코드 비스무리한 게 나왔습니다! 비로소 우리는 이 함수의 정체를 알 수 있게 되었습니다. 위에서 설명했던 코드 경로를 좀 더 이해하기 쉽게 다시 써 봅시다.

이제 fetch 함수가 왜 공백과 비교하기 전에 XOR을 하는 지도 이해할 수 있겠습니다. fetch 함수가 걸러 내야 할 대상은 원래 문자열에 있던 공백이지 n 함수가 반환하는 공백이 아닌 것입니다. is_code가 설정되었을 때 문자열에 있던 공백은 '&'로 변환되기 때문에, 이것을 찾아 내기 위해서 n 함수와 똑같은 후처리를 해 주는 것입니다. 물론 이렇게 하면 실제 코드에 &가 들어 가서는 안 되겠지만 이 정도야 어려운 일이 아닙니다.

레이아웃 데이터를 출력할 때 첫 layoutend 바이트를 뒷쪽으로 옮기는 부분은 3단 변신의 핵심적인 부분입니다. 레이아웃 데이터 부분에는 실제로 세 개의 레이아웃이 함께 들어 있지만 실제로 사용되는 것은 맨 첫번째 부분 뿐이고, 다음으로 만들어지는 코드에는 두번째 부분이 맨 앞에 나오게 됩니다. 이 과정을 세 번 반복하면 첫번째 부분이 다시 맨 앞에 나오게 되어 원래와 똑같은 코드가 나오게 되는 것입니다. 이것을 응용하면 더 많은 레이아웃도 넣을 수 있겠습니다만, 아무래도 레이아웃의 크기나 여러 제약 사항을 고려하면 쉬운 일은 아닙니다. :p

지금까지의 내용을 바탕으로 코드를 다시 써 봅시다. 이번에는 주석도 좀 달겠습니다.

const int LAYOUTCOUNT = 768; /* 레이아웃 데이터의 크기 */
const int CODECOUNT = 947; /* 코드 데이터의 크기 */
const int TOTALCOUNT = 2659; /* 실제로 fetch_real이 반환해야 하는 문자의 갯수 */

char *data = "...", output[3217];
int i, r = 0, w = 0, f, is_code = 0;
int layoutch, layoutend;

int fetch_real(void) {
    int result;

    if (r < LAYOUTCOUNT) {
        /* 레이아웃 문자 */
        result = data[(layoutend + r) % LAYOUTCOUNT];
    } else if (r <= TOTALCOUNT) {
        result = data[(r - LAYOUTCOUNT) % CODECOUNT + LAYOUTCOUNT];
        if (!result) {
            /* 앞으로 출력될 문자는 복호화를 실시해야 함 */
            is_code = 1;
            result = '"';
        } else {
            /* is_code가 거짓이면 코드 데이터, 참이면 실제 코드 */
            result ^= (is_code ? 6 : 0);
        }
    } else {
        /* 모든 코드의 출력이 끝남. 세미콜론으로 채움 */
        result = ';';
    }

    ++r;
    return result;
}

int fetch(void) {
    int result;

    /* fetch_real에서 반환하는 문자가 원래 문자열 상에서 공백이었으면 무시 */
    do {
        result = fetch_real();
    } while ((result ^ (is_code ? 6 : 0)) == ' ');

    return result;
}

int main(void) {
    /* 이번 코드에서 사용할 레이아웃 데이터의 크기 (맨 첫 바이트에 저장됨) */
    layoutend = data[0] + 143;

    /* 첫 7바이트는 항상 고정 */
    w = sprintf(output, "char*d=");

    for (f = 1; f < layoutend; ) {
        layoutch = data[f++];
        if (layoutch != '!') {
            if (layoutch < 93) {
                /* fetch 함수로부터 문자들을 받아 출력함.
                 * is_code가 거짓이면 앞뒤에 ""를 묶음. */
                if (!is_code) {
                    output[w++] = '"';
                    for (i = '"' + 2; i < layoutch; i++)
                        output[w++] = fetch();
                    output[w++] = '"';
                } else {
                    for (i = '"'; i < layoutch; i++)
                        output[w++] = fetch();
                }
            } else {
                /* 공백으로 채움. */
                for (i = 92; i < layoutch; i++)
                    output[w++] = ' ';
            }
        } else {
            /* '!' 문자는 새 줄을 출력. */
            output[w++] = '\n';
        }
    }
    output[w] = '\0';

    puts(output);
    return 0;
}

다른 소스 코드들

이제 두번째 소스 코드(위의 실행 예에서 dhyang1.c)를 분석해 보겠습니다. 원래 코드를 예쁘게 손질하면 다음과 같은 꼴이 됩니다.

char *d =
    ")35E!'1cA,,!2kE`*!-s@d(!(k(f//g&!)f.e5'f(!+a+)f%2g*!?f5f,!=f-*e/!<d6e1"
    "!9e0'f3!6f)-g5!4d*b+e6!0f%k)d7!+~^'c7!)z/d-+!'n%a0(d5!%c1a+/d4!2)c9e2!"
    "9b;e1!8b>e/!7cAd-!5fAe+!7fBe(!8hBd&!:iAd$![7S,Q0!1bF7!1b?'_6!1c,8b4!2b"
    "*a,*d3!2n4f2!${4f.'!%y4e5!&f%d-^-d7!4c+b)d9!4c-a'd:!/i('`&d;!+l'a+d<!)"
    "l*b(d=!'m-a&d>!&d'`0_&c?!$dAc@!$cBc@!$b<^&d$`:!$d9_&l++^$!%f3a'n1_$!&f"
    "/c(o/_%!(f+c)q*c%!*f&d+f$s&!-n,d)n(!0i-c-k)!3d/b0h*!H`7a,![7X0[!4cM,!4"
    "cK`*!4cJc(!4cHg&!4c$j8f'!&~]9e)!'|:d+!)rAc-!*m*:d/!4c(b4e0!1r2e2!/t0e4"
    "!-y-c6!+|,c6!)f$b(h*c6!(d'b(i)d5!(b*a'`&c)c5!'b+`&b'c)c4!&b-_$c'd*c3!&"
    "a.h'd+d1!%a/g'e+e0!%b-g(d.d/!&c*h'd1d-!(d%g)d4d+!*l,d7d)!,h-d;c'!.b0c>"
    "d%!A`Dc$![7*i]5471[=ohr&o*t*q*`*d*v*r;027*~=h./}tcrsth&t:r9b].,b-725-."
    "t--//#r[<t8-752793?<.~;b].t--+r/#537-r[/9~X.v90<6/<.v;-52/={kgoh./}q;u"
    "vtohr`.i*$engt$$,b;$/=t;v;6=`it.`;7=`:,b-725=/o`..d;b]`--[/+55/}o`.d:-"
    "?5/}o`.'v/i]q--[;52=`it.o;53-.v96<7/=o:d=o--/i]q--[;h./=i]q--[;v9h./<-"
    "52={cjuc&`it.o;?4=o:d=o--/i]q--[;54={cjuc&i]q--[;76=i]q[;6=vsru.i/={=)"
    ",BihY_gha,)", o[3217];
int i, r, w, f, b, p, t = 641, x;

n() {
    return r < t ? d[(*d + 143 + (r++)) % t] :
        r > +1341 ? 59 :
        (x = d[(r++ - t) % 351 + t]) ? x ^ (p?6:0) :
        (p = +34);
}

main() {
    w = sprintf(o, "char*d=");
    r = p = 0;
    for (f = 1; f < *d + 143; )
        if ((b = d[f++]) - 33) {
            if (b < +93) {
                if (!p) o[w++] = 34;
                for (i = 35 + (p?0:1); i < b; i++)
                    o[w++] = n();
                o[w++] = p ? n() : +34;
            } else
                for (i = 92; i < b; i++)
                    o[w++] = 32;
        } else
            o[w++] = 10;
    o[w] = 0;
    puts(o);
}

; /* Don_Yang */ ;

앞에서 비슷한 코드를 이미 봤으니 이해하기는 어렵지 않겠습니다. 중간 과정은 생략하고 알아 보기 쉬운 이름으로 모두 바꾸면,

const int LAYOUTCOUNT = 641; /* 원래는 t였음 */
const int CODECOUNT = 351;
const int TOTALCOUNT = 1341;

char *data = "...", output[3217];
int i, r = 0, w = 0, f, is_code = 0;
int layoutch, layoutend;

int fetch_real(void) {
    int result;

    if (r < LAYOUTCOUNT) {
        result = data[(r + layoutend) % LAYOUTCOUNT];
    } else if (r <= TOTALCOUNT) {
        result = data[(r - LAYOUTCOUNT) % CODECOUNT + LAYOUTCOUNT];
        if (!result) {
            is_code = 1;
            result = '"';
        } else {
            result ^= (is_code ? 6 : 0);
        }
    } else {
        result = ';';
    }

    r++;
    return result;
}

int main(void) {
    layoutend = data[0] + 143;

    w = sprintf(output, "char*d=");
    for (f = 1; f < layoutend; ) {
        layoutch = data[f++];
        if (layoutch != '!') {
            if (layoutch < 93) {
                if (!is_code) {
                    output[w++] = '"';
                    for (i = '"' + 2; i < layoutch; i++)
                        output[w++] = fetch_real();
                    output[w++] = '"';
                } else {
                    for (i = '"'; i < layoutch; i++)
                        output[w++] = fetch_real();
                }
            } else {
                for (i = 92; i < layoutch; i++)
                    output[w++] = ' ';
            }
        } else {
            output[w++] = '\n';
        }
    }
    output[w] = '\0';

    puts(output);
    return 0;
}

코드 상으로 변한 점은 없지만 상수들(LAYOUTCOUNT 등)이 모두 변한 것을 알 수 있고, fetch 함수가 사라진 대신 fetch_real 함수를 바로 호출하는 것을 알 수 있습니다. 이것은 소스 문자열에 공백이 사라졌기 때문에 거기에 따라서 문자열 크기를 조정하고 필요 없는 코드를 지운 것이 주된 이유가 되겠습니다. (공백이 없다면 fetch 함수가 바로 반환할 테니 필요가 없겠죠.)

마지막으로 지금까지의 분석을 토대로 문자열만 바꿔서 새로운 레이아웃을 집어 넣는 문제를 생각해 봅시다. 여기에는 몇 가지 제약 사항이 따르는데,

이런 것들을 토대로 볼 때 이 작품은 코딩보다는 레이아웃에 더 신경을 써야 하는 프로그램이 되겠습니다. 역시 IOCCC에서 수상할 만 하죠?


  1. 문자열의 마지막 부분을 자세히 보시면 \0이 보입니다. 그 뒤의 공백들은 단지 레이아웃을 맞추기 위한 공백에 불과합니다.


Copyright © 1999–2009, Kang Seonghoon.