みずぴー日記

人間の再起動ボタンはハワイのビーチにある

HaskellでOSを作る試み

函数型なんたらの集い 2014 in Tokyo - connpassで、前にやっていたajhcでmini-osを書いていたときの話をしてきた。

下書き

せっかく書いたので発表のときの下書きをそのまま載せておく。

スライドを作るときに一部変えたり、下書きの段階では図をいれてなかったりするが、おおまかな流れは変わっていない(はず)。

自己紹介

みなさんこんにちは。地方枠で参加したmzpです。 普段は名古屋でRailsプログラマやってます。

導入

突然ですが、OSを作ってみたいですよね! プログラマたるもの1度はOSとかエディタは自作してみたくなりますよね!!そして、自作するなら当然、愛してやまない関数型言語を使いたいですよね。

そうです、この発表は関数型言語を使って自作のOSを作ろうとした話です。

整理

さて、ひとことでOSといっても様々な種類があるので、それを整理しておきましょう。

  • デスクトップOS: いわゆる普通のPCで動くOS。実機でやろうとするとハードルが高いので、まずは特定のエミュレータでの動作を目指すのが定石っぽいです。「30日で作る自作OS」みたいな本もでてました。
  • 組込み用のRTOS: いわゆるマイコンなどの組込み機器で動くOS。ITRONなどが有名ですね。OSはタスク管理をメインで行なって、ハードウェア制御などにはあんまり踏みこまない。

まあ、それぞれ特徴があるんですが、がっつりやるには結構つらいですよね。

  • デスクトップOS: いわゆる普通に使えるようになるまでが大変。(もうちょっと書きたい)
  • 組込み用のRTOS: どこまでがOSでどこからがアプリなのかいまひとつはっきりしない。あんまりモチベーションが湧かない。

いずれにせよちゃんとしたOSを作るには相当の労力が必要そうです。もうちょっと手軽なゴールが欲しいですよね。

vim = OS

ここで何ができたらOSって言えるかを考えてみましょう。 プロセス管理やタスク管理ができればいいんでしょうか。それともハードウェアレイヤを抽象化できればいいのでしょうか。まあ調べればちゃんとした定義がでてくるでしょう。

ところで世の中にはこういうことを言う人が一定数いますよね。

Emacs/VimはOSである」

あ、ちょっとエディタ戦争をしたいわけではないので、vimの変りにあなたの好きなソフトウェアをいれてもらってかまわないですよ。(例: Firefoxとか)

これをつきつめていけば、こういうことが言えるのではないでしょうか。

「****というアプリケーションが動けばOSである」

もうちょっと推し進めましょう。

「Hello, worldが動けばOSである」

…まあ、いろいろ言いたいことはあるかと思いますが、先に進みましょう。

uni-kernel/exo-kernelの紹介

世の中にはこういう風に単一アプリケーションを動作させるOSがいくつかあります。例えば、Mirageというフレームワークを利用するとOCamlのプログラムをXen上で動作させることができます。Xenで走らせれるということはAmazonEC2上で動作するので、わりと動作範囲も広いですね。 他に類似のものとしてはHaskell用のHaLVM、Erlang用のErlangOnXen、Java用のOSvなどがあります。

単一アプリケーションを直接動作させることのメリットとしては

  • 余分なものがないのでセキュリティが高い
  • 余分なファイルシステムやネットワークスタックがないので効率が良い
  • OS全体に渡る最適化をかける余地がある(たぶんやってない)

などが挙げられています。実際にMirageのWebサイトは、Mirageで作らているので見てみるとよいと思います。

この仕組みを自作OSにも流用すればわりと簡単にHello,world OSが作れそうです。

Mirageの仕組み

ErlangOnXenはソースコードが公開されていないので詳細は不明ですが、あとの二つは図のようにXen Hypervisor、そのの上にCで書かれた最低限のOS部分、その上に各言語のランタイムがのっています。最低限のOSはlibc相当の機能くらいは提供してることが多いみたいです。

先程のMirageフレームワークでは、そのOS部分はmini-osと呼ばれるOSが担当しています。これはXenのソースツリーに含まれています。

主な機能としては

  • コンソールの初期化
  • ノン・プリエンプトなタスク切り替え
  • ネットワークIO

などがあります。

これ相当のものを何か関数型言語で書くことを目標にしていきます。

方針

目標が決まったので次は方針を考えます。 関数型言語でOSを記述しようという取り組み、わりとメジャーで、いくつかあります。

  • XXXX
  • YYYY
  • ZZZZ

(何か埋める)

Metasepiの紹介

そして、その一つにMetasepiというプロジェクトがあります。これはボクの次の次に発表するmasterqさんがメインで進めているもので、現実的なOS、つまりUnixライクなカーネル関数型言語で作るのが目的です。

現時点での成果として

  • NetBSDのサウンドドライバのHaskell
  • (あとはmetasepiのページから引用する)

などがあります。

λカ娘の紹介

また開発のためにHaskellコンパイラのforkや関連ドキュメントの翻訳などをやっています。詳しいことはλカ娘に書いてあります。もといこれが一番くわしい文書です。

スナッチ開発の紹介

Metasepiでは「スナッチ開発」という開発手法を提案しています。 これはフルスクラッチでOSを開発するのではなく、既存のOSの一部を関数型言語で書き直していきながら関数型OSを作り上げていく手法です。

図にあるように、既存のCで書かれた関数を関数型言語で書き直し、FFIを利用して他のCの関数を呼び出したり、呼ばれたりします。

なんかファミコン時代のゲームが名前の由来らしいですが、ファミコン世代ではないのでよく分かりません。

スナッチ開発のメリットは、既存の安定した設計を流用できるので安心感がある点と、簡単に成果を得られるのでモチーベーションを維持しやすいという点があります。

