livaの雑記帳

livaの雑記帳

OSとか作ってみたい

qemuのNVMeが微妙なlegacy割り込みをする話

 

こんな呟きを見かけたので、原因究明しました。uio_pci_genericに濡れ衣を着せられているような気がして、uioがとてもとても可哀想そうだったからです。

ただ、いくら僕がuioを心から愛しているとはいえ、原因を探っていたら土曜日が消えてしまって大変辛いので、ブログに書いて供養したいと思います。

 

前提

  • QEMU上でLinuxを動かし、uio_pci_genericを用いてNVMeデバイスを制御している。
  • NVMeデバイスはエミュレーションである(PCIパススルーではない)
  • QEMUのバージョンは2.9.0
  • Linuxカーネルは4.4.0(uioは4.4.0付属のもの)
  • uioなので、割り込みはLegacyなpin-based割り込み(MSIMSI-Xでない)

 

QEMUの挙動

とりあえずQEMUのNVMeのエミュレーションコードで、割り込み周りを探すとこんなコードが見つかります。

 

hw/block/nvme.c

static void nvme_isr_notify(NvmeCtrl *n, NvmeCQueue *cq)

{

    if (cq->irq_enabled) {

        if (msix_enabled(&(n->parent_obj))) {

            msix_notify(&(n->parent_obj), cq->vector);

        } else {

            pci_irq_pulse(&n->parent_obj);

        }

    }

}

 

 

pci_irq_pulseという関数がありますね。

 

include/hw/pci/pci.h

/*

* FIXME: PCI does not work this way.

* All the callers to this method should be fixed.

*/

static inline void pci_irq_pulse(PCIDevice *pci_dev)

{

    pci_irq_assert(pci_dev);

    pci_irq_deassert(pci_dev);

}

 

