わかるUNIXの歴史第 5 回 / 自己ホスティングの原点

gcc を着火した cc の祖先 ── 最初の C コンパイラは、どう自分を作ったか

Cコンパイラの誕生 ── 自分で自分を C で書いた C コンパイラは、C コンパイラが無いと作れない。第3回の輪の、いちばん深いところ。まだ C が存在しない世界で、最初の C コンパイラは何が作ったのか。 答えは「B から少しずつ育てた」。

必要な道具:第3回(gcc の3段ブートストラップ) 種明かし:言語を拡張しては再コンパイル

第3回で「最初の gcc は、そのマシンの既存の cc で着火した」と言い、その cc の祖先へ、と予告しました。祖先とは、1970年代初頭に Dennis Ritchie が書いた最初の C コンパイラです。でもここで、輪はいちばん深いところに来ます ── C コンパイラは C で書かれている。なのに C が世界にまだ1つも無い。 借りてくる「既存の C コンパイラ」すら存在しない。ではどうやって? 答えは、いきなり C を書いたのではなく、すでにあった B という言語から、C を少しずつ育てたのです。

01C は設計されたのではなく、B から“育った”

C は白紙から設計された言語ではありません。系図はこうです ── BCPL →(Ken Thompson が)B →(Dennis Ritchie が)C。1970年に PDP-11 が入ると、B の「型が無い・語(ワード)単位」という性質が、バイト単位の PDP-11 に合わない。そこで Ritchie は B に型(charint など)を足し始めます。この途中の言語は NB(New B)と呼ばれましたが、あまりに短命で、まともな仕様書も残っていません。B に型が生え、機械語を吐くように改造されていくうちに、それは C になっていました。

/* 言語の系図(ざっくり) */ BCPL ──▶ B // Thompson, 型なし・ワード単位(PDP-7) B ──▶ NB // Ritchie, 型を足す(PDP-11のバイトに合わせる) NB ──▶ C // 機械語出力へ。1973年、UnixカーネルをCで書き直し

02自己ホスティングの鶏と卵 ── いちばん深いところ

第3回の gcc は「既存の cc を1つ貸してくれ」で済みました。借りる相手がいたからです。でも最初の C コンパイラには、借りる相手がいない。C はまだ存在しないのだから。これが自己ホスティングの鶏と卵の、最深部です。

今回の急所 ── 「全部のC」を「無」から作ろうとしない

解けないように見えるのは、「完全な C コンパイラを、何も無い所から一気に作る」と思うから。実際はそうしません。すでに動く小さな言語(B)から始め、コンパイラに機能を1つ足しては再コンパイルし、扱える言語を少しずつ広げていく。各段は、1つ前の段のコンパイラだけで作れる。無から C を作るのではなく、B を C へ連続変形させるのです。

◇ ◇ ◇

03種明かし ── 言語を拡張しては、再コンパイルする

この「育て方」を、階段で見ます。下の図のスライダーを進めてください。各段で、コンパイラが書かれている言語と、コンパイラがコンパイルできる言語が、追いかけっこしながら上がっていきます。両者が C で出会った瞬間が、自己ホスティングの成立です。

図1:言語拡張の階段。B のコンパイラに型・機械語出力・構造体…を足しては再コンパイルし、扱える言語を C まで広げる。土台の種(B/アセンブリ)は最後に退場する
自己育成ループ(feature をひとつ足すたびに)

① コンパイラに新機能(型・構造体…)を足す。② 今のコンパイラで再コンパイルする。③ すると、その新機能をコンパイラ自身のソースコードでも使えるようになる。④ ①へ戻る。

この小さな輪を回すたび、言語もコンパイラも一段ずつ豊かになる。「作ったばかりの機能で、自分自身を書き直す」 ── 自己ホスティングならではの、雪だるま式の成長です。

04種の退場 ── C↔C の輪が、自走を始める

やがて、C コンパイラはC で書かれ、C(自分のソース)をコンパイルできる地点に達します。こうなると、着火に使った B の種はもう要りません。第3回で stage1 を捨てたのと同じ ── 用ずみの足場は外す。1973年、Ritchie と Thompson はこの C で Unix カーネルそのものを書き直しました。下の図で、種を切って、C↔C の輪が自走を始める瞬間を見てください。

図2:種の退場。最初は B(種)が C コンパイラを着火する。自己ホスティングに達したら B との縁を切っても、C↔C の輪は自分だけで回り続ける
いまは B(種)が C コンパイラを着火している段階。まだ B に頼っている。
この回の位置づけ ── 第0回の輪は、ここで生まれた

第0回で見た「gccgccgcc …」の自己ホスティングの輪。そのいちばん最初のひと回しが、この Ritchie の C コンパイラです。gcc の輪も、あなたが今使うコンパイラの輪も、元をたどればここで点火された同じ輪の続き。C が「自分で自分をコンパイルできる」言語になった瞬間が、その後の全部の起点でした。