なので、ボクもこれを真似して、自作OSを作ることにしました。

ajhcの紹介

今、metasepiはATS2という言語を推してるんですが、当時はajhcというHaskellコンパイラを推してたましたし、ボクもそれを使うことにしました。

ajhcの最大の特徴は「ghcじゃない」ということです。大事なことなのでもう1回いいます。ajhcはghcとは違うHaskellコンパイラです。Haskell98の仕様は満しますが、ghc拡張は使えません。

まあネガティブな話ばかりしていてもしょうがないので、ちゃんとしたメリットの話をします。

  • Haskell -> C -> 実行ファイルという変換をしています。
  • ランタイムが小さい && 依存しているシステムコールの数が非常に少ないので移植しやすい。
  • 中間にC言語がはさまってるせいか、非常にC言語とのやりとりが楽です
  • masterqさんがアレしたので関数がリエントラントになっていて、割り込みもただしく扱えます。

ATS2については、次の次の発表で紹介があると思うのでここでは省略します。

現時点での成果

まずは現時点での成果を紹介します。

mini-osの全機能のうち、以下の機能をajhcで置き換えました。

  • AAA
  • BBB
  • CCC

(前に発表した資料があったはず。引用する)

デモ

そしてこのOSはXen上で動作し、Hello worldを出力します。 デモをやってみましょう。

  • デモ(3分)

さあ動きましたね。やったー。

つらかったこと

さて、ここからはこれを作るときにしたつらい話をしたいと思います。みなさん、つらい話好きでしょ?

前半はOSをHaskellで書くときの苦労、後半はajhcのバグをいくつか踏み抜いた話です。

ajhcの名誉のために言っておきますと、踏み抜いたバグはだいたい直してあるので今は安全です。OSを書くときの苦労は、たぶんもうどうしようもないですね。誰かなんとかしてください。

ポインタ操作(没スライド)

まず、つらいのはポインタ操作です。 ご存知の通りC言語ではポインタ操作を多用します。というか多用しないなら別の言語使ったほうがいいんじゃないですかね?

ajhcでポインタはPtr αで表わされます。これは幽霊型になっていて、aに何がきても意味は変わらないんですが、ポインタ同士を取り違えないようになってます。

そしてポインタ演算は以下のようになってます。

コードの引用

C言語と異なり型ごとの幅が分からないので、自分でオフセット計算をする必要があります。

型の恩恵あんまりないし、オフセット計算が面倒だし、なんかC言語で書いたほうが簡単そう…。

FFIの紹介

次は構造体を扱うのがつらいという話です。が、その前にajhcのFFIを紹介します。

C言語のコードを呼び出すときはこういう宣言を事前にする必要があります。

コードの引用

またC言語から呼ばれる関数を定義するときは以下のようにします。

コードの引用

このとき、HaskellとCの型は次のように変換されます。

コードの引用

この辺は普通なんですが、問題はこの表に載っていない型です。

構造体の扱い

C言語でプログラムを書くとき基本的な型だけでやることはあまりなく、普通は構造体を使います。 例えば、mini-osには以下のような構造体がでてきます。

XXX

ajhcで構造体を扱うときは以下のようにオフセットベースでアクセスするのが定石です。

YYY

ただこれがめちゃくちゃつらいです。

  • とある構造体のとあるフィールドにアクセスしたいだけでも、全フィールドのオフセットを知らないといけない。
  • ネストしてる構造体がつらすぎる。
  • typedefされていると各フィールドの幅がわからない。
  • 環境依存で決まる型だとつらすぎる。

というわけで、これは以下のようなラッパーを書いて逃げました。構造体へのアクセスはぜんぶC言語側でやって、Haskellはその関数を呼ぶだけです。

ZZZZ

IOモナド

最後はみなさん大好きなIOモナドの話です。

異論はあるかもしれないですがIO型は副作用を持つ関数とそうでない関数を見わけるためのマークとして使えます。

ただ、大半のC言語の関数は副作用を持つので、C言語とやりとりをする関数もIO型を返す必要があります。

つもりHaskellの関数が全部こんな感じにIO型を返す必要があります。

XXX
YYY
ZZZ

そして、ほぼすべてをdo記法で書く必要があるので、こんな感じのコードになってしまいます。

XXXX

Haskellで書くメリットがちょっとなくなってしまいますね。

コンパイラのバグ

ここまでがHaskellでOSを書くときにつらい話です。

最後に、踏みぬいたajhcのバグの話を2つほど紹介します。繰替えしになりますけど、これはもう直ってます。

  • FFIでCの関数を呼ぶが、そのポインタがGCに回収されてしまう。ルートオブジェクトに追加するようにして回避した。
  • GCが利用中のエリアを未使用と勘違いしてしまって、GC中に死ぬ

この辺のつらいところとしては、

  • Haskellは遅延評価されるので、どんな処理が走ったかわかりづらい
  • さらにGCは独立したステータスを持っているので、プログラム本体とは独立して動くため、再現コードが作りづらい。
  • 自作OSなのでデバッグ環境が整っていない。printfデバッグも一定量を過ぎるとバッファがあふれて固まる。

といったところがあります。

自動生成されたCのコードを読みながら、必死でパッチを書いてました。つらかった。。

まとめ

さて、まとめます。まとめますと、HaskellでOSを作ろうとしたけどつらかったよ…、という話です。 何がつらかったかと言うと

  • ポインタ演算をHaskellでやるのが面倒
  • 構造体をFFIで操作するのが面倒
  • 全部IO型になってしまってつまらない
  • ajhc自体のバグを何個かついた

ということです。

今後の課題

ajhcつらいよねって話はすでにmetasepi側でもでてて、今はATS2という言語にフォーカスを当ててるそうです。 次の次の発表、楽しみですね。