pci_irq_pulseはPCI Device Status Register内にある、Interrupt Status Bitを立てて、直後に下げます。(pci_irq_assertやpci_irq_deassertはhw/pci/pci.c内のpci_set_irq経由でpci_irq_handlerを呼び、pci_update_irq_statusでInterrupt Status Bitを立てます

 

で、まあFIXMEなんてコメントがついているわけですが、このFIXMEに書かれている事が本質的な答えです。

 

とはいえ、ひとまず話を続けましょう。

pci_irq_assertによってInterrupt Status Bitが立つので、QEMUはゲストOSに対して割り込みを発生させます。

 

uioの挙動

割り込みが発生すると、drivers/uio/uio.c内のuio_interrupt経由でdrivers/uio/uio_pci_generic.c内のirqhandlerが呼ばれます。

/* Interrupt handler. Read/modify/write the command register to disable                                                                                                                                     

* the interrupt. */

static irqreturn_t irqhandler(int irq, struct uio_info *info)

{

  struct uio_pci_generic_dev *gdev = to_uio_pci_generic_dev(info);

 

  if (!pci_check_and_mask_intx(gdev->pdev))

    return IRQ_NONE;

 

  /* UIO core will signal the user process. */

  return IRQ_HANDLED;

}

で、drivers/pci/pci.c内にあるpci_check_and_mask_intxは同じファイル内のpci_check_and_set_intx_maskを呼ぶわけですが・・・

static bool pci_check_and_set_intx_mask(struct pci_dev *dev, bool mask)

{

struct pci_bus *bus = dev->bus;

bool mask_updated = true;

u32 cmd_status_dword;

u16 origcmd, newcmd;

unsigned long flags;

bool irq_pending;

 

/*

* We do a single dword read to retrieve both command and status.

* Document assumptions that make this possible.

*/

BUILD_BUG_ON(PCI_COMMAND % 4);

BUILD_BUG_ON(PCI_COMMAND + 2 != PCI_STATUS);

 

raw_spin_lock_irqsave(&pci_lock, flags);

 

bus->ops->read(bus, dev->devfn, PCI_COMMAND, 4, &cmd_status_dword);

 

irq_pending = (cmd_status_dword >> 16) & PCI_STATUS_INTERRUPT;

 

/*

* Check interrupt status register to see whether our device

* triggered the interrupt (when masking) or the next IRQ is

* already pending (when unmasking).

*/

if (mask != irq_pending) {

mask_updated = false;

goto done;

}

 

origcmd = cmd_status_dword;

newcmd = origcmd & ~PCI_COMMAND_INTX_DISABLE;

if (mask)

newcmd |= PCI_COMMAND_INTX_DISABLE;

if (newcmd != origcmd)

bus->ops->write(bus, dev->devfn, PCI_COMMAND, 2, newcmd);

 

done:

raw_spin_unlock_irqrestore(&pci_lock, flags);

 

return mask_updated;

}

 

 中でPCI_STATUS_INTERRUPTが立っているかどうかを確認して、立っていなかったら(mask != irq_pending)、割り込みが発生してない旨を返しています。

 

これによってirqhandlerがIRQ_NONEを返してしまい、uioに対して割り込み待ちを掛けていて、かつゲストOSには割り込みが届いているにも関わらず、NVMeドライバにそれが通知されないわけですね。

 

試したくなる解決策1

Ahhhhh!!!! F*ck UIO!!!!と叫んだ人がいるかは分からないですが、これは割り込みが通知されているにも関わらず、きちんと通知しないuioのバグだろう、だからuioを修正するんじゃ、という解決策が思い浮かびます。

 

が、これはダメです。

 

なんでuio_pci_genericがpci_check_and_mask_intxをわざわざ呼んでいるかを考えましょう。PCIのlegacyな割り込みは、複数のデバイスが割り込み線を共有しているため、割り込みが飛んできてもデバイスドライバはそれが自分のデバイス宛の割り込みかどうかを確認しなければいけません。その確認というのがInterrupt Status Bitが立っているかどうかを読む、という物です。

つまり、irqhandlerがやっている事、ないしはpci_check_and_set_intx_maskがやっている事は全くもって正しいわけですね。

 

これはむしろ、pci_irq_pulseで、assert後にすぐdeassertしてしまうNVMeドライバが悪い!

 

試したくなる解決策2

じゃあpci_irq_pulseをpci_irq_assertに書き換えればいいじゃないか、と思うかもしれませんが、そうは問屋が卸しません。

 

pci_irq_assertしたら、どこかでpci_irq_deassertをする必要があります。PCIのlegacy割り込みはlevel triggerなので、deassertしないと無限に割り込みが飛んできます。

ならばどこかでdeassertすればいいわけですが、これが難しい。

 

QEMUxHCIエミュレーションを読むと、InterrupterのInterrupt Pendingフラグがクリアされた(デバドラが1を書き込んだ)時にpci_irq_deassert相当の事をやろうとしており、これは実機の挙動とも一致します。

一方で、NVMeの実機の挙動を見てみると、PCIの割り込みが届いて、PCI割り込みをdisableするとInterrupt Status Bitが下がってしまいます。つまり、デバイスレジスタ空間のステート遷移によってInterrupt Status Bitを下げるのではなく、PCIレジスタ空間のステート遷移によってBitを下げていると思われます。(ココらへん若干怪しいので、間違ってたらごめんなさい)

 

一方で、QEMUPCI割り込みがdisableされても、それをデバイスをエミュレーションしているクラスに通知する機構がありません。このため、QEMUのNVMeドライバを弄ってpci_irq_deassertを発行するのは正直難しいと思います。たぶんNVMeエミュレーションを書いたIntelの人も、そこら辺を悩んだ上でpci_irq_pulseを使うという暴挙に出たのでしょう。

 

本当の解決策

NVMeにおいて、本当にPCI割り込みがdisableになる事でInterrupt Status Bitを下げるのなら、QEMUPCIエミュレーションの中にデバイスに対して割り込み禁止を通知する機構を実装するのが良いでしょう。ただし、本質的な解決策ではあっても、あまり綺麗に実装できる予感がしないので、僕は遠慮しておきます。

 

現実的な解決策

実機の挙動と変わってしまいますが、INTMCというNVMeのレジスタに対する書き込みによってInterrupt Status Bitを下げるのが最も良さそうに僕は思います。

たぶん世の中のデバドラはこの修正を施してもうまく動くのではないでしょうか。

 

真の解決策

そもそも論として、なぜこの問題が表面化していないかというと、今時NVMeのデバドラでlegacyな割り込みを使う物がないからです。まあIntelの人も「どーせpin-based割り込みなんて使われないし、適当な実装でいいや」って思って実装したんじゃないですかね。

だからMSIを使う、というのが真の解決策です。

え?uioでMSIが使えない?こんなパッチがあるじゃろ?なんで適用されてないのかよく分からんけど(流し読みした限りだとセキュリティが云々って言ってるけど、英語読むの面倒になったので誰かやり取り要約して欲しい)、まあ手元で適用したuioデバドラを動かせばいいんじゃないでしょうか。

 

ところで、vfioだったらMSIフルサポートだったりしませんかね?もしそうなら乗り換えようかな。vfioはIOMMU(Vt-D)がないと使えないっぽいから採用を見送ったのだけど、No-IOMMU modeなんてのもあるらしいし???

 

USB3.0のデバドラを書いてみた

f:id:liva_h:20171114114104j:plain



ザ!車輪の再発明!!って感じですが、いつものごとく懲りずにやってます。

3ヶ月くらい、ありとあらゆる事を放り出してずっとUSBのデバドラ書いてたのですが、途中ソウルジェムを濁らせたりしながらも、ようやく動いたのでブログに書いて供養しようと思います。

 

話の流れとしては、以下の記事の続き。

raphine.hatenablog.com

 

タイトルは分かり易さを重視したが故に情報量が少ないので、きちんと説明すると、

  • USB3のホストコントローラ(xHCI)のデバドラを自作し、
  • キーボードから入力を受けられる事を実機で確認した

というものです。

 

また、以下の点を注記しておきます。

  • キーボードを動かす事しか考えてないので、USB3の特徴である広帯域通信はやってない。
  • 自作OS向けにデバドラを書いたのではなく、あくまでLinuxの上で動くデバドラを書いた。

まあ前者に関しては、xHCI自体の設計が綺麗なので、たぶん僕のデバドラでもそこそこ帯域は出るんじゃないかなー、なんて思ってるんですが、そもそもBulk TransferとかIsochronous Transferとかは実装してなくて・・・。そのうち気が向いたらやります。

後者に関しては、ちょっと深い理由があって(後述)こうなりました。もちろん、近いうちに今回書いたコードを自作OS向けに移植するつもりですし、移植する事を前提とした設計にしています。

 

前置きが長くなりましたが、ここから本編。

 

用語解説

  • ホストコントローラ:USBデバイスを集中制御するコントローラ。こいつの下にUSBのポートがぶら下がっていて、ポートにデバイスを差すとコントローラがデバイスの制御を始める。デバドラはホストコントローラと通信する事で全てのUSBデバイスを制御する。
  • xHCIUSB3.0/3.1向けのホストコントローラ
  • uio_pci_generic:Linuxにおいては、本来デバドラはカーネル空間で動かす必要があるが、ユーザー空間でもデバドラ開発ができるようにする魔法の仕組み。魔法の仕組み自体はuioで、uio_pci_genericはPCIバイス制御ができるようにするためのもの。

 

事の始まり

端的に言えば、酒の席で余計な事を口走ってしまったんです。

 

「ぼく、xHCIのデバドラ書くことにしました!」

 

この言葉を発する前にちょっとしたやり取りがあったのですが、ここではそれは割愛するとして、、、どんな理由があっても、「男に二言は無い」と言いますし、口に出した以上はやらざるを得ないなぁ、と。

 

今年の春先は、「いかに四月病でも、USB3.0のデバドラなんて絶対書かないぞ!」(2.0と1.1のデバドラ書くのがあまりに大変すぎたから)って思ってたんですがね。何でこんな事言っちゃったんだろう。

 

カレンダー確認したら、この言葉を発したのがちょうど今から3ヶ月前だったわけですが、この時は「まあ大変だけど1,2週間くらい本気でやればできるでしょ」なーんて甘く見てました。。。

当時は調子に乗ってたので、こんな事を呟いてますね。この時の自分、ぶん殴りたい....

 

まあそんなこんなでUSB3.0のホストコントローラのデバドラを書き始める事になったのでした。

 

実装方針

書くのは良いのですが、ただ自分のOS向けにデバドラ書くだけというのも芸がないなぁと思ったので、ちょっと新しいチャレンジをしてみる事にしました。

 

Linuxユーザー空間で動くxHCIデバイスドライバを作る

 

これによって、デバドラの開発効率が大幅に上がり、かつ他の人に気軽に遊んでもらいやすいかなぁ、なんて事を思ったわけです。

前者は分かりやすくて、自分のOSのデバドラとして作ると、デバドラ内やOS内にバグがあった時にOSごとクラッシュしたりして辛いわけですが、ひとまずLinuxアプリケーションとして作れば、libcをバンバン使えますし、(将来的に自分のOSに移植する際はlibcの部分を取り除く必要があるとはいえ、デバッグのためには好きなように使える)クラッシュしてもセグフォするだけなので、すぐ修正して再実行できます。PCリブートして、ファームウェアの画面眺めて、自分のOS起動するのとか、もう僕は待てません。

デバドラを書く時って、少なくとも最初の頃はQEMU上で自分のOSを動かしながらその上でデバドラ動かして、っていう形式を取る事が多いと思うのですが(実機でデバッグするのは大変だから)、この方式ならQEMU上で開発するよりもずっとずっと楽です。実機上での開発なので、QEMUが対応してないハードウェアもデバッグできますし。

唯一問題があるとすれば、割り込みのオーバーヘッドが大きい事なのですが、まあそれは自作OSに移植する際に解決する問題だし、そもそもそれ以前にその程度のオーバーヘッドが問題となるような凄い物を作ってないだろ!という話も。

ユーザー空間でデバドラを書く方法としては、uio_pci_genericを使っています。技術的な話については、以前記事を書いたので、こちらを参考にしてください。

raphine.hatenablog.com

raphine.hatenablog.com

raphine.hatenablog.com

 

暗雲

でまあ、最初はxHCIのデバドラくらいすぐ書けるだろとか舐めてたんですが、なんかちょっとづつ雲行きが怪しくなってきたんですよね。

上で、「どうせこんなの1,2週間で書けるだろ」みたいな事を宣ってたという話を書きましたが、これには一応僕なりの根拠がありまして。以前USB2.0のホストコントローラ(EHCI)のデバドラを書いた時、USB1.1のホストコントローラ(UHCI)と仕様がほぼ一緒だったので、本当に1,2週間で書けてしまったんです。

なので、某セキュリティ・キャンプとか、某サイボウズ・ラボユースの合宿(OBとして参加)とかでも、「もうすぐ書き終わります〜」みたいな余裕ぶっこいた顔をしてましたよ、ええ。

 

でも、蓋を空けてみたら、xHCIEHCIから大幅に仕様が変わっていて、てかよくこんな仕様設計したなぁ、と正直感心してしまったわけですが、まあインターフェースとしては明らかに複雑になってました。

それを仕様書を読みながらチマチマ書いてたら2ヶ月経ってました。はい。

この時は、「千里の道も一歩から、毎日少しでも進捗生めば何時か終わる」って唱えてました。マラソンかよ。

 

想定外

xHCIのデバドラがほぼ書けて、ついにキーボードが使えるようになると思ったのですが、ここで事件が起こります。

 

 

そう、コントローラの下にぶら下がっていたのはキーボードではなく、ハブだったのでした。というわけで、ここからハブのデバドラを書き始める事になります。

 

ま、本当はそんな事する必要は無かったんだけどな!!!

 

May the Force be with me!

論文の学会投稿と重なりつつも、それを見なかった事にしてハブのデバドラを書き続けたらハブのデバドラが書けました。結果、論文の修正と校正プロセスが炎上しました。まあそれはまた別のお話。

キーボードのデバドラは既に書いた事があるので、よし、これ組み合わせれば動くぞ!と思っていたのですが、なぜか動かない。。。Interrupt Endpointよ、なぜ君はデータを送ってこないのか。。。

ここらへんからシステムプログラミングの醍醐味である、念力デバッグの出番です。なんか「う〜〜〜ん!」って迷走瞑想すると、問題のある箇所が分かる!的な。

まあ現実は、QEMU上で走らせてみて、QEMUxHCIエミュレーションコードをトレースして状態遷移を監視してみたり、Linuxのデバドラによって初期化された後のメモリダンプと比較したり・・・実機のデバッグ、タノシイ!!!

最終的には、仕様書眺めてるとなんか一箇所が光り始めたので、そこを確認したらバグが見つかりました。宗教始められるかもしれない。

 

キーボードいっぱいコレクション

大きなバグを潰したら、なんかキーボードの製品毎に違う挙動を示したりするようになったので、原因究明をする事になりました。もちろんデバドラのどこかに問題があって、その問題の発現が製品ごとに微妙に違う、という話であって、製品によって制御を変えなければいけないとかそんな事ではないです。

 

その中で遭遇した面白案件。

 

  • 某Sサプライ社の安物キーボードが初期化時に必ずエラーになる。僕の私物のREALFORCEはちゃんと初期化できるのに。東プレすげぇ!流石高級キーボードは違う!!って夜の研究室で叫んだ。
  • なんかふとした拍子に安物キーボードも動くようになったので、さてはと思ってUSBのコネクタ部分に力を掛けたら100%動くようになった。(=コネクタの接触不良)
  • ちなみに、安物だから悪いのではなく、偶然この個体が悪かっただけでした。(同じ製品の別の個体を試したら何もしなくても動いた)某Sサプライ社さん、誹謗中傷して申し訳ございませんでした。
  • HHKBがキーボードだと?あれはハブだよ?

 

最後について。

研究室にあるHHKBはLiteなので、まあHHKBのパチモンみたいな奴なわけですが、いずれにせよHHKBってUSBポートあるじゃないですか。キーボードが他のデバイスをぶら下げる事はできないので、HHKBの内部構造としては、ハブがあってその下にキーボードが1つぶら下がってる、という物になります。(なんかこう書くと当たり前に見えるけど、デバッグしてた時は完全に失念していたのでした)

 

でね、さっきホストコントローラの下にぶら下がってたのがハブだったって話。

あの時は、このハブがxHCIに内蔵されてるハブ(仕様書内のEmbedded Hub相当)だと思ってたんですよ。

 

 

はい、大嘘です。お前が今見てるそれはHHKBだ!!

まあそんなわけでとりあえずキーボード動かしたいだけならハブのデバドラは書かなくて大丈夫です。HHKB使いでないなら。

僕は諸事情によりあまりHHKBをdisれないのですが、まあでもUSBホストコントローラのデバドラ書く人はHHKBやめてREALFORCE使おうな!

 

動いたぞ

いろいろありましたが、一応動いたので、動画。

左のIntel NUCにUSBキーボードを挿して、NUC上でLinuxを動かし、このLinuxの上で自作xHCIドライバを動かしています。右のMacにキーボードから飛んできたパケットを表示していますが、これは単にNUCのLinuxsshしただけ。

 

一応以下の環境で検証済み。検証環境は他にもあるので、そのうちそいつらでも動かさないと。

  • Intel NUC(Kaby Lake)

BOXNUC7i7BNH 《送料無料》 - 自作PC・PCパーツが豊富!PC専門店【TSUKUMO】

  • なんか強いマシン

X11SAE | Motherboards | Products - Super Micro Computer, Inc.

xHCIモードに設定、USB2.0ポートに繋いでもちゃんと動いた。

 

コードはここに転がしてます。HHKBだと(ハブが挟まると)落ちるバグがあるので、そのうち治します。何も変更加えてないけど動くようになりました。ただの接触不良だったっぽい。

github.com

uio_pci_genericでデバドラを書く<その3>

その2はこちら。

 

今回は厳密にはuio_pci_genericの話ではないのだけども、uioでデバドラを書く上で必須となるであろうお話。

 

当たり前だけど、DMAができないと多くのデバドラは書けない。

DMAするためには良さげなページと物理アドレスが欲しいですよね。

じゃあどうやってページとその物理アドレスを手に入れるか、というわけなのだけど。

 

結論から書くと、hugepageをallocateして、/proc/self/pagemapを舐める。簡単。

 

まずなんでhugepageを使うかというと、普通にmallocした場合、allocateしたページがカーネルによって勝手にスワップされて消えちゃうかもしれないじゃないですか。

hugepageだとそんな事がないらしい。

 

Huge pages cannot be swapped out under memory pressure.

https://www.kernel.org/doc/Documentation/vm/hugetlbpage.txt

 

hugepageをallocateしたい時は、事前に(デバドラプログラムを実行する前に)以下のようにしてhugepageを予約しておく。

echo 20 > /proc/sys/vm/nr_hugepages

 

その上で、デバドラ内で以下のような感じでmmapを呼ぶと2MB huge pageが割当たった仮想アドレスが戻り値として帰ってくる。

mmap(NULL, 2 * 1024 * 1024, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_HUGETLB | MAP_ANONYMOUS | MAP_POPULATE, 0, 0);

 

次に/proc/self/pagemapというのを舐めて、仮想アドレスから物理アドレスに変換する。mmapで受け取った値をそのまま食わせれば良い。

stackoverflow.com

 

ほぼこのコードをコピペしたら動く。なんか一箇所コンパイルエラーが出たような気がしないでもないが、自明すぎて忘れてしまった。

 

おしまい。

uio_pci_genericでデバドラを書く<その2>

その1はこちら。

 

今回はxHCIレジスタ空間を触ってみる。

 

参考になるのは、DPDKのソースとか。

 

前回やった通り、PCIレジスタ空間は/sys/class/uio/uio0/device/configをopenしてread/writeすれば良かったのだが、BARが指すPCIバイスレジスタ空間についてはまた別の方法を用いてアクセスする。

 

BAR0のI/O空間であれば、/sys/class/uio/uio0/device/resource0をmmapして、メモリアクセスすれば良い。

 

実際に使えている事を試したいので、XHCIをenableして、USB Status RegisterのHCHaltedフラグが1から0に落ちる事を確認してみる。

 

コードは以下の通り。例によってエラー処理はしていない。

#include <stdio.h>

#include <unistd.h>

#include <stdint.h>

#include <fcntl.h>

#include <errno.h>

#include <assert.h>

#include <sys/mman.h>

 

int main()

{

  int uiofd = open("/dev/uio0", O_RDONLY);

  int configfd = open("/sys/class/uio/uio0/device/config", O_RDWR);

 

  // read bar0                                                                                                                                                                                              

  uint32_t bar0;

  pread(configfd, &bar0, 4, 0x10);

 

  // write 0xFFFFFFFF to BAR0 and get size                                                                                                                                                                  

  uint32_t tmp = 0xFFFFFFFF;

  pwrite(configfd, &tmp, 4, 0x10);

  uint32_t size;

  pread(configfd, &size, 4, 0x10);

  size = ~size + 1;

 

  // write back bar0                                                                                                                                                                                        

  pwrite(configfd, &bar0, 4, 0x10);

 

 

  int fd = open("/sys/class/uio/uio0/device/resource0", O_RDWR);

  void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE,

                    MAP_SHARED, fd, 0);

  close(fd);

 

 

  // get Capability Registers Length                                                                                                                                                                        

  uint8_t caplen = ((uint8_t *)addr)[0];

 

  // clear bit 0(Run/Stop) of USB Command Register                                                                                                                                                          

  ((uint32_t *)addr)[caplen/4 + 0] &= ~1;

 

  usleep(1000);

 

  // read USB Status Register                                                                                                                                                                               

  printf("%08x\n",((uint32_t *)addr)[caplen/4 + 1]);

 

  // set 1 to bit 0(Run/Stop) of USB Command Register                                                                                                                                                       

  ((uint32_t *)addr)[caplen/4 + 0] |= 1;

 

  usleep(1000);

 

  // read USB Status Register                                                                                                                                                                               

  printf("%08x\n",((uint32_t *)addr)[caplen/4 + 1]);

 

  return 0;

}

 

