標準ライブラリの結果が変わる話

#include <string.h>
#include <stdio.h>

int main() {
	char a[] = {"a"}, z[] = {"z"};
	printf("array: %d\n", memcmp(a, z, 1));
	printf("literal: %d\n", memcmp("a", "z", 1));
}

このコードの実行結果は以下の通り。

array: -25
literal: -1

memcmpを2回呼んでおり、どちらも引数としてazという文字列が渡されている。

コンパイルされた実行ファイルをオブジェダンプして中身を見てみた。

実行環境

  • OS: MacOS
  • CPU: x86-64
  • コンパイラ: clang
Disassembly of section __TEXT,__text:

0000000100003f00 _main:
100003f00: 55                           pushq   %rbp
100003f01: 48 89 e5                     movq    %rsp, %rbp
100003f04: 48 83 ec 10                  subq    $16, %rsp
100003f08: 48 8d 75 fc                  leaq    -4(%rbp), %rsi
100003f0c: 48 8d 7d fe                  leaq    -2(%rbp), %rdi
100003f10: 66 8b 05 7d 00 00 00         movw    125(%rip), %ax
100003f17: 66 89 45 fe                  movw    %ax, -2(%rbp)
100003f1b: 66 8b 05 74 00 00 00         movw    116(%rip), %ax
100003f22: 66 89 45 fc                  movw    %ax, -4(%rbp)
100003f26: ba 01 00 00 00               movl    $1, %edx
100003f2b: e8 34 00 00 00               callq   52 <dyld_stub_binder+0x100003f64>
100003f30: 48 8d 3d 61 00 00 00         leaq    97(%rip), %rdi
100003f37: 89 c6                        movl    %eax, %esi
100003f39: b0 00                        movb    $0, %al
100003f3b: e8 2a 00 00 00               callq   42 <dyld_stub_binder+0x100003f6a>
100003f40: 48 8d 3d 5c 00 00 00         leaq    92(%rip), %rdi
100003f47: be ff ff ff ff               movl    $4294967295, %esi
100003f4c: 89 45 f8                     movl    %eax, -8(%rbp)
100003f4f: b0 00                        movb    $0, %al
100003f51: e8 14 00 00 00               callq   20 <dyld_stub_binder+0x100003f6a>
100003f56: 31 c9                        xorl    %ecx, %ecx
100003f58: 89 45 f4                     movl    %eax, -12(%rbp)
100003f5b: 89 c8                        movl    %ecx, %eax
100003f5d: 48 83 c4 10                  addq    $16, %rsp
100003f61: 5d                           popq    %rbp
100003f62: c3                           retq

callqが3回しか呼ばれていない。 一方でesi-1が直接入れられているのがわかる。 memcmpを呼び出すのではなく、実行ファイルの生成時点で結果が埋め込まれているらしい。

文字列リテラルは、constであるためコンパイル時に値が判明している。 そのため、コンパイラは実行時に計算するのではなく、コンパイル時に計算し実行ファイルに値を埋め込む最適化を行う。

LLVMのドキュメントにそれっぽい説明があった。 memcmpはビルトインとして埋め込まれるらしい。

https://clang.llvm.org/docs/LanguageExtensions.html#string-builtins

この最適化を行わないようにするためには、-fno-builtinオプションをコンパイル時につける。

$ gcc -fno-builtin main.c && ./a.out
array: -25
literal: -25

想定通りの値が取得できた。

memcmpの結果が-1, 0, 1のいずれかになる理由はよくわからない。 フラグだけ見て、dec命令とか使ってるのかな。