메아리

Tour de IOCCC: 1984/decot

#define x =
#define double(a,b) int
#define char k['a']
#define union static struct

extern int floor;
double (x1, y1) b,
char x {sizeof(
    double(%s,%D)(*)())
,};
struct tag{int x0,*xO;}

*main(i, dup, signal) {
{
  for(signal=0;*k *x * __FILE__ *i;) do {
   (printf(&*"'\",x);   /*\n\\", (*((double(tag,u)(*)())&floor))(i)));
    goto _0;

_O: while (!(char <<x - dup)) { /*/*\*/
    union tag u x{4};
  }
}


while(b x 3, i); {
char x b,i;
  _0:if(b&&k+
  sin(signal)       / *    ((main) (b)-> xO));/*}
  ;
}

*/}}}

첫 해 출품작인데 뭐 이렇게 복잡합니까? 게다가 저기 눈에 걸리는 이름들은 또 뭘까요?

뭘 하는 프로그램인가?

컴파일하고 실행하려고 하면 이런 결과를 얻습니다. (당시 코드는 전처리기의 기괴한 면을 많이 응용하기 때문에 -traditional-cpp가 보통 필요합니다.) 리눅스에서는 -lm 옵션이 더 필요합니다.

$ gcc -traditional-cpp decot.c -o decot
decot.c: In function ‘main’:
decot.c:28: error: invalid operands to binary + (have ‘int *’ and ‘double’)

이 에러를 잘 살펴 보면 포인터 연산에 정수가 아니라 실수를 넣으려 했기 때문에 문제가 생겼다는 걸 알 수 있습니다. 이런. 옛날 코드라고 했지만 이건 좀 심하군요.

이 프로그램을 컴파일되게 하려면 소스 코드에서 다음 줄을:

  sin(signal)       / *    ((main) (b)-> xO));/*}

다음과 같이 고쳐야 합니다:

  (int)(sin(signal)     / *    ((main) (b)-> xO)));/*}

이렇게 고치고 컴파일을 해 보면 다음과 같은 결과를 볼 수 있습니다.

$ gcc -traditional-cpp decot.c -o decot
$ ./decot
'",x);  /*
\

...이게 무슨 뜻이죠? 이해하기 힘드니 그냥 스포일러를 봅시다.

1984 decot      prints garbage, weird cpp defines (keywords for others)

아하. 그러니까 이 프로그램은 원래 전혀 의미가 없는 내용을 출력하는 게 맞군요. 뭐 첫 대회니까 그러려니 합시다.

어떻게 동작하는가?

코드를 분석하기에 앞서서 전처리기를 한 번 통과시키도록 하겠습니다.

이 코드에서 좀 흥미로운 점은 매크로의 이름에 double과 같은 C 예약어를 썼다는 점인데요, 사실 이런다고 전처리기가 알아 듣는 건 아니기 때문에 적법한 코드가 됩니다. 또한 double(a,b)와 같이 선언된 매크로 함수는 double (x1, y1) 같이 중간에 공백을 넣어도 동작한다는 정도는 기억해 두시고요. 따라서,

extern int floor;
int b,
k['a'] = {sizeof(
    int(*)())
,};
struct tag{int x0,*xO;}

*main(i, dup, signal) {
{
  for(signal=0;*k *= * __FILE__ *i;) do {
   (printf(&*"'\",x);   /*\n\\", (*((int(*)())&floor))(i)));
    goto _0;

_O: while (!(k['a'] <<= - dup)) {   /*/*\*/
    static struct tag u ={4};
  }
}


while(b = 3, i); {
k['a'] = b,i;
  _0:if(b&&k+
  sin(signal)       / *    ((main) (b)-> xO));/*}
  ;
}

*/}}}

(참고로 중간에 *x와 같이 되어 있는 것은 *=로 바뀌었습니다. 현대적인 전처리기는 이를 * =의 두 토큰으로 분리하는데, 여기에 대해서는 1985/sicherman을 참고하시길 바랍니다.)

저자가 열심히 사람들을 낚으려고 노력했기 때문에 코드를 해석하는데 좀 곤란할 수도 있을 것 같습니다. 이런 분들을 위하여 코드를 예쁘게 다시 포매팅해 보면,

extern int floor;

int b, k['a'] = {sizeof(int(*)()),};

struct tag {int x0,*xO;} *main(i, dup, signal) {
    {
        for (signal = 0; *k *= *__FILE__ * i; )
            do {
                (printf(
                    &*"'\",x);  /*\n\\",
                    (*((int(*)())&floor))(i)
                ));
                goto _0;

            _O:
                while (!(k['a'] <<= -dup)) {    /*/*\*/
                    static struct tag u = {4};
                }
            } while (b = 3, i);

        {
            k['a'] = b, i;
        _0:
            if (b && k + sin(signal) / *((main) (b)->xO));

        /*}
        ;
        }

        */
        }
    }
}

조금 마음이 안정되는 것 같지 않습니까?