これを実行すると、一回目のprintfでは1が表示され、二回目のprintfでは0が表示される。きちんとMMIOを用いたデバイス制御ができている事が分かる。

 

続きはその3

uio_pci_genericでデバドラを書く<その1>

男もすなるuioといふものを、女もしてみむとてするなり。

 

uio、流行ってますね。というか、今時Ring0でデバイスを制御しても新鮮味がないです。

なのでuioを使ってパパっとデバドラを書けるようにならなきゃなぁ、というぼんやりとした焦りを感じていたのですが、いろいろな都合でuioでデバドラを書く流れになったので、調べつつ知見を軽くブログにアウトプットしていこうかと思います。あんまり資料も無さそうですし。

一応の前提として、僕は主にPCIバイスのドライバを書く事が多いので、Linuxのuio_pci_genericを使って、PCIデバドラを作るというのがこのシリーズのテーマです。あと、PCIの仕様を既に理解している人向けです。

 

uio_pci_genericのカーネルモジュールは既にロードされているとして、実際にコードを書く所から始めます。今回は、Vendor IDとDevice IDを取得する所まで。

 

実験に使うPCIバイスXHCI(USB3.0 Host Controller)なのですが、まずはXHCIのデバドラをuio_pci_genericに割り当てる作業です。

 

