C++コンパイラのビルドコップ

GoogleのLLVMチームの仕事のひとつとして、Clang C++コンパイラ(最近ではGCCと同じくらい広く使われている)の保守作業がある。継続的にCVS HEADのClangで社内のC++ソースコードなどをコンパイルして、もしそれがうまく動かなかったら、Clang(あるいは言語に対して誤った仮定をしていたコードがあったならそちら)を直すというようなことをやっている。

このビルドコップ作業は一週間単位でLLVMチーム内で回している。その役割が回ってきたら、一週間の間、開発版のコンパイラで巨大なコードベースをコンパイルして、コンパイラのバグがでたら何かしら対処するというわけだ。

この仕事が結構大変。コンパイラのバグなんてメタだから基本的にわけがわからないし、僕はLLVMもClangもあまり詳しくないので、なんなんだこれはと思いながらエラーを直すというはめになる。

簡単なエラーは、ほとんどソースを理解していなくても簡単にわかる。たとえば、コマンドラインのフラグの名前が変わったみたいなやつだと、見ればわかるし、それに対応するには単にフラグの名前を書き換えればよい。

難しいエラーは辛い。正しいコードをコンパイルできないというのは簡単な方で、正しいコードが普通にコンパイルできるのだが、その後ユニットテストで失敗する、といったものはデバグが難しい。

こないだのエラーは結構わけがわからなくてほとんど丸一日費やしてしまった。

なんだか社内のコードのユニットテストが失敗するのだが、どうも調べてみると、実行ファイルがロードされて実行が開始された後、main関数に至る前に、別の関数の中でエラー終了しているらしい。

bisectしてみると問題のパッチがわかった。たしかにそのパッチを手元で元に戻してみると問題が再現しなくなるので、どうもそれが原因であることは確からしい。しかし、どうみても特に問題がありそうなパッチには見えない。

元のパッチの作者に連絡はしてみたけど、巨大な依存関係のある社内のコードをそのまま送るのは不可能だ。そもそもビルドもできないだろう。かといってオープンソースプロジェクトで誰かがコミットしたパッチを正当な理由づけなくロールバックするというのも難しい。

そこで再現条件をいろいろ調べてみると、グローバル変数のオブジェクトのコンストラクタがなぜか2回呼ばれているのが原因だとわかった。グローバル変数のコンストラクタはmain関数より前に呼ばれるのでこれはありえることだ(2回呼ばれるのはおかしいが)。

C++のグローバル変数のコンストラクタはいったいどういうメカニズムで呼び出されるんだっけ? と思いながら、objdumpなどでさらに実行ファイルを調べてみると、.ctorセクションに同じコンストラクタのポインタが複数個あるのが問題だということがわかった。

C++グローバル変数の初期化は、.ctorセクションに入っている関数ポインタの配列の要素をスタートアップルーチンが順に呼び出すことで行われる。 .ctorセクションには確かに同じコンストラクタのアドレスが何個も連続して入っていた。

元々の.oファイルの方を見てみると、正しいオブジェクトでは.ctorセクションのリロケーションが別々の(複数存在する)text.startupセクションを指しているのに、間違ったオブジェクトファイルでは、全部同じ(おそらく最初の).text.startupセクションを指してしまっていた。

これでは同じコンストラクタが何度も呼ばれてしまうわけだ。

問題がわかったら、次はなるべく短いコードでバグを再現させなければいけない。 これにもそれなりに時間がかかった。結局、COMDAT groupが異なるコストラクタがあればよいということがわかった。下のようなコードをコンパイルすると本来は”i=1 j=1"が表示されなければいけないのに、バグっているコンパイラでは”i=0 j=1"が表示されるようになってしまっていた。

#include <iostream>

int i = 0, j = 0;

class C {
public:
C() { i++; }
};

template<typename T>
class D {
public:
D() { j++; }
C *f() { return &x; }
static C x;
};

template<typename T> C D<T>::x;

int main() {
D<void> d;
d.f();
std::cout << "i=" << i << " j=" << j << "\n";
}

このコードをメーリングリストに送って、元のパッチを書いたひとに直してもらうことにした。こういった作業はすべて公開のメーリングリストで行われている(ので書いても問題ないわけだが)。

まったく意味不明なユニットテストの失敗からC++コンパイラのミスコンパイルの修正までほとんど一日かかった。コンパイラのバグ取りは大変だ。でも僕らがこういう努力をしているのでリリース版では重大なミスコンパイルはほとんどないと思う。

Email me when Rui Ueyama publishes or recommends stories