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]"