# echo "8086 1e31" > /sys/bus/pci/drivers/uio_pci_generic/new_id

echo -n 0000:00:0c.0 > /sys/bus/pci/drivers/xhci_hcd/unbind

echo -n 0000:00:0c.0 > /sys/bus/pci/drivers/uio_pci_generic/bind

 

参考:Chapter 5. Generic PCI UIO driver

 

 

で、uio_pci_genericを用いたとても小さなコードを書きます。

まあ、このページの劣化版ですね。

Example code using uio_pci_generic

 

こんな感じ。エラー処理とかはきちんとやっていませんが悪しからず。

#include <stdio.h>

#include <unistd.h>

#include <stdint.h>

#include <fcntl.h>

#include <errno.h>

 

int main()

{

  int uiofd = open("/dev/uio0", O_RDONLY);

  int configfd = open("/sys/class/uio/uio0/device/config", O_RDWR);

 

  uint16_t vendor_id, device_id;

  pread(configfd, &vendor_id, 2, 0);

  pread(configfd, &device_id, 2, 2);

  printf("Vendor ID: %x, Device ID: %x\n", vendor_id, device_id);

  return 0;

}

 

Vendor IDはPCI Configuration Spaceの0byte目からの2byte、Device IDは2byte目からの2byteなので、preadを用いて、configfdから該当オフセットと該当サイズ分だけ読んでいます。

 

