NimでOS開発したいねという話

これは自作OS Advent Calendar 2017 の10日目の記事です.遅刻です.

アアアアアすみません!!!!!

では謝罪も終わったところで(こいつ反省してんのか?),Nimで自作OSしたいねーっていう話をしていきたいと思います.

そもそもNimってなによ?

プログラミング言語のひとつで,最近僕が気に入ってるやつです.

Nim - Wikipedia

構文とかはかなりPythonっぽくて,インデントでブロックができます. フィボナッチ数列とかはこんなかんじ

proc fib(n: int): int =
  if n < 2:
    return n
  else:
    return fib(n-1) + fib(n-2)

echo(fib(30))

見た目はPythonっぽいですがintとか出てきてますね.そう,Nimは静的型付け言語なんです.

なんでNim?

さて,ここまで来て「え?そんなPythonみたいなやつどうやってベアメタルで動かすの?mrubyみたいなのあるの?*1」と思われた方もいるかもしれませんが違います.

なんでかというと,そもそもNimはコンパイル言語だからなんですね. コンパイル言語なので,コンパイルするとバイナリが出てきます(それはそう).

しかし,Nimのコンパイル手順はちょっと特殊です.コンパイルしても直接オブジェクトファイルとかは出てきません. じゃあなにが出てくるのかというと,なんとビックリ,C言語のコードが出てきます.面白くないですか?

そのため,Nimをコンパイルしてバイナリを出力する手順は,

Nim ==nimコンパイラ==> C ==Cコンパイラ==> バイナリ

というかんじになっています. コンパイラというよりトランスパイラに近いのかも.

Cだけではなく,C++/Objective-C/JavaScriptなどにも変換出来るらしいですね.すごい.

では,実際どんなかんじなのか,まずはちょっと普通に使ってみましょう.

nim-lang.org

ここの記事そのまんまなのでインストールは割愛します.

では,初めににハローワールドしてみます.

echo "Hello, World!"

これをhello.nimというファイルに保存します. で,コンパイル言語なのでコンパイルして実行形式のバイナリを作ることができます.

$ nim compile hello
Hint: used config file '/etc/nim.cfg' [Conf]
Hint: system [Processing]
Hint: hello [Processing]
CC: hello
CC: stdlib_system
Hint:  [Link]
Hint: operation successful (10984 lines compiled; 1.404 sec total; 17.938MiB peakmem; Debug Build) [SuccessX]

compileオプションはcとしてもOK.((ここをcppとするとC++に,jsとするとJavaScriptに変換出来ます))

これで,helloという実行形式バイナリが出来ました.

$ file hello
hello: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=184748d3c78ff375408fdf598d6217f3256f5721, not stripped
$ ./hello
Hello, World!

しかし,先程書いたように,Nimは直接バイナリなりアセンブリなりは吐かず,代わりにCコードを生成します. そのCコードはどこに行ったのかというと,nimcacheというディレクトリの下に生成されています.

$ ls ./nimcache
hello.c  hello.json  hello.o  stdlib_system.c  stdlib_system.o

hello.c,stdlib_system.cというCコードが生成されていて,それをCコンパイラコンパイルしたhello.o,stdlib_system.oというオブジェクトファイルが出来ているのがわかります*2

stdlib_system.cは共通のAPIラッパーみたいなやつなので,生成されたプログラムの本体はhello.cです.

hello.cを見てみると,ちょっとゴチャゴチャしています*3

int main(int argc, char** args, char** env) {
        cmdLine = args;
        cmdCount = argc;
        gEnv = env;
        NimMain();
        return nim_program_result;
}

main関数はこんな感じです. コマンドライン引数とかをグローバル変数にとっておいて,NimMainという関数を呼び出しているだけですね.

...と思ったら,んんん!?main関数に3つ目の引数がある!?

調べてみたら,ここから環境変数が得られるらしいですね.初めて知った...

moge32.jugem.jp

じゃあNimMainはどうなってるのかというと,PreMainやらinitStackBottomWithとかいう関数を呼んだ後,NimMainInnerという関数が呼ばれ,そこからNimMainModule を呼んでいます.