중간에 끼어 들어 있는 두 개의 주석에도 주의합시다. 이 두 주석은 저자가 (한 번 더) 사람들을 낚기 위해서 준비한 것으로, 당연히 우리는 무시해야 합니다. 주석을 지우면,

extern int floor;

int b, k['a'] = {sizeof(int(*)()),};

struct tag {int x0,*xO;} *main(i, dup, signal) {
    {
        for (signal = 0; *k *= *__FILE__ * i; )
            do {
                (printf(
                    &*"'\",x);  /*\n\\",
                    (*((int(*)())&floor))(i)
                ));
                goto _0;

            _O:
                while (!(k['a'] <<= -dup)) {
                    static struct tag u = {4};
                }
            } while (b = 3, i);

        {
            k['a'] = b, i;
        _0:
            if (b && k + sin(signal) / *((main) (b)->xO));
        }
    }
}

이제 중간에 있는 goto _0;을 유심히 봅시다. 이 역시 낚시로, 위에 있는 _O(알파벳 대문자 O)가 아니라 아래에 있는 _0(숫자 0)으로 가는 코드가 되겠습니다.

중간에 있는 실행되지 않는 코드를 지울 때 주의할 점은, 첫번째 _O는 for와 do~while 루프 안에 있지만 두번째 _0은 루프 바깥에, 아무 것도 없는 중괄호로 묶인 블럭에 있다는 것입니다. 따라서 무작정 goto까지 지울 수는 없고, 일단은 실행되지 않는 게 확실한 코드만 지우도록 하겠습니다.

extern int floor;

int b, k['a'] = {sizeof(int(*)()),};

struct tag {int x0,*xO;} *main(i, dup, signal) {
    {
        for (signal = 0; *k *= *__FILE__ * i; )
            do {
                (printf(
                    &*"'\",x);  /*\n\\",
                    (*((int(*)())&floor))(i)
                ));
                goto _0;
            } while (b = 3, i);

        {
        _0:
            if (b && k + sin(signal) / *((main) (b)->xO));
        }
    }
}

do~while 루프는 조건식을 평가하기 전에 goto를 만나기 때문에 정확히 한 번만 실행됩니다. 따라서 do~while 루프는 걷어 버려도 괜찮습니다.

extern int floor;

int b, k['a'] = {sizeof(int(*)()),};

struct tag {int x0,*xO;} *main(i, dup, signal) {
    {
        for (signal = 0; *k *= *__FILE__ * i; ) {
            (printf(
                &*"'\",x);  /*\n\\",
                (*((int(*)())&floor))(i)
            ));
            goto _0;
        }

    _0:
        if (b && k + sin(signal) / *((main) (b)->xO));
    }
}

이제 b 변수를 봅시다. 이 변수는 다른 어느 곳에서도 설정되지 않는데, 전역 변수로 되어 있으므로 항상 0이 되어야 할 것입니다. 그럼 아래 if 문은 뭘까요?

if (b && k + sin(signal) / *((main) (b)->xO));

일단 if 문의 본체가 빈 문장(;)인 건 그렇다고 쳐도 b가 0이므로 나머지 식은 평가할 필요도 없이 조건식은 0이 됩니다. 논리 연산자 &&||는 이런 식으로 평가를 안 해도 결과가 뻔할 경우 더 이상의 평가를 멈추고 바로 결과를 반환하는 특징이 있는데, 이를 단축(short-circuit) 연산이라 합니다. 따라서 이 if 문은 아무 일도 하지 않습니다.

if 문을 날리고 사용되지 않는 b를 없애면 다음과 같이 됩니다.

extern int floor;

int k['a'] = {sizeof(int(*)()),};

struct tag {int x0,*xO;} *main(i, dup, signal) {
    {
        for (signal = 0; *k *= *__FILE__ * i; ) {
            (printf(
                &*"'\",x);  /*\n\\",
                (*((int(*)())&floor))(i)
            ));
            goto _0;
        }

    _0: ; /* 표준 C에서는 레이블 뒤에 빈 문장이라도 있어야 함. */
    }
}

이제 for 문의 조건문을 봅시다. *k *= *__FILE__ * i;라고 되어 있습니다. 이걸 말로 풀어 쓰면 대강 이렇게 되겠군요.

k의 첫번째 원소(*k == k[0])에, __FILE__의 첫 원소와 i를 곱한 값을 곱한다.

여기서 __FILE__은 지금 처리하고 있는 파일의 이름을 나타내는 문자열로 치환되는 특수한 이름입니다. 이 경우 "decot.c" 같은 문자열이 되겠습니다만, 뭐 아무 거라도 상관은 없겠죠.

한편 i는 원래 main 함수의 인자로 들어 왔던 건데, 이 값은 무엇일까요? 한 번 다음과 같이 main만 남기고 코드를 컴파일해 봅시다.

