借り初めのひみつきち

仮ブログです。

2023年のご活躍

今年一年の活動を振り返ってみます。

シンプルでそこそこの圧縮率が出る組み込みに有用な非可逆画像フォーマット「MPIC」を作りました。

neriring.hatenablog.jp

また、去年作った画像ツールをMPICに対応しました。 最初は画像の表示と保存しかできないアプリでしたが、いくつか簡単な画像加工できるように改良中です。

github.com

自作OS関連の活動ではRISCVの調査を始めていますが、今のところハローワールドとネタで作った変なサンプルくらいしか出せる成果がありません。

neriring.hatenablog.jp

neriring.hatenablog.jp

MYOS は今年前半にあったRUSTの仕様変更の煽りを受けてモチベ大幅低下中で、ハイブリッドアーキテクチャ対応が今年一番の改修となっています。

現在の MYOS には初期の設計に起因する根本的な問題がいくつか残っているため、来年あたりに大規模改修に着手したいと思ってます。

その他に大きめのプロジェクトが1個、小さいプロジェクトが数個裏で動いていて一緒にここでご紹介したいかったですが、諸事情により今のところ何も出せる情報がありません。笑

来年こそ形になるといいですね。

RISC-V で自作OS

じだいは RISC-V です。

github.com

RISC-V とは

Windows でよく使われている x86 は 40 年以上前から基本的に互換性を持ったまま拡張を続けた結果、現代ではほとんど必要ない機能との互換性のためにかなり複雑な仕様になっています。 また、 Arm に比べると高価で発熱が多く構成の自由度が低いので Windows 以外の用途ではあまり使われません。

Windows 以外でメジャーな Arm は組み込みからサーバーまで幅広い用途に使われますが、ライセンスやロイヤリティの関係でベンダーの自由度が制限されていて離れるベンダーが増えてきています。

ある程度の性能が必要なコンピュータは今までは x86 か Arm の実質2択でしたが、最近はシンプルで自由な RISC-V が注目されています。

RISC-V の命令セット

RISC-V の命令セットは、32bit 基本命令セットの RV32I、 64bit の RV64I、 32bit のサブセットの RV32E という主要な基本命令セットがあり、その後に数文字のアルファベットで拡張命令セットの種類を表します。 Rust は E 基本命令セットに対応していないので I 基本命令セットのチップを選択する必要があります。

M は乗除算(Multiplication)命令セット、 A はアトミック(Atomic)命令セット、 F は浮動小数(Floating-point)命令セット、 D は倍精度浮動小数(Double-precision)命令セット、 C は圧縮(Compressed)命令セット、 S は S(Supervisor) モード実装済み、 U は U(User) モード実装済みをそれぞれ表します。 (ファームウェアが動作する最上位の M(Machine) モード は全てのチップに実装されています。)

M の乗除算命令セットは多くの RISC-V チップが対応していますが、乗除算(特に除算)は回路が複雑で規模が大きくなるので組み込み向けの小規模なチップではこの命令が実装されていないことがあります。 F や D の浮動小数演算も一部の分野を除いてほとんど使わないので回路規模を抑えるために対応していないチップがあります。 このようなチップで未対応の演算が必要な場合、加算・減算・ビット演算・表引き等を組み合わせて代用する必要があります。 *1

A のアトミック命令セットは主にマルチタスク環境のスレッド間で変数を正しく同期するために必要な命令群で、特にマルチプロセッサ環境で重要です。 例によって組み込み用途では必要ないのでサポートしていません。

C の圧縮命令セットは、 Arm の Thumb のように 32bit 長の基本命令セットに対し一部のよく使われる命令を 16bit の短いフォーマットで表現できる命令セットになります。 Thumb との最大の違いは Thumb ではモード切り替えが必要なのに対し、 RISC-V は最初からオペコードに命令長を区別するビットパターンが定義されているのでモード切り替えが必要なく通常命令と圧縮命令が混在できる点が違います。 多くの RISC-V チップが C 命令セットをサポートしているので RISC-V のコードは 16bit 命令と 32bit 命令が混在していることが多いです。

