この記事は"Aizu Advent Calendar 2015"25日目として書かれた.
はじめに
ファイナルベント担当のもぷりです.
今年のAizu Advent Calendarも自作OSについて書いていこうと思います.
ゆっくり書いていたらIntroductionがだいぶ長くなってしまったので読み飛ばしてもらっても全然問題ありません.
今までの自作OS
ソースコードはすべてgithubに公開してある.
mopp/Axel
今までは全て、C言語とアセンブラ(nasm)を使って開発を行ってきた.
対象アーキテクチャはx86_32のみで、不完全なものも含むが、物理/仮想メモリ管理、割り込み、ユーザプロセス、システムコール、ATAデバイスの読み書き、FATファイルシステム対応などなど
はりぼてOSにならって、こんな感じの画面をつけたりもした.
(こんなところで書くのも何だけれども、OS自作本は本当に偉大だと思う)
コメント抜きのコード行数で1万行程ある、我ながらよく書いたものだと思う.
が、今夏、ついに思い立ってRustでOSを実装し直すことにした.
加えて、それの先駆けとして、いろいろ調べたことをQiitaの記事としてまとめてある.
Introduction Rust for Creating Your Operating System
どうしてRustなのか (どうしてCではないのか)
どうして、Rustなのかを書く前に、どうして今までCで書いてきたかを述べておく.
一般的にOSを記述する言語といえばもちろんCだと思う.
(と言うと、いや別にC++とかDとかでも書けますよねって思う人もいると思いますが、まあ待ってください.)
数十年前のUnixに始まり、様々なOS(Linux, Minix, FreeBSD)がCで書かれてきた.
Githubなどに存在する自作OSの多くもCで書かれている.
でも、今は2015年であるし、OSはCで書かれねばならないという理由は無いはずだ.
自分自身で考えては見たが、それらしい理由は思いつかなかった.
ぶっちゃけ、個人的な意見としては、指定したABIとアーキテクチャ向けのオブジェクトファイルが生成できる言語ならば、あとは開発者の力量次第ではと思っている.
だから、闇のC++パワーをお持ちならばどうぞ存分に発揮してくれればいいし、D言語君だってウェルカムだ.
では、なぜ、Cで今まで開発をしてきたのか大きく分けて2つの理由がある.
- 自分の中で一番書けると思っている言語がCだから
- 参考にするWebサイトや書籍のほとんど(書籍に限れば全てと言っていいかもしれない)Cで書かれているから
また、前述したが、オープンソースになっているOSがCで書かれていることが多いため、非常に参考にしやすい.
(とどのつまり、コピペして試しやすいということ)
結局のところ、この記事を書くにあたって考えなおしてみたところ、自分の場合は、手持ちでなんとかなるならなんとかしてしまおうということだったのだと感じた.
と、以上の理由でC(とアセンブラ)で開発をしてきた.
しかし、段々とつらみを感じてきた.
Rustで書きなおそうと思い立った一番の理由はこのつらみから来ている.
自分が感じたC言語のつらみを以下に列挙してみる.
- 標準ライブラリにはListもQueueもStackも無い
- ジェネリクスが無い
- なので、処理の抽象化がきれいに書けない
- 継承などももちろん無い
- エラー処理を考え始めるとコードが爆発する
- アーキテクチャ依存の処理切り替えがマクロオンパレード
- テストコードを書いて管理するのも楽ではない
この一覧を見て、いやいや、Cでもできるじゃんって思った人がいたら、"Cハッカー症候群"(下記リンク参照)を疑ったほうがいいかもしれない.
確かにCでも出来る、githubやstackoverflowを探せばいくらでもbest practiceが出てくるだろう.
でも、そういうことではないのだ.
端的に言えばもっと楽がしたい(じゃあ、なんでOSなんて作ってんだってのは聞かないでください)
自作OSで言えば、「よーし、ListとQueueを自分で実装するぞー楽しいー!」ではなく、「ああー、メモリ管理が出来る!!!プロセスが走る!!I'm GOD!!!」という楽しみ方をしたいわけだ.
また、Cばかり書いていては、新しいパラダイムなどなどについて行けなくなる.
なので、一応ある程度の自作OSを作成し、少しばかりの知見も溜まってきたところで、
丁度、自作OSがいくつか作られているモダンな言語であるというRustの噂を耳にして、書き直すに至った.
以下に非常に面白い記事を参考として上げておく、ぜひ2つセットで読むことをおすすめする.
特にふたつ目にある"Cハッカー症候群"というのは非常によい言葉だと思う.
本の虫: 記録からみるLinus TorvalsのC++観
スラド: taro-nishinoの日記: C++についてLinus Torvaldsへの反論
Raspberry Pi Zero
記事名にも入っているから察しがつくとは思うが、Raspberry Pi Zeroを入手したので、前々から考えていたARMアーキテクチャでの自作OSにも挑戦してみることにした.
Raspberry Pi Zeroは12/12,13にFab蔵にて開催されたハッカソン(Hack-a-thon ZERO)に参加してありがたく頂いてきた.
大変小さくていい感じ.
ついでに、ディスプレイももらうことが出来た.
ADAFRUIT PITFT 2.2" HAT MINI KIT - 320X240 2.2" TFT - NO TOUCH
zeroは5ドルだが、このディスプレイは25ドルする.
なので、zeroが5台買えるということになるのだがもう意味がわからない.
また、Fab蔵にはレーザーカッターもあり、社員の方にその場でぱぱっとレーザーカッターを使って、フレームも作ってもらった.
上の写真にある透明なパーツはオリジナルフレームであり、大変おしゃれ.
これで自作OSが動いたらさぞかし気分がいいはずだ.
やったこと
- Lチカ
- ディスプレイの制御
以下はデモ動画.
Lチカしつつ、アスキーアートとAxelたんを交互に表示している.
画像はディスプレイがRGB565フォーマットだったので、gimpでCの構造体にデータを変換してから、rustのコードに埋め込んだ.
Rustでやってみた感想
Rustに移行して、書いてみて思ったRustの感想を簡単にまとめてみる.
最近の言語なら大抵できるだろって事柄も多く含まれているが、やっぱりCで開発してきたことが主な原因だと思う.
便利だと感じたこと
- 普通にコードを書く分にはメモリ安全で安心
- traitでの束縛
- 既に存在する型にtraitの実装を自分で追加できる
- moduleがある素晴らしさ
- moduleを使うよって書いておくとパス内から勝手に探してコンパイルしてくれる
- テストをコンパイラがサポートしている
- システムコールなどのOSやアーキテクチャに依存しないライブラリがlibcoreとして存在する
- 名前空間がある(Cでは精々ファイルローカルなので辛かった)
- マクロが強い
- 条件付きコンパイルでアーキテクチャ固有処理の切り分けがCのマクロより良い感じに出来る
悩んだこと
- 最適化で、Lチカなどを取りあえず動かすためによくやる1万回の空ループなどが消される
- memory mapped IOでレジスタへのpollingなども容赦なく消してくる(libcoreのvolatileな関数群を使うと対処できる)
- 実行ファイルの容量がデカい(OSのバイナリサイズが最適化無しdebug info込みだと1MBを超える)
- 型システムや所有権がしっかりしているのでCほど適当に書けない(コンパイルエラーになる)
- 関数オーバーロードがない (きっとtraitを使ってうまいことやるんだろうと思う)
- 可変引数がない (マクロをうまいこと使うと解決できる)
- optimize-levelが3だと動くのに0だと動かない場合があった (今でも動かないので謎)
traitを使って、以下のような感じで16bitの上半分と下半分を取り出せるのは、なかなかいい感じかと思う.
trait Parts {
type T;
fn lo(&self) -> Self::T;
fn hi(&self) -> Self::T;
}
impl Parts for u16 {
type T = u8;
fn lo(&self) -> u8
{
(self & 0xFF) as u8
}
fn hi(&self) -> u8
{
((self >> 8) & 0xFFu16) as u8
}
}
fn main() {
println!("hi 0x{:x}", 0xFF10.hi()); // hi 0xFF
println!("lo 0x{:x}", 0xFF10.lo()); // lo 0x10
}
しかし、書き始めだからかRustで書いてうまく行かずに、Cで書いてみたら普通に動いたということが2回程あった.
自分が書いたrustのコードを眺めてみても、Cっぽさがあり、まだまだ使いこなせていない感があるが、公式ドキュメントやチュートリアルがしっかりしているので、参考にしてrustらしいコードを書いていきたい.
やはり書いていると、新鮮な感じがする上、Rubyなどを書いているときみたいな感覚もあればインラインアセンブラもかけるので、書いていて楽しい.
今後の目標としては、ひとまず、急いで作ったぐちゃぐちゃなモジュールを整理しなおしたいところ.
また、x86側でも、Cで実装した部分まで追いつくことと、以前うまくできなかった全体の設計をうまいことやっていきたいなと考えている.
おわりに
この記事を読んで少しでも自作OSに興味を持ってくれた人がいたら嬉しい限りです.
おまけ
Bare-metalでLチカ
おまけとして、Raspberry Pi ZeroでのBare-metalプログラミングについて少し書いてみる.
まずはやっぱりLチカ
組み込み業界のHello WorldであるLチカは避けて通れない.
ちなみに、ハッカソンではこのLチカのおかげでRaspberry Pi Zeroをもらうことが出来た.
環境構築
Raspberry PiはARMアーキテクチャのCPUであるから、普段使っているx86向けのコンパイラでは実行ファイルを出力出来ない.
なので、クロスコンパイラとそのツールチェインが必要になる.
Arch Linuxではパッケージリポジトリに"arm-none-eabi-gcc"や"arm-none-eabi-binutils"が存在するのでこれを使用する.
sudo pacman -S arm-none-eabi-gcc arm-none-eabi-binutils
arm-none-eabiというのはtarget tripleでarmアーキテクチャでOS無し、ABIとしてEABIというふうに読む.
Rustのtarget指定などもこれで行うため、最初によくわからなくて苦労した.
プログラム
GithubにあるコードはもちろんRustで書かれているが、とりあえず動かすためにアセンブラで書いてみる.
.section .boot_kernel
.globl boot_kernel
boot_kernel:
/* Setup the stack. */
mov sp, #0x8000
/* Only for Raspberry Pi B+ and zero. */
blink_led:
ldr r0,=0x20200000
mov r1,#1
lsl r1,#21
str r1,[r0, #0x10]
1:
mov r1,#1
lsl r1,#15
str r1,[r0, #0x2C]
mov r2,#0x90000
2:
sub r2,#1
cmp r2,#0
bne 2b
mov r1,#1
lsl r1,#15
str r1,[r0, #0x20]
mov r2,#0x90000
3:
sub r2,#1
cmp r2,#0
bne 3b
b 1b
次にリンカスクリプト
ENTRY(boot_kernel)
SECTIONS
{
. = 0x8000;
.text :
{
KEEP(*(.boot_kernel))
*(.text)
}
.rodata :
{
*(.rodata)
}
.data :
{
*(.data)
}
.bss :
{
*(.bss)
}
}
コンパイル
以下が実行ファイルの生成手順になる.
arm-none-eabi-as ./boot.S -o boot.o
arm-none-eabi-ld -T link.ld boot.o -o blink.elf
arm-none-eabi-objcopy -O binary kernel.img blink.elf
最終的に生成されたkernel.imgがOSプログラムになる.
これをRaspberry Piのブートローダーが読み込むようにしてやる.
まず、普通にraspbianを公式の手順にしたがってSDカードにインストールしてやる.
Linuxであればddコマンドが早い.
それが終わったら、SDカードの第一パーティション(ブートパーティション)にkernel.imgというファイルが存在するはずなので
これを、先ほど生成したファイルに置き換える.
コマンドでやると以下のようになる.
デバイスなどは適宜修正してほしい.
sudo mount /dev/mmcblk0p1 /mnt/
sudo sync
sudo cp kernel.img /mnt/kernel.img
# For Raspberry Pi 2
# sudo cp $1 /mnt/kernel7.img
sudo umount /mnt/
あとは、普通に電源を入れるとLEDが瞬き始めるはずだ.
ちなみに、このkernel.imgというファイルだが、kernel7.imgというファイルも存在する.
これはRaspberry Pi 2用なので注意してほしい.
7というのは、Raspberry Pi 2のみCPUアーキテクチャがARM7だかららしい.
おかげで2時間ぐらいつまずいた.
簡単なプログラムの説明
やっていることは簡単でLEDに接続されているGPIOピンを出力に設定し、0x9000回の空ループしつつ、GPIOの出力をHiとLo交互に切り替えている.
ここで注意が必要なのが、LEDが接続されているGPIOピン番号と、レジスタがマップされているアドレスである.
LED接続ピンは、モデルAとA+では16番、B+, 2B, Zeroは47番となっている.
マップ先のベースアドレス(peripheral base addresses)がデータシートには0x20000000とあるが、Raspberry Pi2 Bのみ0x3F000000になっている.
さらに、調べても記述を見つけられなかったのだが、デフォルトのピンのプルアップ/プルダウン設定がモデルで違うようだった.
(LEDが、GPIOピンをHiにしたときに光るか、Loにしたとき光るか逆ということ)
これは、ZeroとB+で同じプログラムを同時に試していて気がついた.
気づいた瞬間は夜中の3時だったことも相まってかなり腹がたったことを覚えている.
一通りやってみることで、LEDを光らせるのもなかなか簡単ではないということが改めて実感出来た.
参考
一番上のBCM2835のデータシートだが、非常に誤植が多いので、その下のerrataというページを見ながらやることを強くおすすめする.
BCM2835 ARM Peripherals
BCM2835 datasheet errata
RPi Low-level peripherals
RPi BCM2835 GPIOs