libc非依存の言語処理系(もしくは、自作CPUへの移植の苦労話)

自作CPUへの移植が大変だったのでメモ。

まず、今回ターゲットにする環境の特徴を軽く説明しておくと

Cコンパイラと入っても不完全なので、cファイルはコンパイルできてもlibcに相当するものはまだない。C89(C90)のサブセットしかサポートしていないので、ビットフィールドが無かったり、charが32bitだったり、構造体渡し・構造体返し・構造体代入がなかったりする。最終的にはこいつでOSをコンパイルする予定なのだけれど、OSもよそから持ってきたものの移植なので、いざ実際にビルドしたときに動かないとuccのバグなのかOSのバグなのか切り分けが難しくなってしまう。そういうわけでそこそこ大きめのプログラムをOSの前にコンパイルしてみたいなということになった。

そこで、こいつにpicrin(benz)を移植してみた。

移植したところ

苦労したところはいくつもあるので順不同でつらつら書いてみる。

  • benzのc99 → c89移植

    まず初めにbenzが元々c99を使いまくっていたのでそれをc89に移植した。こいつがなかなか大変だったけど…本筋とはあんまり関係ないので省略。

  • ビットフィールドを全削除

    GC周りで一箇所つかっていたので削除。そもそもビットフィールド自体かなり処理系依存なのでまあ仕方ないかなというところ。

  • uccがfloat/doubleのコンパイルに対応していないのでNO_FLOATモードを作成。マクロでスイッチ。

    uccは(少なくとも移植を始めた当初)はfloatをサポートしておらず、実際に値として扱えるものが整数かポインタしかなかった。benzはもちろんscheme処理系なので浮動小数点数を扱えるのだけれど、それらが使用できない。というわけでマクロでfloatをオフできるようにした。幸いにもschemeの規格的には実はintのみをサポートする処理系でも正しいscheme処理系らしい。まあ、FPUがないCPUだってあるわけでこれも移植性を上げるためには必要かなというところ。

  • malloc/freeは外からアロケータを与える形に変更。

    これはluaとかでお馴染みの機能。そもそも今までサポートしてなかったのがよくなかった… 内部で使うhash tableがガンガンmallocを使いっていて、そいつらにカスタムアロケータを設定するためにhash tableが1word太ってしまった。あまり嬉しくないけどひとまずは仕方ない。そのうち直す。

  • 値の内部表現を変更

    schemeは動的型付言語なので全ての種類の値がひとつの形式にboxされている。いままでのbenzでは (1) シンプルなstructで値を表現するboxingと、(2) nan-boxingと呼ばれるx86上でのみ使えるboxingの2種類のboxing をビルド時に切り替えられるようにしていた。しかし残念ながらuccには構造体渡しがないため(1)が使えず、(2)も当然使えない。というわけで、値をunsigned longの中にエンコードするword-boxingというboxingの形式を新たに作成した。よくあるタグ付きポインタみたいな形式だ。intの範囲が229-1〜-229になってしまうけれどまあこれも仕方ない。

  • GCをチューニング

    GCその他のメモリ初期サイズを変更。どうせ足りなくなると自動的にreallocされるものが大半だから変えてもschemeレベルの挙動に変わりはない。4MBしかないメモリを上手く使うための必要な変更。

  • libc相当の関数を全て自作して置き換え

    memcpyやstrcmpも無いので、自作。ビルド時に外部から見つけさせるか、自前のものを使うかを切り替えられるようにした。とはいえlibcの中でもいくつかの関数はアセンブリが必要になるので、そいつらはmallocと同じように外部から与えてもらうようにした。

  • setjmp/longjmpは相当の関数をやはり外部から与える。

    上述の通り。さすがにこれらはアセンブリで書いてもらった。他にもabortを使用していたのでそれも合わせて外から与えてもらうように。

  • 標準入出力に相当するFILEポインタもどき(xFILE *)も外から与える。

    今まではstdioがあることを前提として直接stdin/stdout/stderrを使用していたので、外から与えるAPIに変更。

以上の変更で「libbenz.soは」完全にlibc非依存になった。使うときにはどうしてもそのプラットフォーム依存の関数を使わなければいけないけど、benzからしてみれば知ったこっちゃない。結果benzを使ったプログラムがシミュレータ上で動いて、シリアルにS式を投げると評価結果のS式が返ってくる何かが出来上がった。元々の目標だったuccのバグも修正できたしbenzの移植性もかなり上がった。なにより自作CPU上でGCが動くっていうのはかなり価値が高い。なにか面白い応用を考えたいところ。ちなみにここでの変更は以下のリポジトリにまとまっている。まだ公式リポジトリにはバックポートされていないけど暇ができた時に誰かがやってくれるんじゃないかな。