概要と自己紹介

金津穂氏(以下、金津):「AArch64とOS入門」ということで金津が発表いたします。

はじめにですが、「これからArmでOSを自作したい!」という人向けのまとめ資料になります。なので、すでにArmでお仕事している人、とくに組み込み向けだったりとかすでにOS開発とかしている人にとってはもう既知の情報しかない。あと、リファレンスマニュアルを自分で読める人にとっては、それを読んだほうが確実な情報が手に入るんじゃないかなと思います。

Armと題してますけど、基本的にはAArch64だけにします。AArch32はちょっと面倒くさいので触りません。Cortex-MなどのMシリーズはまた別の機会ということで、今回はAシリーズ、これを発表していきます。

まず私、orumin。Twitter「kotatsu_mi」でやっています。主にunikernelとか仮想化とかをやってて、博士学生です。

OSに必要なプロセッサー機能

では始めていきます。まずOSに必要なプロセッサーの機能とは何でしょうか?

OSの中でも、モダンなOSに求められる必須要素というものが存在すると思います。最近は、WindowsだったりLinuxだったりMacといったモダンなデスクトップOS、あるいは組み込みでもモダンなOSが多いと思いますが、こういったものではマルチタスクやメモリ保護、そして計算資源の抽象化や多重化といったものが前提となっています。

今回は、この中でもマルチタスクやメモリ保護について主にフィーチャーしていこうかと思います。ということで、主にマルチタスクに必要な割り込みやコンテキスト退避、メモリ保護に必要なMMU、これに絞って説明をしていきたいと思います。

Armの名前

まず本題に入る前に……。みなさんArmの名前、いつも混乱していると思います。先ほどtnishinagaさんにもいろいろと説明してもらいましたが、一度おさらいしておくと、ArmはArm社のプロセッサーということで、もともとはイギリスのAcornという会社が作っていた、Acorn RISC Machinesという特定のAcorn社のPC向けのプロセッサーでした。

そのあとAdvanced RISC Machines、ARM Ltd.、Arm Holdings plcと主体が変遷してて、現在Arm Holdingsはソフトバンクが買収しています。もともとArmはすべて大文字「ARM」でしたが、現在、先頭のAだけ大文字の「Arm」というのが正式名称です。

また、64-bit Armのアーキテクチャ名は基本的に「AArch64」で、「Arm64」というのはGNUがreferして使っている名前です。「Intel 64」が正式名称でありながら、GNU、GCCだと「x86-64」のほうを使うのと同じような理由です。

AArch64では先ほどtnishinagaさんが説明したようにA64。AArch32ではA32とT32の命令セットが利用できます。

商標のページを見てても、記事タイトルとかですべて大文字にするような何か必要性がないかぎりは、基本的には不適切な大文字化は避けよと書かれています。

Armの実行モデル

では本題です。まずArmの実行モデルについて話していこうと思います。これからは、基本的にはIntel 64のCPUプロセッサーと比較して話していきます。

まずIntel。みなさん知っているかと思いますが、Intelはリング(Ring)といったものを使って実行権限を分離しています。Ring 0・1・2・3がありまして、Ring 0が特権モード、いわゆるOSカーネルを動作させるモードで、Ring 3がみんなが使っているブラウザなどのアプリケーションです。

Ring 1・2は、本当はOSのドライバとかをよりRing 0よりも小さな権限で実行することでセキュリティを担保する、そういった理由で作られているのですが、実際のところ誰も使っていません。

というのも、基本的には特権モードが多すぎるとOSをそれに特化させて実装しなければいけないんですが、x86-64だけで動くPCなどではそれでいいんですけど、Windows NTだったりLinuxだったり複数のアーキテクチャで動く必要のあるものは、基本的には2つのモードがあったとしてもRing1・Ring2といったものは存在しないため、それらは使わないことになってしまって、実質2モードしかないことになっています。

一方、Armを見てみましょう。ArmではRingではなくて、Exception Level(例外レベル)という名前で実行モードを分けています。Intelと違って、数字の若いほうから権限が弱くなっていて、数字の大きいほうが権限が一番強くなっています。0から順番にUser、OS、Hypervisor、そしてSecure Monitorとなっていまして、Secure Monitorというのは基本的にTrustZoneなどのセキュアOSで実行するものになっています。

TrustZoneについては後ほど説明があると思うので、今回は省こうと思います。Hypervisorについても、ぬるぽへさんがあとでしゃべると思うので、今回の資料では、EL0とEL1、この2つのモードに絞って説明していこうと思います。

AArch32の実行モデル

