やっぱりお腹が痛くなる
#C言語クイズ
— Yutaka Hirata (@yutakakn) 2023年1月3日
実行結果はどうなるでしょう?#include <stdio.h>#include <stdbool.h>
int main(void)
{
char buf[5] = "2023";
char c1 = buf[0];
char c2 = 2[buf];
bool cmp = (c1 == c2);
printf("%s\n", cmp ? "true" : "false");
return 0;
}
この 2[buf] みたいな書き方もできちゃうというのが記憶の片隅にあり、true と印字されるんだろうなと思ったので最初の選択肢をポチッとした。 言語仕様で決まっているのかどうだったかまでは覚えてないけれども、とりあえずコンパイラがどんな機械語を生成するのか確認してみた。
環境
コンパイル&実行
やはり true が印字される。
[yusuke@albirex foo]$ gcc -g foo.c -o foo [yusuke@albirex foo]$ file ./foo ./foo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=12cde68dd82759dc3f81ce929093e43480f3c6f7, not stripped [yusuke@albirex foo]$ ./foo true [yusuke@albirex foo]$
disas main
やはり 2[buf] は buf[2] のように処理されている。。。
(gdb) disas main Dump of assembler code for function main: 0x000000000040052d <+0>: push %rbp 0x000000000040052e <+1>: mov %rsp,%rbp 0x0000000000400531 <+4>: sub $0x10,%rsp スタックを16バイト伸長する 0x0000000000400535 <+8>: movl $0x33323032,-0x10(%rbp) ベースポインタから -0x10 のところから "2" "0" "2" "3" 0x000000000040053c <+15>: movb $0x0,-0xc(%rbp) nullターミネート "\0" 0x0000000000400540 <+19>: movzbl -0x10(%rbp),%eax buf[0] の値をバイト幅で eax へ 0x0000000000400544 <+23>: mov %al,-0x1(%rbp) eax の下位のバイトを rbp-1 へ 0x0000000000400547 <+26>: movzbl -0xe(%rbp),%eax 2[buf] の値をバイト幅で eax へ 0x000000000040054b <+30>: mov %al,-0x2(%rbp) eax の下位のバイトを rbp -2 へ 0x000000000040054e <+33>: movzbl -0x1(%rbp),%eax rbp-1に取っておいた値をバイト幅で eax へ 0x0000000000400552 <+37>: cmp -0x2(%rbp),%al eax の下位バイトと rbp-2 の値を比較 0x0000000000400555 <+40>: sete %al ...以下省略... 0x0000000000400558 <+43>: mov %al,-0x3(%rbp) 0x000000000040055b <+46>: cmpb $0x0,-0x3(%rbp) 0x000000000040055f <+50>: je 0x400568 <main+59> 0x0000000000400561 <+52>: mov $0x400610,%eax 0x0000000000400566 <+57>: jmp 0x40056d <main+64> 0x0000000000400568 <+59>: mov $0x400615,%eax 0x000000000040056d <+64>: mov %rax,%rdi 0x0000000000400570 <+67>: callq 0x400410 <puts@plt> 0x0000000000400575 <+72>: mov $0x0,%eax 0x000000000040057a <+77>: leaveq 0x000000000040057b <+78>: retq End of assembler dump. (gdb)
メモリ上の配置
分かりにくいけど、こんな感じ。
+------ rbp-0x10。つまり buf[0] | +---- rbp-0xe。 つまり buf[2] であり 2[buf] | | +-- rbp-0xc。 つまり buf[4] | | | +-- rbp | | | | v v v v --------+-+-+-----------+------- 2023n 22 // n は \0。2, 0, 2, 3 は、数値ではなく char。 --------+-------------+++------- | ^^ +-- rsp || |+-- buf[0] からのコピー。つまり char c1 +--- 2[buf] からのコピー。つまり char c2
gcc オプション
ちなみに、-g をつけない場合も、main の disas は同じだった。-g はデバッグ情報を持つセクションを用意するだけで生成するマシン語には影響しないと思っておいてよさそう。 -O1 だとこんな感じ。もはや true を印字するのみ。
(gdb) disas main Dump of assembler code for function main: 0x000000000040052d <+0>: sub $0x8,%rsp 0x0000000000400531 <+4>: mov $0x4005e0,%edi 0x0000000000400536 <+9>: callq 0x400410 <puts@plt> 0x000000000040053b <+14>: mov $0x0,%eax 0x0000000000400540 <+19>: add $0x8,%rsp 0x0000000000400544 <+23>: retq End of assembler dump. (gdb) (gdb) x/s $rdi 0x4005e0: "true" (gdb)
そして、gcc がどんなアセンブラを作るのかだけみたいのなら、gdb など使わずとも gcc -S foo.c とやれば良い。数値が 10 進数で表記されている。
[yusuke@albirex foo]$ gcc -S foo.c [yusuke@albirex foo]$ cat foo.s .file "foo.c" .section .rodata .LC0: .string "true" .LC1: .string "false" .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movl $858927154, -16(%rbp) movb $0, -12(%rbp) movzbl -16(%rbp), %eax movb %al, -1(%rbp) movzbl -14(%rbp), %eax movb %al, -2(%rbp) movzbl -1(%rbp), %eax cmpb -2(%rbp), %al sete %al movb %al, -3(%rbp) cmpb $0, -3(%rbp) je .L2 movl $.LC0, %eax jmp .L3 .L2: movl $.LC1, %eax .L3: movq %rax, %rdi call puts movl $0, %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)" .section .note.GNU-stack,"",@progbits [yusuke@albirex foo]$