Debian wheezy で Clang をビルドする

English version is not available.

はじめての Clang

Stephen Prata という方の書いた、C、C++ の入門書、『C Primer Plus 6th Edition』(2014年) と『C++ Primer Plus 6th Edition』(2012年) を読み始めたら、たいへんによくできた入門書だと感心すると同時に、C も C++ も、まったく時代についていけてないことを思い知らされました。新しい規格については、へぇ、こんなことができるのかぁ、と思うようなものもあり、リハビリテーションをかねて、ちゃんと読んでみることにしました。実は、プログラミング言語の入門書の中では、『プログラミング言語 C』(K&R)のぶっきらぼうな reticence が今でも大好きで、手放せずにいるのですが、これこそ時代遅れの典型なのでしょうね。

PC UNIX では、プログラミングをしなくても、コンパイラのお世話になることが多いので、使ったことがないという人は稀だと思いますが、Linux 標準の C、C++ コンパイラは、もちろん GCC です。それで不足に思うことはないし、それどころか、ほぼどんな環境でもソースがコンパイルできるという現実を作っているのは GCC と GNU のユーティティのおかげにほかなりません。今回は新しい規格の勉強ということで、コンパイラも新しいものを使おうと思って、Clang を試すことにしました。聞くところによると、C11 や C++11 だけでなく、C++14 のドラフトまでフルサポートしているそうです(業界では常識なのでしょうか)。ところが、これがそう簡単にはいきませんでした。以下はその顛末です。Stephen Prata さんの二冊については、また別の機会に書こうと思います。

Debian wheezyでコンパイル

メモリ256MBの壁

公式ページclang: a C language family frontend for LLVMの Get Started を参考に、Subversionリポジトリをチェックアウトして、ソースツリーを用意し、

$ gcc --version
gcc (Debian 4.7.2-5) 4.7.2

$ ../llvm/configure --prefix=/home/charlus/opt/ --enable-optimized --enable-assertions=no --enable-targets=host

$ make

として、コンパイルを始めたら、メモリもスワップもほぼ使い切った状態が丸二日間続き、しかも最後は、ほとんどのユーザアプリケーションが落ちるという始末です。mysqld のログを見ると、メモリアロケーションができず、真っ先に落ちています。カーネルだけがかろうじて生きているという状態でした。さらに、まだ unittest が終わっていません。メモリ 256MB で迂闊にコンパイルしてはいけないようです。configure オプションには unittest を省略するオプションがないようなので、どうにもなりません。

システム再起動後、もう一度途中からやり直して、ようやくコンパイルが終わりました。

Clang はすでに セルフホストできるようになっているということなので、これも試しました。Clang がインストールされている環境では、configure を走らせると、自動的に Clang を使うようになっています。ところが、make の途中、Illegal instruction で落ちます。

謎の Illegal instruction を追う

調べてみると、いろいろなコードで Illegal instruction が頻発します。条件を絞り込んで、下のコードだけで再現できるところまでは突き止めました。C の方は、float や long double では発生しませんが、printf() を呼ぶと、float、double ともに Illegal instruction となります。C++ では、float や long double にすると Illegal instruction にはならないことがわかりました。また、std::cout を使っても、使わなくても結果は同じでした。

整数ではなにも起こりませんから、明らかに浮動小数点数の扱いが原因です。

これだけ明らかな現象なのに、どうやら、あまり困っている人がいないようです。調べてもそれらしい報告があまり見当たりません。しかも、Illegal instruction で調べると、古い情報ばかりヒットします。最適化オプションをつけたときに Illegal instruction になるという例が多くありましたが、上の例では、

$ clang -O3 test.c
$ clang++ -O3 test.cpp

と、最適化オプションをつけるとちゃんと実行できるファイルができます。この問題について触れていると思われる投稿記事を手がかりに整理してみました。

Google で "clang illegal instruction" をキーワードに検索すると、Machine Cycle というブログのIllegal Instruction in Code Compiled with Clangという投稿が似たような症状を報告しています。valgrind と objdump でチェックして、pxor 命令が犯人だとつきとめていて、たいへん役立ちました。使っている CPU は、32bit Mobile AMD Athlon XP で、64bit CPU でコンパイルすると、問題なく動作するということです。調べてみると、Mobile Athlon XP は、MMX、SSE、3DNOW! をサポートしているようです。作者によると、-mno-sse オプションでちゃんと動作したとのことです。後で述べるように、この PXOR という命令は SSE というよりも、MMX で導入された命令のようですが、MMX をサポートする Mobile Athlon XP でこれがエラーになる理由はわかりません。

