はじめに
ゼロからの自作OS入門を知っていますか?知らない人はとりあえず買いましょう。ポインタの解説から始まる自作 OS の入門書です。楽しいぞ!
この本の中で MikanOS という OS を作るんですが、残念ながら MikanOS にはネットワーク機能がありません...。せっかく自分で OS を作ったのにインターネットに繋げないのです。
「なんだと⁉️ふざけるな😡」と思った(思ってない)私は、著者である uchan さんが講師をされているセキュリティキャンプ2021 Y-1 トラック (自作OSゼミ) に乗り込み、MikanOS に NIC ドライバを作ることになったのである...。
というわけで、ここでは私がセキュリティキャンプで取り組んだ、"MikanOS に NIC ドライバを実装する"という内容を紹介しようと思います!「俺も MikanOS に NIC ドライバ作ってインターネットに繋ぎたいぜ!」という方、ぜひ読んでみてください!
前提
この記事では、ゼロからの自作OS入門の 7 章ぐらいまでの知識が必要になります。
作るもの
NIC にはいろいろあるらしいですが、今回は E1000 の NIC を作っていきます。E1000 とは、Intel 82545EM ギガビット イーサネット NIC のエミュレート バージョンです。仮想マシン (QEMU とか) に搭載されているけど、実機バージョンもあるということです。つまり、E1000 の NIC ドライバを実装すれば、QEMU 上でも実機上でも動くのです。
参考資料
先に参考資料をあげておきます。
-
https://pdos.csail.mit.edu/6.828/2020/readings/8254x_GBe_SDM.pdf
E1000 のデータシート、つまり仕様書的なものです。鬼のように長いですが、当然全て読むわけではないです。重要なのは 3 章と 14 章あたりです。 -
http://yuma.ohgami.jp/x86_64-Jisaku-OS-4.pdf
I-218V という NIC の実装が書かれています。これも E1000 と互換性があるらしく、実はこの記事を読むとほとんど同じようなものが作れます。
実装の概要
まず、NIC ドライバの実装にあたって、ディスクリプタリングというものを知っておかなければなりません。
ディスクリプタリングは、ディスクリプタを Queue にして並べたデータ構造です。ディスクリプタには、送信ディスクリプタと受信ディスクリプタがあり、それぞれ送信されるバッファと受信されるバッファについての情報を書き込むものです。
ドライバは、メモリ上にディスクリプタリングの領域を確保し、そのベースアドレスを NIC に教えてあげます。すると、NIC はそのメモリ上のディスクリプタリングを読み書きすることで、送信処理や受信処理を実現します。
さらに、ディスクリプタリングは Head と Tail というポインタを持っていて、これは未処理のディスクリプタと処理済みのディスクリプタを管理するためのポインタです。
とりあえずここでは、ディスクリプタリングとかいう Queue をメモリ上に作って、それを NIC に教えてあげればいい感じになるんだな〜ということをわかっておけば OK です。
次に、送信処理と受信処理の概要をもう少し詳しく説明します。
送信処理
ドライバ側がやる操作は以下の通りです。
- 送信したいバッファをメモリ上に用意する
- 送信したいバッファの Offset と Length を、TAIL が指しているディスクリプタの次のディスクリプタに書き込む。
- TAIL の値を一つ増やし、その値を NIC に教える(これにより、TAIL は上の操作で書き込んだディスクリプタを指すようになる)
以上の操作により、TAIL の値が書き込まれるのをトリガーに、TAIL が指すディスクリプタを読んで、メモリから指定された Offset と Length でバッファを読み、送信を行います。
↑送信ディスクリプタ
今回の実装ではここまで理解していれば十分ですが、HEAD と TAIL についてもう少し詳しく説明してみます。
HEAD は、NIC が処理(送信)が完了したディスクリプタのうち一番最後のものを指しています。
TAIL は、NIC が処理(送信)すべきディスクリプタのうち一番最初のものを指しています。
簡単にいうと、「ここまでできたよ」っていうのが HEAD で、「ここまでやらなきゃだよ」というのが TAIL です。
よって、NIC はHEAD = TAIL になるまで、HEAD と TAIL の間にあるディスクリプタを、HEAD をインクリメントしながら順番に処理していきます。
具体例で言うと、HEAD = 1, TAIL = 3 のとき、ディスクリプタ 2, ディスクリプタ 3 の順に処理されて、HEAD = 3, TAIL = 3 となって止まります。
HEAD は NIC が動かすもので、TAIL はドライバが動かすもの、という点に注意です。
受信処理
ドライバがやる操作は以下の通りです。
- TAIL をインクリメントする
- TAIL が指すディスクリプタから Offset と Length を読み取り、そのバッファを取得する
↑受信ディスクリプタ
これで受信ができる理由を説明します。
NIC 側の受信時の動作は以下の通りです。
- HEAD が指すディスクリプタから Offset (BufferAddress) を読み取り、そのメモリアドレスに受信したバッファを書き込む
- バッファの Length をディスクリプタに書き込む
- その他の受信情報をディスクリプタに書き込む
- HEAD をインクリメントする
つまり、NIC は受信したバッファを勝手に指定したメモリ上に書き込んでいるので、ドライバはそれを読み込めばいいということです。
HEAD は NIC が次に使う(書き込む)ディスクリプタを指しています。
TAIL はドライバが次に処理する(受信する)ディスクリプタを指しています。
つまり、ドライバは TAIL = HEAD になるまで順番に TAIL と HEAD の間にあるディスクリプタを、HEAD をインクリメントしながら順番に処理していきます。
具体例で言うと、TAIL = 1, HEAD = 3 のとき、ディスクリプタ 2, ディスクリプタ 3 の順に処理して、HEAD = 3, TAIL = 3 となって止まります。
送信処理と同じですね。
今回のまとめ
というわけで、まずは NIC ドライバが何をすればいいのかというのをざっくりまとめました。
次回、実装に取り掛かります!
次回: 初期化編