借り初めのひみつきち

仮ブログです。

my new gear...

前回の日記でお察しかと思いますが、さいきん Rust + UEFI で OS がどのくらい作れるのか検証していました。

TL; DR

結論から言うとマルチスレッドが動く程度までは動いて、今後も継続して開発していこうと思います。💪(๑╹ω╹💪๑ )

Rust で OS

Rust 特有の苦労した点はスレッドの所有権周りで、よくわからないタイミングでスレッドが解放されてしまうバグに若干悩まされました。 最終的にはスレッドプールがスレッドを所有し、スケジューラーは一時的に借用する形で落ち着きました。

とはいえ、おかしなコードを書こうとするとだいたい叱ってくれるので Rust は優しいかな、と思います。

じんせいはロックフリー

Rust 固有の問題ではないところでとてもハマっていて、ロックフリーのキューを作るのに苦労しました。 当初は以前 C で作ってなんとなく動いていたものを移植したのですが、考慮漏れでデータ破壊が起きることが発覚し、 その後リストで実装し直したものも安定してるとは言えない状態です。

SMP のスケジューラのキューは複数のプロセッサから同時に読み書きされるためロックフリーで実現するのはかなり要求仕様が厳しく、 ロックフリーキューを何度か作り直して結局安定させることができませんでした。

しかし、実は同じコア内ではデータ競合が決して起こらないので、ロックしてしまえば簡単に実装できます。当面はロックするバージョンを使おうと思います。

Rust で UEFI + ACPI

uefi-rs はとりあえず Rust で UEFI プログラミングするには便利ですが、中にはちゃんと対応してない API もあります。 今のところ致命的な問題にはぶつかっていないですが、 MP protocol が non-blocking mode で使えないのがちょっと気になりました。 そもそも MP protocol のちゃんとした仕様が手に入らず SMP の初期化に使っていいのかどうかも怪しかったので、 SMP の初期化には以前書いた IPI を使ってリアルモードから起動する方法をベースに作ることにしました。

同じところで提供されている ACPI ライブラリの対応は深刻で、 FADT の内容がパース後のオブジェクトからほとんど参照できず、テーブルの自由な検索などもできず、したがって BGRT を表示するような使い方もできず、 AML パーサーは qemu でいくつかのメソッド呼び出しは成功したものの実機ではエラーでうまくパースできないという状況で、ちょっと実用に耐えないかなと思います。 いまどきの PC の OS では ACPI が必須で、とりあえず簡単に動かすためにこのライブラリを使いましたが、今後別のライブラリの導入を検討したり自作する必要がありそうです。

まとめ

そんな感じでいろいろ勉強しながら、マルチタスクでキーボードやマウスの入力ができる程度の完成度になりました。

f:id:neriring16:20200616225253p:plain

単体のプロジェクトとして名前も決まってないし当面は練習や勉強がメインになるのでレポジトリの宣伝はしません。 もう少し形になってきたら、以前作りかけた例の OS の後継としてリリースすることになりそうな気がします。

Rust で UEFI のハローワールド

さいきん Rust のべんきょうはじまりました!

Rust とは

安全に低レベルプログラミングができるナウい言語っぽいです。 メモリ管理が厳しいので初学者はコンパイルを成功させるだけでも一苦労です。

UEFI とは

2000年ごろに BIOS を代替する目的で開発された PC 向けの新しいファームウェアです。 当初はマイナーな存在でしたが Window 8 の登場とともに市販の PC に広く利用されるようになり、今後は PC の BIOS は廃止されて UEFI が搭載されることになっています。

UEFIHello World

さて、 Rust で UEFI のプログラミングするにはどうすればいいでしょうか。

まずは公式から Rust をインストールしましょう。

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

インストールが終わったらシェルを再起動しましょう。

次に、 UEFI でプログラミングするには cargo-xbuild というツールを使ってクロスビルドが必要ですのでこれもインストールします。

$ cargo install cargo-xbuild

また、ベアメタルプログラミング全般で nightly のコンパイラが必要になることが多いらしいのでこれも使えるようにしておきます。

$ rustup install nightly

これでツールの用意ができました。

つぎにプロジェクトを作成します。

$ cargo new uefi-rs-hello

