借り初めのひみつきち

仮ブログです。

raspberry pi ベアメタルでマルチコアを利用する方法

入手困難とうわさの raspberry pi を偶然にも入手たので最近触っています。

raspi の CPU は4コアで動作しますが、現在のファームウェアは最初のコア (ID 0) のみ起動します。*1

他のコアがどうしているかというと、ファームウェア物理アドレス 0x0000_00E0, 0x0000_00E8, 0x0000_00F0 の値を監視しているので、ここを書き換えてやれば起動できます。

例えば 0x0000_00E0 に 0x0008_0000 (_start のアドレス) を書き込むと ID 1 のコアが _start から実行開始します。

実際に書き込む場合、マルチコアでメモリ書き込みを保証するために Release のメモリーオーダーで書き込み、 sev 命令でイベントを通知します。

Rust だとこのような感じになります。

    for p in [0xE0, 0xE8, 0xF0] {
        let p = &*(p as *const AtomicUsize);
        p.store(_start as usize, Ordering::Release);
        asm!("sev");
    }

このコードを実行すると各コアが _start から実行開始するので、それぞれ mpidr_el1 レジスタを読み出して下位2ビットからコア ID を取得し、シフトした決め打ちのスタックポインタを設定し、 bl 命令で初期化関数を呼び出します。

_start:
    mrs     x1, mpidr_el1
    and     x1, x1, #3
    cbz     x1, 2f

    lsl     x2, x1, #16
    add     x2, x2, #0x10000
    mov     sp, x2

    bl      _smp_main

1:  wfe
    b       1b
2:
    (省略)

これだけでマルチコアが起動できました。

APIC の設定を弄ったりロングモードの用意が必要な x86-64 に比べると、 Arm64 のマルチコア起動はスッキリしていて簡単にできますね!

と、言いたいところですが、このままでは atomic 変数の read-modify-write や CAS 操作に必要な命令 (stxrなど) が正しく動作しません。 CAS 操作ができないと spinlock を実装できないのでコア間で同期を取る事ができません。 つまり、コアごとに完全に役割分離しているような特殊な用途を除いてマルチコアを活用する事ができません。 CAS 操作を正しく実行するためには、 MMU とキャッシュの設定が必要になります。

この辺り x86 は適当に書いてもなんとなく動きますが、 arm では真面目にちゃんと設定しないと意図した通りに動いてくれないので難しいです。

なお、 Arm 系 CPU は基本的に同様のプロトコルで起動できるようで、各コアの監視アドレス 0xE0, 0xE8, 0xF0 は DeviceTree の各 CPU にある cpu-release-addr で確認できます。

  cpus {
        #address-cells = <0x01>;
        #size-cells = <0x00>;
        enable-method = "brcm,bcm2836-smp";
        phandle = <0xdb>;

        cpu@0 {
            device_type = "cpu";
            compatible = "arm,cortex-a72";
            reg = <0x00>;
            enable-method = "spin-table";
            cpu-release-addr = <0x00 0xd8>;
            phandle = <0x28>;
        };

        cpu@1 {
            device_type = "cpu";
            compatible = "arm,cortex-a72";
            reg = <0x01>;
            enable-method = "spin-table";
            cpu-release-addr = <0x00 0xe0>;
            phandle = <0x29>;
        };

        cpu@2 {
            device_type = "cpu";
            compatible = "arm,cortex-a72";
            reg = <0x02>;
            enable-method = "spin-table";
            cpu-release-addr = <0x00 0xe8>;
            phandle = <0x2a>;
        };

        cpu@3 {
            device_type = "cpu";
            compatible = "arm,cortex-a72";
            reg = <0x03>;
            enable-method = "spin-table";
            cpu-release-addr = <0x00 0xf0>;
            phandle = <0x2b>;
        };
    };

*1:過去のファームウェアは全てのコアが一斉に起動していた時代があるようで、ネットで raspi ベアメタルのやり方を調べるとスタートアップ時にコアを判別して ID 0 以外のコアで無限ループするやり方がよく紹介されています

私の OS のご紹介

ここ数年 Rust で meg-os という名前の自作 OS を作っています。

github.com

過去に何度か0から作り直しているので、似たような名前の別のものを昔見たり聞いたりしたことがあるかもしれません。

現在のコードベースのコードネームは「Maystorm」となっていて、これは2020年5月に正式に開発を開始したのが由来です。 なお、初期のコードネームが「myos」だったのでこのブログでは「myos」と表記されてることも多いです。

主要なターゲットは 2020年前後の x64 PC です。 一時期古い PC 用に機能縮小した x86 版も並行で開発していましたが、しばらく放置している間に現在では互換性がほとんどなくなってしまいました。 いずれ時間ができたら x86 版の開発も再開したいと考えています。

