論文紹介:A Binary-Compatible Unikernel ー Unikernelの解説も添えて

元論文:https://dl.acm.org/citation.cfm?id=3313817

Linuxアプリケーションをそのまま動かせるバイナリコンパチブルなUnikernelを構築しました、という話です。
前回の記事に引き続きUnikernel関連な話題ですが、意外と自分のブログではUnikernelについての話題を触れていなかったので関連論文も合わせて紹介したいと思います。

論文概要

  • タイトル:A binary-compatible unikernel
  • 著者:Pierre Olivier, Pierre Olivier, Pierre Olivier et. al.
  • 会議:Proceedings of the 15th ACM SIGPLAN/SIGOPS International Conference on Virtual Execution Environments (VEE 2019)

仮想環境関連のトップカンファレンスであるVEEでの論文です。

Unikernelについて

Unikernelという概念が初めてでてきたのはASPLOS 2013でのMirageOSの論文です。
Unikernels: library operating systems for the cloud
仮想環境上でアプリケーションを走らせるために、アプリケーションごとに独立した仮想Linuxマシンを立ち上げるのは、どうしてもたくさんの計算資源を消費してしまいます。
しかし、Dockerなどのコンテナ仮想化では、特にマルチテナントな仮想環境ではセキュリティの懸念が残ります。
そこで彼らのMirageOS、アプリケーションを走らせるためのOSをライブラリとして直接リンクさせるライブラリOS形式を用います。
通常、OSは複数のアプリケーションを動かすために計算資源の仮想化を行うわけですが、シングルアプリケーションのためならそのような抽象化レイヤーはいらないという考えです。
ライブラリOSの概念自体は古くからあり、SOSP ‘95で発表されたExokernelでも用いられている概念です。
ただし、当時はあまりにも極端すぎるアプローチで、特にそのライブラリOS用のデバイスドライバは個別で開発する必要がありました。
しかし、仮想環境上ならば、デバイスは仮想デバイスという形でVMM側から共通のインターフェースで与えられるので、仮想デバイス用のドライバさえつくってしまえばこの問題をクリアできます。
要するに、計算資源の仮想化をVMM側に完全に任せることで上に乗っかるOSはさほどがんばる必要がない、という考え方です。
このアプローチはかなり強力で、評価では起動時間・バイナリサイズを大幅に削減することが示されています。これはシステムコールが関数呼び出しとして実現できる・必要な関数のみリンクさせることでサイズを小さくできるなどによるものです。
MirageOSはOCamlで書かれていてGCのコストや型安全性の保障のためのオーバーヘッドがあるはずですが、それを上回るメリットがMirageOSの設計にはあったというわけです。

MirageOSには1つ大きな問題があり、それはアプリケーションをOCamlでMirageOS用に書き直さなければならない、ということです。
彼らの主張としては、今までの型安全ではない言語でかかれたシステムはバグを含んでいることがしばしばあるので、むしろ積極的に書き直すべきだという立場をとっています。
一理あるかもしれませんが、やはり何もかも今までの資産を切り捨てるというのは難しいのがUnikernelの課題としてありました。

Unikernelやそれに類似したシステムは今までいろいろと提案されてきました。
Usenix ATC ‘14で発表されたOSvでは、Linux ABIを提供することでLinuxアプリケーションを動かしたり、JVMを提供したりと、より広く使われている言語をサポートしていますが、アプリケーションそのものは再ビルドする必要があります。
RumpkernelはNetBSDのデバイスドライバを抽出して、OSなしでもアプリケーションで使うことができるというものですが、これもアプリケーションは再ビルドする必要がありました。
つまりこれらの手法はソースコードが入手できることが前提のもので、コンパイル済みのバイナリはサポートできません。

Unikernelを用いたシステムとしては以前紹介したJitsuや前回のLightVMの論文でもいくつか提案はされています。
Unikernelの小さいバイナリサイズ・実行オーバーヘッドの少なさがあってのシステムなのですが、アプリケーション構築のためのエンジニアリングコストをいかに小さくできるか、という課題があったわけです。
LightVMの論文ではTinyxというLinuxディストリビューションを必要最低限に小型化する方法が提案されていましたが、この形式ではUnikernelの恩恵はあまり得られず、論文でもこれは折衷案であるということを認めていました。

提案手法:HermiTux

彼らはこのアプリケーションの移植コストというものを減らすために、再ビルドなしで完全なLinuxでのバイナリコンパチビリティを保ちつつUnikernelの恩恵も得られるものという手法を提案しています。
提案手法のプロトタイプをHermiTuxと名付けています。これはHermitCoreというUnikernelをベースにつくられているからです。

HermiTuxはLinuxのABIのルールに従い、ロードおよびランタイムにエミュレートしてあげる必要があります。
ソースコードの入手を前提としていないため、Linuxシステムの呼び出しを事前に置き換えておいたり、違うライブラリをリンクすることはできません。
また、アプリケーションはライブラリを動的にも静的にもリンクしてある可能性があるので、その両方のパターンを考慮しなければなりません。
HermiTuxでは、uHyveという軽量なハイパーバイザをVMM上(今回はKVMを用いている)をまずは知らせ、その上でLinuxアプリケーションとHermiTuxのカーネルを動かしています。

ロードの仕方も論文には書かれていますが、Linuxシステムコールをどう処理するかが特におもしろかったので、今回はそこだけを見ていきます。
システムコールハンドラは通常であれば、特権レベルの切り替えが発生するのですが、HermiTuxではその必要がありません。そのため、その切り替えがない分、システムコールを速く処理できます。
しかし、呼び出しそのものはsyscall命令によって行われているため、普通の関数呼び出しとしてい実現される通常のUnikernelよりはどうしても遅くなってしまいます。動的にCライブラリがリンクされている場合は、Cライブラリそのものの中にあるシステムコールを予め関数呼び出しに置き換えることによりこの問題を回避できます。
Cライブラリ実装のひとつでであるMuslはシステムコールがマクロを経由して呼び出されているため、Coccinelleというコード変換ツールを使い自動的に置き換えられるとしています。