私の Debian マシンは、Pentium III ですから、MMX、SSE はサポートしていますが、SSE2 ~ SSE4 はサポート外です。cpuinfo の結果は下の通り、sse のフラグはありますが、調べるまでもなく、32bit プロセッサです。

$ cat /proc/cpuinfo
processor       : 0
vendor_id       : GenuineIntel
cpu family      : 6
model           : 8
model name      : Pentium III (Coppermine)
stepping        : 6
microcode       : 0x8
cpu MHz         : 996.591
cache size      : 256 KB
fdiv_bug        : no
hlt_bug         : no
f00f_bug        : no
coma_bug        : no
fpu             : yes
fpu_exception   : yes
cpuid level     : 2
wp              : yes
flags           : fpu vme de pse tsc msr pae mce cx8 sep mtrr pge mca cmov pse36 mmx fxsr sse up
bogomips        : 1993.18
clflush size    : 32
cache_alignment : 32
address sizes   : 36 bits physical, 32 bits virtual
power management:

Machine Cycle と同じテストをしてみると、次のような結果でした。

$ clang --version
clang version 3.5.0 (trunk 207087)
Target: i386-pc-linux-gnu
Thread model: posix

$ clang -g test.c

$ valgrind --tool=memcheck ./a.out
...省略...
vex x86->IR: unhandled instruction bytes: 0xF3 0xF 0x5A 0x45
==6590== valgrind: Unrecognised instruction at address 0x8048441.
==6590==    at 0x8048441: main (test.c:4)
...省略...

$ objdump -d -S a.out | grep 8048441
 8048441:       f3 0f 5a 45 f8          cvtss2sd -0x8(%ebp),%xmm0

同様に、C++ でも実験すると、下のようになります。

$ clang++ -g test.cpp

$valgrind --tool=memcheck ./a.out
...省略..
vex x86->IR: unhandled instruction bytes: 0xF2 0xF 0x2A 0xC1
==7416== valgrind: Unrecognised instruction at address 0x80484a0.
==7416==    at 0x80484a0: main (test.cpp:2)
...省略...

$ objdump -d -S a.out | grep 80486d1
 80484a0:       f2 0f 2a c1          cvtsi2sd %ecx,%xmm0

インテルが公開している、Intel® 64 and IA-32 Architectures Software Developer Manualsの 11章、12章、13章がそれぞれ SSE、SSE2、SSE3 の説明にあてられているので、これを参照すると、PXOR は、MMX の命令として、CVTSS2SD と CVTSI2SD の二つは SSE2 の命令として説明が出てきます。アセンブリは詳しくないので、間違いないかどうかはわかりませんが、MMX は 64 ビットのレジスタに、8 ビットの整数を 8 つ、16 ビットの整数を 4 つ、32 ビットの整数を 2 つ、それぞれパックしたデータとして格納し、平行して演算が実行できるようにしたものだそうです。SSE はそれの浮動小数点数版ということで、こちらは 128 ビットのレジスタになっていて、単精度浮動小数点数を平行して計算できるそうです。32 ビットだとすると、4 つをパックしてあるということになります。XMM0 ~ XMM7 がここで使われるレジスタです。

CVTSS2SD は単精度浮動小数点数を倍精度に変換する命令、CVTSI2SD は 32 ビットの整数を倍精度浮動小数点数変換する命令で、SSE では単精度しか扱えないのに対して、SSE2 では倍精度が扱えるようになっています。SSE2 は、Pentium 4 以上でないとサポートされません。

GCC を使っても同じ instruction を生成できるかどうか確認してみました。SSE2 コードを出力するには、-mfpmath=sse と -msse2(-msse3…) をコマンドラインオプションに指定すればいいようです(3.17.17 Intel 386 and AMD x86-64 Optionsを見ると、x86-64 ではこれがデフォルト)。まずは、C ファイル。

$ gcc -g -mfpmath=see -msse2 test.c
...
$ objdump -d -S a.out | grep 8048434
 8048434:       0f 5a c0                cvtps2pd %xmm0,%xmm0

次に C++。

$ g++ -g -mfpmath=sse -msse2 test.cpp
...
$ objdump -d -S a.out | grep 8048495
 8048495:       f2 0f 10 05 40 85 04    movsd  0x8048540,%xmm0