STRING_LITERAL(TM_xLHv575t3PG1lB5wK05Xqg_2, "Hello, World!", 13);
〜〜〜
NIM_EXTERNC N_NOINLINE(void, NimMainModule)(void) {
        nimfr_("hello", "hello.nim");
        nimln_(1, "hello.nim");
        printf("%s\012", ((NimStringDesc*) &TM_xLHv575t3PG1lB5wK05Xqg_2)? (((NimStringDesc*) &TM_xLHv575t3PG1lB5wK05Xqg_2))->data:"nil")
;
        fflush(stdout);
        popFrame();
}

ゴチャゴチャはしていますが,printf"Hello, World!"を表示しているのが分かります. この関数が変換されたNimコードの本体のようです. なので,生成されたCコードを確認したくなったらここらへんを見ましょう.

というわけでやってみる

さて,これでNimがちゃんとCに変換されてprintfで文字列表示をしているのが分かりましたが,本題はここからです. 本題は何だったかというと,NimでOSを作りたいんです. 要するにNimをライブラリとかが全然ない環境でも動かしたいんです.

じゃあどうすればいいか?ここまでくれば簡単ですね. Nimコンパイラが生成したCコードで呼び出している基本的な関数を専用のやつに置き換えてしまえばいいんです!

とはいっても,printfやらputsをいちいち作るのは面倒なので,もうちょっと楽な手段を使います. 実はNimは,デフォルトでCに変換するだけあって,Cとの連携がしやすくなっています.具体的に言うと,インラインアセンブラみたいにCが直接書けたり(emit),Cの関数をNimの関数として呼び出すことが出来ます.

Nim Backend Integration

詳しくは公式のドキュメントを見ましょう.

で,こんな感じにやればベアメタルでNimが使えるのでは?と思って試行錯誤してなんとか標準Cライブラリ無しでフィボナッチを計算して出力してみたというのが,以下のリポジトリです.

github.com

しかし,このrepoをよく見てもらえると分かると思うんですが,なんか結構色々やってますね. Cのコードが66%もあります.

何故こんなことになったのかというと,あのstdlib_system.cがかなり曲者でした. 最初はstdlib_system.cを自作して置き換えてやればいいかなあとか思っていたんですが,バージョンによって少しづつ変わるみたいなのと,GCの処理などがあったので弄るのをやめました. では具体的には何をやったのかというと,stdlib_system.cでincludeしているstdio.hとかのヘッダを自作して,そのうちの必要な関数をちまちまと実装しました. そして,標準Cライブラリを使っていないので,アセンブラでwriteシステムコールのラッパーも書きました.*4

まあ手抜き実装なので,エラーが発生したときに呼ばれるsignalとかはマクロで虚無に書き換えています.

でも,これはあまりにも面倒ではないですか? まあもちろん,mrubyをハイパーバイザに移植とかよりは断然楽なんですが,ここからこれを使ってOSを書いていくとなると,環境構築のコストが高いです. そして,僕が作ったものも手抜き実装なので色々と抜けているところがあります.これならC++やRustでいいじゃん...ってなっちゃいますよね.

Rustとか特に

https://os.phil-opp.com/

とか出てますし.*5

「は?ベアメタルでNimやるのダルすぎ.C++/Rust使うわ」

と思うじゃん???

いやね,僕もそう思ったんですよ. でもね,違ったんですよ. あっ待ってそこの人!まだチャンネル変えないで!ここからすごいの!!! 衝撃の真実はCMの後で!

CM

github.com

えーっと,x86エミュレータを作ってm...作ってるはずです. いや進捗出てなくて申し訳ない,というか学校関連特に夏休みあたりがアすぎたはい僕もやりたいんですよでも時間がえ?時間は無かったら作るもの?それはそうはい作ります!作るぞ!!!(宣言)

衝撃の真実的なsomething(575)

はい,CM終わり(CMか?)

CMが終わったので予告していた衝撃の真実です!!!

結論から言いましょう!!!

先程僕がやったことはすべて無意味です!!!

