Cコンパイラを作っていると、セルフホストを目指した瞬間、今まで味方だったlibcとの対決を迫られる熱い展開になります
前提
- 低レイヤを知りたい人のためのCコンパイラ作成入門
- C言語でCコンパイラを書いてセルフホストを目指す場合の話
- x86_64のLinuxとgcc
- 上記の環境に限った話かどうかについては明記しない予定
libcとの付き合い方
libcを使わない
- つまり自分でlibc(の一部)を書きます
- ファイルIO(標準出力も含む)とメモリ操作(例えばalloc/cpy/cmpな各種)など、いくつか必須っぽいものがある
- libcにはシステムコールラッパーとしての役割がある
- 自分でlibcを実装する場合にはシステムコールをアセンブリで出力する
- システムコールのABIは、C言語の関数呼び出しABIとは異なる
- 引数と返り値の扱いについてはABIが同じOSもあるが、x86_64 Linuxでは異なるはず(未確認)
- いずれにせよ、通常の関数のように
call
では呼べない
- 必要なものだけ書くなら数は多くないはず(難易度は不明)
- ちなみに、私がセルフホストを達成した時点で使っていたlibcの関数その他は30個ぐらいでした
- 必要最低限な自作libc代替を使ったCコンパイラ作成は、制限プレイとして楽しそう
libcの実装とヘッダファイルを使う
- まず、「gccによる(自作コンパイラの)コンパイル」では普通(?)のCプログラムとして(例えば)
#include <stdio.h>
します - 「自作コンパイラによるコンパイル」では、大きく以下の2パターンがあります
「自作コンパイラによるコンパイル」でlibcのヘッダファイルを使う場合
-
#include
や各種マクロの展開などは、gccのプリプロセッサに任せることができますgcc -E ${BEFORE} > ${AFTER}
- プリプロセッサを作りたい人はここで頑張ります
- ただし、プリプロセス後のlibcのヘッダファイルには様々なものが含まれます
- 自作コンパイラで対応予定だけど未実装のもの(例えば、
struct
) - 自作コンパイラで対応予定のないもの(例えば、
float
unsigned long int
) - gcc拡張っぽいもの
- 自作コンパイラで対応予定だけど未実装のもの(例えば、
- この様々なものに対応するか無視する必要があります
- この対応及び無視の負担がどの程度なのかは不明
「自作コンパイラによるコンパイル」でlibcのヘッダファイルを使わない場合
-
呼び出そうとしている関数が実際に存在するかどうかのチェックを自作コンパイラで行わず、リンカに任せます
-
つまり以下のようなソースを自作コンパイラの入力とします
int main(void) { // 関数宣言も#includeも無しでいきなり呼び出す printf("Hello, world!"); return 0; }
-
自作コンパイラで関数宣言を扱わない段階までは、これが簡単です。
libcの実装を使うが、libcのヘッダファイルを使わない
関数宣言
- C言語は互換性の問題からか、関数の引数をチェックしない関数宣言が可能です
- 例えば
int printf();
という宣言を用意すると、自作コンパイラでも扱いやすいし、gccも正当なものとして扱ってくれます- libcで定義されているものを型違いで再宣言すると、gcc等はwarningが出ます
その他のテクニック
- 複雑な構造体がポインタで扱われていて、自分で直接操作しないのであれば。
void *
として扱うのが簡単かもしれません。具体的には、FILE *
成果
Cコンパイラのセルフホスト達成した!https://t.co/hhoBGS3kJo
— ぎ (@guignol_ninja) March 28, 2020