IMAFD の組み合わせは汎用命令セット (General) と呼んで G の別名が付いているので、セットでよく使われる IMAFDC を省略して GC と表記することもあります。

Linux 等の汎用 OS をサポートするチップでは IMACGC(IMAFDC) の組み合わせがよく使われ、さらに S と U の両方実装されていますが SU は明記されないことも多いようです。

一方、組み込みでは RV32EC という組み合わせのチップもあります。 一口に RISC-V と言っても意外と多くの命令セットのバリエーションがあります。

ということで、 rust のターゲットは riscv64gc-unknown-none-elf で行きます。

OpenSBI

RISC-V で汎用 OS が起動するスペックのコンピュータは RISC-V Supervisor Binary Interface (SBI) という規格のファームウェアが使われているのでハローワールドは簡単に作れます。

試しに QEMU を起動してみるとこのような画面で OpenSBI が起動します。 OpenSBI は SBI のオープンソース実装です。

SBI の S は Supervisor の略で RISC-V で通常の OS が起動する S モードのことを指しています。

SBI を呼び出すには a7 レジスタに EID (extension ID) をセットして S モードで ecall 命令を実行します。*2 また、 a0 レジスタに戻り値が返ってくるので戻り値が必要ない場合でも lateouta0 レジスタを除外指定します。

ハローワールドに必要な putchar の EID は 1 なので a7 レジスタに 1 をセットして次のような関数を作ります。

fn sbi_putchar(ch: u8) {
    unsafe {
        asm!(
            "ecall",
            in("a7") 1,
            in("a0") ch as usize,
            lateout("a0")_
        );
    }
}

qemuRISC-V ハローワールド

Rust はクロスコンパイルが簡単で RISC-V にも対応しているので、 rust と qemu をインストールすればすぐに環境が整います。

表示するメッセージを bytes() イテレーターで1バイトずつ分解し、さっき作った sbi_putchar() を呼び出せばハローワールドの主要な部分は完成です。

    for ch in "Hello, World!\n".bytes() {
        sbi_putchar(ch);
    }

これにベアメタルで動くためのおまじないの #![no_std]#![no_main]、パニックハンドラーを追加するととりあえず最小限のハローワールドは動きます。

しかし、これをベースに拡張することを考慮するとスタックポインタの設定とBSSの初期化もしておいた方が良いでしょう。

関数の途中でスタックポインタを変更するとコンパイラが作るプロローグやエピローグのスタックの処理で不整合が起きる可能性があるので、プロローグ・エピローグを生成しない naked 属性の関数が必要です。 しかし、執筆時点の rust は RISC-V の naked 関数をサポートしていないので、今回は global_asm で代用します。

スタックポインタを設定してメイン関数を呼び出す処理を global_asm で実装し、この関数をエントリーポイントにします。

