IOCCC 1987 "unix"

みんな大好きIOCCC
その中でも有名な、1987年David Korn制作のunix.c。
http://www0.us.ioccc.org/1987/korn.c

これの丁寧な解説ページ。
http://kzk9.net/column/ioccc.html

以外と簡単な内容だったけれど、少し戸惑ったところを書いておく。

よくわからなかった点

勘違いに気づくまでの過程を書くので、途中で大きな間違いが書いてある事に注意。

解説ページでは&("\021%six\012\0"[1])を"%six\012\0"と同値と言ってるけど、これがどうも腑に落ちない。これは分かりやすく書き下すと

const char *p = "\021%six\012\0";
p + 1;    // これ

になるのではないか。そうなら最初の'\'を飛ばして"021%six\012\0"になるんじゃないか?(ここで大きな勘違い)

(狭義)コンパイル

よくわからないんで、コンパイルしてアセンブリコードを見てみる。*1
コンパイラgcc 4.2.1
-Sオプションつけてコンパイルする。気になる箇所だけ載せる。全文は付録として最後に載せる。
まず文字列の入ってる.rodataセクション。

.LC1:
        .string "\021%six\n"
        .string ""

おお、"\021"はそのまま書かれるのか。(さらなる勘違い)。
次に.textセクション。printfへの第1引数について。

        movl    $.LC1+1, %edx    # 第1引数の値を作る
        movl    %eax, 4(%esp)
        movl    %edx, (%esp)
        call    printf

printfの第1引数は$.LC1+1か。やっぱり"021%six\012\0"になるよなぁ…。ますます意味が分からない。*2

アセンブル

じゃあバイナリを見てみよう。アセンブルしてオブジェクトファイルを作る。hdコマンドで見る。気になるところは文字列の部分だからgrepで絞る。そして例の文字列の部分に色を付けておく。

% hd unix.o| grep "%six"
00000080  66 75 6e 00 11 25 73 69  78 0a 00 00 00 47 43 43  |fun..%six....GCC|

アセンブリコードでの"\021%six\n"は色を付けた値になっている。すなわち"\021"は3つの文字ではなく、11の1文字になっている。
ここでようやく合点がいった。エスケープシーケンスは、printfが解釈してる訳じゃなくて、アセンブラが解釈してるんだ。だからやっぱり'\021'、16進数で11の1バイトが飛ばされて"%six\012\0"になるんだ。すっきり。

解説ページへのちょっとしたつっこみ

解説ページには誤りと思う箇所がある。ここでそんなに重要な点ではないけど一応書いておく。

ここでLF + '\0'は'\n'と等しい

今までの調査から判断するにこれは間違いだろう。アセンブリコードの"\021%six\n"の次には空文字列""がある事から、LF('\012'or'\n')と'\0'が並んでも1文字の'\n'にはならないように思う。文字列として"\n"になる、と言うなら「等しい」と言えるだろう。

コードの振る舞いには影響しないことだけども。

付録: unix.sのアセンブリコード
        .file   "unix.c"
        .section        .rodata
.LC0:
        .string "fun"
.LC1:
        .string "\021%six\n"
        .string ""
        .text
        .p2align 4,,15
.globl main
        .type   main, @function
main:
        leal    4(%esp), %ecx
        andl    $-16, %esp
        pushl   -4(%ecx)
        pushl   %ebp
        movl    %esp, %ebp
        pushl   %ecx
        subl    $20, %esp
        movl    $97, %eax
        movsbl  %al,%eax
        movl    %eax, %edx
        movl    $.LC0, %eax
        subl    $96, %eax
        leal    (%edx,%eax), %eax
        movl    $.LC1+1, %edx
        movl    %eax, 4(%esp)
        movl    %edx, (%esp)
        call    printf
        addl    $20, %esp
        popl    %ecx
        popl    %ebp
        leal    -4(%ecx), %esp
        ret
        .size   main, .-main
        .ident  "GCC: (GNU) 4.2.1 20070719  [FreeBSD]"

*1:C言語での遊びだから少し野暮な事かも

*2:'\n'に気づいていれば…