これをコンパイルしてroot権限で実行すれば、

Vendor ID: 8086, Device ID: 1e31

 

簡単ですね。

 

その2へ続く

Toy OS meets Rump Kernel

Raphine(僕の自作OS)上でRump Kernelを立ち上げる事ができるようになった。

 

Rump Kernelとはなんぞやという話を簡単にすると、

 

  • NetBSDベースのLibraryOS
  • NetBSDカーネルとアプリケーションをstatic linkして1つのバイナリにできる。(Unikernel)
  • Linuxユーザーランドや、Xen、ベアメタル上で実行可能

 

Rump Kernelを使えば、どんな環境でもNetBSDアプリケーションが動かせるわけである。

 

さて、自作OS屋さんの悩みとして、以下のような物がある。

  • バイスを制御するためには自分でデバドラを書かなければならない。(デバドラを自分で書くのは大変なので、使いたいけども使えないデバイスが出てきたりする)
  • 互換性を持たない場合、既存のアプリケーションが動かせない。

 

これらの悩みに対する、ちょっとセコい解決策として、自作OS上でRump Kernelを起動できるようにする、というのを思いついたので、実装してみることにした。

 

これによってどんな事ができるようになるかというと、

とはいえ、NetBSDが諸々の大事な事をやり、自分のOSは特に仕事をしていないので、「自分のOSでいろいろやっている感」はない。