いやあ,やってて気付いたこととかもあるので個人的には無意味ではなかったので良かったんですが,超絶楽な他の方法がありました.

気がついたきっかけはNimのコンパイルオプションを調べていた時のことです. 適当にスクロールしていたら,"embedded"という文字列を見かけた気がして,見たら,あったんです.

Nim Compiler User Guide

Nim for embedded systems

...え?embedded?組み込み?組み込み向けに使えるの????

The standard library can be avoided to a point where C code generation for 16bit micro controllers is feasible. Use the standalone target (--os:standalone) for a bare bones standard library that lacks any OS features.

え?--os:standalone?なんですかそのいかにもって感じのオプションは!?

色々調べてみたら,CPUとかGCの設定も出来ることが分かりました. また,

using Nim / Nimrod for micro-controllers (embedded) - Nim Forum

を見てみると,

Check out nimkernel for an example of using Nim to program bare metal.

ん?????

github.com

え??????

というわけで,僕がやろうとしていたことは既にやられていたのでした.

そして,このrepoでは僕が先程やっていたようなことはやっていません!

やってみた2

さて,超簡単な解決策が見つかったので,今度こそNimでOS(っぽいもの)を作っていきましょう!

基本的には(nimkernel)GitHub - dom96/nimkernel: A small kernel written in Nimと似たようなかんじでやっていきます.

まずは,main.nimを作ります.

proc main() {.exportc.} =
  return

これがmain関数になります..exportc.というのはこの関数をCの関数にする指定です. このmain関数はブートローダーから呼ばれるので,この指定を付けておきます.

で,あとはさっきの--os:standaloneとかのオプションをつけてコンパイルして色々弄っていけばいいんですが,いちいちコンパイルオプションを打つのは面倒です. こういう時はMakefileを使うのがよくある手ですが,Nimでは他の方法があります. main.nim.cfgというファイルを作って,そのファイルにコンパイルオプションを列挙するだけでOKです. コンパイルオプションは,--os:standaloneの他に,--gc:none,--deadCodeElim:onとかも付けておくと良いです.

あと,デフォルトのmain関数を作らない--noMain,変換後のCコードをコンパイルしたオブジェクトファイルをリンクしない--noLinkingなどもお忘れなく.

では,ちょっとコンパイルしてみましょう.

$ nim c main
Hint: used config file '/etc/nim.cfg' [Conf]
Hint: used config file 'main.nim.cfg' [Conf]
Hint: system [Processing]
lib/nim/system.nim(2708, 11) Error: cannot open '/home/sksat/prog/nim/nim-os/panicoverride'

失敗してしまいました.どうやら,panicoverrideというファイルが無いと言っています.

ここで,もう一度 Nim Compiler User Guide を見てみると,

For the standalone target one needs to provide a file panicoverride.nim. See tests/manyloc/standalone/panicoverride.nim for an example implementation. Additionally, users should specify the amount of heap space to use with the -d:StandaloneHeapSize= command line switch. Note that the total heap size will be * sizeof(float64).

とあります.panicoverride.nimを作らないといけないみたいですね.

とりあえず空のファイルを作ってコンパイルしてみると,

$ nim c main
Hint: used config file '/etc/nim.cfg' [Conf]
Hint: used config file 'main.nim.cfg' [Conf]
Hint: system [Processing]
lib/nim/system.nim(2714, 12) Error: undeclared identifier: 'panic'

panicという関数は必須のようです. 同様に,rawoutputという関数も必要なようなので,とりあえず空の関数を作っておきます.

proc rawoutput(s: string) =
  return

proc panic(s: string) =
  return

で,またコンパイルしてみます.