静的にリンクされている場合は、実行時にバイナリを置き換えるというテクニックを用いています。x86の命令は残念なことに可変長でsyscallは2バイト、通常の関数呼び出しであるcall命令は5バイトです。
そのため、syscall命令の次の命令までまとめて別のスニペットへのjmp命令として置き換えて、スニペット中で関数呼び出しと次の命令を実行して戻ってくるという方法で実現されています。

ひとつの問題として、システムコールの種類は非常に多く、すべてサポートするのは大変ということです。この研究でもすべてのシステムコールはサポートしきれていませんが、それでもいくつかのメジャーなアプリケーションをサポートできています。
システムコールのサポートを追加していくのは実装コストだけでなく、カーネルサイズの増大も引き起こします。そこでこの研究ではそれぞれのシステムコールをモジュール化し、必要なシステムコールのみをカーネルに含められるようにしています。
問題はアプリケーションがどのシステムコールを使うかです。straceなどのツールで実際にどのシステムコールが呼ばれるかを解析するのは、そのテスト実行のコードカバレッジに依存するため安全ではありません。
静的にライブラリがリンクされている場合、静的解析を行うことでかなりの部分を解析することができます。動的にリンクされる場合は、どのライブラリ関数が呼ばれているかを解析することにより、その関数から呼ばれているシステムコールを解析することにより特定することができます。
これらのテクニックにより大幅にカーネルサイズを減らせます。

また、HermiTux用のデバッガとプロファイラといった開発ツールのサポートも行われています。

評価

評価では、通常のLinux VM、Dockerコンテナ、OSv、Rumprunと各種ベンチマークを用いた比較を行っています。
ベンチマークにはLMbench、PARSEC、redis、SQLiteなどが用いられています。
Hello Worldアプリケーションではバイナリサイズ・起動時間・メモリ使用量を大幅に減らせることが示されていて、他のベンチマークのパフォーマンスでもかなりの性能を示しています。

まとめ・感想

Linuxアプリケーションをバイナリコンパチブルで動かせるHermiTuxの手法について解説しました。

Unikernelはかなり攻めた手法でなかなか実運用されているケースは聞かないですが、段々とその活用の仕方と移植コストの軽減策が出てきていて非常におもしろいですね。
将来、コンテナ型仮想化に代わる選択肢として広く使われることになるのか楽しみです。

Unikernelについてもっと掘り下げて知りたい方は、Wikipedia(英語)でも記事になっている他、こちらのQitta記事とかがいいかもしれません。

論文紹介:My VM is Lighter (and Safer) than your Container

元論文: https://dl.acm.org/citation.cfm?id=3132763

VM(仮想マシン)をたくさん立ち上げたいとき、VMM(仮想マシンモニタ、ハイパーバイザ)のどういうところがボトルネックになるかを分析した上で、VMMを改善し、Unikernelを用いてコンテナシステムより軽量で(かつセキュアな)システムを構築しました、というような話です。

論文概要

  • タイトル:My VM is Lighter (and Safer) than your Container
  • 著者:Filipe Manco, Costin Lupu, Florian Schmidt et. al.
  • 会議:Proceedings of the 26th Symposium on Operating Systems Principles (SOSP ‘17)

システム系のトップカンファレンスとしておなじみのSOSPで発表された論文です。
著者はNEC Laboratories Europeとブカレスト工科大学(ルーマニアの大学)の所属の方々です。

背景

DockerやLXCのような軽量な仮想化はひとつのトレンドになっている。例えばGoogleでは自社のサービスを全てコンテナ化して動かしており、Container as a Service(CaaS)としてAmazonやMicrosoftのAzureもコンテナプラットフォームを提供している。
コンテナ型仮想化はXenやKVMのようなハードウェアレベルの仮想化に比べると起動が早く、インスタンスごとのメモリ使用量も少なくてすみ、1つのホストによりたくさんのインスタンスを立ち上げられる。
しかし、コンテナ型仮想化ではインスタンスはホストOSから400以上ものsyscallのAPIを与えられ、それらをセキュアに保つのは難しい。これらのAPIを利用することでインスタンスがリソースを大量に消費するようなDoS攻撃を他のインスタンスに対してしかけることも考えられる。
そのため、複数のユーザーが存在するような状況ではコンテナ型仮想化を使うことはセキュリティ上の懸念から難しい。

研究の要求仕様

彼らは既存のハードウェアレベル仮想化のVMMのボトルネックを分析した上で、LightVMという軽量なType-1のハイパーバイザ(XenのようにOSを介在することなくハードウェア上で直接動くVMM)を開発した。
ゴールとしてコンテナのような軽量性を目指していて、具体的な仕様としては

  • インスタンスの高速な起動
  • 大量のインスタンスが建てられること(メモリ使用量とインスタンスイメージのサイズが小さい)
  • インスタンスの一時停止と再開が高速にできる

ということを掲げている。

軽量なVM

まず第一にVMそのものが軽量である必要がある。多くの場合、VM(ないしコンテナ)は1つのアプリケーションのみを動かすことが多い。
そこに注目することで、VMに含まれる機能を削れば軽量なVMをつくることができる。
有名な既存研究としてはUnikernelが存在する(参考:Unikernels: Library Operating Systems for the Cloud)。
これはOSをライブラリ化してしまい、対象アプリケーションに直接リンクさせることにより、非常に小さいVMをつくるというものである。
OSの機能をシングルアプリケーションを動かすことに特化させられ必要な機能のみをリンクするので強力なアプローチである一方、
Unikernelが提供するAPIはLinuxなどの既存のAPIとは互換性がないため、多くの場合、既存のアプリケーションをそのまま動かすのは非常に難しい。

もう一つの軽量なVMをつくる方法として、Tinyxが存在する。
これは特定のアプリケーションを動かすことに特化したLinuxディストリビューションを自動的にビルドするというツールだ。
この研究ではこの2種類のVMと通常のLinuxのVMを使い各種実験を行っている。