まあそうなると、「それってNetBSDで良くない?」って思う人もいると思うけど、これをどう活用するかは今回の話の趣旨から外れてしまうので、割愛。

 

 

話を戻して、とりあえずRump KernelがRaphine上で動きましたよ、という事なのだけども、軽くその実装について書いてみる。

 

全体構成はこんな感じ。

f:id:liva_h:20170810175344p:plain

Rump Kernelにはベアメタルで動かすためのバックエンドコードがあり、これを改造して、新しいバックエンドを作った。Rump Kernelがベアメタル上で動作する時はRing0なので、今回改造したRump KernelバックエンドコードもRing0で動く。

イメージ的にはRump KernelというカーネルモジュールがRaphineの中で動く、といった感じ。実際の実装としてはRing0上で動くアプリケーションという風に言う事もできるし、RaphineがRump Kernelのハイパーバイザとなっている、という事もできるかもしれない。

 

実装は割と荒削りなので、pciをサポートしていなかったり、タイマ周りが雑だったりするのだけど、とりあえず以下のようなコードをNetBSDアプリケーションにして、Rump Kernel内に組み込んで起動する事ができるようになった。

#include <stdio.h>

#include <unistd.h>

int main() {

  printf("test1\n");

  printf("test2\n");

  return 0;

}

 

