Arch64のMMUとTCR

金津穂氏(以下、金津):次にMMUについて見ていきます。Armの場合、TTBR(Translate Table Base Register)というページテーブルを保持するレジスタが2つありまして、これがIntel 64におけるCR3レジスタとほぼ同等になっています。ここにページテーブルを登録して、実際にページウォークを実行します。

このTTBRですね。ここ「x」って書いてあるんですけど、TTBRはTTBR0とTTBR1があって、それもそれぞれException Levelごとに存在します。

例えばTTBR0はアドレス0番から256TB目のアドレスまでの48-bitの仮想アドレス空間を保持、TTBR1のほうは0xFFFF0000_00000000から0xFFFF_FFFFFFFFまでの256TBを保持する。そういったかたちで仮想アドレスを分離して、それぞれにユーザーアプリケーションとカーネルを置くといったかたちで、カーネル用のページテーブルとユーザー用のページテーブルを別々に登録することができます。ただし、HypervisorとSecure MonitorのEL2とEL3にはTTBR0しか存在しません。

MMUの設定はTCRというレジスタで実行します。このTCRについて見ていきます。 TCRはこの図のとおり64-bitの長さを持っているレジスタになっています。まず1つ目、ここの0番目から5番目の6-bitのT0SZ(T0サイズ)、16-bit目から21-bit目のT1SZ(T1サイズ)を見てください。ここ6-bitでテーブルのページの最初のレベルとページの粒度などを指定できます。ページの粒度などを指定すると何が起きるかは、また後ほど説明します。

ほかにも、物理アドレス、バーチャルアドレスは最大48-bitなんですけど、これは単に48-bitに限ることなく、いくつかの出力サイズを指定することができまして、最小で32-bitの物理アドレスを前提にしたものがIPA sizeで指定できます。

また、TG0、TG1というのがあるんですけど、ここの2-bitでページの変換の粒度を指定することができます。

ページの変換の粒度が指定できるとはどういうことかといいますと、Intel同様、Armv8は4-Levelのページウォークができるのですが、ここの4-Levelのページテーブルの中でも粒度をこのように、L1PTが1GBのブロック、L2PTが2MBのブロック、L3のPTが4KBのブロックを指定するようにすると、オフセットアドレスは12-bitになりまして、この場合はIntelと同じ4KBページになります。

ただし、同じ4段階でも最初のページテーブルのエントリすべてがL2のページテーブルを指すようにして、L2のPTは32MBブロック、L3PTは16KBのブロック指定にすると、オフセットアドレスが14-bitに変更されて、この場合ページがなんと4段階ですが16KBページに変わります。

このようにIntelと異なって、ページの段数を減らさなくても、いくつか、4KB、16KB、また64KBのようなページの粒度が変更するといったことが特徴の1つになります。

ではページテーブルに登録するエントリについて見ていきます。ページテーブルに登録できるエントリは、まず次のレベルのページテーブルを指定するテーブルディスクリプタ。これは段数0番目から2番目に指定できます。末尾のbitは「11」で、真ん中にnext-level table addrを登録しています。

また、ブロックエントリが1から2番目とLevel3、最後のページテーブルの段数で、それぞれ末尾のbitが「01」「11」と変えられています。また、ページフォールトを起こすためのinvalid entryは末尾が「0」のときになっています。

ここのブロックエントリなんですけど、このブロックエントリにはupper attr(attribute)とlower attr(attribute)という2つメモリ属性を設定するビットがありまして、ここにそれぞれUnprivileged eXecute Never、Privileged eXecute Neverなど、あるいはlower attrにはAccess Flag(AF)、Shareable Attribute(SH)、Access Permission(AP)、Secure bit(NS)などなど、ページの共有の許可とか実行の許可とか、そういったもののパーミッションについてのビットを立てることができます。

また、一番末尾。この2から4ビット目のindex(idx)というところには、MAIRというレジスタに対するインデックスを指定できます。このレジスタは8-bitのエントリを8つ持った8×8の合計64-bitのレジスタなんですけど、このテーブルの中に登録されているメモリタイプを使うことによって、特定のエントリがキャッシュ可能かどうかということが細かく指定できます。

先ほどのここのページのところにある、TCRのMMU設定レジスタの中にあるこのORGNとIRGN、この2つのビットについて見ていきます。ここのビットの設定の仕方によって、変換テーブルをキャッシュ可能なメモリに載せるかどうかといった変換したときの状態、TLBとかのキャッシュの状態を設定することができます。それぞれ「non-cacheable」「write-back write-allocate」「write-through」「write-back。ただしwrite-allocateができない」といった、4つの状態を指定することが可能になっています。

MMUのコード例

最後にMMUのコード例を見ていきます。これはとても簡単なコードです。MSRという特殊な権限レジスタを設定するコードで、X0に例えばページテーブルのポインタを設定しておきますと、このページテーブルポインタをTTBR0に書き込む。また、TTBR1に書き込む。最後にX2というところにTCRのレジスタの設定を書いておきまして、これをTCRに書き込む。こうすることでMMUの設定が完了します。ただし、そのあとに命令同期バリア、Instruction Synchronization Barrier(ISB)という命令で一度同期しないと、正しく書き込みが終わらない可能性があります。