既存のVMMのボトルネック

この研究では著名なType-1ハイパーバイザであるXenを分析して、大量のインスタンスを立ち上げた時のボトルネックを調べている。
まず、1000のVMを立ち上げる、という実験をしている。全てのVMは起動後アイドル状態になるようにしてインスタンスが増えてもリソースを消費しないようにしたにもかかわらず、起動時間がインスタンスが増えるに従い激減していることがまずわかった(VMはDebian、Tinyx、Unikernelそれぞれで試している)。
原因はXenStoreというVMの情報を管理するツールに自身の情報を登録するフェーズにあることがわかった。

そこで彼らのLightVMではXenStoreを使わずnoxsという新たなVM・VMM間のコミュニケーションを共有メモリを介して行うアーキテクチャを設計した。
また、xl/libxlの代わりとしてchaos/libchaosというツールスタックも実装した。
XenではVM起動時にVM側のデバイス側のインターフェースを初期化するにはまず、dom0がデバイスのバックエンドからイベントチャンネルなどの必要な情報を取り出し、それをXenStoreに格納する。
その後、VM側はXenStoreに問い合わせることによってそれらの情報を取り出す。このXenStoreとのインタラクションは頻繁に発生し起動が遅くなる原因となる。
LightVMではdom0がバックエンドから取り出した必要な情報をハイパーバイザ内のページに格納する。その後、VM側でハイパーコールを使うことでそれらの情報を取り出しVM側で情報を持つようにする。
その後、デバイスのバックエンドとのやり取りはイベントチャンネル介し直接やり取りできるようになる。

また、彼らは同じようなVM作成時に実行されるコマンドのうち多くは、VM作成前に実行できるということに注目した(例えば同じメモリ容量を割り当てるVMであれば事前にメモリをアロケートしておく)。
そこで、libchaosでは作成時のフェーズをprepareとexecuteという2つのフェーズに分け、prepareはVM作成コマンドが発行される前に予め行っておき、VM作成のための土台をプールしておく。
VM作成コマンドが実行されると、そのプールから土台を持ってきて、そのVMのためにコンフィギュレーションをチェックした上で実際にVMを起動させる。

評価

Dockerとブートタイム、マイグレーション、メモリ使用量などを比較している。詳細は割愛するが、Docker並のパフォーマンスが実現できている。

まとめ

Xenのボトルネックを詳細に分析した上で、LightVMという新しいアーキテクチャのVMMを設計し、Dockerにもひけを取らない軽量な仮想化フレームワークを構築した。
ユースケースとしては、モバイルエッジコンピューティング(クラウド上ではなくモバイル端末にプロセスを直接走らせる)やAmazon Lambdaのような短時間の計算リソース提供サービスなどが考えられる。
この研究ではXenをターゲットとしたが、KVMなど他のVMMでもこの手法は使えるだろうと論文では主張している。
また、用いるVMとしてUnikernelはアプリケーション移植のエンジニアリングコストが高く、Tinyxは通常のLinuxのVMよりはパフォーマンスがいいが、Unikernelほどではなない。
そのような面からDocker並の使いやすさはまだ備えていない。

Rustで自作組込みOSを書くプロジェクト「ErkOS」を公開しました

だいぶ前からRustでOSを書こうとしていて、ようやく一山越えて形になってきたのでレポジトリを公開しました(気まぐれでまた非公開にするかもしれませんが)。

構造

appディレクトリに実際のボードで動かしているデモが入っています。ボードはNucleo-F429ZIを利用しています。
qemu_appはqemu用のデモです。
nightlyコンパイラと必要なツールチェーンをインストールすればcargo buildでコンパイルでき、qemuのデモはcargo runとすれば動くと思います。

kernelモジュールがスケジューラだったり、プロセスなどのOSの機能を実装したものになっています、
archがARMv7-M固有の機能のためのライブラリ(NVICやSystickなどもこちら)、deviceはボード固有のペリフェラルドライバとなっています。
rtlogThe Embedonomiconのチュートリアルでつくったモジュールに手を加えたものです。
utilはアーキテクチャやボードに依存しない機能を入れるためのモジュールです。

機能としては

  • スレッドモードで動くプロセスをラウンドロビンスケジューラでスケジュールできる
    • 10ms毎のSystick割り込みでプロセスが切り替わる
  • プロセスからSVC命令でカーネルの機能を呼び出せる
    • シリアルをつかったprint命令
    • プロセス自身を特定のIRQが呼び出されるまでスケジュールされないようにする命令
  • 実行するプロセスがない場合は割り込み待ちのスリープ状態に遷移する
    あたりが実装できました。こうしてみるとまだまだ機能は足りないですが、おおよその骨格は出来上がってきたのかなあと思ってます。

実装

異なる種類のボードで動かすことを想定して、コアとなる機能を実装したkernelモジュールはボード固有の機能に依存しないように気を配りました。また、スケジューラも地味に独立したモジュールにしました。これは将来、スケジューリングポリシをユーザーが選べるようにするのを目標にしたからです(以前つくったT-Visorでも同じようなことを目指しました)。
本当はアーキテクチャとの依存性もなくし、Arm以外のアーキテクチャへの対応もできるようにしたかったのですが、とりあえずは諦めました。

ペリフェラルのインターフェースは当初はできるだけRust Embeddedチームの成果物を使う予定でした。svd2rustやcortex-mのクレートはよくできていて、これらを使うことでペリフェラルを簡単に扱ういい感じのインターフェースができます。
しかし前に述べたように、独自のモジュールを使っています。これはsvd2rustなどの方針として、ペリフェラルを触るためのインターフェースが実行時にシングルトン化されています。また、各々のインターフェースは実行時にしか取得できません。
これがかなり問題で、割り込みハンドラとmain関数で両方でデバイスを触りたい、といった時にどのように共有させるかというのが問題になります。no_std環境のため、MutexやArcといったものは使えないのも厳しいです。一応、Option型のグローバル変数として共有すればいいのですが、unsafeで囲ってnullチェックもしなければならない、とそれなりにめんどくさいです。
もちろん、むやみやたらにインターフェースを共有するのもそれはそれでよくないのですが、とりあえず動くものを書きたかったのと、自分の勉強にもなるだろうということで自前のものを使う、という判断にしました。

