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)
입니다. 포인터 연산과 캐스팅이 얽혀 있어서 보기 힘드니 하나 하나 분해하면,
- 이 수식은 어떤 함수(
(*((int(*)())&floor))
)를argc
라는 인자로 호출합니다. - 이 함수는 어떤 포인터(
((int(*)())&floor)
)가 가리키는 함수입니다. - 이 포인터는 어떤 포인터(
&floor
)를int(*)()
, 즉int
를 반환하는 함수의 포인터로 변환합니다. 인자 목록이()
이므로((void)
가 아니라) 인자의 형이나 갯수의 검사는 하지 않습니다. - 원래 포인터는 (위에서
int
로 선언된)floor
의 포인터입니다.
따라서 floor
라는 이름은 원래 int
가 아니라 함수였다는 얘기가 됩니다. 실제로 floor 함수는 다음과 같이 선언된, 표준 라이브러리 함수입니다.
double floor(double x);
...당연하지만 함수 타입은 전혀 맞지 않습니다. (argc
는 int
였죠!) 따라서 이런 식으로 함수를 호출하는 것은 매우 위험합니다. 하지만 다행인지 불행인지 잘 실행되는군요. 그나저나 printf의 첫 인자에 들어 있는 문자열에는 %
문자가 없으므로, 어차피 이 값은 계산되어도 사용되지 않습니다. 따라서 그냥 이 인자를 날려 버릴 수 있습니다:
int main(int argc, char **argv) {
printf("'\",x); /*\n\\");
return 0;
}
이거 하나 출력하려고 이런 코드를 쓴다니 기괴하긴 하지만 그래도 다른 코드보다는 덜 복잡했네요.
사실 나머지 두 인자는
argv
와envp
입니다. 마지막 세번째 인자는 일부 C 구현이 전통적으로 지원하던 환경 변수 배열입니다. 2000/natori에 인자가 세 개인 main에 대한 자세한 설명이 있습니다. ↩