自作Cコンパイラでセルフホストを達成した。

Rui先生の低レイヤを知りたい人のためのCコンパイラ作成入門を参考に作成していたCコンパイラ、Hello, Worldできたら結構満足して休憩していたけど、再開してなんとかセルフホストまでたどり着いた。

いつセルフホストできるようになるのかは全然わからなくなって、途中結構つらかった。

はまったとき他の人のブログとかを参考にしたので、感想や考えていた方針などを一応書き残しておく。

主な進捗の振り返り

  • 2019年12月9日。最初のコミット。
  • 2020年2月10日。Hello, World!が実行できた。
  • 2020年2月15日から2020年の8月13日までは,ちょっと飽きて一旦休憩。
  • 2020年9月19日。セルフホストができた。

ma38su.hatenablog.com

Hello, Worldからセルフホストまで一か月くらい?全体で3ヶ月くらいか。

まずはセルフホストを目指す

とりあえずは最短でセルフホストを目指すのがよい気がする。 なぜかというと、第一世代のテストは常にテストが通る状態を維持できるけれど、 セルフホストのテストは、セルフホストできるまで通らない状態が続く。 この段階でいろいろ機能を追加していくと、デバッグ対象が増えて大変な気がする。

まぁ、セルフホストできないのは機能が足りてないこともあるので、 何が最短なのかを判断するのもそれなりに難しかったけど。

例えば、当初、signed、unsignedはTokenizeの段階で読み飛ばしたのだが、 セルフホストできない理由は、unsignedを読んでいないからかも? 符号拡張とゼロ拡張を適当にやっているのは危険かも?と不安になって実装した。 (が、chibiccとはではunsigned扱ってないみたいなので、いらんのかも)

ヘッダファイルの扱い

普通のC言語のコードを、できるだけそのままコンパイルできるようにしたかったので、 ヘッダファイルを読んでコンパイルするようにした。

ヘッダファイルを利用したC言語コンパイルするにあたっては、 C言語標準ライブラリのlibcとリンクするか、libcも実装する必要がある。 すると、手加減なしで、typedefとか、static const unsigned signedなどの型定義がでてくる。 この辺読み飛ばしたり、適当にごまかしたりするのがそこそこ骨が折れた。 (大体は読み飛ばしてよい)

それだけではなくて、ヘッダファイル中には、 GCC依存?のbuiltin関数を使ったマクロなども結構埋まっていて、 そのあたりの対処もめんどくさかった。 とはいえ自分でプリプロセッサ書くよりは楽だと思うけど。

GCC互換をどこまで求めるか?

セルフホストを目的とするとき、GCCと全く同じ動きをする必要は必ずしもない。 (とセルフホストできたから言い切れるだけだけど。)

例えば、途中、ローカル変数の配置がGCCと逆順になっていることに気づいたが、 原理的には問題ないと思って、そのままにした。 (これ構造体のメンバの配置だったら当然まずいんだけど)

ただまぁ、うまくいかないときは、 寄せたほうがトラブルは少ないのも確かなので安易にGCCに寄せる判断をしがち。

ハマったところ

よく言われている、第一世代では通るテストが第二世代だとコンパイルできないということは、 やっぱりあってここが最も苦労した。そんなデバッグ方法のノウハウは普通持ち合わせていない。

人によってはまりどころは違いそうだけれど。

関数フレームサイズの決定

これコンパイラ組んでみて、とてもよく理解できたことなんだけど、深く考えずミスってしまったところでもある。

自分の場合は、スコープの取り扱いというか、関数フレームサイズの計算が間違っていて、 関数呼び出し時などに、ローカル変数が上書きしてしまうことを稀に起こしてしまっていた。 これなかなか原因に到達できず苦労した。

ただ、関数フレームのサイズ間違えても意外と動くので、普通に(C言語の)テスト書いていても検出できないことも多い。

スタックは積みっぱなしにしない。

スタックマシン?なれると便利で気軽に積んでいくんだけど、スタックに積んだものはちゃんと使いきるか、あまっても使いきれるように計画的に処理する必要がある。 スタックに積んだけどやっぱりいらなかったみたいなことはありうるが、その時ほったらかすと、 スタックポインタがずれておかしなことになることもありうる。

これも、意外と動くので、普通に(C言語の)テスト書いていても検出できないことも多い。

デバッグ方法

ここからはTIPS的なところ。

低レイヤ~で紹介されているerrorとerror_atの実装が便利なので、これで大体足りる。 他にもいくつかできることはあるので紹介する。

アセンブラにコメントを出力する。

ただ、これはアセンブラを開いて確認しないと気付かない。

printf(" # coment\n");

標準エラー出力を使う

標準出力でアセンブラを吐くので、デバッグには標準エラー出力を使う。

fprintf(stderr, "DEBUG");

GDBを使う

SEGV箇所の特定にはgdbが便利。

gdbを使ってコンパイル済みの実行ファイルを指定し,引数を指定して実行すればどの関数でSEGVしたか教えてくれる。

./gdb ./38cc
(gdb) run 引数

あまり積極的には使っていないのだけれど、ほかにもいろいろできるらしい。 ただ、第二世代のときはgdbでできることも限られるらしい。

セルフホストを終えて

セルフホストできた日は達成感とか高揚感があって一日気分よく過ごせた。 フルマラソンを歩いてでもなんとか完走したような感じに近いかな。

ただ、もうちょっと低レイヤの理解を深めるにはやっぱりOS書かないとダメかなぁと思っている。

追記

OSも書いてみたけど、まだまだわからない。とりあえずリンカの理解が必要な気がしている。

ma38su.hatenablog.com ma38su.hatenablog.com