Rustはテストのシステムもあるのですが、テストはstdに依存しているためそのままでは実行できません。そこで、Writing an OS in RustのTestingを参考にQEMUを利用してテストが走る環境をつくりました。
しかしながら、やはりアーキテクチャやペリフェラルに直接関わるテストはどう書けばいいのか思いつかず、現状QEMUを利用するテストはないです。
代わりに、utilはstd環境でも動くようにアーキテクチャへの依存を切って普通にテストを書きました。

今後の展望

まだまだ機能が足りていないなあという感じです。とりあえず今やりたいと思っていることは

  • ヒープ領域が存在しないのでallocを実装する
  • プロセス間の通信
  • 優先度を考慮したスケジューリング
  • ペリフェラルドライバの充実

その他、他のボードやアーキテクチャへの移植もゆくゆくはやっていきたいですね

Rust LT会

Rust LT会 #4にLT登壇枠で参加してきました。
この会に参加するのははじめてなので少々ためらったのですが、LT枠が結構余っていたのと、たまにはこういう場で発表して自身のモチベーションあげようということで、簡単ではありましたが、低レイヤープログラミングについての話をしました。

発表スライド

発表時間には間に合ったものの、会の開始時間には遅刻してしまい運営の方々にはご迷惑をおかけしました。
登壇枠で参加するときは特に時間に余裕を持たなければ…

ちょっとした補足

いくつか会場で出た質問とか、スライドでは深く突っ込んでいない内容についてちょっとした補足をしておきます

no_stdプログラミングでのクレート

低レイヤープログラミングでは基本的にはstdクレートは使えないため、stdクレートに依存している大半のクレートは使用できません。
しかし、中にはno_stdでつくられているものもあり、そういうものはcrate.ioでno_stdキーワードがついています。 が、たとえno_stdでつかえるものでも作者がキーワードを設定していないことも多いです…

追記:no_stdというカテゴリーをつけるほうが一般的っぽい

それでも個人的にはクレートシステムの存在はありがたく、すでに様々な便利なクレートの存在のおかげで、低レイヤープログラミングといえどもフルスクラッチで書くのを避けることもできます。
C言語などではデファクトといえるこういうシステムがなかったのでありがたいと思っています。

Redoxについて

Redoxのレポジトリを見ればわかるのですが、なんとRustコンパイラのフォークをサブモジュールとしてとりこんでいます。
どこを変更したかは具体的には追えていないのですが、所有権に関する制約を緩和するためにRustコンパイラを変更する必要があったという記述をどこかで見た記憶があります。
Rustコンパイラそのものをビルドする必要があるので、Redox全体のビルドは結構大変です。dockerイメージが同梱されているので、そちらを利用するといいでしょう。

一方で、このブログでも何度か登場したTockはnightlyコンパイラでビルドでき、発表された論文ではその部分が貢献のひとつとして強調されていました。
TockはRustで書かれているという以外にも、組込みOSの設計として面白いアーキテクチャをしているので、興味があれば是非みてみるといいと思います。

スライドについて

reveal-ckというのを使ってつくりました。
いつもならばPowerPointかGoogle Docsでつくるのですが、markdown形式でつくれてウェブページとして公開できるのはいいかも、と思い試験的に使ってみました。
しかし、スライドの文字の位置や大きさをもっと調整したかったとかを考えると、やはりPower Pointとかでつくったほうがいいのかも、というのが正直な感想です。
また次回、LT会等の機会があったら別のシステムもいろいろ検討したいと思います。

ARMv7-Mで動作モードを利用してコンテキストスイッチ

Cortex-M4のボードで自作OSに挑戦している。
Cortex-M4の準拠するARMv7-Mアーキテクチャでは2つの「動作モード」(Operating Mode)が存在する。

  • スレッドモード
  • ハンドラモード

リセット状態ではスレッドモードで、割り込みが発生するとハンドラモードに移行して割り込みハンドラが実行される。
スレッドモードは特権が存在し、特権・非特権の2つの状態があり、リセット状態では特権状態である。ハンドラモードでは常に特権状態となる。
特権の状態を切り替えるにはハンドラモードに突入してから切り替える、もしくは特権状態ならばCONTROLレジスタに書き込むことで非特権状態になれる。

この特権の状態を利用することで、カーネル(スケジューラ)部分は特権状態で動かして、通常のプロセスに相当する部分は非特権状態で動かすということができる。
では、どうやってリセット状態からプロセスを起動するか。
ARMv7-Mでは割り込みが発生するとスタックポインタに自動的に一部のレジスタ状態をプッシュし、
ハンドラモードからの復帰時にスタックポインタに保存されたレジスタを復元する。
スタックポインタはメインとプロセスの2種類あり、通常のスレッドモードでは、特権状態ではメインを、非特権状態ではプロセスを使う。
つまりプロセスを初期状態から起動させるには

  1. プロセススタックポインタにプロセスの初期状態をプッシュしておく
  2. svc命令でSVCの割り込みハンドラをハンドラモードで実行
  3. ハンドラから非特権状態のスレッドモードに復帰
  4. プロセスが起動される

という流れになる。

注意するべき点としては

  • 浮動小数点の機能が有効化されている場合はより多くの状態を保存するため、スタックのフレームサイズが代わる
  • スタックはCCR.STKALIGNがセットされていない場合は4バイトに、セットされている場合は8バイトにアラインされている必要がある
  • xPSRレジスタのうちThumbモードを有効にするビットは立てる必要がある(ARMv7-Mでは常にThumbモードで実行されなければならない)。よって初期値は0x01000000である必要がある
  • スタックに保存されないレジスタに関しては別途なんらかの手段で保存しておかないと複数プロセス間の切り替えができない