どちらも Illegal instruction を再現できますが、Opcode が違います。CVTPS2PD も MOVSD も、インテルのマニュアルでは、上と同じく SSE2 で扱える命令として扱われています。

これで、Debian のバグリポート、clang: defines SSE macros with -m32 on amd64(2011年7月なので、ちょっと古いですが) で言われていることが理解できます。i386 Pentium の GCC はデフォルトのマクロ定義で、__SSE__ や __SSE2__ が外されているけれども、Clang では、デフォルトでマクロが定義されていて、たぶんこれを外す configure オプションがないということでしょう。

$ gcc -m32 -E -dM - < /dev/null | grep SSE

$ clang -m32 -E -dM - < /dev/null | grep SSE
#define __SSE2__MATH__ 1
#define __SSE2__ 1
#define __SSE_MATH__ 1
#define __SSE__ 1

たぶんこれは Debian 固有の問題ではないので、Clang パッケージのメンテナがパッチを作ってパッケージングし直しているかもしれません。これがバグと認定されるか、あるいは、少なくとも Debian では修正が必要と認定されればのことですが(*1)。 さて、原因はわかりましたが、Clang の configure では、

$ uname -a
Linux debian 3.2.0-4-686-pae #1 SMP Debian 3.2.54-2 i686 GNU/Linux

$ ../llvm/configure --enable-targets=host

としてもお構いなしに、デフォルトで SSE2 命令を吐き出すのが仕様ということのようです。もう少し調べると、cfe-dev メーリング・リストのログに、[cfe-dev] Preventing clang from generating SSE2 symbols(2012年3月)で始まるスレッドを見つけました。最後は、

CFLAGS に -march=i586 をつければいいじゃね?

で終わっています。もう Pentium みたいな古い CPU はサポートしない、ということですかね。諦めて -march=i(3|4|5|6)86 や -march=pentiumpro みたいなオプションをつけて実行するか、オプションつきのエイリアスを作るしかなさそうです(*2)。i686 には、SSE2 をサポートする Pentium 4 や Celeron も含まれていますが、実際に -march=i686 オプションをつけると、SSE2 命令は吐かず、Illegal instruction で止まることもなくなります。

  1. Debian の Clang パッケージを使ってみたくなりますが、バージョンが古いのでいまいち手が伸びません。
  2. -mcpu=cpu_type オプションは存在しません。

セルフホストしてみる

CFLAGS を設定しろ、というアドバイスをもとに、もう一度セルフホストしてみました。configure を実行すると、ルートにできる Makefile.config を下のように書き換えて、make しました。もちろん、これで SSE2 命令を使わない Clang ができるわけではありませんが、少なくとも、コンパイルは通るはずです。

$ ../llvm/configure --prefix=/home/charlus/opt/ --enable-optimized --enable-assertions=no --enable-targets=host
$ make

今度は落ちずに、確かに最後までコンパイルができました。コンパイルの速度が速く、半日(12時間くらいか?)で終わります。カーネルのコンパイルと同じくらいでしょうか。何より、メモリの使用量が少ないのが驚きです。モニタしていたわけではないのではっきりしませんが、256MB でも余裕でしたし、スワップもほとんど使っていないと思われます。FreeBSD が Clang に移行したというのも、ライセンスの問題だけではないような気がしてきます。ちなみに、Linux のカーネルは、まだビルドできないようです。

libc++を作る

C++14 ドラフトを100%実装したという、libc++ ライブラリを作ってみました。公式ウェブサイトでは、サポートプラットフォームが、Mac OSX i386 と Mac OSX x86_64 としか書いてありませんが、Linux でもビルドできます。基本的には、公式のサイトに書いてある通りですが、わが家のマシン特殊事情ということで、CFLAGS と CXXFLAGS を追加してあります。

$ CC=clang CXX=clang++ CFLAGS="-march=i686" CXXFLAGS="-march=i686" cmake -G "Unix Makefiles" -DLIBCXX_CXX_ABI=libstdc++ -DLIBCXX_LIBSUPCXX_INCLUDE_PATHS="/usr/include/c++/4.7.2/;/usr/include/c++/4.7.2/i486-linux-gnu/" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local ../libcxx
$ sudo make install

問題なくインストールされました。試してみると...