この時点で以下のようなツリー構造になっているはずなので uefi-rs-hello フォルダに移動します。

.
`-- uefi-rs-hello
    |-- Cargo.toml
    `-- src
        `-- main.rs

Rust で UEFI のプログラミングをするには uefi-rs というライブラリを使うのが簡単です。 Cargo.toml の [dependencies] に以下のように記述します。

[dependencies]
uefi = { git = "https://github.com/rust-osdev/uefi-rs.git" }

いよいよ src/main.rs を書いていきます。

#![feature(abi_efiapi)]
#![no_std]
#![no_main]
use uefi::prelude::*;
use core::panic::PanicInfo;
use core::fmt::Write;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

#[entry]
fn efi_main(_handle: Handle, st: SystemTable<Boot>) -> Status {

    writeln!(st.stdout(), "Hello, world!").unwrap();

    loop {}
    // Status::SUCCESS
}

最初の3行は Rust で UEFI のプログラミングに必要なおまじないです。 #![feature(abi_efiapi)]EFIAPI を使うことを指定してるみたいです。 #![no_std]#![no_main] は通常のライブラリ(std)を使わないベアメタル環境という指定と通常のmain関数から始まらないことを指定してます。

use uefi::prelude::*;uefi-rs の基本的な定義を取り込むのに必要です。

use core::panic::PanicInfo;panic の定義まわりは Rust のエラー処理に必要です。 通常 panic は std の中に定義されていますが、今回は no_std を指定しているので自前実装が必要です。 Hello World 程度のプログラムでは panic は不要なのでただの無限ループにしてます。

use core::fmt::Write;writeln! マクロを呼び出すためのおまじないです。

#[entry]efi_main 関数がエントリポイントであることを指定します。

fn efi_main(_handle: Handle, st: SystemTable<Boot>) -> Status がメイン関数に相当する定義になります。通常の UEFI と基本的には同じ内容です。

_handle: Handle はイメージハンドルです。今回は使わないので名前の先頭に _ を付けてます。

st: SystemTable<Boot>EFI_SYSTEM_TABLE を受け取る指定です。 EFI_SYSTEM_TABLEUEFI のプログラミングにおいて最も重要な構造体で、 uefi-rs ではシステムテーブルが SystemTable<Boot>SystemTable<Runtime> の2種類あります。これはそもそも UEFI が Boot Services 環境と Runtime Services 環境の2種類あるためで、 efi_main の呼び出し時は SystemTable<Boot>st.exit_boot_services 呼び出し後は SystemTable<Runtime> が使用できます。 Rust の所有権の仕組みを利用して st.exit_boot_services 後は元の SystemTable<Boot> に誤ってアクセスできないようになっています。*1

writeln!(st.stdout(), "Hello, world!").unwrap(); がハローワールドの本体です。ここまで来るまで長かった・・・

writeln! は Rust 標準のマクロで、あとに続く文字列をフォーマットして出力します。 Rust では名前の最後に ! がつく関数っぽいものは全てマクロです。 st.stdout()EFI_SYSTEM_TABLE の中にある EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL を取得する処理です。 EFI_SYSTEM_TABLE の構造が若干 stdin/stdout を意識したような構造になっているのでこのようなメソッド呼び出しになっているのだと思います。 最後に付いてる unwrap()writeln! の出力が Result になっているためアンラップしてます。この処理がなくても警告が出るだけで一応動作します。

最後の行の loop {} は無限ループしています。コメント化している Status::SUCCESS の方に書き換えるとシェルに戻る御行儀の良い UEFI アプリケーションになりますが、ハローワールドでは必要ないでしょう。

main.rs ができたら以下のようなコマンドでビルドできます。

$ rustup run nightly cargo xbuild --target x86_64-unknown-uefi --release

初回はちょっと時間がかかり、無事ビルドが成功すると /target/x86_64-unknown-uefi/release/uefi-rs-hello.efi として出力されます。

これを qemu からファイルアクセスできるようにして実行します。

f:id:neriring16:20200517001646p:plain

いかがでしたか。

多分これが一番簡単だと思います。

*1:しかしこの仕組みだと exit_boot_services の失敗時のリカバリができないのでは...?

AssemblyScript の配列

さいきん AssemblyScript を触ってました。*1

AssemblyScript とは、 WebAssembly にコンパイルできる TypeScript のサブセット言語です。
WebAssembly の世界と JavaScript の世界は別の世界なので結構勝手が違いますが、 AssemblyScript を使うと同じ TypeScript ファイルを参照したりできるようになり、両者の溝が若干低くなります。

WebAssembly を使う時は大量のデータを高速に処理する必要がある場面が多いと思うので、配列の受け渡しは重要な要件です。しかし、実は容易ではありません。

WebAssembly.Memory

現在の WebAssembly にはオブジェクトの概念がありません。実はポインタすらありません。

あるのは int や double のような少数のスカラ値と、 WebAssembly.Memory という謎のオブジェクトだけです。

WebAssembly.Memory は内部に ArrayBuffer を持っていて、 WebAssembly 内部でメモリアクセスする命令はこの ArrayBuffer を対象とします。
現在の WebAssembly では1モジュールあたりひとつの Memory を扱うことができ、この中身をどう利用するかは WebAssembly 内部のコードの責任となっています。
WebAssembly に対応するコンパイラはこの ArrayBuffer を適当に配分してオブジェクトを割り当て、 ArrayBuffer の中の何バイト目かを示すインデックス値 (wasm32 の場合は int32) をポインタのように扱うことでオブジェクトを表現しています。意外と原始的ですね。

このインデックス値を WebAssembly の外の世界から見るとただの整数にしか見えないので、オブジェクトに変換するには WebAssembly.Memory の内部の ArrayBuffer から探さなくてはなりません。

AssemblyScript の配列

AssemblyScript の場合、exportされたクラスの情報はライブラリで吸収して JS のクラスとほとんど同じように扱うことができます。
しかし、それ以外のオブジェクトはそう単純ではありません。

オブジェクトを入出力する関数は JS から見ると number の入出力に見えます。
これは先述のように WebAssembly.Memory の内部の ArrayBuffer のインデックス値となっています。

文字列は getString みたいなユーティリティー関数でアクセスできますが、それ以外のオブジェクトはメモリから直接読み書きしないといけないようです。*2

メモリ上のオブジェクト構造は以下のページに情報があります。

Memory - The AssemblyScript Book

f:id:neriring16:20200401205336p:plain

AssemblyScript には JavaScript の Array は存在せず、実質全部 TypedArray であることがわかります。
また、通常の配列は ArrayBuffer と ArrayBufferView の二段構成になってることがわかります。

これを参考に以下のような関数を作ると AssemblyScript の配列に JavaScript からアクセスできるようになります。
this.wmem は先述の WebAssembly.Memory のことで、 instantiate 時に前もって作成するかまたは instantiate の戻り値のオブジェクトの memory というキーにあります。

getArray(ptr: number): Uint8Array {
    const buffer = this.wmem.buffer
    const view = new DataView(buffer)
    const base = view.getUint32(ptr + 4, true)
    const size = view.getUint32(ptr + 8, true)
    return new Uint8Array(buffer, base, size)
}

これで、大量のデータの入出力も ArrayBuffer や TypedArray 経由で高速にやりとりできるようになりました💡

なお、 JavaScript のメモリ管理は通常 GC を使いますが AssemblyScript は独立したリファレンスカウント方式のため、グローバル変数で固定的に確保されてないオブジェクトの扱いはサポート関数の __retain や __release で管理しないといけなくなってちょっと面倒になります💦

*1:といってもこの原稿を書き始めて既に一月近く経って成果物は公開前に開発中止しそうな勢いですが...

*2:バージョンによって違うかもしれません

WASM 自作 PC エミュレータ日記/202001

自作PCエミュレーターの進捗です。

GitHub - neri/vpc: A PC Emulator implemented by WebAssembly

はりぼてOS

f:id:neriring16:20200125112734p:plain

前回投稿時点では 起動画面が出るところまでの動作でそれ以降の動作は不安定になってましたが、その後の改良で現在ではほとんどのアプリが動くようになりました。
マウスも「Pointer Lock API」というものを使ってかなり理想に近い動作をするようになりました。タッチパネルの動作をどうするかが現在の課題です。
また、どうもmmlplayer のタイマーがうまく動いていないらしく途中で再生が止まってしまう不具合を確認しています。

最も近い隣人

画像を拡大縮小するアルゴリズムはいくつかあって多くのアルゴリズムは近くのピクセルの値と演算して補完しますが、「Nearest Neighbor」というのは直訳すると「最も近い隣人」になり、最も近い場所にあるピクセルをそのまま描画するというかっこいい名前とは裏腹に最も単純なアルゴリズムです。

f:id:neriring16:20200125110437p:plain

f:id:neriring16:20200125110458p:plain

ピクセルアートを整数倍表示するときはこの「Nearest Neighbor」を使うとボケのない綺麗な描画できます。
しかし整数倍にできなかったり風景写真やアウトラインフォントなどの描画は「Nearest Neighbor」を使うとガクガクしてしまうので、設定画面で切り替えできるようにしました。
iOS端末では「Nearest Neighbor」は常時オフにした方が全体的に良好な描画結果になるようです。

デバッグ機能、Step Over、Break on MBR

もともとCPUはステップ実行できるようになっていて、実行中にレジスタの値を読み書きできるように設計されていましたが、デバッガといえるほどの機能が充実していませんでした。
そこで、インターフェースを改良していろんなコマンドを実行できるようにしました。
現在、MS-DOS 付属の debug コマンドのコマンドに近いインターフェースを実装しています。
アセンブル機能はありませんが、メモリのダンプ、逆アセンブルレジスタの参照と編集、ステップ実行などのデバッグ機能が利用できます。
実行中に「DevTool」にある「Step」ボタンを押すか、「ICEBP」という隠し命令 (オペコード 0xF1) を実行するとデバッグモードになります。

f:id:neriring16:20200125113022p:plain

また、新たに Step Over (ptrace) 機能も実装しました。
これは次に実行する命令が CALL 命令や INT 命令だった場合に「次の次の命令」にブレークポイントを設定し、それ以外の命令の場合は通常のステップ実行する機能で、デバッグ時に関数の中身まで関心がない場合にステップを省略することができます。
この機能はハードウェアブレークポイントで実装しています。ハードウェアブレークポイントが設定されると CPU 実行時に CS:EIP の値を監視するので実行速度が1〜2割程度低下しますが、有効になるのはデバッグの一時的な実行時だけでソフトウェアブレークポイントよりメリットが多いので採用しました。

f:id:neriring16:20200125110638p:plain

さらに「Break on MBR」という機能も実装しました。
この機能を有効にすると起動時に MBR にジャンプした瞬間にデバッグモードになって OS の起動をデバッグできるようになります。

自作フォントエディタの話

今年作ったソフトの紹介をします。

このソフトウェアはビットマップフォントを編集することができます。
ビットマップフォントというのは名前の通りフォントデータをピクセル単位のビットマップで扱うフォント形式で、昔のOS・自作OS・組み込み環境などで主に使われており、現在の WindowsMac などで通常使われているアウトラインフォントとは異なる形式です。

もともと自作 OS で使っていた FONTX2 形式のフォントを編集しようとしたところ、昔使っていた編集ソフトが現在の環境に合わなかったり色々と細かい仕様が気になってきたので自作することになりました。

当初は Electron 向けに開発していましたが、ブラウザからアクセスできた方が便利ということで現在はオンライン版がメインとなっています。 PC やスマフォなど多くの環境でフォントを編集できます。

フォントの編集

f:id:neriring16:20191124055931p:plain

起動すると色々画面が出てきます。

新しくフォントを作成するときは「General Info」にフォント情報を入力します。
よく使われるフォントサイズは 8x16 で、このソフトで編集可能な最大サイズは 32x32 です。
既存のフォントを編集するときはこの部分は触る必要がありません。

フォントを編集するには左上の「Glyph Selector」に文字や文字コードを入力して文字を選択します。
すると真ん中のキャンバスに選択した文字が拡大表示されるので適当にクリックすると描画されます。

右側にあるToolsの「Apply」をクリックすると編集した内容が適用されて下のプレビューに反映されます。
内容に満足したら「Glyph Selector」で次の文字を選択して編集します。

セーブ・ロード・エクスポート

このソフトは「Apply」した時などに localStorage に保存されるので、次回同じブラウザで開いた時に続きから編集できます。
クラウドセーブはそのうち対応したいけど今のところ非対応です。

f:id:neriring16:20191124060028p:plain

上部メニューの「Save/Load」をクリックすると「Save/Load」画面が表示されます。
この時テキストボックスに表示される内容が現在編集中のフォントデータになります。この内容を自分自身で管理する事もできます。
自分自身で管理している別のデータをロードしたいときはテキストボックスの内容を置き換えた後に「Load」ボタンを押してください。

「Open」ボタンを押すとローカルのファイル選択ダイアログでファイルを開くことができます。
ドラッグ&ドロップに対応しているブラウザの場合はドラッグ&ドロップでもファイルを開くことができます。
はりぼてOSの「hankaku.txt」も読み取りのみ対応しています。

「Web」ボタンを押した画面でURLを入力するとインターネット上のフォントデータをロードすることができます。
Ajaxを利用するので対象のサーバーがクロスオリジンに対応している必要があります。
なお、本アプリのURLのハッシュの後にフォントデータのURLを入力すると起動時にそのデータをロードします。*1

「Export」ボタンを押すと指定した形式でデータをエクスポートできます。
形式を選択した後の画面で「Export」ボタンを押すとローカルファイルに保存できます。
一部のブラウザは「Export」ボタンを押してもうまくファイルが保存できない場合があります。その場合はテキストボックスの内容をコピペして保存してください。ただしこの方法でバイナリファイルは扱えません。

現状全ての機能に対応している形式は「FONTX2」形式となります。

f:id:neriring16:20191124182733p:plain

自作OSなどに組み込む場合は「C Header」形式でエクスポートすると簡単に扱える気がします。

f:id:neriring16:20191124182656p:plain

PNGイメージをエクスポート・インポートすることもできますがオマケ機能です。フォント情報が欠落して正しくインポートできなくなる事があります。

Fork や Patch

GitHub 上に GPL で公開されているので何かあったらご自由にどうぞ。

github.com

x86 の汎用レジスタのルーツ

x86 の汎用レジスタは AX CX DX BX です。AX BX CX DX ではありません!
この由来について 8086 のルーツから探ってみたいと思います。

Intel 4004

Intel が初めて作った CPU は電卓を作るために開発された 4004 でした。
このことから 8086 は電卓の子孫と言われることがあります。本当なのでしょうか?

8086 の祖先は 8080 であり、 8080 の祖先は 8008 でした。
では 8008 の先祖が 4004 かというと、同時期に同じ会社が作ったものだからパッケージ技術等の多少の共通点はあるにしてもそれ以上の類似性は認められません。両者を調べれば調べるほど 8008 が 4004 の後継とは考えにくいです。
8008 の後継の 8080 が出るのと同時期に 4004 の後継の 4040 がリリースされていることなどからも両者は別のシリーズだと考えられます。

ちなみに 4004 の汎用レジスタは Accumulator とは独立した16個の4ビットレジスタがあり、偶数番号のレジスタと奇数番号のレジスタをペアにして8組の8ビットレジスタとして使える命令もあったようです。
汎用レジスタの個数も合計容量も 8008 より多いです。

Intel 8008

f:id:neriring16:20191113075713p:plain

8008 は Intel が初めて作った8ビット CPU で 8080 や 8008 の先祖になります。

8008 の機械語の基本構造は8進数です。 8bit の機械語が 2bit + 3bit + 3bit の構造になっていて、 3bit で同じグループに属する8種類の命令を指定したり、8種類(以下)のレジスタを指定したり使われました。例えば ADD, ADC, SUB, SBB, AND, XOR, OR, CMP の8種類の ALU 命令は同じグループに属した同じ構造の機械語になっています。 CPU によってニーモニックや順番は多少変わるもののこれらは 8080 を経て 8086 に現在も受け継がれている特徴のひとつです。

8008 には A B C D E H L の7つの8ビットレジスタがありました。8番目のレジスタに相当する M という仮想レジスタは後述の HL ペアレジスタで指定するメモリアクセスを指しています。

A レジスタは別名 Accumulator という ALU に直結された演算のためのレジスタで、ほとんどの演算命令は A レジスタが暗黙のソースオペランドになります。
なお、古い CPU では ALU に直結された同様のレジスタはよく使われました。

H レジスタと L レジスタは High/Low のペアになっていて、仮想レジスタ M にアクセスする命令は HL レジスタペアでメモリアドレスを間接参照するメモリアクセスに使われました。
ただし 8008 にはペアレジスタの概念がなく H レジスタと L レジスタはあくまで独立したふたつの8ビットレジスタだったので、 256 バイト境界を超えるブロック転送はかなりめんどくさかったようです。

B C D E レジスタは特に決まった用途のない一時的なデータを格納するレジスタだったようです。

8008 のスタックは CPU 内部に7レベルの固定スタックが存在していました。7つと中途半端なのはおそらく PC レジスタと合わせて8つのレジスタからマルチプレクサで切り替えるような構造になってたんじゃないかと思います。

フラグは Sign, Zero, Parity, Carry の4つあって演算の動作や分岐方法は 8080 や 8086 と大きくは変わらなかったようですが、大きな違いとしてフラグレジスタのようなまとまったレジスタが存在しないので値を直接取得したり設定したりすることはできなかったようです。

Intel 8080

f:id:neriring16:20191109030510p:plain

8080 は 8008 の後継の CPU で、汎用レジスタは 8008 と同様に A B C D E H L の7つの物理レジスタと仮想レジスタ M がありました。
バイナリは非互換ですがアセンブリソースレベルでは 8008 の上位互換があったようです。

8080 で進化した点のひとつに、ふたつのレジスタをペアにして BC DE HL という3組の仮想的な16ビットレジスタにすることで一部の命令で16ビット処理ができるようになり256バイト境界を超えるメモリ転送などの処理が楽になりました。

スタックは CPU 外のメモリに追い出されて SP レジスタで指定する現代的な方式に変わりました。ただし SP レジスタにアクセス可能な命令はかなり限定されていて汎用レジスタとはだいぶ扱いが違いました。

また、フラグがフラグレジスタ (F) という8ビットのレジスタとしてまとまり、PUSH / POP 命令のみ A レジスタとフラグ(F)レジスタを組み合わせたレジスタペア (PSW/AF) も使うことができました。*1
フラグレジスタのビット配置は現在の x86 の rFLAGS の下位8ビットとほぼ同じ内容で、謎の予約ビットはこの時代から謎の予約ビットとして存在しました。

8080 の代表的な OS として CP/M が存在しました。
CP/Mシステムコールは基本的に C レジスタに機能番号、 DE レジスタにパラメータを入れて CALL 5 で呼び出しました。

Intel 8085 / Z80

f:id:neriring16:20191109025458p:plain

8085 は 8080 の公式の上位互換 CPU で一部命令が拡張されましたが、あまり使われませんでした。

Z80 は 8080 の設計者が Intel から独立して作った CPU で 8080 とバイナリレベルでほぼ上位互換があり、たくさんの8ビットコンピュータに採用されました。

Z80 で拡張された汎用レジスタに IX, IY というメモリブロック転送のための16ビットレジスタがありました。
IX, IY レジスタデコーダ回路のトランジスタを節約するために HL レジスタのデコードを流用していたので、非公式の隠し命令を使うことで IXH, IXL, IYH, IYL のような8ビットレジスタとして使うこともできました。

Intel 8086

f:id:neriring16:20191109025510p:plain

ご存知 8086 は 8080 の後継として作られた16ビット CPU です。
8086 は 8080 と 8008 の間ほどではないですが 8080 系 CPU のソフトウェアが移植しやすいように設計されていました。

8086 の汎用レジスタは8個の16ビットレジスタがありました。そのうち半分は4組の8ビットレジスタペアとして使うこともできました。

4組の16ビットレジスタペアはそれぞれ役割 (Accumulator, Counter, Data, Base) に応じて A C D B で始まる名前が付けられ、 16bit ペアの時は X、8bit の時は上位(High) の H と下位(Low) の L の接尾辞が付きました。

そして、これらのレジスタは 8080 の汎用レジスタとほぼ同等の機能と使い方をしました。

A レジスタは Accumulator として使われ、 AL レジスタになりました。
B/BC レジスタZ80 で Counter として使われました。 8086 では CL/CX レジスタが引き継ぎました。
DE レジスタは他の汎用レジスタに比べると特化した目的が少ない Data として使われたため DX レジスタになりました。
HL レジスタはメモリ関節アドレスに使われました。 8086 ではアドレッシングモードが増えたものの Base Address として依然として重要なため BX レジスタになりました。
F レジスタは拡張して rFLAGS レジスタになりましたが、8080 の AF ペアで使われた名残で 8086 では LAHF/SAHF 命令で AH レジスタと関係があります。
IX/IY レジスタは Index として使われ、Source/Destination を明示してより用途が明確な SI/DI レジスタになりました。一方 SI/DI の8ビットバージョンである SIL/DIL は AMD64 の登場まで待たねばなりませんでした。

8086 の代表的な OS である MS-DOS の初期のバージョンは CP/M のクローンと言われるほどよく似ていて MS-DOS ファンクションコールの最初の30個くらいは CP/M とほぼ同じだったため、アセンブラレベルで変換することで 8080 の CP/M のプログラムが 8086 の MS-DOS である程度動いたと言われています。

このように 8080 と同等の機能を持つレジスタの名前が後付けで変更されたために AX CX DX BX という不規則な順番になったと当初仮定していました。
しかし、実は 8080 の時点で汎用レジスタ機械語上の順番は A B C D E H L ではありませんでした。

汎用レジスタのルーツはなんとなくわかったものの順番の理由は不明でした。 Intel の偉い人に聞いてください。

*1:このレジスタペアは 8080 では PSW というわかりにくい呼称でしたが、 Z80 では AF という構造がわかりやすい呼称に変わりました。

任意コード実行可能なホームページ作った。

今年作った自作アプリの紹介します。

TL; DR

このアプリは Web ブラウザ上で PC をエミュレーションします。
技術的には WebAssembly、 Canvas、 WebWorker、 WebAudio、 WebMIDI などを利用しています。

かんたんな紹介

もともと osz という自作 OS をサイトで紹介するためにブラウザで実行可能な PC エミュレーターを探していました。
JavaScript で動作するものはいくつか見つけたのですが、パフォーマンスなどに満足できるものがなかったので自作することになりました。
「Playground」という名前はブラウザ上で手軽に自作 OS を試してもらおうという願いでつけられた名前です。

現在は FreeDOS や osz がそれなりに動くようになり、ゲームなどもある程度動きます。

f:id:neriring16:20191214214819p:plain
f:id:neriring16:20191214215425p:plain
f:id:neriring16:20191214215224p:plain
f:id:neriring16:20191214214844p:plain

はりぼて OS は起動画面が出るものの、今一歩動作不良状態です。

f:id:neriring16:20191214214954p:plain

技術的なお話

実は数年前に今とは別のアプローチで実装した同名のエミュレータがありました。
エミュレータ本体は 7shi 氏の 8086run*1 をサーバーで実行し、ブラウザ上のターミナルエミュレータと WebSocket で接続して利用していました。
それなりに動いていましたが、サーバーで実行する必要がある等の制約のため、公開前に開発終了になりました。

それから数年、 WebAssembly が登場してこなれて来たようなので新たに自作することにしました。

このエミュレータは CPU コアと周辺デバイスが完全に独立しており、フロントエンドは現在ベタ書きの JavaScript です。
バックエンドは CPU コアが WebAssembly (clang) 、周辺デバイスは TypeScript で一緒に WebWorker で動作しています。
現在は UI が貧弱なためデバッグ支援機能などがあまり多くないですが、一応 CPU コアは外部からレジスタ操作をしたり1命令ずつ実行する機能があり、ユニットテストではこれらの機能を node.js から呼び出しています。

CPU 世代はリセット時にいつでも変更できますが、世代の判別によく使われる命令を除いてほとんどの命令の動作には影響ありません。本来実行できない世代の命令でも実行できます。

画面モードはテキストモードとグラフィックスモードがあり、テキストモードではブラウザのフォントを使って描画するので近代的なデバイスの上では非常に綺麗なレンダリングになります。グラフィックスモードは現在 320 x 200 モードしかサポートしていません。

WebMIDI に対応していて MPU-401 互換デバイスとして振る舞うので iPad で SOUND Canvas から音を鳴らすような使い方もできます。

f:id:neriring16:20191214220746p:plain