連載案内: Rust + WebAssembly で書いている x86 エミュレータ rustx86 の開発記。グラフィックス編は #1 VGA mode 13h・#2 Linux のフレームバッファ (efifb) と来て、今回は X。他の連載は ネットワーク編 #1・ディスク編 #1・JIT編 #1。
前回までで、Linux のカーネルが自分でフレームバッファに描けるようになった。画面・キーボード・ディスクは揃った。X を立てるのに足りないのはマウスだけである。今回はまず 8042 の第2ポートに PS/2 マウスを生やし、その足で Alpine の Xorg を squashfs に焼き、startx でブラウザの canvas に xterm が開くところまで行く。

6b: PS/2 マウス — 8042 の第2ポート
PC の PS/2 マウスは、キーボードと同じ 8042 コントローラの第2ポート (AUX) にぶら下がっている。配線も 2 線で同じ、違うのは話す中身だけ — キーボードは「どのキーが上下したか」、マウスは「どれだけ動いてどのボタンが押されたか」を 3 バイトのパケットで送る。
rustx86 の 8042 はこれまでキーボード専用で、コマンドバイト (0x20 で読み 0x60 で書く) を固定値 0x45 で返していた。Linux の i8042 ドライバはこれで「第2ポート無し」と判定していた。起動ログにはずっとこう出ていた:
i8042: Warning: Keylock active
i8042: Failed to disable AUX port, but continuing anyway... Is this a SiS?
i8042: If AUX port is really absent please use the 'i8042.noaux' option
Linux は第2ポートを止めてからコマンドバイトを読み返し、bit5 (AUX 禁止) が立っていることで存在を確かめる。固定値ではそこで落ちる。コマンドバイトを実体にし、0xA7/0xA8 (AUX の禁止/許可)、0xA9 (AUX のテスト)、0xD3 (AUX 側へのループバック — Linux はこれで経路の生死を見る)、0xD4 (マウスへ転送) を足した。ステータスには AUX の印 (bit5) と「キーロックされていない」(bit4) も。
// core/src/dev/chip/kbd.rs — どの線を上げるかは 8042 が決める
pub fn take_irq(&mut self) -> Option<u8> {
if self.irq_asserted { return None; }
let aux = if self.has_output { self.output_is_aux }
else if !self.keys.is_empty() { false }
else if self.aux_ready() { true }
else { return None };
let enabled = self.command_byte & if aux { CMD_AUX_IRQ } else { CMD_KBD_IRQ } != 0;
if !enabled { return None; }
self.irq_asserted = true;
Some(if aux { 12 } else { 1 })
}
マウス素子は素の 3 ボタン PS/2 (ID 0)。リセット (0xFF → ACK, 0xAA, 0x00)、ID、サンプルレート、解像度、有効化、状態問い合わせ。動きは報告中のときだけパケットにする (実機も報告停止中は何も送らない)。
// core/src/dev/chip/mouse.rs
fn push_packet(&mut self, dx: i32, dy: i32) {
let xov = dx.abs() > MAX_DELTA;
let yov = dy.abs() > MAX_DELTA;
let dx = dx.clamp(-MAX_DELTA, MAX_DELTA);
let dy = dy.clamp(-MAX_DELTA, MAX_DELTA);
let b0 = 0x08 // 同期ビット: OS はこれでパケットの頭を見つけ直す
| (yov as u8) << 7 | (xov as u8) << 6
| ((dy < 0) as u8) << 5 | ((dx < 0) as u8) << 4
| self.buttons;
self.out.extend([b0, dx as u8, dy as u8]); // Y は**上が正** (画面座標と逆)
}
拾い物: スレーブ PIC の連結が未配線だった
マウスの割り込みは IRQ12、つまり2 個目の 8259 (スレーブ) の線 4 である。PC/AT は 8 本では足りず、2 個目を 1 個目の IRQ2 にぶら下げた。rustx86 にはスレーブ PIC の実体はあったが、連結の配線が無かった — IRQ8〜15 を使う装置が今まで一つも居なかったのである。スレーブの挙手をマスタの IRQ2 に現し、INTA でマスタが IRQ2 のベクタを出したらスレーブに聞き直す。
// core/src/lib.rs — INTA
if let Some(mut vec) = self.devices.pic[0].acknowledge() {
// マスタの IRQ2 は「スレーブに聞け」の合図
if vec == self.devices.pic[0].vector_base().wrapping_add(bus::isa::IRQ_CASCADE) {
if let Some(v2) = self.devices.pic[1].acknowledge() { vec = v2; }
}
cpu::interrupt(self, vec);
}
カーネルの側: psmouse はどこに居るか
psmouse も mousedev も evdev も、このカーネル (Alpine linux-lts 6.18.35) には焼き込まれておらず、Alpine の initramfs-lts にも入っていない。モジュールは走っているカーネルと同じ版でなければ載らないが、Alpine のリポジトリにある apk は既に 6.18.44 に進んでいた。
救いは netboot の配布物にあった。vmlinuz-lts と同じディレクトリに modloop-lts (全モジュールの squashfs、242MB) があり、同じ 6.18.35 だった。initramfs を焼くとき、そこから i2c-core psmouse mousedev evdev の 4 つだけ unsquashfs で抜く。psmouse は proto=bare — 素のマウスしか居ないので、Synaptics/ALPS/IntelliMouse の拡張を順に試すプローブ (ゲスト時間で約 1.5 秒) を省く。
これで起動ログが変わった。
serio: i8042 KBD port at 0x60,0x64 irq 1
serio: i8042 AUX port at 0x60,0x64 irq 12
input: AT Translated Set 2 keyboard as /devices/platform/i8042/serio0/input/input0
input: PS/2 Generic Mouse as /devices/platform/i8042/serio1/input/input3
mousedev: PS/2 mouse device common for all mice
回帰テストでは、ホストから動きを 1 回入れ、head -c 3 /dev/input/mice | hexdump -C が 29 05 fd (左ボタン・右へ 5・下へ 3 を PS/2 形式で) を吐くことを見る。ブラウザでは画面をクリックすると pointer lock で捕獲し、相対移動を Worker 経由で 8042 へ入れる。抜け道は Ctrl+Alt+G / Esc / タブの blur。検証に使っている埋め込みの閲覧面は pointer lock を許さない文書だったので、断られたら canvas 上の座標差を相対化するソフト捕獲に落ちる作りにした。
6f: X 入りのイメージ — gcc ディスクと同じ流儀で
X のイメージは ディスク編 の gcc ディスクと同じ立て付けにした。道具箱 (Linux コンテナ) の中で apk --arch x86 に Alpine の x86 パッケージを引かせ、ミニ initramfs の木の上に重ね、無圧縮の squashfs に焼く。ブラウザへは gz で運び、ホスト側で 1 回だけ解く。