$ nim c main
Hint: used config file '/etc/nim.cfg' [Conf]
Hint: used config file 'main.nim.cfg' [Conf]
Hint: system [Processing]
Hint: main [Processing]
Hint: gcc -c  -w -w -I$lib -ffreestanding -O2 -Wall -Wextra  -I/usr/lib/nim -o /home/sksat/prog/nim/nim-os/nimcache/main.o /home/sksat/prog/nim/nim-os/nimcache/main.c [Exec]
In file included from /home/sksat/prog/nim/nim-os/nimcache/main.c:10:0:
/usr/lib/nim/nimbase.h:482:13: エラー: 配列 ‘Nim_and_C_compiler_disagree_on_target_architecture’ のサイズが負です
 typedef int Nim_and_C_compiler_disagree_on_target_architecture[sizeof(NI) == sizeof(void*) && NIM_INTBITS == sizeof(NI)*8 ? 1 : -1];
             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Error: execution of an external program failed: 'gcc -c  -w -w -I$lib -ffreestanding -O2 -Wall -Wextra  -I/usr/lib/nim -o /home/sksat/prog/nim/nim-os/nimcache/main.o /home/sksat/prog/nim/nim-os/nimcache/main.c'

あー,これはターゲットが32bitなのにコンパイルに使用しているgccが64bit向けだから起きてるエラーですね.nimkernelみたいに別のコンパイラを使ってもいいんですが,面倒なので-m32オプションをつけてしまいましょう. Cコンパイラコンパイラオプションを書き換えるのは--passcオプションで出来ます.

$ nim c main
Hint: used config file '/etc/nim.cfg' [Conf]
Hint: used config file 'main.nim.cfg' [Conf]
Hint: system [Processing]
Hint: main [Processing]
Hint: gcc -c  -w -w -I$lib -ffreestanding -O2 -Wall -Wextra -m32  -I/usr/lib/nim -o /home/sksat/prog/nim/nim-os/nimcache/main.o /home/sksat/prog/nim/nim-os/nimcache/main.c [Exec]
Hint: gcc -c  -w -w -I$lib -ffreestanding -O2 -Wall -Wextra -m32  -I/usr/lib/nim -o /home/sksat/prog/nim/nim-os/nimcache/stdlib_system.o /home/sksat/prog/nim/nim-os/nimcache/stdlib_system.c [Exec]
Hint: operation successful (5319 lines compiled; 0.254 sec total; 3.758MiB peakmem; Debug Build) [SuccessX]

--os:standaloneオプションを付けているので,stdlib_system.cでincludeしているのはnimbase.hだけになっています.素晴らしい.

さて,これでmain関数が出来たので,あとはmain関数をブートローダーから呼び出してあげればいいですね.

今回はちょっと遅くなっちゃったのでnimkernelのboot.Sを拝借しました.

boot.Sをアセンブルしてやると,必要なオブジェクトファイルが揃います.

$ as --32 boot.S -o boot.o

後はこれらをリンクして,バイナリを作ります.

$ gcc -T linker.ld -o main.bin -m32 -ffreestanding -nostdlib boot.o nimcache/main.o nimcache/stdlib_system.o

あとはQEMUで起動してみるだけです.((今回はマルチブート仕様に則ったバイナリになっているので,-kernelオプションで起動できます.))

$ qemu-system-i386 -kernel main.bin

f:id:sksat:20171212203420p:plain

まだ何もしていないので何も起こりませんが,無事起動出来ているようです.

...と思ってnimkernelの画面描画のコード持ってきて動かしてみても失敗しました....何故...というかこれmultiboo 2じゃないんですか...

まあ,自分でコード書けってことですね.

あと,nimkernelについてはそもそもビルドが通りませんでした.これについてももうちょっと調べてみます.

ということで不完全燃焼なかんじですが今回はここまでにします.期末試験中だし.

*1:そういうのが気になる人はベアメタル,というかハイパーバイザ上でmrubyを動かしてブイブイ言わせてるchikuwaitさんというガチプロがいるので調べててみると面白いかも?

*2:hello.jsonコンパイル/リンクのコマンドの設定みたいなやつです

*3:自動生成なのでこればっかりは仕方ないですが,かなりマクロを多用しているからです.まあCコンパイラが最適化してくれますし,基本的にこれらの生成されたCコードを見ることはないので良しとしましょう

*4:setjmp/longjmpは面倒だったのでGCCにくっついてきてるやつを使いました

*5:Rustもやってみたいとは思っている