てけもぐ Tech 忘備録

Cのコンパイラを自作した時の話

対象読者

コンパイラ自作とか同じ様なところにひっかかる方がいれば

解決すること

コンパイラ作成に限らず、同じ様なお題目でハマるのを避ける

内容

低レイヤを知りたい人のためのCコンパイラ作成入門 を読んで、自分もコンパイラを作りたくなりまして。忘れるともったいないので、セルフホストした時に気づいたことを幾つか書いておきます。(アセンブリまで。浮動小数点などセルフホストに必要でなかったものは断念)。

  • スタックマシン最高(ただの感想)
    • 文脈自由文法とか再帰下降構文解析も最高なんですけど、スタックマシンはその単純さに対して処理の畳込み具合とかが最高
  • 結局のところ、アドレス、型の大きさ、変数への offset、値の解釈。この辺。
  • BNF から再帰下降パーサの書き方の理解は大事なので、腰を落ち着けて。
  • 当たり前だけれど、関数の入力は複数、出力はひとつ。出力がひとつなのは、スタック扱うとこれがとてもありがい話。
  • 関数の本体はテキストセグメントに置くけれど、ローカル変数はスタックの最初(x64の場合は番地の高い方)にまとめて領域を確保する。関数を呼ぶ度にスタックは用意されるその領域へのアクセスは、スタックの始まりを保持するベースポインタ(rbp)と、スタックポインタ(rsp)で行う。これらが変わることで幾つでもスタック領域を確保したり操作したり出来るようになる。再帰呼び出し等も特別な仕組みは必要ないのって素晴らしい。
  • 関数呼び出しはかなりめんどくさい...。「低レイヤを..」にも書いてある様に、x86-64 の関数呼び出しの際には、スタックポインタ(rsp)が16の倍数になってないといけません。これが下の幾つかと一緒になるとすごくめんどくさい..
    • 引数が 6 個まではレジスタ経由で、それ以上はスタックに入れての呼び出し
    • 可変長引数
    • 引数の中での他の関数を呼び出し
    • 一番は、構造体(ポインタではなく)を引数に入れて呼び出す機構(結局インプリせず...)。
      • ある程度の大きさまでは引数のレジスタに詰め込んで渡し、それ以上はスタックに積む。
  • 上の関数呼び出し時の 16bit アライメントには、チェックコードを関数の呼び出し前と呼び出し後に挿入。16bit アライメントエラーはどんな関数でも起こるわけではないので、関数呼び出しが進んでからだと発生箇所が特定しづらい。
  • スタックをひとつ返す操作(式)と、返さない操作(文)の違いが超重要。スタックに積んだ値は必ず回収せねばならないので、セミコロン(;)を付けて文に昇格させる大切さが分かる。じゃないと関数呼び出しの際の 16bit アライメントにひっかかったりする。
  • C言語の配列の話の投稿でも書いたけれど、配列の扱いは要注意だった。1次元はいいけれど多次元と配列と構造体の操作が合わさるとなかなか面倒。
  • 当たり前だけれど、アドレスを一度 dereference して内容を取ってしまうと後戻り出来ない。
  • ぶっちゃけ、構造体・共用体の関数への値渡しや等号による代入は要らないと思った。このサポートって何か理由があったのだろうか...。ポインタ経由でいいと思う。
  • 何でもそうだが、テストコードが非常に非常に重要。リグレッションテストを手軽に出来るようにしないと、コード修正時の他箇所の破壊に気づかなかったりで、途中で諦めていただろうと思う。
  • これもコンパイラの自作に限った話ではないけど、デバッガーで頑張る前にチェックコードを入れること。デバッガーを使うと、デバッガー上でのコード追跡とコード上でのコード追跡と、2つやることになるけれど、チェックコードを使うと即場所が特定出来る。
  • 「低レイヤを...」の初出の例の通りに、自分はノード構造体等をリンクリストを使って最後まで通したが、ある程度進んだら中核となるデータ構造はそこそこ柔軟なものにした方がいいと思う。単純なままだと何かとひっかかる。

こんなところかな...。もっとあった気がするけれど、気づいた時に書かないとやっぱり忘れる。

この、低レイヤを知りたい人のためのCコンパイラ作成入門のサイト、素晴らしいです。

Tags