パッケージは xorg-server xf86-video-fbdev xf86-input-evdev xinit xkbcomp xkeyboard-config twm xterm と bitmap フォント。GPU は無いので描画は全部ソフトウェア — fbdev ドライバが /dev/fb0 に直接描く。udev が居ないので xorg.conf に装置を全部書く (event0 がキーボード、event1 がマウス)。
ロードマップには Tiny Core Linux が候補に書いてあったが、ISO 起動にはまだ無い ATAPI + El Torito が要る。いま揃っている部品 (efifb・PS/2・virtio-blk + squashfs・apk の道具箱) で最短なのは自前の squashfsだった。
startx が X を出すまでに踏んだ壁 — 3枚

① Xorg が SIGSEGV
最初の startx は 6 秒で落ちた。
(EE) Segmentation fault at address 0xbfb499f0
(EE) Caught signal 11 (Segmentation fault). Server aborting
スタック番地である。CPU エミュレーションの穴かと身構えたが、イメージの大きさが先に答えを言っていた — 79 パッケージで 277MB、うち libLLVM.so が 195MB。xorg-server の依存で mesa が入り、mesa の llvmpipe が LLVM を引きずる。GLX 拡張が読み込まれると libGL → libLLVM と初期化が走り、そこで落ちていた。
GPU の無い fbdev に GLX は無意味である。xorg.conf で Disable "glx" し、mesa と LLVM の実体もイメージから捨てた。SIGSEGV は消え、イメージは 282MB → 90MB (gz 34MB)、gcc ディスクと同じ重さになった。LLVM のどの命令で落ちたかは追っていない — 使わないものを追う理由が無い (台帳には載せた)。
② FBIOPUT_VSCREENINFO succeeded but modified mode
次は fbdev ドライバがモード設定で拒まれた。ログの少し上にこう出ている。
(**) FBDEV(0): Depth 24, (--) framebuffer bpp 32
X の fb 層は 24bpp のパックド画素を扱えない (8/16/32 のみ)。深度 24 に対して bpp 32 を要求する。一方、efifb は check_var を持たないので、カーネルの fb_set_var は要求を無視して今の var をそのまま返す。X は返ってきた var が要求と違うのでこのエラーを出す。
前回 24bpp にしたのは、sysfb の simplefb 表に無い形式で efifb へ落とすためだった。32bpp の普通の並び (x8r8g8b8) は表にあり、申告すると誰も掴まない装置が生まれる。そこで 32bpp で赤を第 2 バイトに置く — [詰め物, R, G, B] (r@8 g@16 b@24)。表に無い並びのまま、X が飲む bpp になる。