global_asm!("
.section .text.init
.global start
start:
    la sp, __stack_top
    j os_main
");

また、エントリーポイントを .text セクションの先頭に配置しないと正しく動作しない環境があったので専用のセクション (.text.init) を作って後でリンカースクリプトで配置します。

次に、メイン関数の最初にBSSを初期化します。

BSS は初期値が 0 のデータをまとめたセクションで、主に初期値のビット値が 0 になるグローバル変数やスタティック変数などに使われます。 BSSセクションの内容は全部 0 になることが期待されているので、バイナリファイルにはアドレスとサイズのヘッダ情報のみ記載すれば良く、バイナリファイルのサイズを削減できます。 しかし、組み込みやOS起動時などの簡易的なプログラムローダーの場合はBSSが初期化されていないことがあるので、メイン関数の一番最初に BSS の場所とサイズを求めて 0 で初期化します。

    unsafe {
        let bss = &__bss as *const _ as *mut u8;
        let bss_end = &__bss_end as *const _;
        bss.write_bytes(0, bss_end as usize - bss as usize);
    };

メインプログラムが完成したら、既知のターゲットではないのでリンカースクリプトを作ります。

qemuRISC-V では RAM が 0x80000000 から始まり SBI が配置されるので、次のページ境界の 0x80200000 からプログラムが配置されるようにします。 スタックトップやBSSの位置を指すラベルもリンカースクリプトで定義します。

ENTRY(start)

SECTIONS {
    . = 0x80200000;

    .text : {
        KEEP(*(.text.init));
        *(.text .text.*);
    }

    .rodata : ALIGN(4) {
        *(.rodata .rodata.*);
    }

    .data : ALIGN(4) {
        *(.data .data.*);
    }

    .bss : ALIGN(4) {
        __bss = .;
        *(.bss .bss.* .sbss .sbss.*);
        __bss_end = .;
    }

    . = ALIGN(4096);
    . += 128 * 1024;
    __stack_top = .;
}

リンカースクリプトができたら、これを読み込むように .cargo/config.toml を編集します。 ついでにターゲットの指定もここでしておくと楽です。

[build]
target = "riscv64gc-unknown-none-elf"
rustflags = ["-C", "link-args=-Tsrc/riscv.lds"]

これをビルドして実行します。

OpenSBI の動作ログが表示されたあと最後にプログラムで指定したメッセージ (Hello, World!) が表示されています。

It works!

実機 (VisionFive2, U-Boot) でハローワールド

執筆時点で RISC-V で動作し簡単に入手可能なパソコンはありません。 Linux が動作する SBC は何種類か出ていて比較的簡単に入手できるので、ここでは StarFive の VisionFive2 を使います。

おそらく Linux をサポートする機種では U-Boot というブートローダーが主に使われています。

VisionFive2 には Raspberry Pi 互換の 40 ピン GPIO 端子があり、シリアルポート (UART) の信号が出ています。 6pin GND、8pin TX(GPIO14)、10pin RX(GPIO15) を USB シリアルポートに変換するアダプタに繋ぐとパソコンと通信できます。

パソコンでシリアル通信できるアプリを起動してボードの電源を入れると OpenSBI のロゴが表示された後に U-Boot が起動しているのが確認できます。 Mac ならターミナルで screen /dev/tty.usbserial-XXX 115200 と入力すれば起動します。(XXXは環境によって変わるので /dev/ から似たようなファイルを探してください)

U-Boot が起動したらコンソールからカーネルを読み込んで bootelf コマンドを実行すると qemu と同じようにハローワールドが動作します。

U-Boot はSDカードなどからカーネルを起動できますが、テストのために何度もビルドしてコピーして繋ぎ直すのがめんどくさかったので、ここでは Ethernet で接続して tftpboot コマンドを使って TFTP 経由で起動してみました。

U-Boot は自動的にメモリを割り当ててファイルを読み込んでくれる機能がないため、 ELF ファイルを読み込む場合は本来のロード先と別のアドレスを指定して一旦ファイルを読み込んだ後、 bootelf コマンドを実行する時に U-Boot が ELF ヘッダーにある本来のアドレスに配置します。

次の一歩

OpenSBI はコンソールの入出力(通常はシリアルポート)、タイマーの設定やマルチコアの初期化に必要な処理などをサポートしているものの、その他のハードウェア構成情報は Device Tree を参照する必要があります。 *3

qemu で起動した場合エントリポインタの引数の a1 レジスタから Flattened Device Tree (FDT/DTB) がそのまま取得できるので、これをパースして virtio に対応すれば一般的な OS としての機能は大体実現できそうです。 *4

U-Boot で bootelf コマンドで起動した場合は Device Tree が取得できません。 U-Boot に Linux カーネルとして認識させれば Device Tree が取得できますが、その場合は ELF 形式から https://www.kernel.org/doc/Documentation/riscv/boot-image-header.rst のドキュメントに準拠した形式のバイナリに変換し、 booti コマンドで起動するように変える必要があります。

Device Tree に対応してもハードウェアの構成がわかるだけで、それぞれの機能のドライバは自分で実装する必要があります。 世の中のすべてのデバイスに対応するのは無理なので、実際には特定機種専用の OS になって Device Tree の構成情報はあまり必要ないかも知れません。

実機 (Milk-V Duo, U-Boot)

Milk-V Duo の場合はどうでしょうか。 Milk-V Duo は非常に安価で Raspberry Pi Pico とほぼ同じサイズですが RV64IMAFDC と標準的な命令セット *5 を一通りサポートして Linux が動作する興味深い SBC です。

Milk-V Duo の UART は 16pin と 17pin が TX/RX になっており、 GND はすぐ隣の 18pin を使うと便利です。

スペックは控えめになっており、メモリ消費を抑えるためか公式が提供している U-Boot も機能が大幅に削減されていて bootm コマンドでしか起動できません。

bootm コマンドで起動できるイメージはカーネル・Device Tree・initrdを専用のツールでひとつのファイルにまとめたもので、ビルドのためにツールが必要でちょっとめんどくさいです。

そもそも、 Milk-V Duo は買ってきたそのままの状態だと OpenSBI も U-Boot も起動しません。 公式の提供するファームウェアイメージを SD カードにインストールする必要があります。

github.com

公式イメージの最初のパーティション (FAT) にある fip.bin というファイルがファームウェア (FSBL + OpenSBI + U-Boot) のイメージファイルになります。 公式のビルドツールをダウンロードして設定をカスタマイズした後ビルドしてできた fip.bin を置き換えることで U-Boot を含んだファームウェアのカスタマイズができます。 また、同じパーティションにある boot.sd はデフォルト起動する Linux のイメージなので、自作 OS で使う SD カードの場合は削除した方が便利です。

U-Boot をカスタマイズして booti コマンドで起動するイメージを作ることに成功しました。

VisionFive2 と Milk-V Duo のメモリレイアウトは異なるため、同じイメージで動かそうとすると起動時一番最初にメモリレイアウトを調整するようなコードがないとうまく動かないと思います。

いかがでしたか?

RISC-V の SBC はハローワールドが簡単に動作することがわかりました。

しかし、 RISC-V は x86 PC のような典型的なハードウェア構成というものが存在しないため、全ての環境で動作する OS を作るのは容易ではありません。

また、 x86 や Arm に比べると市場全体の成熟度が低く様々な問題に突き当たることがあります。 成熟にはまだまだ時間を要するでしょう。

その先には自由が広がっています。自由な RISC-V の世界を開拓しましょう!

*1:なお、8bit時代のCPUのほとんどは乗除算に未対応で、浮動小数演算がハードウェアでサポートされるようになったのも32bit以降が多いので、古いチップや組み込み環境では今でも使われているテクニックです。

*2:ecall 命令は通常エスカレーションが発生するので現在の動作モードによって動作が異なります。

*3:例えば、 SBI で考えてもタイマーの設定に必要な値は Device Tree で timebase-frequency を取得しないと計算できません

*4:実際のところ、 qemu 専用なら virtio は 0x1000_1000, 0x1000_2000 ... 0x1000_8000 のどこか、 virtio-vga は 0x3000_8000 に PCI コンフィグレーションレジスタが固定で存在するようなので、 Device Tree の依存度はそれほど多くありません。

*5:EC や IMAC ではない!!

QEMU RISC-V virt マシンで VGA を使う

さいきん QEMU RISC-V virt マシンで何ができるか色々探していたところ、なんと VGA が使えることがわかったので記事を書きます。

github.com

最新の RISC-V で古き良き VGA が動いてるのが面白いだけです。実用性はあまりありません。笑

解説

qemu で virtio-vga を使うと PCIバイスが生えてきますが、実は本来の virtio の機能のほかに PCI VGA の機能もあります。

そもそも RISC-V で PCIバイスにアクセスする方法ですが、 DeviceTree から /soc/pci を探し、 pci-host-ecam-generic というプロトコルを使ってコンフィグレーションレジスタにアクセスできます。*1

本来はここで PCIバイスの列挙が必要ですが、 QEMU は固定値っぽいので省略してアドレス 0x3000_8000 から始まる PCI コンフィグレーションレジスタで virtio-vga にアクセスできます。

QEMU RISC-V のファームウェアでは全く設定されていないようなので BAR とコマンドレジスタを自力で設定する必要があります。 サンプルでは適当なアドレスで決め打ちします。

https://github.com/neri/riscv-vga-sample/blob/main/src/main.rs#L22

BAR の設定が終わったらいよいよ VGA の設定をします。 通常の VGA は I/O アドレス 0x3C0〜に主要な機能がありますが、 virtio-vga では BAR2 の MMIO + 0x400 〜の MMIO レジスタとして実装されています。 これをうまく設定すると VGA として動きます。

ここではみんな大好き 320x200 8bit カラーのモード13hと同じ設定をします。

https://github.com/neri/riscv-vga-sample/blob/main/src/vga.rs

設定が終わったら embedded-graphics の DrawTarget として動作するラッパークラスを作ります。

https://github.com/neri/riscv-vga-sample/blob/main/src/main.rs#L99

これで簡単に画面が表示できました。

しかし、よく考えてください。 解像度 320x200 8bit カラーしか使えない VGA より、普通の virtio-vga として使った方がもっと広くて綺麗な画面が使えます。 それとも 640x480 の 4bit プレーンがお好みですか?そんなわけありませんよね。

いかがでしたか?

調べてみたけどあまり役に立ちませんでした。 多分これが一番無駄だと思います。

*1:実は ECAM というのは PCIe のコンフィグレーションのことなので、古き良き PCI バスの場合は pci-host-cam-generic になります。ここでは使いませんが。

Intel APX

面白そうな拡張が発表されたようです。

www.intel.com

Intel APX は一言で言うとx86汎用命令のAVX化です*1

AVX では新しいプレフィックスを追加することで今までの SSE 命令ではエンコーディングできない機能が追加されました。 APX もこれに似ていて、今までの x86 命令に AVX によく似たプレフィックスを追加することで、新しい機能が追加されます。

他にもいくつかの拡張がありますが、主要な機能は以下の3つあります。

汎用レジスタの倍増 (16 + 16 = 32)

レジスタというのはCPU内部にあってコアとほぼ同じ速度で動作する超高速で小容量のメモリで、大きく分けるとCPUの動作を制御するシステムレジスタと、アプリが自由にデータを格納するのに使える汎用レジスタに大別できます。

汎用レジスタは汎用という名前が付いていますが、実際には一部のレジスタは使い道が決められています。 残りの自由に使えるレジスタコンパイラがローカル変数の一部を割り当てます。 この使い道は ABI というルールで決められていることが多いです。

レジスタは個数が限られているので、関数がある程度の規模を超えるとレジスタに割り当てできないローカル変数が出てきます。 その場合、使用頻度の低い変数はメモリに確保されるので、レジスタを使う場合よりも遅いコードになります。

使えるレジスタの個数が増えると、ローカル変数に割り当てできるレジスタが増えるので、メモリに割り当てるよりも効率的なコードになります。

もちろんこれには欠点もあり、現代の一般的な x86 CPU の環境ではマルチタスクで使われることが多いため、レジスタの数が増えると当然コンテキストスイッチで退避するレジスタの数も増えるので、コンテキストスイッチのコストが少し増えます。

3オペランド形式: NDD (new data destination)

長らく x86 命令はほとんどの命令が2オペランド形式でした。

これは例えば、 ADD A, B という x86 命令は A += B を意味しますが、ここで A に指定したレジスタは必ず破壊されるので、元の A の値を他の処理で使いたい場合は別途他の場所に退避しておく必要があります。

これが3オペランド形式になると、 ADD A, B, C という命令で A = B + C とすることができるので、 B と C の内容はそのままで計算結果を別の場所に書き出せるようになります。*2

フラグの更新抑制: NF (status flags update suppression, hence “no flags”)

これは地味に強力な機能で、今まで x86 の演算命令は必ずフラグが更新されていましたが、抑制できるようになります。

CPUは演算によって結果が 0 になったかどうか、桁溢れが発生したかどうかなどを毎回チェックしてフラグレジスタの内容を更新しますが、 実際のところ演算後のフラグの変化が重要なケースは多倍長演算や条件分岐など一部のケースに限られ、ほとんどの処理では不要な更新が毎回発生していることになります。

不要なフラグ更新はフラグレジスタに対する依存関係も若干発生するので、アウトオブオーダーで命令を並び替えるときに制約になり得ますが、 不要なフラグ更新を抑制することでアウトオブオーダーがより効率的に動作することが期待できます。

x86RISC

実はこれらは一般的な RISC チップには最初からある機能が多いです。

x86RISC 化が進行してると見ることができますが、結構な量のエンコーディングルールが追加されるので RISC には程遠いような複雑な感じです。

CISC 万歳?

*1:厳密には少し違います!

*2:実際は加算は LEA で代用できる場合が多いので恩恵があるのは他の演算ですが

MYOS の現状

前回の記事でチラッと触れたものの正式な告知をしていなかったのでここで現状をまとめます。

もともと MYOS の実装には Rust の nightly の機能が必要だったため、 nightly の機能を積極的に使っていました。

nightly の機能は安定化されていないため、今まで動いてたコードが突然動かなくなる(使えた機能が使えなくなる)というケースが安定版に比べると高い頻度で発生します。 過去に何度か修正を余儀なくされたアップデートがあったと記憶しています。

さて、数ヶ月前のアップデートにより、とある機能*1が使えなくなり、多くの修正が必要になりました。

影響範囲が大きすぎて正確に把握できていませんが、影響を受ける箇所によっては全面的に設計し直す必要があるかもしれないほど根本的に依存していました。

Rust は先進的なバージョン管理を備えたビルドシステムを標準で持っているのでとりあえず今すぐ動いて欲しいなら問題の起きないバージョンで固定することでビルド自体は可能です。 しかし、バージョン固定することで今後実装される新機能を使うことができなくなります。

今後の新機能に対応できなくなるのはプロジェクトの方針として容認できないため、基本的には*2バージョン固定しません。 一方、現在のバージョンでは正常にビルドが通らないのも事実です。

となれば、全面的に書き直すしかありません。

一方で、この規模のプロジェクトを全面的に書き直すほどのモチベーションがすでになくなっているのも事実です。 自発的なリファクタリングならともかく、ライブラリの都合で書き直すなら他にやりたいことがたくさんあります。 人生は短すぎる。

ということで、 MYOS の開発はしばらくお休みします。

モチベーションが回復したら最新の Rust でビルドできるように全面的に書き直します。 しかし、それがいつになるかは現状お約束できません。

nightly の機能は突然消えてしまうことがあるので、今後はなるべく使わないようにしようと思います。

Rust はベアメタルの開発に nightly が必要になることが多いですが、はやく nightly がなくても普通に使えるようになって欲しいと思います。

*1:具体的には const trait が多くの標準ライブラリで無効化され、 const trait 機能自体の見直しがあるという噂です

*2:nightly ビルドが破壊されて復旧を待っている間のような一時的な状態を除いて

x86S

Intel が 64bit モードのみのサブセットの x86 アーキテクチャを検討中という噂が流れてきました。

www.intel.com

検討始めるのが10年遅くないですか?

ここから普及するまでまた何年もかかるのに…

x86 の枷

以前ぼくのブログでも紹介したことがあるように、 x86 には大昔から続いている互換性のために現代ではあまり使われていない無駄な機能や足枷になっている機能が多数存在します。*1

AMD64 アーキテクチャが誕生した当時、 64bit モードではいくつかの歴史的な機能が削除されましたが、 64bit モードから使えないだけで機能自体は CPU に実装されたままでした。 当時はまだ UEFI が一般的ではなく 64bit OS も普及していなかったため、 BIOS で起動して 32bit OS でも従来と同じように動く需要の方が大きかったためです。

今回、それらのすでに使われていない歴史的な機能を CPU コアから削除することで構造シンプルにするのかと思ってましたが、発表資料を見ているとどうやらぼくが思っていた方向とは若干異なるようです。

変更点まとめ

変更点をまとめると以下のようになります。

  • 常時ロングモード固定にする
    • 常時ページング状態
    • リアルモードとプロテクトモードを削除
    • 仮想86モードに関する機能も削除
    • リング 1 と 2 を削除してリング 0 と 3 のみにする
  • 16bit機能の削除
  • セグメント機能を縮小して 32bit モードでも 64bit モードと同じようにフラット固定にする
  • いくつかの互換性のための機能を削除 (IOPLやIOストリング命令など)
  • 上記の変更に伴っていくつかのシステムフラグが固定値になる

  • 4レベルページングモードと5レベルページングモードの新しい切り替え方法を追加

  • PIC や APIC 互換モードを削除して X2APIC 固定にする
  • 64bit モードで起動できる新しい SIPI (Startup IPI) を追加

現代の PC は UEFI を使ってロングモードで起動してそのまま 64bit OS を動かすのでリアルモードやプロテクトモードは通常使用されない機能です。 GDT や IDT をほぼそのままの形で残したのは影響を最小限にとどめるためのバランスを考慮した結果でしょうか。

64bit モード以外を削除したような書き方がされている記事を見かけますが、 64bit OS 上で動作する 32bit アプリは相変わらずサポートされています。*2

リング1と2が削除されたのは、ロングモードではリングプロテクションが0と3以外は機能しない盲腸機能だからでしょうか。*3

IOPL 関係の機能が削除されたのは、 386 が生まれた当時は BIOSDOS アプリを仮想 86 モードで動かす需要がありましたが、ロングモードでは仮想86モード自体が削除され、現代ではアプリが直接 I/O ポートにアクセスする需要がほぼ無くなったためかと思います。

従来4レベルページングと5レベルページングの切り替えは一旦ページングをオフにするためにロングモードを離れる必要がありましたが、 x86S ではロングモードのまま切り替え可能になったので、この機能は通常のプロセッサでもサポートされると嬉しい機能だと思います。

ロングモードはページング機能をオフにできないのでリセット直後のページテーブルがどのような状態になるのか気になりますが、資料では特に触れていないようなのが気になりました。

MYOS の影響

もし実際にこの機能を持った CPU がリリースされたら、 MYOS は X2APIC に対応してないので修正が必要になるでしょう。 当然、新しい SIPI にも対応していないので追加修正が必要です。

逆に、それ以外の削除された機能は現代の OS では利用されていない機能で MYOS も影響はほとんどないでしょう。*4

いかがでしたか?

PIC まわりと OS の初期化処理でいくつか書き換えが必要なものの、 UEFI が普及している現代ではいったん 64bit OS が動作してしまえばほとんど影響のない機能が削除されることで、 CPU をシンプルに設計できるようになる余地が生まれます。

x86 CPU は今までほとんどのプログラムが従来のコードのまま無修正で動くための互換機能でどんどん複雑になっていました。 厳密には細かい動作の違いがあったものの、意図的に互換性を捨てたのは今回が初だと思います。

とはいえ、歴史的に見ると Intel は何度か x86 そのものを捨てようとして失敗して結局互換路線を強いられているので、今後の展開から目が離せませんね。

*1:起源を辿ると 8bit 時代にまで遡る仕様もいくつか存在します

*2:セグメントベースとリミット機能が削除されているのでフラットメモリモデルに限定されますが、現代の主要な OS ではフラット以外のメモリモデルはほとんど利用されていないのでほぼ影響がないでしょう。

*3:ロングモードでもリング1や2で特権命令を直接実行することはできませんが、セグメントによるメモリ範囲で保護することができず、ページングはリング3とそれ以外の区別しかできないのでリング2からリング0のメモリに自由にアクセス可能で、フラットメモリモデルでは実質的にリング0とリング2の間に保護機能が働きません

*4:厳密にははりぼて OS 互換機能が動かなくなりますが、いずれカーネルから削除する予定の機能なのでここでは影響なしと見做します

ハイブリッドアーキテクチャ

第12世代 Intel プロセッサの最大の特徴は、高性能コア (Performance の P) と高効率コア (Efficient の E) によるハイブリッドアーキテクチャを採用していることです。

これは Arm で big.LITTLE と呼ばれていたものによく似ていて、 ひとつのプロセッサで高性能と省電力を両立させるのは難しいので、 高性能プロセッサと省電力プロセッサを両方同じパッケージに入れて切り替えて使おうという考え方です。

一見乱暴に見えますが、現代のプロセッサはマルチコア構成が当たり前で OS もマルチコア前提でコア間のマイグレーション機能が既に実装されているので、スケジューリングする際にどのプロセッサを優先的に割り当てるのか調整するだけで割と簡単に高性能と省電力を両立することができます。

実際にこの機能が使えるプロセッサが登場したので MYOS でも対応したいと考えていたところ、なんとか入手できたので対応してみたいと思います。

起動まで

早速電源を入れて OS のインストールされた USB を挿入して待つこと数秒、起動しませんでした。

どうもページング処理に問題があったようで、今回の件とあまり関係がないのでとりあえずサクッと修正します。

すると次は別の問題が発生しました。なぜか一部のコアが起動に失敗しているようです。

最初は P コアだけ起動して E コアは何か特別な処理をしないと起動できないのかと思いましたが、 CPU のスペックを確認するとなんと P コアの個数とも関係ないことがわかりました。

さらに色々調査したところ、単に最大コア数を制限する処理のバグでした…

今まで8コア以上のCPUを使ったことがなかったので、こんな簡単なバグに気づいてなかったようです。

というわけで、 APICID の振り方の癖が従来のCPUと少々異なる点が気になるものの、起動してコアを認識するところまでは従来の SMP と特に変わりませんでした。

P コアと E コアの識別

単純な SMP として動作すると、 P コアと E コアが特に区別することなくスケジューリングされます。

P コアと E コアを意識してスケジューリングするには、起動後にコアを識別する必要があります。

まず、 EAX = 7 で CPUID 命令を実行して EDX の Hybrid ビットを確認します。

ここが 1 になっていればハイブリッドアーキテクチャ対応 CPU なので、 EAX = 1A で CPUID 命令を実行します。

ここで得られた EAX の値で Core ベースのコア(P)か、 Atom ベースのコア(E)か区別できるので、スケジューラーの初期化時にコアを P-Core と E-Core に分類します。*1

なお、第12世代プロセッサの中には P コアのみや E コアのみの製品も存在していますが、これらの値がどういう動作になるか未検証です。

スケジューラーの調整

P コアと E コアを区別できるようになったら、システムの負荷状況によって P コアに割り当てたり E コアに割り当てたり制御できるようにします。

MYOS のスケジューラーは SMT 対応するために負荷測定して負荷によってコアの割り当てを制御する機能が実装済みなので、そこを改修します。

どのように分配するのがベストかはコアの構成やユースケースによると思うので、完璧な正解というのは難しいかもしれません。 とりあえず、負荷が少ないときは E コアを優先的に割り当てるようにしておきました。

なお、ハイブリッドアーキテクチャ対応の Intel プロセッサには Hardware Guided Scheduling や Intel Thread Director という支援機能が実装されていますが、詳しい使い方がマニュアルに書かれておらずよくわからなかったので今回は対応しません。

いかがでしたか?

自作 OS でも SMP 対応済みであればハイブリッドアーキテクチャ対応はそんなに難しくないことがわかりました。

とはいえ、最近の CPU は動的にクロックが変化するのでハイブリッドアーキテクチャを最大に活かすためには次のステップとしてクロック変更対応も視野に入れると良いと思います。

なお、今回対応したバージョンのリリース予定は未定です。 *2

*1:ハイブリッド対応 Intel プロセッサは俗に Core と Atom を統合したものであると言われていましたが、この書き方によると本当に Core ベースのコアと Atom ベースのコアが使われているようです。 P と E の区別がもう少しわかりやすい名前を使って欲しかったです。

*2:現在の MYOS は最近の Rust nightly の仕様変更の影響でビルドが通らず、とりあえず古いバージョンを指定すればビルドは通りますが、今後の新機能への追従が難しく、今回の変更の影響が大きすぎて対応するモチベーションも大幅に下がっているので、正式な対応時期を約束できない状態です。