細かい動作はリファレンスマニュアルを参照すること。以下に自分が参考にした章を示す(マニュアルのバージョンはE.b)

  • B1.3 Overview of system level terminology and operation
    • 動作モードの定義
  • B1.4.2 The special-purpose program status registers, xPSR
    • xPSRレジスタの詳細
  • B1.5.6 Exception entry behavior
    • 例外発生時の動作について。スタックのレイアウトについても書かれている

UEFIアプリケーションをRustで書く(外部クレートなし)

このあいだのRustのアップデートで、x86_64-unknown-uefiなるターゲットが追加された、と聞いてRustでUEFIプログラミングに挑戦しました。
なお、世の中にはすでにuefi-rsというものが用意されているので、もっと簡単にUEFIアプリケーションを書くことができます。
ただ、このクレートの構造の説明みたいなのがドキュメントとして見当たらず、今までUEFIを触ったことがなかったので、せっかくなのでこのクレートも含め外部クレートなしでのプログラミングをしました。

UEFIとは

BIOSに代わるファームウェアに対するソフトウェアインターフェースのこと。詳しくはWikipediaないし、C言語を使っての開発についてはもっと多くの先例があるのでそちらを参考にすればいいと思われる。

最新の仕様は直接仕様書を見て確認しましょう。今回はversion 2.7に準拠。
Unified Extensible Firmware Interface Forum

準備

今回はx86_64のUEFIアプリケーションをつくります。
まず、x86_64ターゲットが追加されているRustを使う必要があります。また、ターゲットは追加されたものの、標準ライブラリ(stdではなくcoreとか)がまだ整えられていないのでstableではなくnightlyを使う必要があります。
更に、標準ライブラリを組み込むためcargo-xbuildも使う必要があります

1
2
$ rustup default nightly-2019-03-23
$ cargo install cargo-xbuild

また、実行環境としてqemu-system-x86_64とqemu用のUEFIファームウェアであるOVMFが必要です。
自前でビルドする方法はやってみたのですが、途中で詰まってしまったし、めちゃくちゃ時間もかかるのでビルド済みのものをとってきたほうが早いでしょう
https://www.kraxel.org/repos/からjenkins/edk2以下にあるx64用のrpmをとってきてその中にあるOVMF_VARS-pure-efi.fdとOVMF_CODE-pure-efi.fdをとってきました。

できたもの

Githubに置いておきました。
garasubo/uefi-practice
cargo xbuild --target x86_64-unknown-uefiとしてビルドし、qemu-run.shでqemu上で走らせます。ただし、OVMF_VARS.fdとOVMF_CODE.fdをプロジェクトのルートディレクトリに置いておく必要があります。

qemu-run.shを実行するとuefiシェルが立ち上がるので

1
2
Shell > fs0:
FS0: \> uefi-practice.efi

とすると、画面がクリアされた後、Hello Worldします

解説

efi_main関数がエントリポイントになっています。仕様書で言うとEFI_IMAGE_ENTRY_POINTに相当します。
仕様書にはC言語での型宣言が書かれているのですが、これをRustで書いていくことになります。
普通にstructやenumを宣言してしまうとRust独自のABIでコンパイルされてしまうので、reprをつけて互換性を保ちます。詳しくはThe RustonomiconのData Layoutの章が参考になります。
今回の目標はhello worldすることなのですが、UEFIのテキストを出力するためのインターフェースを利用します。
仕様書で言うとEFI_SIMPLE_TEXT_OUTPUT_PROTOCOLです。
僕のコードではreset関数とoutput_string関数が定義されていますが、本当はもっとあります。
他のstructについても今回は使う部分だけ型宣言をして残りはサボりました。使わない関数はusizeでごまかしました。全部実装するのはしんどい。

output_stringはPROTOCOL自体へのポインタと文字列への先頭ポインタを渡す必要があります。この文字列が少々やっかいでUCS-2でエンコーディングされていないといけません。
RustはUTF-8で文字列を扱っているため、1文字が16ビットのUCS-2に変換するのは標準ライブラリだけで簡単にやってくれそうではなかったので、
適当なバッファを用意して、実行時に無理やり16ビット配列に変換しました(uefi-rsも内部ではそうやっていた)。

あとはno_stdではpanic_handlerは自前で用意しなければならないのでそれも忘れずに。

疑問点

uefi-rsを参考にしながらつくったのだが、extern "C"extren "win64"の両方が出てくることがあり、これの違いについてはよくわからなかった。
とりあえずuefi-rsに従いこれらの宣言をつけておいたが、本当はextern "C"としておいても問題ないかも?

まとめ

UEFIの仕様は真面目に実装するのは大変なので、おとなしくuefi-rsを使いましょう

論文紹介:The benefits and costs of writing a POSIX kernel in a high-level language

元論文: https://www.usenix.org/conference/osdi18/presentation/cutler

Go言語でPOSIXインターフェースを備えたカーネルを実装し、その利点と欠点を比較してみた、という論文です。

論文概要

  • タイトル:The benefits and costs of writing a POSIX kernel in a high-level language
  • 著者:Cody Cutler, M. Frans Kaashoek, and Robert T. Morris (MIT CSAIL)
  • 会議:13th USENIX Symposium on Operating Systems Design and Implementation (OSDI ‘18)

去年のUSENIX OSDIで発表された論文です。USENIXはハイレベルな低レイヤー関連の会議が多く、かつ論文をオープンアクセスで提供してくれるのはアカデミアから離れた身としてはうれしい限りです。

背景

著名なOSのカーネルはC言語で書かれている。C言語は直接メモリを操作したり、デバイスのレジスタを操作したりできる。また、ランタイムを介さないためパフォーマンスも高い。一方、安全なコードを書くことが難しく、バッファーオーバーフローやuse-after-freeのようなバグの温床ともなりうる。
Go言語などの高級言語は、型検査やメモリの安全性がC言語よりも強く保証されるものの、多くの場合はランタイムを介しての実行となり、ガベージコレクション(GC)に頼るため、パフォーマンスへの懸念が生じる。
そこで彼らはBiscuitと名付けたPOSIXのサブセットを備えたカーネルをx86_64プロセッサ用に実装し、評価を行った。