05それでも、輪はさらに下へ続く

「B から育てた」で C の鶏と卵は解けました。でも当然、次の問いが待っています ── では、その B は何が作ったのか? B もまた、いきなり存在したわけではない。B は BCPL から来て、PDP-7 のアセンブリの上で動いていました。輪の外の手は、まだ下にあります。次回、B と BCPL、そして「中間コード(O-code)」へ。

正直な線 ── 「B で書いた」と「アセンブリで書いた」の腑分け

(1) 「最初の C コンパイラはアセンブリで書かれた」と紹介されることがありますが、より正確には ── C コンパイラは B のコンパイラから連続的に進化したもので、Ritchie が B のコンパイラに型を足し機械語出力へ改造していく過程で C になりました。その土台(B の処理系)は PDP-11 のアセンブリやスレッデッドコードに支えられています。「B から育った」と「下にアセンブリがある」は両立します。

(2) 「機能を足して再コンパイル」という自己育成ループは実在の手法ですが、実際の1971〜73年の作業はもっと込み入っていて、ここでは骨格を理想化して描いています。(3) 自己ホスティングでも、実行時ライブラリや起動処理などにアセンブリは残ります。「C だけで完結」という意味ではありません。(4) NB は短命で資料が乏しく、B→C の細部には諸説あります。

練習問題(今回の話だけで解けます)
  1. C の系図(3つの言語)を、古い順に並べよ。
    答えを見る
    BCPL → B → C。B は Thompson、C は Ritchie。B に型を足す途中の言語が NB(New B)。
  2. 「最初の C コンパイラを、既存の C コンパイラで作る」ができないのはなぜか。
    答えを見る
    C がまだ世界に1つも存在せず、借りてくる既存の C コンパイラが無いから。第3回の gcc は既存 cc を借りられたが、その原点にはそれが無い。
  3. では最初の C コンパイラは、どうやって作られたか。一文で。
    答えを見る
    既にあった B のコンパイラに機能(型・機械語出力など)を少しずつ足して再コンパイルし、扱える言語を C まで連続的に広げた。無から作ったのではなく B を C へ変形させた。
  4. 自己ホスティングに達した後、着火に使った B の種はどうなるか。
    答えを見る
    不要になり退場する(第3回で stage1 を捨てたのと同じ)。以後は C コンパイラが C で書かれ、自分で自分を再生産できる=C↔C の輪が自走する。

まとめ無から作らず、B を C へ連続変形した

gcc を着火した cc の祖先は、Ritchie の最初の C コンパイラ。ここで自己ホスティングの鶏と卵は最深部に達する ── C コンパイラは C で書かれているのに、借りる相手の C コンパイラが世界に無い。解けたのは、「完全な C を無から作る」のをやめたから。系図 BCPL→B→C のとおり、すでに動く B のコンパイラに型・機械語出力・構造体を足しては再コンパイルし、扱える言語を C まで連続変形させた。

「作ったばかりの機能で自分自身を書き直す」自己育成ループを回すうち、C コンパイラは C で書かれ C をコンパイルできる地点に達し、着火用の B の種は退場(stage1 を捨てるのと同じ)。1973年、この C で Unix カーネルが書き直された。第0回で見た自己ホスティングの輪は、ここで点火された。── でも輪はまだ下へ続く。その B は何が作ったのか? 次回、B と BCPL、そして中間コード O-code へ。

この文書は「わかるUNIXの歴史」シリーズ第5回です。史実:C は BCPL → B(Ken Thompson)→ C(Dennis Ritchie)と発展した。1970年に PDP-11 が導入され、B の移植にはスレッデッドコードが用いられ、アセンブラや dc、B 自身が B で書かれてブートストラップされた。1971年ごろから Ritchie はコンパイラを機械語生成へと変えつつ変数へのデータ型を導入し(この過渡的言語が New B, NB)、これが C へと連続的に発展した。NB は短命で完全な記述は残っていない。1973年初めに現代 C の骨格が整い、同年夏に PDP-11 版 Unix カーネルが C で書き直された。自己ホスティング(自作言語で書いたコンパイラを自分でコンパイルできる状態)は、初期の種コンパイラを別言語(アセンブリや既存言語)で用意し、言語を拡張しては再コンパイルして到達する標準的手法で、到達後は種を退役できる。「最初の C コンパイラはアセンブリ製」という要約もあるが、実態は B コンパイラからの連続的進化であり、その基盤に PDP-11 アセンブリがある。自己ホスティング後も実行時処理などにアセンブリは残る。 ── 印刷する場合はブラウザの「印刷」から「PDF に保存」を(印刷版では図の操作と解答は静止/非表示になります)。

印刷 / PDF 化:⌘+P(Windows は Ctrl+P)。画面では図1のスライダーで言語拡張の階段を、図2のボタンで種(B)の退場と C↔C の自走を試せます。「答えを見る」で解答が開きます。