その前に、いちど、AArch32の実行モデルを説明していこうと思います。先ほどException Levelという話をしましたが、こちらの図のException Level 1のところにService Call(svc)・Abort(abt)・IRQ(irq)・FIQ(fiq)・Undefined(und)・System(sys)、これら6つモードが書いてあります。

これはどういうことかと言いますと、もともとAArch32・ARMv7では7つCPUの動作モードがありまして、特権モードは7つのモードのうちの6つ、1つUserモードだけ非特権モードになっており、それぞれモードによって汎用レジスタがバンク切り替えすることによっていろいろなモードを切り替えて実行する、そういったことになっていました。

実はAArch64でもAArch32モードに切り替えることができるので、途中でそのモードに……6つのモードを意識した状態で実行することも可能なのですが、今回の発表ではそこはなるべく触れないようにしていきます。ですが、たまに古いOSの実装とか読むときには、これも必要となってくるので、頭の片隅に置いておくと何か役に立つかもしれません。

AArch64の割り込み

AArch64の割り込みについてです。AArch64はAArch32と違って、いくつかレジスタとか増やしていて、基本的に割り込みに関係するのはここらへんのレジスタになります。

まずPSTATE。これは条件フラグとか現在の例外レベルといった状態を記憶しておきます。例外が実際に発生したときには、SPSRというレジスタにPSTATEを保存して退避して、別の例外レベルにジャンプします。SPSRは例外レベルごとに1〜3まで3つ別々に存在しているため、例えばOSの中でHypervisor Call(HVC)を発行したら、そのときのPSTATEをまたSPSR_EL2に保存してといったかたちで、順番にジャンプすることが可能です。

そしてリンクレジスタ(LR)。これは先ほど説明がありましたね。サブルーチンからのリターンアドレスを記憶しますが、例外リンクレジスタというELRというものも存在します。これは例外からのリターンアドレスを記憶するもので、リンクレジスタと基本的に使い方は同じですが、例外のハンドリングのときに使用します。

次にベクタテーブルを見ていきます。AArch64はベクタテーブルを割り込みレベルEL1〜3それぞれ別々に持っていて、それぞれVBAR_ELnというアドレスからのオフセットでアクセスできます。

例えばVBAR_ELnがcurrent ELをSP0でとか書いてありますけど、これはどういうことかといいますと、例えばOS、EL1の中でもう一度OSの割り込みが発生するNested IRQが起きたときに同じ例外レベルでハンドリングすることになったら、SP0を使ってそのまままた例外に返ってくる。そのときの例外の種類によってIRQだったりFIQだったりSErrorだったりとかで、それぞれ0x080、0x100、0x180などにジャンプして、そこに登録されている例外ハンドラを実行するかたちになっています。

また、SP0だけじゃなくて、SPは例外レベルポイントごとにあったりするんですけど、SPSelというビットがあったりするので、それでSPを切り替えたりすることもできます。

より低い例外レベルに遷移したときにAArch64からAArch32に切り替えるなどもあるので、それぞれより低い例外レベルにジャンプしたときのベクタはAArch64モード版とAArch32版でそれぞれ別々に用意されていて、0x780までベクタが存在しています。

割り込みフローとコード例

このときの割り込みのフローを実際に見てきますと、EL0 User Programで、例えばSupervisor Call(SVC)、いわゆるシステムコールを発行したらIRQが起きるので、そこでProgram Counterレジスタ(PC)を、Exception level Link Register(ELR)、例外のリンクレジスタに保存。また、現在の状態はPSTATEからSPSRに退避して、実際の割り込みを実行し始める。

割り込みのハンドラの中でまたレジスタ退避だったりとか割り込みを有効化することで、さらにnestedして別のinterruption handlerが呼ばれる。そういったこともあります。

基本的にはLRとPSTATE、PCなど使っていい感じにELの変化などでレジスタに退避できるので、けっこう便利な感じになっています。直感的だと思います。これは。

最後に割り込みについて実際のアセンブリコードを見ていきます。これは公式のドキュメントで見てみたんですけど簡単で、いっぺん壊される可能性のあるレジスタをSPに退避して、read_irq_sourceとかそういった割り込み原因を読むようなコードにブランチしてから、Cで実装されたハンドラにブランチしてまた割り込みに返ってくるみたいな素朴な実装になっています。また、ERETを使うことによって、ELRに保存したリターンアドレスを使ってもとの場所に返ってきます。

この実装の場合はSPSRやELRの退避をしていないので、nestedの割り込みをハンドルすることはできません。

(次回につづく)