この論文はカーネルの実装にC言語が用いられるべきか、高級言語が用いられるか、という主張をするものではなく、あくまで高級言語を用いた実装がどのようなものかという指標を提供するものだと著者らは位置づけている

実装

Go言語とアセンブラのみで書かれていて、C言語は全く用いていない。
Biscuitを動かすにはGo言語を動かすためのランタイムが必要で、通常であればランタイムはカーネルの機能を呼び出す。
今回は呼び出すカーネルがないので、代わりの機能を提供するShimというレイヤーを用意している。
Goのランタイムは標準のものにGCのタイミングを調整するなどの変更を加えたものを使用している。

割り込み対応、マルチプロセス、ファイルシステム、ネットワークスタックなどPOSIXのカーネルなら備えている機能はおおよそされている。

GCとヒープ枯渇問題

Goはmark-and-sweep方式のGCを持っていて、確保していたメモリがある程度使われるとヒープ領域を適宜拡大していくということをしている。
Biscuitではこの通常のヒープ領域拡大を無効化して独自のタイミングでヒープ枯渇問題を回避している。
ヒープ領域が足りなくなったとき、Biscuitは

  1. キャッシュやソフトステイトのような保持する必要のないものを解放する
  2. システムコールが消費するであろうヒープ領域のサイズをあらかじめ確保しておく
  3. キラースレッドにより異常にヒープ領域を消費しているプロセスを殺す
    ということをしている

システムコールの消費するヒープの量を計算するためにMAXLIVEというツールを開発し、静的解析により事前に必要なヒープの量を計算している。
コールグラフからアロケーションの量を計算していくのが基本だが、ループによる繰り返し処理がからんでくるとアノテーションをつけることでヒントを与えたり、例外的な処理をして回避したりしている。

評価

この研究では高級言語をカーネルの実装に用いることでの利点・欠点を具体化するのが目的なので、評価軸もそれに沿ったものになっている。

まずそもそも高級言語の機能がどれくらい用いられているのか、というのを他のGoで書かれたソフトウェアとの比較を行っている。
GoのコンパイラとMoby(Dockerがつくったコンテナを用いたシステムのフレームワーク)と比較して同じくらいだとしている。
GCで管理されるオブジェクトが有用な場面とそうでない場面もある、と具体例も上げている。
有用な例としてはpollのシステムコールでは入力待ちのスレッドを起こすためのヘルパーオブジェクトのを挿入するが、そのヘルパーオブジェクトのフリーのタイミングを気にしなくても競合が起きないことをあげている。
一方、TCPのコネクションオブジェクトはTCPのシャットダウンプロトコルを実行することが必要だが、Goのfinalizerではオブジェクト間の循環ができないためそのままでは難しいので、そのようなオブジェクトに対してはBiscuit自身が参照カウントを管理している。
また、LinuxのCVEを参照し、Use-after-freeやOut-of-boundsなどのGoでは脆弱性になりえないものとそうでないものを分類している。65個の深刻な脆弱性のうち40個はGoなら防げるとしている。
一方、Goのランタイムやパッケージ自体にも14のCVEが報告されて、うち4つはカーネルにとって深刻な脆弱性になりうるとしている。

アプリケーションベンチマークとしては、CMailbench(メールサーバーをモデルとしたforkとexecにより仮想メモリシステムへのストレス試験)、NGINX、Redisを実際に動かし、Linuxとの比較、ヒープサイズと使用メモリ割合に対するパフォーマンスなどの実験を行っている。

関連研究

カーネルの実装に高級言語を用いる例は様々ある。以前ここでも紹介したRust製カーネルのTockも一例として挙げられている。
しかし、これらの研究はC言語との比較というよりも、新しいコンセプトのOSということに重きを置いている。
カーネル用に既存の言語を改変して用いるというものもあるが、この研究ではGoそのものに大きな変更は加えないようにしている。

まとめ

Go言語を用いてBiscuitと名付けたPOSIXカーネルを実装し、C言語を用いた場合との比較を行った。
高級言語のもたらすパフォーマンス低下は15%を下回るとした一方、だからといってGoを使うべきだと結論づけてはいない。
しかし、Go言語でカーネルを実装する際の恩恵やコストを明らかにし、Goのような高級言語を使うべきか否かの判断材料になるであろうとしている

Rust製組込みOS TockでC言語アプリケーションを動かす

Tockとは

このブログでも何度か紹介したRust製組込みOSです。
以前の記事
ターゲットはCortex-MのようなCPUリソースが限られたようなプロセッサです。
Rust Embeddedグループ発足前から公開されていて、Rust純粋でちゃんとしたOSを組む先駆けにもなっています。Rustで書かれているだけでなく、組込みOSの設計としてもおもしろいものになっています。
以前は専用ボードへの実装しか公開されていなかったのですが、最近になりSTM社製のNUCLEO-446REボードのサポートが追加されました。
手元にあったNUCLEO−429ZIボード用のサポートも追加してもらえるようPRを投げ、無事マージされたことにより、自分の手元でも動かせるようになりました。

このTockの性質として、ユーザーアプリケーションは独立にビルドする仕組みとなっているため、任意の言語で書くことができるというものがあります。
今回はC言語を用いて簡単なアプリケーションを書いてみて、さらにその仕組みを簡単に見ていきたいと思います。

サンプルアプリケーション

libtock-cというライブラリがTockのカーネルを叩くための各種関数を提供しています。
これを用いて簡単なアプリケーションを書いてみました

garasubo/tockapp

このアプリケーションはキーボードの入力を受けるとLチカが動き出し、もう一度入力を受けると止まる、というものになっています。
サンプルアプリケーションのレポジトリではlibtock-cをgitのサブモジュールとして取り込んで、ビルドはlibtock-c内のAppMakefile.mkに全面的に依存しています。