主な特徴として以下のような特徴を備えています。

  • POSIX との互換性は目指していません。
  • アプリケーション・カーネルの両方で Rust でのプログラミングを前提にしています。
  • 自作の WebAssembly ランタイムを搭載し、標準アプリケーションの形式として WebAssembly をサポートしています。
  • マルチコアや HyperThreading に対応したスケジューラーを備えています。
  • 半透明や角丸のウィンドウを表示可能なウィンドウシステムを搭載しています。
  • はりぼて OS のアプリケーションをエミュレーション実行できます。

謎の SSE 例外

先週、 myos で hd audio に対応しましたが、波形メモリに直接波形を書き込むことで beep 音を生成していました。

この方法だと波形生成後のフィルター処理などが困難で、複数のアプリケーションで同時に音を鳴らすこともできませんでした。

ということで、複数のオーディオノードを接続してフィルタ処理をかけるスケジューラーを実装します。

デジタルオーディオデータは 16bit がよく使われますが、フィルタ処理の最中には 16bit の精度を超える計算をすることもあったりクリッピングの手間があるのでフィルタなどの計算は浮動小数演算で処理し、最終的に波形メモリに書き込む際に 16bit 値に丸めるようにします。

実は myos では結構前に SSE を解禁していますが、浮動小数演算を実際に使ったのは今回がはじめてでした。

QEMU で動いたので実機で試したところ、 Inexact-Result (Precision) Exception という謎の例外が発生しました。

f:id:neriring16:20220220203409p:plain

調べてみると SIMD 例外の一種で、割り切れない演算をした際など演算結果に誤差が生じたときに発生する例外のようです。 多くの場合そこまで厳密な計算したいわけではないので、ほとんどのケースでこの例外はマスクされる想定のようです。

SSE では MXCSR というレジスタを使って浮動小数演算の動作を制御することができます。

f:id:neriring16:20220220205827p:plain

このレジスタの下位 6bit は SSE 例外が派生した場合にどの例外が発生したかを示していて、 7bit 〜 12bit では例外を種類ごとにマスクするかどうか選択することができます。また丸め処理なども制御することができます。

ということで、スレッドの初期化時に LDMXCSR 命令を実行して 12bit 目の Precision Mask を 1 にセットすることでこの例外を抑制することができました。

今月の myos

久しぶりの更新になります。

現在のバージョンの myos には明確な内部目標がありました。 Intel HD Audio の対応です。 *1

HD Audio は近年の PC にはかなりの割合で搭載されており、仕様も公開されていてそれなりに情報が出揃っているので実装可能だろうという目論見で実装開始しました。

QEMU で音を鳴らすだけであればネットによくある情報だけで十分実装可能ですが、実機で鳴らす場合にはいくつかの注意点があります。

実機の HD Audio は QEMU よりも多くのノードが繋がっており、実際に音を鳴らすことが可能なノードを特定する確実なロジックは今のところよくわかりませんでした。 HD Audio で音を再生する際に登場する主要なノードとして Pin と DAC があり QEMU では DAC と Pin が直結されていますが、実機では間に Mixer が挟まっているケースが多いです。

現状の実装では音が鳴る機種と鳴らない機種があります。

実際に実装してみると XHCI に比べればだいぶ単純ですが、それでも必要な構造体がかなり多くなってしまいました。

f:id:neriring16:20220213134343p:plain

まだネイティブアプリは未対応で、 haribote OS の MML player (mmlplay.hrb) くらいしか音が鳴りませんが、 myos でも音が出るようになりました。

BEEP 音ジェネレーターはかなり適当な実装なので、将来的にはオーディオデータのスケジューラーやミキサーも実装したいです。

*1:余談ですが HD Audio の開発時のコードネームは Azalia という名前で、現在の myos のコードネームの Azalea と偶然にも非常によく似ています。

QOI 画像ビューワーつくった

最近 QOI (Quite OK Image Format) という新しい画像形式が登場しました。

QOI — The Quite OK Image Format

PDF1 ページに収まる単純な仕様でエンコーダーデコーダーもC言語で数百行と単純で高速ですが、PNGよりやや圧縮率が悪い程度というトレードオフがなかなか優秀なフォーマットです。

アルゴリズム上、写真などの自然画よりもイラスト画像に効果があり、デコーダーの軽さも考慮するとアセット画像に向いてそうです。

さて、新しい画像形式が生まれた時に問題となるのが、サポートするソフトが少ない問題です。

QOI 形式の画像は本格的な画像編集はあまり求められておらず、内容の確認とフォーマットの変換ができれば十分だと思います。 公式サイトにはいくつかツールが紹介されていますが、動作環境が微妙だったりビルドがうまく通らなかったりしてぼくの環境では簡単に使えるツールがありませんでした。