$ cat hello.cpp
#include <iostream>
int main() {
  std::cout << "Hello, libc++!" << std::endl;
  return 0;
}
$ clang++ -stdlib=libc++ hello.cpp
hello.cpp:1:10: fatal error: 'iostream' file not found
#include <iostream>
         ^
1 error generated.

あらあら、インクルードファイルが見つけられません。clang++ のサーチパスを確認すると、ちゃんと /usr/local/include があります。

$ echo | clang++ -Wp,-v -x c++ - -fsyntax-only
clang -cc1 version 3.5.0 based upon LLVM 3.5.0svn default target i386-pc-linux-gnu
ignoring nonexistent directory "/usr/lib/gcc/i486-linux-gnu/4.7/../../../../include/i486-linux-gnu/c++/4.7"
ignoring nonexistent directory "/include"
#include "..." search starts here:
#include <...> search starts here:
  .
  /usr/lib/gcc/i486-linux-gnu/4.7/../../../../include/c++/4.7
  /usr/lib/gcc/i486-linux-gnu/4.7/../../../../include/c++/4.7/i486-linux-gnu
  /usr/lib/gcc/i486-linux-gnu/4.7/../../../../include/c++/4.7/backward
  /usr/local/include
  /home/kjm/opt/bin/../lib/clang/3.5.0/include
  /usr/include/i386-linux-gnu
  /usr/include
  End of search list.

/usr/local/include ディレクトリを見ると、libc++ のヘッダファイルは、/usr/local/include/c++/v1 ディレクトリにインストールされています。この v1 というのは、ソースツリーの include/CMakeLists.txt で定義されていました。まあ、そういうことなのでしょう。

ということで、インクルードパスを指定してやり直しです。

$ clang++ -stdlib=libc++ hello.cpp -I/usr/local/include/c++/v1
$ ./a.out
./a.out: error while loading shared libraries: libc++.so.1: cannot open shared object file: No such file or directory

おっと、こんどは実行するのに、ライブラリが見つかりません。自動的に ldconfig を実行してくれるわけではないのですね。サイトの文書では、インストールパスの prefix が、/usr になっていましたっけ。

$ sudo /sbin/ldcofnig
$ ./a.out
Hello, libc++!

やっと終わりました。

$ ldd a.out
  linux-gate.so.1 =>  (0xb77c3000)
  libc++.so.1 => /usr/local/lib/libc++.so.1 (0xb76fb000)
  libm.so.6 => /lib/i386-linux-gnu/i686/cmov/libm.so.6 (0xb76d5000)
  libgcc_s.so.1 => /lib/i386-linux-gnu/libgcc_s.so.1 (0xb76b8000)
  libc.so.6 => /lib/i386-linux-gnu/i686/cmov/libc.so.6 (0xb7554000)
  libstdc++.so.6 => /usr/lib/i386-linux-gnu/libstdc++.so.6 (0xb7468000)
  libpthread.so.0 => /lib/i386-linux-gnu/i686/cmov/libpthread.so.0 (0xb744e000)
  librt.so.1 => /lib/i386-linux-gnu/i686/cmov/librt.so.1 (0xb7445000)
  /lib/ld-linux.so.2 (0xb77c4000)

C++ABI ライブラリに、libc++abi(Clang)やlibcxxrt(FreeBSD)を使うと、GCC とは非互換となるので、今回は使いませんでした。

使ってみる

node.js をビルドしてみました。

$ curl http:/nodejs.org/dist/v0.10.28/node-v0.10.28.tar.gz -o node-v0.10.28.tar.gz
$ tar zxvf node-v0.10.28.tar.gz
$ cd node-v0.10.28
$ CC=clang CXX=clang++ CFLAGS=-march=i686 CXXFLAGS=-march=i686 ./configure --prefix=/home/charlus/opt
$ make

Illegal instruction で止まります。よく見ると、flags が configure に無視されて、_DARWIN_USE_64_BIT_INODE=1 という妙なオプションがついています。

直接 common.gypi を編集して、cflags と cflags_cc に -march=i686 を設定して、やり直すと、ちゃんと Linux の clang だと思ってくれたようです。コンパイルも無事終了します。

$ make install
$ node
> process.versions
{ http_parser: '1.0',
  node: '0.10.28',
  v8: '3.14.5.9',
  ares: '1.9.0-DEV',
  uv: '0.10.27',
  zlib: '1.2.3',
  modules: '11',
  openssl: '1.0.1g' }
>

Leave a Reply