SerenityOSを実機に持ち込む、ドライバを1つずつ合わせながら
(sdomi.pl)- SerenityOSは主にQEMUではうまく動作していたが、実際のノートPCでは起動・デバッグ・ストレージアクセスまで順番に行き詰まり、ハードウェア対応の空白が露呈した
- 実験対象は Intel Celeron N4020、4GB DDR4、32GB eMMC、1366×768 TN画面を備えたDell 3100 Chromebookで、期待していたCr50ベースのクローズドケースデバッグはこのボードでは失敗した
- Cr50経路が塞がれたため、RP2040ベースのPi Picoを内部に入れてUARTとSPIフラッシュへ直接接続し、CircuitPythonとserprogでPicoCCDという仮のデバッグ・フラッシング装置を作成した
- 初期ブートログは、PCIの先にあるMMIO 16550 UARTをすぐ使うのが難しかったため、ChromeOS ECが記録するIO port 0x80を低速な一時出力チャネルとして活用して取得した
- eMMC対応は、SD/MMC初期化の違い、SDHCI電源制御の欠落、SD専用コマンドの無効化といった問題を経て一部のグラフィックセッションまで到達したが、性能・安定性・パッチ整理はまだ残っている
実機として選んだ Dell 3100 Chromebook
- SerenityOSにより深く関わろうとする中で、最初に目についた弱点は、QEMUでは動くが実機対応が不足しているという点だった
- UEFI対応は spholz がすでに進めていたため触らず、この作業では master ブランチのカーネルが TianoCore UEFI ランタイム上で GRUB から起動できれば十分だった
- メイン開発マシンと同じ機材でOSをデバッグする方法は避けたく、実際に日常利用できる程度には比較的新しいハードウェアを対象にした
- Allegroで安価なChromebookを探し、Dell 3100を95PLN、約25EURで購入した
- Intel Celeron N4020、2コア、Hyper-Threadingなし
- 4GB DDR4
- 32GBオンボード eMMC
- UHD600 IGPが駆動する1366×768 TN画面
- USB-A 2基、USB-C 2基、3.5mmジャック
- Dellの上位ビジネスノートより良いと感じたキーボード
- この機材は以後octopusというホスト名で呼ばれる
Cr50ベースのデバッグへの期待と失敗
- Chromebookを選んだ大きな理由は、Cr50セキュリティチップと組み込みコントローラがクローズドケースデバッグに便利な機能を提供しているためだった
- 2018年以降のほぼすべてのChromebookでは、SuzyQケーブルでUSB-Cポートの1つをデバッグに使え、Cr50は通常3つのttyUSBデバイスを公開する
- 内部Cr50コンソール
- APコンソール、つまりChromebookのシリアルポート
- cros_ecコンソール、つまり組み込みコントローラ
- 目標はノートPCを開けたまま配線を垂らさずにシリアルコンソールへアクセスすることで、cros_ecのキー入力エミュレーションや電源状態制御まで考えると、簡易的なKVMのような形も可能に見えた
- 実際のoctopusではCr50 CCDは動作しなかった
- 新しいSuzyQケーブルを作り、別のChromebookで半田付け状態も検証したが失敗した
- octopusは、Dellが基板上の一部抵抗を実装していないためCCDが動作しない数少ないノートPCの1つだった
- 特定のポート向きと充電器接続条件で限定的に成功したという話もあったが、この機材ではまったく動作しなかった
- 後で確認した情報では、欠けている抵抗はUSBブリッジ自体ではなくSPIフラッシングにだけ影響するはずともされており、Cr50デバッグがまったく動かなかった正確な理由はいまも不明である
Pi Picoで作った PicoCCD
- Cr50経路が塞がれた後、この機材内部の空きスペースに一般的なPi Pico基板を入れられるか確認したところ、十分収まることが分かった
- 類似ノートPCの回路図も参照したが、octopusと正確に一致する回路図はなかった
- 基板上の大きなデバッグポートの1つはIntel関連のJTAGとテストポイントで、目的には合わなかった
- 別の1つはGoogle Servoだったが、Googleは複数のデバッグプローブをServoという名前で出しており、文書も限られていて見つけにくかった
- 関連資料として Servo文書 を参照した
- GlasgowのUARTアプレットで周波数検出を有効にし、Linuxが
/dev/ttyS1に繰り返し出力している間に、UART TXらしいパッドを直接プロービングした- TXパッドは数分で見つかった
- RXは能動的な送信が必要で、誤ったラインに触れると基板がリセットされることもあり、実際に2回リセットが起きたため難しかった
- その後、EC用のRX/TXピンも約10分で見つかった
- 半田付けした線はUV硬化エポキシで固定し、6か月間接続トラブルはなかった
- RP2040のSPI周辺機能も使ってフラッシュチップに6本の線を半田付けし、write-protectピンへ向かうトレースを切ってGNDへ接続することで、Cr50の許可なしに書き込みアクセスを得た
- ソフトウェアにはCircuitPythonを選んだ
- USBマスストレージとしてスクリプトやデータを載せられるためである
- UARTを
cdc_acmUSBデバイスへブリッジする作業は簡単だった - SPIフラッシュにも接続したため、フラッシング機能も必要だった
- 一般的なオープンソースのEEPROM・SPIフラッシングツールとして flashrom を使い、SPIをUARTでプロキシする serprog が目的に合っていた
- stacksmashing の pico-serprog というC実装はあったが、BIOSを書き込むたびにPico自体を再フラッシュする必要があり、用途に合わなかった
- その代わり CircuitPython で serprog を実装し、Glasgow serprogアプレット を大いに参考にした
- できあがったコードは、急ごしらえのクローズドケースデバッグソリューション PicoCCD としてまとめられ、リポジトリは ForgejoのPicoCCD にある
- WeirdTreeThing も同様の目的でRP2040向けのCコードを書いており、彼のPicoCCD版 も存在する
SerenityOSのブートログ取得
- デバッグ用にAlpine Linuxをインストールし、外部でビルドしたSerenityOSカーネルを持ち込むための基本ユーティリティを整えた
- その後、ビルドマシンから成果物を自動ダウンロードし、カーネルを展開して上書きし、ユーザー空間の
.tarを展開するGRUBエントリまで備えた構成へと発展した - 変更後にテストまで至る反復時間は、執筆時点で約20秒で、ベアメタルハッキングとしてはかなり良好な水準だった
- 最初のGRUBブート項目は実質
multiboot /Kernel serial_debugだったが、画面にもシリアルポートにも何も出力されなかった - 画面出力の問題については、GRUBブート項目に
insmod all_videoを追加する方法を見つけ、それだけでは解決しなかったものの正しい方向ではあった - シリアル出力がないことのほうが大きな問題だった
- corebootログは数秒前までは入ってきていた
- この機材のUARTは従来のポートマップ16550ではなく、MMIOベースの16550Aだった
- Linuxログでは
ttyS0とttyS1がMMIOアドレス上の16550Aとして表示されていた lspciでは Intel Celeron/Pentium Silver Processor Serial IO UART Host Controller がPCIデバイスとして見えていた
16550 UARTとport 0x80による回避策
- 伝統的にIBM PCの周辺機器はx86 CPUのポートI/Oにマップされ、
outbやinbのような命令でアクセスされていた - 多くのデバイスはその後MMIOへ移ったが、シリアルポートは高速競争が重要ではないため古い方式が残り、デバッグポートとして有用だった
- 一般的な環境なら、
outb 0x3f8, 0x41のように書けば反対側でAを受け取れ、初期化も短いため小規模プロジェクトでは実装しやすい - octopusのUARTはPCIの背後にあるMMIOデバイスで、SerenityOSのブート初期段階ではPCI初期化まで期待しにくかった
- SerenityOSにはPCIバス実装があるが、ブート段階があまりに早すぎた
- 既存の
PCISerialDeviceもMMIO文脈で使われたことがなかった - デバッグ出力なしでこのドライバを作るのは理想的ではなかった
- ChromeOSデバイスの組み込みコントローラは、IO port 0x80へのすべての書き込みをログに残す
- このポートは伝統的にPOST状態報告に使われる
- マザーボードの7セグメントブートコード表示器はport 80をデコードしている
- Linuxで
/dev/portにバイトを書き込むスクリプトで仮説を検証したところ、cros_ecコンソールでそのバイトを読めた - SerenityOSの起点である
Kernel/Arch/init.cpp周辺にIO::out8(0x80, 1);のようなコードを入れて進行位置を追跡し、Memory::MemoryManager::initialize(0);で落ちている箇所まで絞り込んだ - その後、シリアル書き込みルーチンのアドレスを0x3f8ではなく0x80へ変える方法を試した
- 最初は多くのバイトが出るが、cros_ecが安定して中継できずオーバーフローが発生した
- さらに進むとcros_ecログ出力自体も破損した
- 実際のシリアルチップと違って大きなバッファがないためである
- 各書き込みの間に大量の
nopベースのウェイトステートを挿入して回避した- ブートメッセージを全部出すと数秒だった起動時間が数分へ延びた
- それでもベアメタルデバッグとしては受け入れられるコストと見なした
- cros_ecログ行を自動でパースしてASCIIにデコードするため、
picocom、watch、grep、sed、cut、xxdを組み合わせたBashワンライナーを使った
フレームバッファと最初のグラフィック出力
- ブートログを確保した後、数日かけてコードベースを直接読み問題を理解しようとしたが、最終的にはコミュニティに助けを求めた
- spholz が当時開いていた SerenityOS PR #24435 を教えてくれ、そのブランチでビルドするとgeneric framebufferが動作した
- 画面には、処理は失敗しているのに成功したように見える状態の結果が表示され、その後ストレージ問題が本格的に表面化した
eMMCとSD/MMC初期化の問題
- 既存のStorageManagementクラッシュは、SD Host Controllerの初期化失敗後にコントローラ一覧が空であるという assertion に至っていた
- ログには
PCI: Failed to initialize SD Host ControllerとASSERTION FAILED: !m_controllers.is_empty()が見えていた - 結果として
StorageManagement::enumerate_storage_devices()でカーネルパニックが発生していた
- ログには
- octopusには32GBのeMMCチップがあり、SerenityOSにはすでに一部のSDドライバがあるため、MMC対応を追加すればよさそうに見えた
- SD/MMCカードを扱うには、大きく3つの要素が必要だった
- Host Controller: 現代の機材では通常、SD Associationが仕様策定したSDHCI
- Host Controllerに接続されるバス: この場合はPCI
- Hostとカードが通信するためのプロトコル実装
- クラッシュログ上ではSerenityOSに前の2要素はあり、残る問題はプロトコル側だった
- SDプロトコルは公開仕様があるが、MMCは2007年にJEDEC標準になって以降、正式アクセスに費用が必要になった
- SDとMMCでは初期化シーケンスが異なる
- SerenityOSはCMD0を送り応答を待つ形で開始しており、これはSDとMMCの両方で通るはずだった
- 次に電圧設定のためCMD8を送るが、MMCはこれをサポートしないのでエラーになるはずだった
- その後カードをリセットしてMMCとみなす手法を提案する資料もあった
- 別の資料ではCMD8とCMD58の結果の組み合わせでSDバージョンや容量種別まで分ける、より包括的な流れが示されていた
- 高度な互換性チェックはすべて実装せず、基本チェックのみ実装した
電源制御レジスタの欠落と解決
- MMC初期化フローは要約すると次の段階だった
- リセット後、クロックを400KHzに設定
- 1ms待ってから74クロックをさらに待つ
- CMD0を送信し応答待ち
- 応答の31ビット目が1になるまでCMD1を繰り返す
- ループ終了後、その値をOperating Conditionsレジスタとして保存
- SD専用レジスタの問い合わせを除き、SD初期化アルゴリズムを継続
- 任意でHigh-Speed互換性を検出し、複数のHSモードの1つを有効化
- コードは4段階目付近までは進んだが、その後eMMCがどの要求にも応答しなかった
- 数日間原因が分からなかったが、コントローラリセット関連コードを除去するとeMMCが応答し始め、問題は
reset_host_controller()関数に絞られた - この関数のおかしな点は、標準仕様上
host_configurationレジスタが見当たらなかったことだった- 以前のコードでは複数のレジスタが恣意的な
host_configurationグループ2つにまとめられていた - 初期化も完全ではなく、最初のグループを単純に0へ設定していた
- 以前のコードでは複数のレジスタが恣意的な
- 最初のグループにはカード電源レギュレータを制御するPower Controlレジスタが含まれていた
- eMMCを使うすべての実装を含む一部ハードウェアでは、このレジスタがカード自体を有効化するのに必要だった
- 電源レールをスロットへ直結する別設計では、この設定を無視できる場合もある
- 暫定対応として、
host_configuration_0に元から入っていた値を取得して使った - 根本問題は、カードへ電源を入れないまま通信しようとしていたことだった
- その後、SDカードにだけ有効な特定コマンドを見つけて無効化するのにさらに数時間かかり、コントローラからより意味のあるデバッグ出力が得られるようになると、残りの作業は比較的平凡に進んだ
現在の状態と残る作業
- 最終的にSerenityOSは非常に遅いながらも、一部が破損したグラフィックセッションの起動に成功したが、すぐにフリーズした
- このグラフィックセッションの問題と、フレームバッファを再び正常化した過程は続編で扱う予定である
- 全体の作業は約6か月にわたる学習プロセスで、その間にも別の作業が進行していた
- パッチを整理し、今年中にupstreamへ投げることが次の目標である
1件のコメント
Hacker News のコメント
NetBSD のドライバーをカスタムカーネルに合わせることは比較的簡単だと読んだことがあり、Serenity もそういう道を取れるのではないかと思う
新興 OS にとってデバイスドライバーは大きな障壁
ハードウェア構成がほぼ固定されているので、ドライバーを作る/取り込むことや、システムのテストがしやすくなる
最小限の基盤サポートだけで、ドライバーをユーザー空間で実行できる
https://en.wikipedia.org/wiki/Rump_kernel
ただしドライバーについてはよく分からない
Apple が最初からやってきた方式はおおむねこれだと思うし、コンシューマー向け Unix 系で唯一大きく成功した例でもある
System76 もほぼその例で、Frame.work も似ているが、OS 自体への注力はやや少ない
あらゆる条件が不利なマシンで動くようにしたのは本当にすごいハックで、才能ある人たちが途方もない努力を注いだ結果に見える
こういう記事を読むと、ドライバーと OS の世界にどう入門すればいいのか気になる
あまりに複雑に見えて、どこから始めればいいのかよく分からない
これはメモリマップド I/O(MMIO)と呼ばれる。通常のアプリケーションでは、カーネルがハードウェアメモリへの直接アクセスを防ぐため実行できない
始めるには、対象 CPU 向けの機械語を出せる Rust/C++/C/Zig のような言語が必要で、ランタイムや GC がないほうがよい。低レベル言語が初めてなら、例が多い C を勧める
対象 CPU の基本的なアセンブリも学ぶ必要があり、一部の命令は高水準言語の組み込み関数として提供されていない場合がある
その次に hello world カーネルを書きながら、CPU がカーネルをどのように起動するのか、実行モードと権限レベルがどう分かれているのかを学ぶことになる
その後 x86 では 64 ビット命令を使うために long mode へ移行するなど、望む形に CPU を設定し、通常この段階で仮想メモリも設定する
ここまで来ると、CPU が OS とどう噛み合うのか、利用可能なデバイスを列挙してメモリ位置を見つける方法がつかめるようになり、その後はファイルシステムやスケジューラーなど多くの作業が残る
OS 上で動くソフトウェアと OS カーネルの違いは、結局そのコードを実行している現在のCPU モードであり、最高権限では通常のアプリが使えない命令を使える
後には FreeBSD のドキュメントの中に隠れた宝石のような資料を見つけ、その中でも FreeBSD Architecture Handbook と FreeBSD Developers' Handbook は特に役に立つかもしれない
https://lwn.net/Kernel/LDD3/
https://docs.freebsd.org/en/books/
Minix は非常にきれいに書かれていて、カーネルも 5 千行程度で、複数の教科書で扱われている
簡単なサーバーを実装し、カーネルハックもしてみたが、Minix はマイクロカーネルなので、ほとんどのドライバーがそういう形で動作する
授業資料を事前に読み、講義はほとんど聞かなかったのに 10 点満点で 8 点を取った
NetBSD と SerenityOS についても良い話を多く聞いており、Andreas はライブ配信で多くの開発を進めていた
どこから始めるかが分かれば、実際には楽になる
たとえばデバイスドライバーは、コンピューター上で実行される他のプログラムが何らかのデバイスにアクセスし制御できるように、インターフェースを公開する役割を持つ
https://m.youtube.com/watch?v=juGNPLdjLH4 は良い短期集中講義
Arduino のようなもので、PC と情報をやり取りする簡単な USB デバイスを作ることもできる。例: https://m.youtube.com/watch?v=yTc2GLXfCOY
その次は、興味のあるサブシステムが何をするのか、どう動かすのかを理解してコードを書けばよい。ストレージデバイスやグラフィックデバイスなどがこれに当たる
Raspberry Pi もこうした実験の良い出発点になり得る。例: Writing a bare metal operating system for the raspberry pi https://github.com/babbleberry/rpi4-osdev
https://wiki.osdev.org/Bare_Bones
SerenityOS のコンセプトと Ladybird ブラウザーが好きなので、こうした進展はうれしい
Ladybird は独立プロジェクトになっただけでなく、もはや SerenityOS を対象プラットフォームとは見なしていない
Ladybird は独自の Serenity レイヤーを徐々に取り除き、より主流の代替手段に置き換えているところ
主に Linux を使う立場としては、Ladybird が Linux 上で本当の代替になっていくのが楽しみ
だが SerenityOS ファンとしては、Ladybird に注がれていたエネルギーと革新が SerenityOS から抜けていくのは残念
Chromebook のハックで助けが必要なら、chromium-os-dev メーリングリストで聞けばよい
誰かが CCD を動作させる手助けをしてくれそう
https://groups.google.com/a/chromium.org/g/chromium-os-dev?p...
Depthcharge ブートローダーも TFTP によるネットワークブートをサポートしている
自分でビルドして SPI にフラッシュする必要はあるが、カーネルを反復開発するときにはとても良い機能
https://chromium.googlesource.com/chromiumos/platform/depthc...
SerenityOS はすでに実機ハードウェアで動いていたと思っていたけど、まだ全部 QEMU の中だけで動いているの?
ただし、語るほどのドライバーがほとんどなく、最も基本的な意味でしか動かず、特定のハードウェアでしか可能ではなく、おそらくそれほど快適には動いていなかったはず
今回の試みは、少なくとも一つの実ハードウェアプラットフォームで信頼して動作するようにすること
Serenity は実装方法に同意できないことがあっても、ずっと印象的
こういう恐ろしいものを見に来たんだった
doas dd seek=$((0x$1)) bs=1 count=1 of=/dev/port < <(xxd -p -r <<< "$2")