幸い Rust のライブラリはすでにいくつか提供されているようなので WebAssembly でラッピングしてウェブアプリにしてみました。

https://nerry.jp/image-viewer/

github.com

構造としては IMG タグで読み込んだ画像を Canvas に描画する簡単なウェブアプリに QOI フォーマットの入出力機能を追加した感じです。 この方法だと PNG の入出力はブラウザに任せればいいので相互変換が簡単に実装できます。

これで QOI 画像の表示や変換が簡単にできるようになりました。

ちなみに myos の最新版でも QOI 形式に対応していて一部の画像リソースは QOI 形式になっています。

誰も教えてくれない AMD64 と Intel64 の違い (ページング編)

Intel64 は AMD64 を参考に実装したので、9割以上のアプリケーションにおいて概ね互換性があります。 一方、船頭多くしてなんとやら、両社の政治的な思惑などが絡んで意図的に非互換になっている部分があったり、細部の互換性をとることができていない部分があったりします。

AMD64 と Intel64 の代表的な差異は色々なところでまとめられているのですぐ見つけることができますが、中には一般に知られていない差異もあります。

以前 EFER.LMA の差異を発見したことがありましたが、今回新しい差異を発見したのでここにまとめます。

neriring.hatenablog.jp

事件編

myos は、現代では Intel64 PC の方が手に入りやすいので Intel64 機で開発されてきました。 そして、ある程度動くようになってきたので家にあった唯一の AMD64 機でも動作確認をすることにしました。

普段使ってないマシンなので埃を落としていざ電源オン、 BIOS のかっこいいロゴが表示された後で myos を起動しているメッセージが一瞬表示されましたが、すぐに暗転して次の瞬間また BIOS のかっこいいロゴが表示されました。

これは起動時に何か問題が起きて強制再起動しているようですね・・・。

犯人さがし

OS を開発している初期の段階では何か設定を間違って例外が発生することが稀によくあります。 一方、そもそも例外処理がうまく動いてないのでそのまま CPU がシャットダウン状態になって再起動してしまうケースが非常によくあります。

このような場合、どうやってデバッグすればいいでしょうか?

答えは簡単、まずは適当なところに無限ループを仕込んで起動テスト。 無事に動作が止まれば無限ループのところまでは動いてるので OK、 無限ループする前に再起動したらループより前に問題があることがわかります。 そして、無限ループを仕込む場所を少しずつ移動しながら起動テストを繰り返すことで問題が起きている場所を絞り込むことができます。

これで、カーネルの序盤にページングを初期化してる部分が怪しいところまでは絞れました。

他の機種では動いているのでページ設定の大枠に問題はないはずです。 しかし、実際にはページ設定で問題が起きています。 このような場合、ページングの拡張属性の使い方が正しくない可能性が高いです。

システム起動時には RECURSIVE PAGE TABLE AREA (ページテーブル操作用に再帰しているページテーブル領域) と DIRECT MAPPING AREA (物理メモリに直接アクセスするための領域) の設定を行っています。

ここで怪しい拡張属性を考えてみると、どちらもシステム全体からアクセスする領域なので GLOBAL 属性を付与していました。 *1 試しに GLOBAL を設定していたビットを0に変えてビルドしてみます。なんと、起動しました。

GLOBAL 属性は i386 よりも後 (P6 世代) になって追加された属性で、 CR4 レジスタの PGE ビットを有効にしないと使えません。 そこで、 CR4 レジスタの設定が間違っているかと思いブートローダーで明示的に CR4.PGE ビットを立てるコードを追加してみました。

しかし、予想に反してこの修正を加えても起動しませんでした。

Global ビットが絡んでいることは明白なのに、これは一体どういうことでしょうか。

解決編

では、分厚いマニュアルを開いて確認してみましょう。

Global ビットはページテーブルエントリの bit8 に存在します。

f:id:neriring16:20210925180451p:plain

AMD64 では PML4E の bit8 は Must be Zero と定義されています。

f:id:neriring16:20210925180544p:plain

一方、 Intel64 では PML4E の bit8 は Ignored と定義されています。

f:id:neriring16:20210925180639p:plain

もっと詳しくマニュアルを見てみると、 Global Page 属性は PTE, PDE, PDPTE のみでサポートされ、 PML4E ではサポートされていないことがわかります。

f:id:neriring16:20210925180809p:plain

また、 PML4E 以外でも Global ビットが実際に有効になるのは最下位レベルページテーブルエントリ (4KBページなら PTE、2MBページなら PDE) のみで、それ以外のレベルのページテーブルエントリ内の Global ビットは無視されます。

f:id:neriring16:20210925182349p:plain