そのあと一度System Control Register(SCTLR)、特殊なシステム制御レジスタ、これをいっぺんX0レジスタに書き出しまして、これの最下位ビットを立ててまた書き戻すことによって、MMUが有効化します。MMUを有効化したあともISBを発行しなければなりません。

すっきりしたAArch64

ということで、ここまでまとめてみます。AArch64では昔のAArch32と違ってモードがすっきりしています。かつてのArmはAcorn社の時代の遺産のために複雑なモードを持っていて、ほかのOSやPCでは使いづらいものでした。これがAArch64ではException Levelに統合されまして、とてもわかりやすくなっています。

またIntelに比べて、例外のときの退避用レジスタ、ページテーブルを保持するポインタなど便利なレジスタがたくさんあるので、わりと今から既存のOSを実装するのと違って、フルスクラッチで実装する場合にはIntelよりも楽に素直に実装できるんじゃないかなという所感です。

周辺機器の情報取得

では、ちょっとした落ち穂拾いですけど、AArch64に対して、OSの実装おける周辺機器の情報取得などについてザッと話しておきます。

AArch64について周辺機器の情報を取るためには、現在、2つ方法があります。まずデバイスツリーによる方法。もう1つがACPIによる情報です。それぞれ見ていきます。

まずデバイスツリーです。大昔のMacとかIBMとかそこらへんで共通して作成されたファームウェアである「Open Firmware」というポシャった仕組みがあるんですけど、これは、そのOpen Firmwareに由来しています。

dtsというところにC言語の構造体みたいな書き方で、デバイスの接続の構造とかバスの構造とかを記述します。これをdtcというコンパイラでblobにしてしまって、これをLinuxの起動時にオプションで渡して読み込んでしまうとカーネルがデバイスの構造を読み取ることができるため、適切なドライバを読み取ることができます。

これは基本的に組み込みでよく使われていて、LinuxのソースツリーやArmに限らず、MIPS、PowerPCなど向けにも含めて、わんさとdtsが入っています。

次にACPIです。これはみなさんが使っているIntel MISC PCでよく使われています。現在UEFIコンソーシアムというところで規格化されていまして、UEFIのシステムは基本的には標準搭載になっています。ACPI自体はUEFIの中で規格されているのと、UEFI自体も、AArch64向けリファレンス実装もありますし、Intel向けのリファレンス実装もあります。これは電源管理にも利用されていまして、サスペンドだとかリブートだとか、そういったときにもACPIを読みます。

ACPIでは、AMLと呼ばれる言語で記述したマシン構成などをファームウェアにストアしておくと、OSはインタプリタを使ってこのAMLを呼び出すことによって電源管理をしたりだとかデバイス構成を読み取るといったことができます。

このインタプリタ、いちから実装するのは面倒くさいんですけど、Intelが標準実装を持っていまして、これは誰でも使えます。ACPI-CAというんですけど、これを使えば自作OSでもだいたいうまくいくはずです。けれども例えばOpenBSDは「ACPI-CAはバグっている」と主張して、自力でインタプリタ実装をしたりとかしています。

Armとブート

次にブートについて見ていきます。具体的にはブートシーケンスではなく、Armのブートに使われるファームウェアなどについて見ていこうと思います。IntelではBIOSかUEFI BIOSが最初のブートコードとして実行されますが、Armではどのような実装が存在するのでしょうか?

まず1番に挙げられるのはU-Bootです。これは多くの組み込みのボード、SoCで利用されていまして、ご家庭のBUFFALOとかのルーターでは基本的にU-Bootが使われています。これは簡単なプロンプトを内蔵していてスクリプトを埋め込むなどもできるので、かなり高度なことができるようになっています。

例えばTFTPを通じてネットワークブートしたり、あとはブートのときにデバイス上のディップスイッチなどの状態を読み取って、それによって起動スイッチを替えるなど、そういったことも可能になっています。

そして先ほど挙げたUEFI。これも最近Armで使えるようになっています。UEFIはもともと2000年頃、Itanium64というIntelのポシャった64-bit ISA向けでしたが、現在ではさまざまな標準実装が存在しており、コンソーシアムにも幅広い会社、各種業界団体が所属していますので、今後より広く使われていくんじゃないかなと思います。

これはアーキテクチャに非依存でProtocolと呼ばれる構造体のやりとりによってAPIを呼び出すといった構造が特徴です。基本的には実装というよりも、このProtocolを用いたファームウェアとプログラムのやりとりのインターフェースを規定しています。このProtocol自体もUEFI Driverを自分で実装すれば拡張することが可能です。