libtock-cのレポジトリ自体にもサンプルコードがあります。
今回のサンプルでは使っていないのですが、Newlibによって実装されたC標準ライブラリやluaのランタイムもあります。

ブートプロセス

サンプルアプリケーションでは普通にmain関数を書いているわけですが、これがTock側からどう呼ばれているかを見てみましょう。
リンカスクリプトがlibtock-cのuserland_generic.ldにあります。まずは、アプリケーションがどのようにビルドされるか見てみましょう。

userland_generic.ld

11行目、ENTRY(_start)となっていますが、これはlibtock/crt0.cに定義されています。26行目の.crt0_headerの構造についてもここで定義されています。

crt0.c

アプリケーションはメモリ上のどこにおかれるかはビルド時には決定できません。さらにCortex-Mには仮想アドレス機構はないため、すべて物理アドレスで扱う必要があります。
そのため、_start関数では4つのメモリレイアウトに関する情報を受けとり、それをもとにアプリケーション上の情報を書き換えるということをやってます。
スタックやヒープ領域、デバッグ用の情報などを設定する他に、グローバルオフセットテーブル(GOT)の書き換えもやってます。
GOTの書き換えは_startで呼び出される_c_startで行われています。
159行目のループ中で最上位ビットで場合分けを行っていますが、
これはリンカスクリプトでROM領域は0x80000000以上の領域、RAM領域は0x00000000に配置されていることを利用して(17、18行目)、
本来アプリケーション領域にある定数を指すものなのか、スタック上に配置されるグローバル変数なのかを判別してアドレスを調整しているためです。

システムコール

カーネルとのやりとりはSVC命令を用いたシステムコールにより実現しています。SVC命令を介しているのでそこのインターフェースさえ何らかの方法で実装できれば、Rust以外の言語でもカーネルの機能を呼び出せるという仕組みです。
これはlibtock/tock.cで実装されていて、サンプルアプリケーションでは直接は呼び出していませんが、libtock-c内の関数を呼び出すことにより間接的に使っています。
システムコールは5つのみです。簡単に説明すると

  • yield: そのアプリケーションの終了をする。アプリケーションはスケジュールされなくなる
  • subscribe: ドライバのコールバック関数を登録する(例:タイマー割り込みで関数を呼び出してもらう)
  • command: ドライバに対して指示を出す(例:LEDを点灯させる)
  • allow: カーネルとアプリケーション間で特定のメモリを共有させる(例:タイマードライバにコールバック制御用のデータ構造体を渡す)
  • memop: ヒープ領域を変更してもらったり、現在のメモリレイアウトの情報を手に入れるなど、メモリの操作を依頼する

おまけ

サンプルアプリケーションでtock_timer_tの実体をユーザーアプリケーション側に持っていて、そのアドレスをカーネルに渡しています。
普通の場合はむしろ構造体の実体はカーネル側に持っておき、そのアドレスなりIDなりをユーザーアプリケーションが持つのが一般的だと思いますが、これには理由があります(以前のブログで紹介した論文にも書いてあります)。
今回は扱いませんでしたが、Tockでは複数のアプリケーションを動かすこともできます。
メモリ領域はアプリケーションごとに独立に持っていて、カーネルや他のアプリケーションとは基本的に共有しません。
もしカーネル側が実体を持っているとすると、あるユーザーアプリケーションが大量のタイマーを要求してきた場合、他のアプリケーションがメモリ不足によりタイマーを手に入れられない場合が考えられます。
アプリケーション側がタイマーの構造体を抱えれば、大量に要求してきたアプリケーションのみがメモリ不足になり、カーネル及び他のアプリケーションが困るということにはなりません。

参考

論文紹介: Towards a Theory of Software Development Expertise

Twitterで見かけたどうやったらソフトウェア開発のプロになれるかの理論を構築したという論文。
ソフトウェアエンジニアにはもちろん、その上司だったり経営者とかにも参考になると思う

論文概要

  • タイトル:Towards a Theory of Software Development Expertise
  • 著者:Sebastian Baltes (University of Trier) et al.
  • 会議:Engineering Conference and Symposium on the Foundations of Software Engineering (ESEC/FSE ’18)

FSEというのはソフトウェア工学のトップカンファレンスの1つ。
どうすればソフトウェアエンジニアとして成長できるか、という話は本であったりブログであったりとそこらじゅうに溢れているが、このような論文となっているのはなかなかないのではなかろうか。
この論文で示されている著者らの構築したソフトウェアエンジニアのプロ(以下、原文に習いSDExp)になるための理論はもちろんのこと、その理論の構築過程もとてもおもしろい。
今回のブログでは理論の構築過程については自分の前提知識が足りなくまだ十分に理解できていないので、彼らの導いた理論の要旨、というよりも自分が印象に残った部分だけを紹介する。

元の論文はarXivで誰でも見られるので、詳しくはそちらへ
https://arxiv.org/abs/1807.06087

手法

彼らの理論はどのようなコンセプトがSDExpになるためには必要で、そのコンセプトがどのように関係しあっているか、というものをモデル化したものである。
この論文では3つのフェーズを経てそのモデルを構築している。

フェーズ1ではGithubとStack Overflowのアクティブなユーザーをサンプリングしてアンケートを取り、
その結果をベースに理論のベースとなるgrounded theory (GT)を構築している。
フェーズ2では既存の文献を用いて、GTに対してそれらの理論を埋め込んでいく形で発展させたモデルを構築する。
フェーズ3ではフェーズ2までの結果を踏まえて、フェーズ1よりも踏み込んだ形のアンケートをつくり、アクティブなJava開発者と長い開発経験を持つ開発者それぞれを対象として回答を得た。
Javaにターゲットを絞ったのは、対象をわかりやすくるため、研究開始時最もメジャーな言語であったJavaを選んだとのこと。
その結果を元に彼らの理論を完成させた。