つまり、 PML4E の Global に相当するビットはもともと存在しないビットなので PML4E に Global ビットを設定しようとしているコードがそもそも間違いです。 両者の仕様の違いにより、 Intel64 では無視され、 AMD64 では不正なビットでページフォルトが発生するという現象が起きていました。

このように PTE, PDE, PDPTE, PML4E は基本的な考え方やビット配置は概ね共通していますが、微妙にサポートされている機能に差異があります。 また、 Intel64 では無視されても AMD64 では厳しくチェックされるといった具合に、両者の細かい挙動は異なるようです。

後日談

PML4E に Global ビットを設定していたコードを修正して無事起動するようになりました。

しかし、起動しても USB をうまく認識せず myos を操作することができませんでした。 Windows を起動してデバイスマネージャで確認してみると、なんと USB デバイスは全部 EHCI に繋がっていました。解散。

*1:追記: Recursive Area に Global を設定すると CR3 フラッシュで消せなくなってめんどくさいことになるのでそもそも使うべきではないと思います。

Compound Device と Composite Device

Compound と Composite の違い、わかりますか?

このふたつの単語、綴りも似ていますが意味も似ていて、ニュアンスの違いを説明しろと言われると正直ちょっと自信がないです。

USB 規格には Compound DeviceComposite Device という2種類の用語が存在していてそれぞれ別の意味を持ちます。

Compound Device

Compound Device は一言で言うと USB ハブを内蔵したデバイスで、 USB ハブとセットになったキーボードやドックなどが該当します。

Compound Device は物理的にはひとつのデバイスですが、内部的には USB HUB とそれにぶら下がった複数のデバイスとして USB バスのツリー上で認識され、それぞれの内蔵デバイスは独立した USB アドレスを持ちます。

Compound Device は USB ハブの Hub Descriptor 内の wHubCharacteristics にある2ビット目のフラグで判別することができます。

f:id:neriring16:20210920224040p:plain

このビットが1の場合、単純に独立した USB ハブではなく、 Hub Descriptor の後半の方にある DeviceRemovable で non-removable と指定されたビットに該当するポートにあるデバイスとまとめた Compound Device ということになります。

f:id:neriring16:20210920224050p:plain

Composite Device

Composite Device は一言で言うと複数の機能 (Interface) を持った USB デバイスで、トラックパッドと一体化したキーボードなどが該当します。 ちなみに、これにさらに USB ハブが付いてる場合は Compound Device でありかつ Composite Device ということになります。

Composite Device は USB バス上でも単一のアドレスを持った単一のデバイスとして認識され、複数の Interface で複数の機能を提供します。

HID と Composite Device

ここから先はちょっと複雑な話になりますが、キーボードやマウスは USB HID という規格のデバイスになります。

HID 規格は人間と機械の間にある全てのデバイスを定義しようとしたとても壮大な規格で、キーボードやマウス以外にもさまざまなカテゴリのデバイスが定義されています。 そして、 HID に対応したデバイスはレポートと Usage によってさまざまな機能を提供しています。 そのため、 HID デバイスは HID という単一の Interface だけでも複数の機能を提供するものがあります。

よくあるキーボードで通常の文字キー以外に、電源関係、マルチメディア操作、その他のショートカットなどに対応しているものがあります。 実はこれらのキーは HID 規格上では別々の Usage として定義されています。

ほとんどの USB キーボードは HID Boot キーボードというクラスに属していて、いくつか定型の決まり事があります。 そして、 HID Boot キーボードクラスの仕様だけでは先述のようなキーは仲間外れになってしまうので、キーボードと別の HID Interface として定義してる場合と、キーボードを含めてごちゃ混ぜの独自の汎用 HID デバイスとして定義している場合があります。

このようなデバイスWindows PC に接続してデバイスマネージャを見てみると、複数の謎の HID デバイスとして認識されることがありますが、先述のように単一の Interface の場合と複数の Interface に分かれている本当の意味での Composite Device の場合があります。

HID デバイスはレポートの自由度が高すぎる反面、複雑すぎて単一機能のデバイスなのか複数機能のデバイスなのかを判断するのがとても難しいです。 HID デバイスの場合は Interface が1つでも本質的には Composite Device とあまり変わらないということがわかるかと思います。

余談ですが、 HID デバイスには実際には提供していない機能を含んだ定型文のようなレポートディスクリプタを返すデバイスが時々存在します。 おそらくいくつかの機能をまとめた便利チップみたいなものがあってそれをカスタマイズして使っているんだと思いますが、キーボードの形をしていないのにキーボードの定義が含まれていたり、マウスの機能がないのにマウスの定義が含まれていたり、どこにも存在しないキーの定義が含まれていることがあるのでレポートディスクリプタをあまり信用してはいけないかも知れません。