$ cat decot2.c
struct tag {int x0,*xO;} *main(i, dup, signal) {
    printf("%#x %#x %#x\n", i, dup, signal);
}
$ gcc decot2.c -o decot2
$ ./decot2
0x1 0xbf92c674 0xbf92c67c
$ ./decot2
0x1 0xbf8c2e04 0xbf8c2e0c
$ ./decot2
0x1 0xbfa0e754 0xbfa0e75c
$ ./decot2 foo
0x2 0xbf95c694 0xbf95c6a0
$ ./decot2 foo bar
0x3 0xbfdddb04 0xbfdddb14

흥미롭게도 첫번째 숫자는 argc에 해당하는 것 같고, 나머지 두 개는 포인터 같아 보입니다. 또한 리턴값으로 들어 가는 무지막지한 포인터(struct tag {int x0,*xO;} *)는 동작에 별 영향을 미치지 않는다는 것도 알 수 있습니다.1

어차피 dup은 평가되지 않은 채 삭제되었으므로, main 함수의 인자를 적절히 정리하도록 하겠습니다.

extern int floor;

int k['a'] = {sizeof(int(*)()),};

int main(int argc /* 원래 i였음 */, char **argv) {
    int signal;

    {
        for (signal = 0; *k *= *__FILE__ * argc; ) {
            (printf(
                &*"'\",x);  /*\n\\",
                (*((int(*)())&floor))(argc)
            ));
            goto _0;
        }

    _0: ;
    }

    return 0;
}

본래 *k, 즉 k[0]에는 sizeof(int(*)())가 들어 있었다는 걸 생각합시다. 포인터의 크기니 최소한 0은 아니겠죠. __FILE__의 첫 문자도 널문자가 아닐테니 0은 아닐 거고요. argc는 항상 1보다 크거나 같으므로 *k *= *__FILE__ * argc라는 수식의 결과는 0이 될 수가 없습니다. (물론 argc를 아주 크게 잡으면 가능할 진 모르겠지만, 그걸 허용하는 운영체제가 있을지 의문입니다.) 따라서,

extern int floor;

int k['a'] = {sizeof(int(*)()),};

int main(int argc, char **argv) {
    int signal;

    {
        for (signal = 0; ; ) {
            (printf(
                &*"'\",x);  /*\n\\",
                (*((int(*)())&floor))(argc)
            ));
            goto _0;
        }

    _0: ;
    }

    return 0;
}

로 정리할 수 있습니다. 이제 for 루프 안의 문장은 항상 한 번 실행되고 루프 바깥으로 빠져 나오므로, 루프를 그냥 덜어 버릴 수 있겠네요. 사용하지 않는 k 배열도 없애겠습니다.

extern int floor;

int main(int argc, char **argv) {
    int signal;

    signal = 0;
    (printf(
        &*"'\",x);  /*\n\\",
        (*((int(*)())&floor))(argc)
    ));

    return 0;
}

signal은 더 이상 사용되지 않으므로 지우고, printf 호출 주변의 괄호를 지우면,

extern int floor;

int main(int argc, char **argv) {
    printf(
        &*"'\",x);  /*\n\\",
        (*((int(*)())&floor))(argc)
    );

    return 0;
}

printf의 첫번째 인자는 &*"..." 형태로 되어 있습니다. 그런데 *"...""..."[0]과 같으니, &"..."[0]이라는 뜻이네요? "..." 자체가 이미 문자열의 첫 문자에 대한 포인터므로 앞의 &*는 그냥 아무 의미도 없습니다.

extern int floor;

int main(int argc, char **argv) {
    printf(
        "'\",x);    /*\n\\",
        (*((int(*)())&floor))(argc)
    );

    return 0;
}

그 다음 인자는 (*((int(*)())&floor))(argc)입니다. 포인터 연산과 캐스팅이 얽혀 있어서 보기 힘드니 하나 하나 분해하면,

따라서 floor라는 이름은 원래 int가 아니라 함수였다는 얘기가 됩니다. 실제로 floor 함수는 다음과 같이 선언된, 표준 라이브러리 함수입니다.

double floor(double x);

...당연하지만 함수 타입은 전혀 맞지 않습니다. (argcint였죠!) 따라서 이런 식으로 함수를 호출하는 것은 매우 위험합니다. 하지만 다행인지 불행인지 잘 실행되는군요. 그나저나 printf의 첫 인자에 들어 있는 문자열에는 % 문자가 없으므로, 어차피 이 값은 계산되어도 사용되지 않습니다. 따라서 그냥 이 인자를 날려 버릴 수 있습니다:

int main(int argc, char **argv) {
    printf("'\",x); /*\n\\");
    return 0;
}

이거 하나 출력하려고 이런 코드를 쓴다니 기괴하긴 하지만 그래도 다른 코드보다는 덜 복잡했네요.


  1. 사실 나머지 두 인자는 argvenvp입니다. 마지막 세번째 인자는 일부 C 구현이 전통적으로 지원하던 환경 변수 배열입니다. 2000/natori에 인자가 세 개인 main에 대한 자세한 설명이 있습니다.


Copyright © 1999–2009, Kang Seonghoon.