SDExpになるには

では、フェーズ3で構築した理論の図を論文から引用して見てみる

この巨大な図だけでは意味がわからないので、いくつか印象に残った部分だけを説明する。
このモデルはconcepts(コンセプト)とrelationship(関係)の2つからなる。
今回はコンセプトと関係の中からいくつか重要なものあるいは印象的だったものについて見ていく。

Tasks

このモデルはTaskを中心に据えている。ここでいうTaskというのは要件定義であったりリファクタリングであったりするわけだが、それぞれのTaskには専用の知識であったり経験がある。
ソフトウェア開発には人が関わってくるので、人の関わり(Work Context)というのも重要な要素であるが、これもTask毎に違う。

フェーズ3にて最も重要だと思うTaskはという質問に対して、

  1. ソフトウェアアーキテクチャのデザイン
  2. ソースコードを書く
  3. 要求を分析・理解する
    が最も多い回答だった。

deliberate practice

この論文でかなり強調されていた関係の1つにdeliberate practice(訳すとすれば計画的実践?)というのがある。
単に同じような経験をたくさん積む、というのはパフォーマンスを向上させるのには貢献しない、むしろ逆効果にすらなり得る。
経験の年数が能力に比例するのは最初の2年程度なので、その後はタスクの難易度を上げていくことでパフォーマンスをあげていくことが必要になる。これがdeliberate practiceである。
このdeliberate practiceにはパフォーマンスをモニターし、フィードバックを与えてあげるメンターなどの存在も必要になる。
また、自分の強みや弱みを把握する自己反省の資質も大事である。

Work Context (Task Context)

ソフトウェア開発にはマネージャーに相談したり、客からの要望に答えたりといったWork Contextが存在する。
フェーズ3で経営者が従業員の開発スキルを向上させるためにするべきことという質問に対する答えをカテゴライズすると

  1. 学習を奨励する
  2. 実験することを奨励する
  3. 情報交換をしやすくする
  4. 自由を認める
    というのが最も多い回答であった。
    具体的には
  • 外部のトレーニングコースの受講を勧める
  • カンファレンスの参加費を負担する
  • “Self-improvement Friday”、”lunch-and-learn session”などのミーティングやイベントで情報交換や学習の機会をつくる

Performance Decline

年を経るごとにパフォーマンスが落ちていくということがあるが、これについても分析している。
フェーズ3での回答をカテゴライズすると

  1. モチベーションの低下
  2. 仕事の環境の変化
  3. 年齢による衰え
  4. 態度の変化
  5. 他のTaskへの移動
    となった。
    同じような仕事をやらされ続けてモチベーションを失う、きつい締切へのプレッシャー、マネージャーのコミュニケーション不足などなどが具体例として挙げられている。

年齢による衰えについてはいくつかの実際の回答を引用している。例えば、「50代のころから新しい言語やフレームワークについていけなくなった」、「物忘れがひどくなってプログラムを書く速度が落ちた」、といったものであるが、
「40歳からritalinのような薬を飲むようにした。これは年老いたプログラマーの間では極めて一般的」などという回答まであった。
ritalinとは、どうやら多動性障害(ADHD)のための向精神剤らしいが(参考:メチルフェニデール)本当に広く使われていたりするのだろうか…

まとめ

SDExpになるための理論を構築したという論文から、構築した理論についてその要素部分をかいつまんで紹介した。
紹介しきれなかったところ、また構築するまでの過程も非常におもしろかったので、興味があれば是非元論文を読んでもらいたい。

今年の振り返り

今年は仕事以外でのインプット・アウトプットを増やすというのを1つ目標にして過ごしてきて、まだまだ少ないけれど去年よりは増えているのでいい傾向かなあと思っている

RustでArmベアメタルプログラミング

以前から興味があったRustでArmベアメタルプログラミングができるようになってきたと聞いて、今年はいろいろな技術資料を漁りつつCortex-Mボードでのプログラミングを始めた。
現状だとUARTモジュールを動かしたり、タイマー割り込みを使ったり程度でまだまだなのだが、ゆくゆくは簡単なOSくらいにまでは進めていきたい。
とりあえず、目下の課題としてはSSD1306のOLEDモジュールを購入したので、それの動作用のライブラリ作成となりそう

ブログ

今年は技術関連であれば9件投稿した。本当は月一ペースで出したかったが、去年は2件しかなかったのでだいぶマシ。
とはいっても内容もまだまだなので、もうちょっとがんばりたい。
モチベーションをあげるためにGoogle Analysticも最近になってようやく導入した。
一応、ネタのストックはあるので、来年の早いうちに1件は出そうと思う。

プログラミングコンテスト

学生時代は頻繁に参加していたが、最近は参加する気が起きなくてスルーしていた。
しかし、なんだか簡単なプログラムを一気に書く瞬発力的なものが衰えてきた気がするので、Rustの勉強も兼ねてAtCoderのBeginner ContestのC問題を中心に時々解くようにした。
今後はちゃんとコンテストに参加してみようと思う。

LFCE

Linux Foundationの提供するLinux Foundation Certified Engineer(LFCE)の試験とそのためのオンラインレッスンを受講した。
結果は合格からは程遠かった。試験内容は口外できないのだが、レッスンに含まれていない内容もかなり出してきて、試験中は外部ネットアクセス禁止でmanしかアクセスできないというハードな内容だった。
一応、もう一回試験を受ける権利はあるのだが、そもそも合格してもメリットが少なそうだし、分野がそこまでしっかり勉強したい部分ではないので見送ると思う。
とはいっても、自分のLinuxシステムの理解の浅さや、すぐにとりあえずGoogle検索に頼ってしまう自分の姿勢は反省すべきだと思った。

その他

TCFMは低レイヤ関係の話を聞けるいい機会なのでモチベーション向上に役立った。
低レイヤプログラミングをがっつりやっているコミュニティに属していないので、情報交換の意味でもそういうコミュニティへの参加を増やせたらなあと思う