起動画面はこんな感じ。

f:id:liva_h:20170810180544p:plain

load rump.binというコマンドでRump Kernelバイナリをロードする。

ELFの解析等をRaphine側でやった後でエントリポイントに飛ぶと、Rump Kernel(NetBSD)がCopyright表示を出して起動する。

初期化が終わると、上に貼ったコードの通りtest1、test2と表示して終了する、といった具合。

 

ソースへのリンクは以下の通り。

Rump Kernelが起動する自作OS

github.com

実装したRump Kernelバックエンド

github.com

 

 

尻切れトンボだけど、終わり。

c++でfinalつけれる時はつけよう、という話

仮想関数を使うとvtableを呼び出すからオーバーヘッドが生じるのは当たり前の話。だから仮想関数にしなくて良い時は仮想関数を使いたくない。

一方で、インタフェースを明示するためには仮想関数を使わなければいけない。インターフェースをソース内で明示したいだけなのに仮想関数のオーバーヘッドが生じるのも癪だなぁと思ってたら、finalつければいいじゃん、となったのでまとめておく。

 

先に書いておくと、たぶん殆どのc++プログラマにとってはあんまり旨味のない話のはず。

 

まずどういう状況かというと、Make時のマクロで使うソースを切り替えたい。

具体的には、aとbという2つのディレクトリがあって、それぞれにtest.hがある。

