この記事は
の 21 日目の投稿です。(詰め込んだ)
最近どうも Zig という言語がアツいという空気感を Twitter など見ていて感じています。TLS 1.3 が Zig で実装されたり、Zig で自作コンテナランタイムが自作されたり、Zig で新しい JavaScript ランタイムが実装されたりと、色んな分野で Zig が試されている段階のようです。そこで自作 OS でも Zig を試してみようということで、Zig で自作 OS プロジェクトを始めました。
一から OS を設計するのは大変なので、今回は既存の小さな OS である xv6 を Zig で再実装するという方針で始めました。
xv6
Wikipedia より: https://ja.wikipedia.org/wiki/Xv6
xv6は、ANSI Cによる、Sixth Edition Unixのマルチプロセッサx86システムへの再実装である。xv6はMITにおけるオペレーティングシステムエンジニアリング (6.828) コースにて、教育を目的として使われている。
コードの行数は 7000 ~ 8000 とかなり小さめで、かつ Unix ライクな OS としての機能を一通り揃えています。
- プロセス管理
- ページング
- 割り込み・システムコール
- ファイルシステム
- マルチプロセッサ
途中経過
ソースを GitHub で公開しました: https://github.com/Saza-ku/xv6-zig
ほんとはこの記事を書いているころには xv6 in Zig を完成させているつもりだったんですが、ぶっちゃけ全然間に合いませんでした(笑)
せっかくなのでできているところまで発表しようと思います。
- カーネル内の動的メモリ管理
- タイマー割り込み
- VGA テキストモード・シリアルポートによる画面表示
- キーボード入力
- ロック
- ディスク (IDE) ドライバ
デモはこんな感じ。見た目的には好きな文字を画面にポチポチできるところまでです。
今はディスク上のファイルシステムから ELF ファイルを読み出してプロセスとして実行するところを実装しています。
Zig について
Zig で自作 OS やっててどうなんという話とか、Zig で自作 OS をやる上での知見を共有しようと思います。これから Zig で自作 OS をやってみたいという人の参考になればと思います。
所感
「モダンな言語機能を持った、ちょっと型がしっかりしている C 言語」という感じです。
まず言語機能についてですが、エラー処理やオプショナル型があるというのが良いです。特にオプショナル型のおかげで NULL ポインタを扱わずに済むのがかなり嬉しいです。(NULL ポインタを生成しようとするとコンパイルエラーが発生するという検査付き) このへんは Rust の書き心地に近いですね。あとは defer とかがあって Go っぽさもある。
型については、暗黙的なキャストができないようになっていたり、ポインタ型にポインタ演算ができる型とできない型があったりと、C 言語よりはしっかりしているという印象です。
また、触ってみると意外とまだまだ発展途上の言語だというのがわかりました。まだメジャーバージョンは 0 だし、パッケージマネージャがなかったり Language Server が貧弱だったりと開発体験も正直良いとは言えません。
ビルドシステム
C や C++ では Make、CMake といったビルドシステムを使うのですが、Zig には標準のビルドシステムが用意されています。これがちょっとおもしろくて、Makefile みたいな記述を Zig 言語で記述します。
記述の仕方としては Makefile によく似ていて、実行したい操作を Step
というものでまとめ、各 Step
同士に依存関係を設定することができます。
リンカスクリプトやアセンブリファイルを入れたりすることができるのも嬉しいところです。
ただし、このビルドシステムのドキュメントが薄すぎて、何ができて何ができないのかよくわからないというのが困ったところです。
FixedBufferAllocator が残念
Zig はメモリ管理を手動で行う必要があるのですが、C 言語で言うところの malloc や free といったメモリ管理関数を Allocator
というインターフェースとして定義しています。(https://ziglearn.org/chapter-2/#allocators)
これにより、メモリ管理ロジックの内部実装を差し替えるみたいなことができます。
標準ライブラリからいくつかの Allocator
が用意されていて、その中で FixedBufferAllocator
というものがあります。
これはプログラマがあらかじめ確保しておいたメモリ領域をいい感じに管理してくれるという Allocator
です。
https://ziglearn.org/chapter-2/#allocators より
test "fixed buffer allocator" {
var buffer: [1000]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
const memory = try allocator.alloc(u8, 100);
defer allocator.free(memory);
try expect(memory.len == 100);
try expect(@TypeOf(memory) == []u8);
}
これは OS を書くときにとっても便利なライブラリだと思ったんですが、内部実装を見てみるとそのアルゴリズムがナイーブすぎてとても使えるものではありませんでした (Zig 0.10 時点)
どんなアルゴリズムかというと、用意されたメモリ領域をスタックとして管理しています。つまり free された領域が一番最後に alloc したものであれば pop し、そうでなければその領域を再利用しないという感じです。つまりメモリリーク的なことが起こりまくります。
SSE 等の CPU 機能を使わないようにする必要がある
コンパイルされたバイナリが SSE 等の OS 依存の命令を生成しないように build.zig
で設定する必要があります。Zig Bare Bones - OSDev Wiki が参考になります。(← このコード全体としては最新の Zig でコンパイルできないので注意が必要です。)
const features = Target.x86.Feature;
var disabled_features = Feature.Set.empty;
var enabled_features = Feature.Set.empty;
disabled_features.addFeature(@enumToInt(features.mmx));
disabled_features.addFeature(@enumToInt(features.sse));
disabled_features.addFeature(@enumToInt(features.sse2));
disabled_features.addFeature(@enumToInt(features.avx));
disabled_features.addFeature(@enumToInt(features.avx2));
enabled_features.addFeature(@enumToInt(features.soft_float));
const target = CrossTarget{
.cpu_arch = Target.Cpu.Arch.i386,
.os_tag = Target.Os.Tag.freestanding,
.abi = Target.Abi.none,
.cpu_features_sub = disabled_features,
.cpu_features_add = enabled_features
};
終わりに
これからですが、ひとまず xv6 を一通り Zig に移して、それが終わったらそれをフォークしてオレオレ自作 OS を Zig で書いていきたいなと思います。NIC ドライバを実装してネットワークに繋げたり、RISC-V に対応したりしたいなと思います。その時にまた Zig で自作 OS をやる知見をまとめようと思います。
Zig はまだまだ発展途上の言語ですが、割と広く認知されていて注目度の高い言語のような気がしますし、文法がシンプルだったり C 言語から移行しやすくなっていたりと、これから低レイヤの分野でどんどん広まっていく可能性は十分ありそうだと思います。Rust と競合する形で採用されていくのかなぁと想像しています。
皆さんもぜひ趣味プロジェクトで Zig 使ってみてください。あえて発展途上の言語を触るの結構楽しいです。