Linux を生んだ親 gcc ── その gcc は、何が作ったのか
gcc がコンパイルしたと分かりました。でも gcc 自身も C で書かれたプログラム。コンパイラがまだ無い所で、最初の gcc は何が作ったのか。 答えは「既存の cc で1回だけ持ち上げ、あとは自分で自分を作る」── 3段ブートストラップ。
Linux を生んだ親は gcc でした。では gcc という親は誰が生んだのか。gcc は C 言語で書かれた、ただのプログラムです。ということは、gcc を動く形にするには C コンパイラが必要。── また輪です。gcc は gcc が作り、その gcc も gcc が…。今回は、この輪を正しく着火し、正しく回す手順を見ます。じつは gcc のビルドには「3回もビルドする」奇妙な儀式があり、その3回目が、コンパイラの正しさを自分で検算する仕掛けになっています。
まず出発点を確かめます。gcc はC言語のソースコードとして書かれています(現代の gcc は C++。これは正直な線で後述)。だから gcc を実行ファイルにするには、それをコンパイルする別の C コンパイラが要る。コンパイラを作るのにコンパイラが要る ── 第0回の輪が、道具そのものの中にも埋まっています。
1987年、Richard Stallman が最初の gcc(GNU C Compiler, 1.0 は1987年5月23日)を公開したとき、それは そのマシンにあった既存の C コンパイラ ── Unix 純正の cc ── でビルドされました。輪の外にあった“よそのコンパイラ”を、着火装置として1回だけ借りたのです。いったん gcc が動けば、次からは gcc が gcc を作れる(自己ホスティング)。
gcc に1行も残っていません(書いた C フロントエンドだけ再利用)。──「借りものでは動かず、結局は書き下ろした」。ブートストラップの現場らしい逸話です。
gcc の主張はこうです ── 「動く C コンパイラを一つだけ貸してくれ。あとは自分で回す」。これは鶏と卵の解決ではなく、先送りです。問いは「では、その最初の cc はどこから?」へと、階段の下段へ送られる。その cc こそ Unix 純正の C コンパイラ=Ritchie の最初の C コンパイラの子孫(第5回)。輪の外の着火装置を、さらにさかのぼる旅は続きます。
ここが今回の白眉です。gcc を正しくビルドする標準手順(make bootstrap)は、gcc を3回ビルドします。下の図で、段を一つずつ進めてください。
stage 1 ── 既存の cc がビルド
まず“よそのコンパイラ”で gcc を1個作る。これは動くが、よそ者の癖(最適化やコード生成)が混じった足場。
stage 2 ── stage1 がビルド
その gcc(stage1) で gcc をもう一度作る。これが初めての「gcc が作った gcc」。
stage 3 ── stage2 がビルド
さらに gcc(stage2) で gcc を作る。stage2 も stage3 も「同じソースを gcc でビルドした」もの同士。
正しいコンパイラなら、「何でビルドされたか」に関係なく、同じソースからは同じ出力を出すはずです。だから gcc(stage2) と gcc(stage3) は ── どちらも「gcc が gcc をビルドした結果」── バイト単位で一致するはず。実際 make compare がこの2つを比較します。一致すれば、コンパイラは自分自身を安定に再現できている=正しく動いている、という自己採点になる。ずれたら、コンパイラのバグか、ビルドの非決定性を疑う。
stage1 は“よそ者cc”が作ったので、最適化やコードにその癖が残るかもしれない。でも gcc でビルドし直すたびに、結果は「gcc が作った gcc」という一点(不動点)へ収束します。stage2 でその点に到達し、stage3 で「もう変わらない」ことを確認する。だから最終製品は stage3で、stage1 は用ずみの足場。下の図で、どんな種(着火装置)から始めても同じ gcc に落ち着くことを見てください。
3段ブートストラップは輪の正しい回し方ですが、stage1 にはやはり「既存の C コンパイラ」が要る。まっさらな新品マシン(コンパイラが1つも無い)では、これができません。ではどうするか ── 別のマシンの gcc で、その新しいCPU向けの gcc を焼く。これがクロスコンパイルで、第8回の「紙テープを別マシンから運ぶ」話の、現代版です(番外編でも深掘り)。
いずれにせよ、着火装置の cc はどこかからもらってくるしかない。その出どころをたどると、Unix 純正の C コンパイラ、そして Ritchie が最初に書いた C コンパイラへ着きます。輪の外の手を、もう一段さかのぼる番です。
(1) 現代の gcc は C++ で書かれ、ビルドには C++ コンパイラが要ります(2012年の GCC 4.8 以降)。「C で書かれている」は歴史的な話。鶏と卵の構造は同じです(コンパイラを作るのにコンパイラが要る)。
(2) stage3 は正しさのためには技術的には不要で、機能的な完成品は stage2 です。3段目は「gcc 自身の最適化を最終バイナリに行き渡らせる」ことと「stage2 と比較して検算する」ための段。(3) 「stage2 == stage3 で一致」は決定的ビルドを前提にします(タイムスタンプ等の混入を除けば一致する、という理想)。ビルドの再現性そのものは番外編②「Bootstrappable Builds」で扱います。(4) そして最大の但し書き ── その“借りてきた cc”は、そもそも信用できるのか? この不気味な問いは、番外編①「Trusting Trust」へ。
Linux を生んだ gcc 自身も C で書かれたプログラムで、動かすには C コンパイラが要る ── 道具の中にも鶏と卵の輪がある。最初の gcc(1987, Stallman)は、そのマシンの既存の cc で1回だけ着火し、以後は自己ホスティングに入った。これは鶏と卵の解決ではなく先送りで、問いは「その cc はどこから?」へ下段へ送られる。
輪の正しい回し方が3段ブートストラップ。既存cc→stage1→stage2→stage3 と3回ビルドし、make compare で stage2 と stage3 のバイト一致を確かめる。正しいコンパイラは何でビルドされても同じ出力を出すはずだから、一致は自分で自分を採点した証拠になる。どんな種から始めても結果は「gcc が作った gcc」という不動点へ収束するので、stage1 は用ずみの足場、stage3 が本番。── では、その着火装置 cc の祖先へ。まずは寄り道して、もう一つの教育用OS・MINIX のコンパイラ ACK を見に行きます。
gcc とは別の思想 ── EM という中間バイトコードを挟んで、多言語・多機種に対応する設計。「中間コードを挟む」という、第6回の BCPL/O-code につながる大発明の、いい予行演習です。
make bootstrap は native 構成で3段ビルドを行い、stage1 を host(既存)コンパイラで、stage2 を stage1 で、stage3 を stage2 でビルドする。make compare は stage2 と stage3 を比較し、正しいコンパイラなら(何でビルドされたかに依らず)同一の出力になることを利用して健全性を検査する。stage3 は正しさのためには技術的に不要で、GCC 自身の最適化を最終バイナリに反映し比較検算するための段。現代の GCC は C++ で記述され(GCC 4.8, 2012 以降)ビルドに C++ コンパイラを要する。着火に用いる既存 cc は Unix 由来の C コンパイラで、その系譜は Ritchie の C コンパイラに遡る(第5回)。 ── 印刷する場合はブラウザの「印刷」から「PDF に保存」を(印刷版では図の操作と解答は静止/非表示になります)。
印刷 / PDF 化:⌘+P(Windows は Ctrl+P)。画面では図1のスライダーで3段ブートストラップを進め、図2のボタンで“種コンパイラ”を変えて不動点への収束を見られます。「答えを見る」で解答が開きます。