a/test.h

b/test.h 

 aのディレクトリのtest.hを使うか、bのディレクトリのtest.hを使うかはmake時のマクロ指定で決める事とする。

たぶんこの時点で普通のプログラミングではあまり起きない状況だろう。

 

それぞれのtest.hには異なるTestクラスがある。そしてこのTestクラスのインターフェースをコードレベルで明示したい。

普通に考えればTestInterfaceクラスを作って、それぞれのTestクラスがTestInterfaceを継承すれば良い。でも、test.hの外から使う時はTestInterfaceではなく、Testクラスなので、インターフェース継承によってオーバーヘッドが生じるのはバカバカしい。(型によって動的に実行コードが切り替わらないなら、vtableを使わずに静的に呼ばれて欲しいじゃない?)

 

そこでfinalを付けてみたら、O0でもきちんと静的に呼び出される事が分かった。

 

サンプルコード

#include <stdio.h>

 

class TestInterface {

public:

  virtual int Func(int i) = 0;

};

 

class Test : public TestInterface {

public:

  virtual int Func(int i) override /* final */ {

    // 上のfinalをつけたり、つけなかったり

    return i;

  }

};

 

int test(Test &t, int i) {

  return t.Func(i);

}

 

int main() {

  Test t;

  return test(t, 1);

}

 

これをこんな感じでコンパイルする

 

g++ -O0 -std=c++11 test.cc

ちなみに、バージョンは g++ (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4

 

んで、逆アセンブルする。

objdump -CD a.out

 

final無しの場合

000000000040067d <test(Test&, int)>:

  40067d:       55                      push   %rbp

  40067e:       48 89 e5                mov    %rsp,%rbp

  400681:       48 83 ec 10             sub    $0x10,%rsp

  400685:       48 89 7d f8             mov    %rdi,-0x8(%rbp)

  400689:       89 75 f4                mov    %esi,-0xc(%rbp)

  40068c:       48 8b 45 f8             mov    -0x8(%rbp),%rax

  400690:       48 8b 00                mov    (%rax),%rax

  400693:       48 8b 00                mov    (%rax),%rax

  400696:       8b 4d f4                mov    -0xc(%rbp),%ecx

  400699:       48 8b 55 f8             mov    -0x8(%rbp),%rdx

  40069d:       89 ce                   mov    %ecx,%esi

  40069f:       48 89 d7                mov    %rdx,%rdi

  4006a2:       ff d0                   callq  *%rax

  4006a4:       c9                      leaveq

  4006a5:       c3                      retq

 

final有りの場合

000000000040067d <test(Test&, int)>:

  40067d:       55                      push   %rbp

  40067e:       48 89 e5                mov    %rsp,%rbp

  400681:       48 83 ec 10             sub    $0x10,%rsp

  400685:       48 89 7d f8             mov    %rdi,-0x8(%rbp)

  400689:       89 75 f4                mov    %esi,-0xc(%rbp)

  40068c:       8b 55 f4                mov    -0xc(%rbp),%edx

  40068f:       48 8b 45 f8             mov    -0x8(%rbp),%rax

  400693:       89 d6                   mov    %edx,%esi

  400695:       48 89 c7                mov    %rax,%rdi

  400698:       e8 25 00 00 00          callq  4006c2 <Test::Func(int)>

  40069d:       c9                      leaveq

  40069e:       c3                      retq

 

というわけで、finalはつけような!