もうすでにサーバ向け・デスクトップ向け、そういったもののArm SoCでUEFI実装が存在しています。NDAの問題で、まだApple Siliconは情報が出ていませんけど、もしかしたらApple EFIの拡張でセキュアブートなども実装するかもしれません。もしApple SiliconがEFIなどを使っているとしたら、今後ArmでもUEFIを使った何かコードを書く機会も増えるんじゃないでしょうか。 (追記:Apple Silicon は残念ながら EFI ではなく Apple が iOS などで用いている iBoot を利用しているようです。)

また、有名なオープンソースファームウェアとしてcorebootというものがあります。corebootはペイロードとしてほかのファームウェア実装を2段目に読み込むことが特徴になっていまして、U-Boot、UEFIのペイロードも存在します

AArch64で動作するOS

では最後に、すでにAArch64で動作するOSについてまとめていこうと思います。だいたいおおよそみなさん知っているとは思うんですけど、LinuxだったりFreeBSD、NetBSD、OpenBSD、DragonFlyBSDといった各種BSD系はだいたいAArch64に対応してきています。

ただしtnishinagaさんも指摘したとおり、Intel PCと異なって、製造・アーキテクチャそれぞれがいろいろ違うので、Arm ISAに対応しているからといって必ずすべてボード・SoCで実装されていて動くとは限りません。ただ、基本的なコアのプロセッサーのアーキテクチャ部分はだいたい対応しているということです。

ほかにいくつかマイナーかつメジャーっぽいOSを挙げてみると、例えばL4マイクロカーネル。本家実装L4Ka::Pistachioはまだ対応してないんですけど、seL4を言われる形式証明をつけたL4マイクロカーネルはなんとAArch64に対応しています。Raspberry Pi3だと動かせるみたいで、公式にもRaspberry Pi3ドキュメントが存在しているので、ぜひみなさん使ってみてください。

それからOSv unikernelというKernel/VM畑の人がいくつか遊んでいた unikernelが存在しているんですけど、これなぜか華為の人たちがAArch64の実装がまだiPhoneしかなかった頃にめちゃくちゃコミットしていて、現在AArch64の実装がなんとAmazon Lambdaで使われているFirecracker、これで動くそうです。激アツですね。

unikernel自体はかなり実装が小さくて、またOSv自体がC++のかなり最近のバージョン使ってけっこうスリムな実装になっていて読みやすいと思うので、ぜひみなさん一度OSv unikernelを試してみてください。コンパイルもLinux上であれば、スクリプトを一発叩くだけで自動でやってくれます。

AArch64には限りませんけど、ほかに、Armで動作するOSも紹介します。基本的にはAArch64、商用に使われるLinuxなどは対応しているんですけど、マイナー、ドマイナーなOSとかは、わりと無視されがちです。例えばRust実装で有名なRedox microkernelはマイナーの中でもかなりメジャーなほうなんですけれども、x64しか対応していません。

一方、かつてApple Macに実装されかけたHaikuOSというBeOSの後継。これはAArch64になぜか対応しています。

ほかには、Windows NTを完全互換することを目指しているReactOSというかなり尖ったOSがあるんですけど、ReactOSはなにをトチ狂ったのかArm実装が存在しています。これを使うともしかしたらまだArm実装できていないようなWindowsのアプリケーションもうまく動く可能性もあります。

ただし、AArch64実装は存在しません。まだARMv7以前の問題になっています。なので、今からHaikuOSじゃなくてReactOSにAArch64実装をコミットするのはかなりおもしろいチャレンジじゃないかなと思います。学生さんとか来年「Google Summer of Code」とかで実装すると、きっといい感じに楽しいんじゃないでしょうか。

まとめ

ということで、以上でした。リファレンスは、このようにまとめています。PDFなどは「#arm_study」のハッシュタグでツイートしたので、後ほどご覧ください。ご清聴ありがとうございました。

司会者:ありがとうございました。TwitterとかYouTube Liveのコメントを見ていて、とくにMMU周り難しくてついていけないみたいな声がいくつかあったんですけど、ここらへんはあれですよね。金津さん、OSを実際に触ったことがないとけっこう難しいから、「へえ」ってわかればいいぐらいですね。きっと経験ないと。

金津:そうですね。一度Intelのページウォークとかのところ、レジスタの設定とかをひととおりやったことがある人だと多少わかるんじゃないかな。あと、Intel64よりも、CR3以外のもいくつかレジスタがあって、便利ということがわかるんじゃないかなと思うんですけど、ちょっと自分の資料の作り方も悪くて、なかなかわかりづらかったかなと思います。

司会者:いやいや、とてもわかりやすかったです。

金津:ただ、あれですよ、基本的にArmのマニュアルはけっこうシンプルで、割り込みも20ページあるPDFだったりとか、アドレス変換についても「Address Translation」というタイトルで10数ページか20ページぐらいのPDFでサクッとまとまっているので、これじっくり読むのでけっこう勉強になるんじゃないかなと思うので、このPDFを読むときに私の資料を参考にしつつ読んでいただければと思います。

司会者:ありがとうございます。

金津:ありがとうございました。