// core/src/boot/bzimage.rs
pub fn at_top_of(ram_bytes: u64) -> Self {
Self { base: (ram_bytes as u32) - Self::RESERVE, width: 640, height: 480, bpp: 32 }
}
// screen_info: [詰め物, R, G, B]
zp[zp::RED_SIZE] = 8; zp[zp::RED_POS] = 8;
zp[zp::GREEN_SIZE] = 8; zp[zp::GREEN_POS] = 16;
zp[zp::BLUE_SIZE] = 8; zp[zp::BLUE_POS] = 24;
zp[zp::RSVD_SIZE] = 8; zp[zp::RSVD_POS] = 0;
予約は RAM 末尾 1MB → 2MB (640×480×4 = 1.2MB)。efifb は framebuffer at 0x7e00000, mode is 640x480x32 と言い、前回の Linux 版 bounce は ioctl で並びを聞いて描くので、何も変えずにそのまま動いた。busybox の fbsplash だけは 32bpp でも [B, G, R, 0] 決め打ちで、回帰はその並びで照合している。
③ X がすぐ降りる
3 度目の startx は (EE) 無しで X が上がり、そして 1 秒で降りた。xinit: connection to X server lost。クライアント側 (.xinitrc → xterm) が死んでいる。
原因は 2 つ重なっていた。1 つは initramfs の init が devpts をマウントしていないこと — xterm は pty を開けず即死する。もう 1 つは、init のシェルは HOME=/ で来るので ~/.xinitrc が見つからず、xterm は SHELL 未設定だと /etc/passwd からログインシェルを引く (この木には無い)。
xterm: No absolute path found for shell: xterm
init で devpts と shm をマウントし、自前の startx で HOME=/root SHELL=/bin/sh を決め打ちした。
#!/bin/sh
# startx — xinit のものは xauth/mcookie を要求するので、自前の短いのに置き換える
export HOME=/root SHELL=/bin/sh
exec /usr/bin/xinit "${1:-/root/.xinitrc}" -- /usr/bin/Xorg :0 vt2 -config /etc/X11/xorg.conf -nolisten tcp
4 度目で X が残った。ps に Xorg・xinit・twm・xterm の 4 つ。
見えたもの
ネイティブでは guestcmd に LFB_DUMP (画面を PPM に落とす口) を足して撮った。wasm でも同じ手順を決定的に踏める — startx を PS/2 で打ち、X が上がったら ls /、マウスを 20 回動かす。冒頭の絵がそれで、xterm に色付きの一覧が出て、I ビームが動いている。キーボードは 8042 → atkbd → evdev → X、マウスは 8042 の第 2 ポート → psmouse → evdev → X。
ブラウザでは「Linux (フレームバッファ)」機でルート FS に「X 入り (ディスク)」を選んで電源を入れ、49 秒でシェル、startx で X。画面をクリックすると捕獲され、X のカーソルが動く。速度は Pentium 級の体感で、xterm で打つ分には気にならない。
回帰の小さな手当て
Linux 版 bounce の検査が 32bpp にした途端に 1 色欠けた。決定的に、毎回同じ色が。kill で止めていたので、「全部消してから全部描き直す」の途中に当たっていたのである。24bpp では偶然 nanosleep の最中に当たっていた。bounce に「N フレーム回して絵を残して終わる」引数を足し、kill をやめた。決定性は、こういう「たまたま通っていた」を炙り出す。
次回
X が立ったので、その上に載せるものが選べる。軽量の WM と古典のアプリ、あるいは DOOM。ISO 起動 (ATAPI + El Torito) が入れば Tiny Core や DSL もこの画面に出る。
今回のコードは PR #209 (PS/2 マウス)・PR #210 (X 入りイメージ・32bpp)。
次回: #4 X に icewm と Dillo を載せたら Chrome が 6 分で落ちた — V8 のダンプから JIT の関数テーブル漏れまで


コメント