slankdevの雑記帳 (2016/12)

超豪華なスペックで通信計測試験をしました。

試験環境はDPDK-16.07 と pktgen-dpdkを使用しました。

実験環境

– Xeon Phi
– Intel x540-T2 2つ
– DPDK 16.07
– uio_pci_generic
– pktgen-dpdk

pktgenはテンプレな使い方でやった. マルチスレッドは考えてない.

実験パターンはおそらく以下をためした

– x540 <—cat6e—> x540
– x540 <—cat6—-> x540
– x540 <—cat5—-> x540
– x540 <—cat6—> GbE SW <—cat5—> x540
– x540 <—cat6e–> 10GbE SW <–cat6e–> x540

それぞれケーブルは2m未満だとおもいます

結果としてどのパターンでもTx/Rx: 9000/6000Mbpsくらいでた.

cat5でもとりあえず10Gでるよ。でも100mのケーブルでの動作保証はありませんってことだと思います。

Xeon Phiだと 10Gの限界がでなかったのはおそらくコアのクロック周波数がたりなかったからだとおもわれる? まあ今度試します。

それからcat5で10Gが出せるのがなっとくいかなかったので、ケーブルをいじめてみた。以下をためした

– 皮膜を剥がした
– ツイストをなるべくほどいた
– 磁石を近づけた
– 蛍光灯に近づけた

そのけっかリンクダウンするパターンがあった。皮膜が重要だったっぽい。

皮膜を剥がしてまきつけたりしたら死んだ。

結論「ひまくだいじ!」

 

256コアマシン向けのOSを作る

お手持ちのマシンのCPUコア、何個でしょうか?

え、4個?8個?うーん、ショボイ。ハイパースレッディング入れて64個?まあ頑張ってますね。

今時、ちょっと奮発して256コア積んだマシンにした方が絶対お得でしょ〜。

なんて会話がもう少ししたら当たり前に聞こえて来るかもしれません。ま、かなり奮発すれば256コアマシンは手に入るので、している所では既にしているでしょう。

Xeon Phi

たぶん???となっている方もいるような気がするので、解説します。

最近、Intelが製造しているプロダクトに、Xeon Phiという製品があります。1GHz程度のショボイコアを64個くらい並べて、並列コンピューティングやると早いよ!(たぶん)っていう代物です。ちなみに、各コアに2つ演算ユニットがついて、さらにハイパースレッディングを積んでるので、物理64コアのXeon PhiはOSから見ると256コアになります。マジでcat /proc/cpuinfoやると256個出てくるよ!

そういえばIntelはPen4の周波数上げができなくなって、Core2とかのマルチコア製品に手を出し始めたわけですが、結局Coreシリーズもコア数をどんどん増やす事はなく、頭打ちになりました。というわけで、プロセス微細化を頑張ってきたのですが、ここにきてまたコア数を増やしていくという戦略を取ったようです。まあムーアの法則、崩壊しかけてるからね、仕方ないね。

Xeon Phiは当然その設計上、シングルスレッド性能は大した事はないです。だから並列アプリケーションとかでないと、その真価を発揮できないわけです。或いは、Web界隈とかだともしかすると使えるかもしれません。全コアでWebサーバープロセスを立てて、片っ端から処理する、みたいな。256コアもあれば、先にネットワークの帯域がボトルネックになりそう。知らんけど。

並列コンピューティングっていうといろいろあって、ネットワーク越しにMPIでほげほげする奴とか、マルチコアを頑張ってガリガリ使うのとか、後最近流行ってるのはGPUですよね。Xeon Phiの一番のライバルとなるのは、明らかにGPUなので、Xeon Phi使い方指南書みたいな本には、「GPUよりも使いやすいぜ!」とか「GPUよりも早いぜ!」とかいろいろ書かれています。「GPU絶対殺ス」っていうIntelの意思が伝わってきて、大変素晴らしいですね、はい。

個人的には、ショボイ並列コンピューティングクラスタ並のコア数でかつ、コア間の通信遅延が少ない、という意味において、すごい時代になったなぁ、って思ってます。もちろん、Xeon Phiを更に並列につなげて並列コンピューティングをしても良いわけで、実際そういうスパコンはあります。かのTianhe-2もそうですし、最近日本の某大学と某大学に入って京の性能を超えてしまったOakforest-PACSとかいうのもそうです。

あと、Xeon Phiのもうひとつ注目すべき点としては、クラスタ状になっているコアの横に帯域幅のめちゃくちゃ広いDRAM(MCDRAM – Many Core DRAM)がくっついた事です。既存のメモリだと256コアがバリバリメモリアクセスする事で帯域幅を使いきってしまうので、こっちを使いましょう、と用意された物ですね。

こいつは所謂NUMAで、コアとメモリコントローラとの間の距離がコアによって違います。もう遅延とかは気にせず、とにかく帯域出そうぜって所に男気を感じますね(?)

まあ、そんな上手くいくのか良くわからない微妙な物を作ってるんですよ、っていうIntelさんのお話です。

KNC

ここからはXeon Phiの世代間アーキテクチャーについて説明していきます。Xeon Phiは世代が変わるごとに大きく設計が変わるので、区別すべき所ではきちんと言い分けなければなりません。

とりあえず、第2世代のKnights Cornerから行きましょう。第1世代のKnights Ferryは製品化されてないのでスルー。

Knights Cornerはリング状のクラスタ構造を持っていました。(「Knights Corner ring」でgoogle画像検索すると沢山画像が出てくる)リング状というのは、単純で設計しやすい反面、コア間の遅延が大きくなるし、帯域使い果たしちゃうし、であまり良い設計とは言えません。実際、聞く限りではそれほど性能はスケールしないようです。残念。ちなみに、Tianhe-2はこれ。(時代的にこれしかなかったからね、しょうがないね)

あと、KNC世代の製品は全てコプロセッサでした。つまりGPUみたいな形をしてて、PCIeにブスっと差して、ホストのCPUからプログラムを転送する、みたいな使われ方をしてた、という事です。コプロセッサといっても、中ではLinuxが動いててsshする事ができたので、それはそれで面白い事ができたのかもしれません。

僕はこいつを一回破壊してるので、もうあまり触りたくはないです。はい。

KNL

で、我らが第三世代、Knights Landingです。こいつはクラスタ構造がメッシュで、遅延も小さくなるし、帯域も広くなったしで、そこそこスケールすると評判です。(例によって「Knights Landing mesh」でgoogle画像検索)Oakforest-PACSはこれ。

さて、ここからが本題。

「「「Knights LandingからはLGA CPUソケット上に乗るようになり、自力でOSをブートできる!」」」

こりゃもうKnights Landing向けにOSを書くしかないじゃない!ってのが今回のお話です。

前置き長かった。。。

あ、KNLでもコプロセッサは残ってます。コプロセッサならPCIeの帯域が許す限り1ノードにコアを増やし続けられるよ!やったね!

あと、KNLはもはやCPUじゃねーかそれ、って感じですが、IntelはCPUと呼びたくないらしいです。なんででしょうね。既存のCPUと同じにされたくないのかな?それならXeonって名前は紛らわしいから辞めた方がいいと思うんだよなぁ。。。

KNL向けのToy OS開発

メニーコアともなると、OSを0から作りなおさなきゃいけないような気がしますが、全くそんな事はないです。ってか、そうなってないのが流石Intelって感じでした。

言うなれば、KNLはOSからすると、ただコアが256個に増えたXeonなんですよね。いやだからもうそれCPUじゃん。

KNL本にもシステムプログラム向けの解説は一切なくて、並列化テクニックの話ばかり書かれています。

OSを作るのに必要な情報は、Intelの技術資料(最新版)に100%乗ってます。Intelの設計すげぇ。

というわけで、ここではマルチコア対応とKNL対応の差異について説明したいと思います。マルチコアをどう実装するか分からない?こっちで軽く解説してるので、割愛→マルチコア

メニーコアの初期化

まず、マルチコアに対応するだけであれば、LegacyなAPIC(xAPIC)のサポートだけで良かったのですが、残念な事にxAPICではAPIC ID(CPUのコア番号)が8bitしかないんですよね。Xeon Phiは途中のAPIC IDが欠番(製造プロセス上、途中にダミーのコアがある)なので、256番目のコアのAPIC IDは295です。つまりxAPICでは全てのコアを扱えないんですね。

というわけで、x2APICのサポートが必須となります。256個使わなくて良いなら、xAPICだけでも動くには動きます。

ココらへんのあーだこーだしてた話がlivaの雑記帳(2016/10)の10/1にあるので、物好きな方はどうぞ。

まあでも逆に言うと、x2APICさえサポートしてしまえば、マルチコアOSでも普通に動きます。Intelすげぇ。

MCDRAM情報の取得

MCDRAMはいろいろまたこいつが面白くてですね。KNLはチップ内にMCDRAMを16GB積んでるのですが、こいつをメインメモリ(DDR4)のキャッシュとして使うモードと、普通に直接MCDRAM使うモード、そしてちょっとだけキャッシュとして使って、残りを直接扱うモードの3つが用意されています。

一番最初のケースだと、OSからはMCDRAMを管理する必要がないので、対応は不要です。まあでもこういうのって、プログラム側が明示的に使いたいものですし、某研究所の偉い人曰く、Flat Mode(直接使うモード)で使うべきとの事なので、ちゃちゃっと実装しました。例によってlivaの雑記帳(2016/10)の10/27にそこら辺の話が軽くあります。

MCDRAMは名前こそかっこいい(?)ですが、実態はただのNUMAノードで、システム的にもNUMAとして扱われます。NUMAという事は、NUMAを取り扱う機構がIntelアーキテクチャー上に既に存在するので、Xeon Phi向けの特殊実装をする必要がないという事です。Intelすげぇ(n度目)

NUMAの情報はACPIのSRATというテーブルから取ってこれます。(ACPIの仕様書は参考資料に記載)テーブル読むだけで良いので、ACPICAとか使わずに自力で解析しても大丈夫です。

で、テーブル読めば分かるのですが、DDR4もMCDRAMも同じ物理メモリ空間にマッピングされていて、先頭の0〜XX GB(XX=搭載DDR4メモリ)がDDR4、XX〜XX+16 GBがMCDRAMという形になっています。なので、メモリアドレスを変えるだけで、アクセスを切り替えられる、というわけですね。楽ちん。

後はプロセスからのNUMA要求があったら、MCDRAMがマッピングされているメモリ空間を適当にページングで仮想メモリ空間にマッピングしてあげればよろし。

最後に

いかがでしたでしょうか?メニーコアマシンとて、恐るるに足らず、といった事が分かってもらえたと思います。256コアマシンがお手元にある方は、Linuxを動かして満足せずに、OSをパパっと作っちゃいましょう。

256コアマシンの素晴らしい所は、マルチスレッディング周りの実装エラーがポンポン発現する(それまで16コアマシンで出なかった、スピンロックの実装ミスが毎起動時に発生したりとか)ので、デバッグにも大変役に立ちます。まあそれだけでXeon Phi導入するのはコスパ悪すぎますけどね。

とはいえ、非常識な値段でもないです。かなり潤ってる社会人なら頑張れば買えるかなー、って感じ。Mac Pro2台分くらいですかね。会社の研究用予算で落とすとか、研究室の予算で落とすとか、が一番楽だと思います。中高生は・・・時が解決するさ。

あと、メニーコアマシンはLinuxだといろいろ辛い部分があるので、それこそ自分たちでOSを作る人達が生きのびる余地がある領域だと思います。楽しいよ!メニーコアマシン!皆もやってみよう!

Q&A

Q. 256コアだとタスクマネージャーどうなるの?

A. やばい

Q. うるさそう

A. 水冷マシンなので、僕の個人用デスクトップPCより静か

Q. 電力やばそう

A. 600W電源でいける

Q. どこで買えるの

A. 業者発注。そうそう、Xeon Phiの取り寄せをしてくれた業者さんが、他にも発注があれば是非うちに、商社通すよりは安くできるよ!って言ってたので、興味があればお気軽にご連絡下さい(?)

時折飛んでくる原因不明な#GPをgdbで原因究明した備忘録

OS開発をしていると、謎な例外が飛んできたり、謎なクラッシュが起きたりして、辛いですよね。普通のソフトウェアだと、プログラムのロジックを丁寧に追えば原因究明できる事が多いのですが、OS開発ではハードウェア側のエラーによって落ちる事があって、そうなるとハードウェアの中がどうなっているかわからない以上、お手上げになってしまうわけです。もちろんハードウェア側のエラーといっても、実際はカーネル側の実装上の些細なミスだったりする事が多いわけですが、たとえそうであっても、それをハードウェアエラーが起きている状態から特定するのは、かなり難しいわけです。

こういう場合に取る方法はいくつかあって、まあ一番簡単なのは第六感によるデバッグですね。「なんとなくここら辺が怪しいから修正してみよう」みたいな。しかし、これはデバッガーの経験値に大きく依存するので、あまり褒められた方法ではありません。

2つ目は、仕様書をとにかく舐めるように読んで、レジスタの値が間違ってないか、初期化シークエンスをきちんと踏んでいるかを確認する、という方法があります。これはとにかく時間が掛かる上に、多くの場合、一回見落とした箇所というのは次も往々にして見落とすので、確実に原因究明できるとは言い難い方法です。

3つ目は、既存のOSのソースコードを読む事です。特にデバドラをデバッグする時とかだとこの方法が役立つ事が多い気がします。既存のOSのコードと、今自分が書いているコードとの差異がどこなのかを徹底的に洗い出すと、問題点が見つかるかもしれないです。この場合、動くコードという模範回答があるので、精神的にはだいぶ楽になります。

さて実機でエラーが発生した場合は、上記のような手法で頑張って解析しなければいけないのですが、ことエミュレータ上でエラーが発生した場合は、もう一つ、とても強力なデバッグ方法があります。それはOSをエミュレーションしているqemuの実行プロセスをデバッグするというもので、QEMUがエミュレーションしているハードウェアをデバッグする事により、本来はブラックボックスなはずのハードウェアの内部状態を丸裸にする事ができます。

この方法は、根気さえあれば確実にバグを特定できるので、デバッグ手法としてはかなり優良な部類に入ります。ま、面倒ですけどね。

 

前置きが長くなりましたが、今回、自作Toy OS上の初期化シークエンスで時々#GP(一般保護例外)が発生していたため、qemuにgdbをつなぐ事で問題を特定した一連のあらましを備忘録的な何かとして書いてみます。

※この記事は自作OS Advent Calender 2016に空きがあったので、ちょうどいいやというノリで書いたものです。

 

例外の発生状況

発生した例外のvector番号:13(#GP)

例外発生時のrip:割り込みの初期化がすべて終わり、以後の割り込みを許可しようとsti命令を発行した直後

発生タイミング:不定期

 

毎回起動時に発生するわけではなく、発生する場合と発生しない場合がありました。

 

とりあえずsti命令で落ちているっぽいので、Intelのマニュアルを元にsti命令でGPを発生する状況について調べます。レジスタのフラグによってGPが発生する可能性があるので、関連する全てのレジスタの値を表示してみますが、どうやら問題なさそうです。

というわけで、ここからqemuをgdbデバッグしていきます。具体的なやり方についてはこちら。また、ここではqemuの内部を追う事に主眼を置いているので、gdbの基本的な使い方については割愛します。

 

sti命令をトレースする

sti命令で落ちているようなので、sti命令をトレースします。qemu/target-i386/translate.c内のsti命令を実行するコード内で例外を発生する部分があるので、そこにブレークポイントを仕掛けます。

    case 0xfb: /* sti */

        if (!s->vm86) {

            if (s->cpl <= s->iopl) {

            gen_sti:

                gen_helper_sti(cpu_env);

                /* interruptions are enabled only the first insn after sti */

                /* If several instructions disable interrupts, only the                                                                                                                                     

                   _first_ does it */

                if (!(s->tb->flags & HF_INHIBIT_IRQ_MASK))

                    gen_helper_set_inhibit_irq(cpu_env);

                /* give a chance to handle pending irqs */

                gen_jmp_im(s->pc - s->cs_base);

                gen_eob(s);

            } else {

                gen_exception(s, EXCP0D_GPF, pc_start - s->cs_base);

            }

        } else {

            if (s->iopl == 3) {

                goto gen_sti;

            } else {

                gen_exception(s, EXCP0D_GPF, pc_start - s->cs_base);

            }

        }

        break;

2箇所ある、gen_exception(s, EXCP0D_GPF, pc_start – s->cs_base)って所ですね。

しかしながら、この2箇所でブレークする事はありませんでした。

つまり、sti命令を実行した事によって#GPが発生したわけではない、という事がわかります。

何が起こっているかというと、sti命令を有効化した事によって、ハードウェア割り込みが有効化され、それに起因して何らかの処理がなされた(例えば実際に割り込みが飛んだ、とか)結果、その処理の中で#GPが発生した、という事になります。でも、そうわかった所で原因が皆目検討もつかないので、もう少し調べてみましょう。

例外発生関数から例外発生要因を特定する

qemu-2.4.1/target-i386/excp_helper.c内にraise_interrupt2という関数があります。(先程のgen_exceptionを掘り下げていて見つけました)

このraise_interrupt2に例外を仕掛ければ、backtraceにより発生要因を特定できます。(gen_exceptionに仕掛けなかったのは、gen_exceptionを経由しない例外があるため。実際に一度仕掛けて上手くいかない事を確認済み)

例外を仕掛ける時は変数intnoが13の場合のみブレークするよう、フィルタリングしておいた方が良いです。他の割り込みも拾われたらデバッグが面倒になるので。

#0  raise_interrupt2 (env=0x2ab57782a6c0, intno=13, is_int=0, error_code=-14,

    next_eip_addend=0)

    at /home/vagrant/qemu-2.4.1/target-i386/excp_helper.c:97

#1  0x00002ab577324506 in raise_exception_err (env=0x2ab57782a6c0,

    exception_index=13, error_code=-14)

    at /home/vagrant/qemu-2.4.1/target-i386/excp_helper.c:125

#2  0x00002ab57734cc56 in do_interrupt64 (env=0x2ab57782a6c0, intno=-1,

    is_int=0, error_code=0, next_eip=0, is_hw=1)

    at /home/vagrant/qemu-2.4.1/target-i386/seg_helper.c:849

#3  0x00002ab57734da93 in do_interrupt_all (cpu=0x2ab577822470, intno=-1,

    is_int=0, error_code=0, next_eip=0, is_hw=1)

    at /home/vagrant/qemu-2.4.1/target-i386/seg_helper.c:1224

#4  0x00002ab57734dc96 in do_interrupt_x86_hardirq (env=0x2ab57782a6c0,

    intno=-1, is_hw=1)

    at /home/vagrant/qemu-2.4.1/target-i386/seg_helper.c:1284

#5  0x00002ab57734df62 in x86_cpu_exec_interrupt (cs=0x2ab577822470,

    interrupt_request=2)

    at /home/vagrant/qemu-2.4.1/target-i386/seg_helper.c:1331

#6  0x00002ab577220675 in cpu_x86_exec (cpu=0x2ab577822470)

    at /home/vagrant/qemu-2.4.1/cpu-exec.c:465

#7  0x00002ab5772503d0 in tcg_cpu_exec (cpu=0x2ab577822470)

    at /home/vagrant/qemu-2.4.1/cpus.c:1402

#8  0x00002ab5772504d6 in tcg_exec_all ()

    at /home/vagrant/qemu-2.4.1/cpus.c:1434

#9  0x00002ab57724f7bf in qemu_tcg_cpu_thread_fn (arg=0x2ab577822470)

    at /home/vagrant/qemu-2.4.1/cpus.c:1068

#10 0x00002ab57945c184 in start_thread (arg=0x2ab57df68700)

    at pthread_create.c:312

#11 0x00002ab57976c37d in clone ()

    at ../sysdeps/unix/sysv/linux/x86_64/clone.S:111

なんだかんだ頑張った結果、得られたバックトレースがこちら。

スタックフレームを辿っていけば、どこかにqemu側が意図的に一般保護例外を発生させている状況があるはずです。

パッと見た感じ、割り込み周りの処理をしている中で一般保護例外を発生させたようですね。これが割り込み周りの処理ではなく、例えばデバイスのエミュレーション処理とかであれば、途中までデバイス関連の関数だった所からexceptionとかいう名前がついた関数を呼び出すようになるので、原因部分がはっきりするのですが、これでは具体的にどこで一般保護例外が生じたのかわかりません。

そこで、intno=13という例外番号がどこででてくるのか、調べてみましょう。とりあえず、フレーム#1ではexception_index=13になっていますね。であれば、フレーム#2、つまりqemu-2.4.1/target-i386/seg_helper.cのdo_interrupt64()実行中に一般保護例外が発生している、と考えるのが妥当です。で、呼び出し元を見てみると、たしかに一般保護例外を呼び出している、と。

    if (intno * 16 + 15 > dt->limit) {

        raise_exception_err(env, EXCP0D_GPF, intno * 16 + 2);

    }

このif文、なかなかふざけていて、調べてみるとintnoが-1でdt->limitが255なんです。-1 * 16 + 15 > 255はどう考えてもfalseなわけですよ。仕方ないからdt->limitの型を調べると、unsignedっていうね。これ、意図してやってるんでしょうけど、どうなのよ、って思ってしまいましたとさ。

さて、intnoが-1である事が一般保護例外の発生要因である事がわかりました。であれば、次はどこでintnoが-1にされたのかをバックトレースで辿ります。バックトレースを睨むと、どうやらqemu-2.4.1/target-i386/seg_helper.cのx86_cpu_exec_interrupt()の中っぽいなぁ、という事が分かるので、ここのコードもまた表示してみましょう。

            int intno;

            cpu_svm_check_intercept_param(env, SVM_EXIT_INTR, 0);

            cs->interrupt_request &= ~(CPU_INTERRUPT_HARD |

                                       CPU_INTERRUPT_VIRQ);

            intno = cpu_get_pic_interrupt(env);

            qemu_log_mask(CPU_LOG_TB_IN_ASM,

                          "Servicing hardware INT=0x%02x\n", intno);

            do_interrupt_x86_hardirq(env, intno, 1);

            /* ensure that no TB jump will be modified as                                                                                                                                                   

               the program flow was changed */

            ret = true;

do_interrupt_x86_hardirqの引数にintnoを渡していますが、このintnoはcpu_get_pic_interrupt()の実行結果であることが分かります。ならば次はcpu_get_pic_interrupt()の中を見てみるしかないですね。qemu-2.4.1/hw/i386/pc.cの中を読みます。

/* IRQ handling */

int cpu_get_pic_interrupt(CPUX86State *env)

{

    X86CPU *cpu = x86_env_get_cpu(env);

    int intno;

    intno = apic_get_interrupt(cpu->apic_state);

    if (intno >= 0) {

        return intno;

    }

    /* read the irq from the PIC */

    if (!apic_accept_pic_intr(cpu->apic_state)) {

        return -1;

    }

    intno = pic_read_irq(isa_pic);

    return intno;

}

ここで-1を返すケースとなると、apic_accept_pic_intr()が0を返したか、pic_read_irq()が-1を返したか、のどちらかになります。とりあえず両方にブレークポイントを仕掛けた結果、前者である事が分かりました。次はqemu-2.4.1/hw/intc/apic.cにあるapic_accept_pic_intr()の中を読みましょう。

int apic_accept_pic_intr(DeviceState *dev)

{

    APICCommonState *s = APIC_COMMON(dev);

    uint32_t lvt0;

    if (!s)

        return -1;

    lvt0 = s->lvt[APIC_LVT_LINT0];

    if ((s->apicbase & MSR_IA32_APICBASE_ENABLE) == 0 ||

        (lvt0 & APIC_LVT_MASKED) == 0)

        return 1;

    return 0;

}

0を返してるケースなので、(s->apicbase & MSR_IA32_APICBASE_ENABLE) == 0 || (lvt0 & APIC_LVT_MASKED) == 0 がfalseになっている、という事です。まあ有り体に言えば、APICが有効でかつLVT0がUnmaskedになっている、という状況です。自分が書いているカーネルのAPIC初期化コードを見直してみると、APICを有効にしてからLVT0のMaskedフラグを立てているので、たしかにそういう状況は起こりえます。そこで、LVT0のMaskedフラグを立ててからAPICを有効にした所、何回再起動しても不可解な#GPは生じなくなりました。めでたしめでたし。

とは書いたものの、いくつか懸念点はありまして。

まず何回再起動しても#GPが生じなくなった、と書いたものの、バグ自体が不定期に起きるので(途中から起動中にキーボードを叩きまくると発生しやすくなる、という事を見つけたのですが)もしかするとバグはまだ残っているのかもしれません。

次に、そもそもstiはAPIC初期化コードとは全く関係ない(APIC初期化のはるか後でsti命令を実行する)のですが、なんでstiを実行したタイミングで例外が飛ぶのかは謎です。キューにでも溜め込まれていたんですかね。

最後に

デバッグログを垂れ流すような記事になってしまいました。

まあ実際はもっと右往左往して無関係な所を調べたりしているので、そこら辺が綺麗になっている分、多少有意義な内容になっている事でしょう。

とりあえず何が書きたかったかというと、QEMU上で変な例外とか、想定外の事態とか起きても恐れる必要はないよ!って事です。

5年前の僕であれば、意味不明な#GPが飛んでくると完全にお手上げで、カーネルを最初から作り直すという選択肢しか取れなかったのですが、(そういえば、QEMUをデバッグする事でカーネル側のバグを治す、というやり方をhigeponさんから教わったのが丁度5年前)今ではこの程度であれば、本腰を入れて調査を始めてから数時間で原因特定からのバグ修正までできるようになりました。成長を感じますね。

皆さんも、QEMUのソースコードって難しそうで読めなさそう、とか思わずにどんどんデバッグに活用しましょう。

ディスクイメージをネットワークブートする

OSのデバッグごときでUSBメモリを挿したり抜いたりしたくない!という人のために。

 

これを使いましょう。

サンワサプライ USB3.0切替器(2回路) SW-US32

https://www.amazon.co.jp/dp/B00A2KXSRK/ref=cm_sw_r_tw_dp_x_wgAtyb1GSAJ1C

(wordpressってplugin入れないとamazonのリンク展開できないのね)

 

もちろん冗談なのですが、それを抜きにして、これは大変便利です。ポチッとボタンを押すだけで、USBメモリの接続先をビルド環境とテスト環境で切り替えられるので。デバッグがめっちゃ捗ってストレスフリーです。
まあでも人間、このボタンを押すのでさえだるくなるんですよ。ボタンを押し忘れて、HDDから起動しちゃう事もありますし。

 

というわけで、(たぶん)最強のブート環境である、ネットワークブートを作ります。

今回解説する方法は、ディスクイメージをネットワークからブートさせてしまうので、自分で書いてるToy OSのデバッグに限らず、linuxがインストールされたHDDのダンプファイルだろうと、OSのインストールディスクだろうと、何でもネットワーク越しに起動できます。(これは自作OS Advent Calendar 2016の記事ですが、OSのデバッグやる人でなくてももしかしたら役立つ・・・かもしれない)

ちなみにlinuxをネットワーク経由で起動するのなら、もう少しだけシンプルにできます。まあその話は今回はしませんが。

 

※注意

最初に軽く書きましたが、今回想定しているのは、しがない個人がより手軽にOSのデバッグをするためにネットワークブートする、という状況です。何十台とあるマシンに一気にプロビジョニング当てる場合は、素直にPXEサーバーを立てて、DHCPからPXEサーバーを教えてあげれば良いと思います。まあたぶんそういう事をしたい人はこのブログ見てないよね。

作業環境はUbuntu 14.04を想定しています。他の環境の場合は適宜読み替えてもらえれば。

あと、UEFIについては試していません。テスト環境はLegacy BIOSから立ち上げるものとしてください。

 

大雑把な解説

iPXEmemdiskを使います。

iPXEは拡張版PXE、という感じのファームウェアで、PXEの機能相当の提供に加え、HTTPや無線LANなどもサポートしています。(詳しくは後述)

memdiskは、ディスクイメージを起動してくれるブータブルバイナリで、iPXEから直接ディスクイメージを起動する事ができないため使用しています。

システム構成はこんな感じです。

netboot

(クリックして拡大)

今回のポイントは、iPXEを使うお陰で面倒な環境構築(物理)をする必要がない、という所です。

PXEを使う場合は、PXEサーバーを立ててDHCPサーバーからPXEサーバーを教えてあげなければならない(参考)ので、自前のDHCPサーバーを用意してあげる必要があり、あまりお手軽ではありません。iPXE であれば、ビルドマシン上でHTTPサーバーを走らせ、イメージを配布するだけで手軽に起動できるようになるため、便利です。ただ、その代わりiPXEイメージの方にビルド環境のIPアドレスを書き込むため、ビルド環境のIPが変わるたびにiPXEイメージの焼き直しが必要になります。まあIPが変わるのなんてPCを再起動したときくらいなので、デバッグを始める前にメディアに焼けば良いわけですし、それすら面倒なら適当なファイルサーバーを用意してあげて、そこにディスクイメージをアップロードする形にすれば、iPXEイメージを焼き直す必要はなくなります。あとはビルド環境を固定IPにしてしまう、なんてのもアリですね。

もう一つiPXEの良い点は、イメージの配信をHTTPサーバーでできる所です。PXEだとTFTPだけど、HTTPサーバーの方が楽。もちろん、プロトコルとしてはTFTPの方が軽量なので、TFTPサーバーを使う方法についてもあとで軽く触れておきます。

 

iPXEを使ってみる

まずは手軽にiPXEを使って起動してみましょう。

最初にビルド環境から設定します。

ビルド環境で用意すべきものは、適当なHTTPサーバー、OSがインストールされているディスクイメージ、そしてmemdiskです。

memdiskはgzip圧縮されたディスクイメージを起動する事ができるので、ディスクイメージは圧縮しておきましょう。

gzip disk

これでdisk.gzというファイルができます。

 

HTTPサーバーはApacheやnginxを使っても良いのですが、以下のようにpythonで起動してしまうのが一番楽だと思います。

$ python -m SimpleHTTPServer 8080

ぶっちゃけ、HTTPサーバーが起動できればなんでもいいので、お好きなのをどうぞ。

ワンライナーWebサーバを集めてみた by @suda_hiroshi on @Qiita

今回は8080番ポートで立ち上げてますが、それもお好みで大丈夫です。ポート変える場合は、以後の内容を適宜読み替えてください。

立ち上げたHTTPサーバーのルートディレクトリに、作成したディスクイメージとmemdiskを起きます。(memdiskはここからsyslinuxをダウンロードし、展開してあげるとmemdiskというファイルがあるはずなので、それを使ってください)

 

HTTPサーバーで開けたポートが外部から見えるよう、ファイアーウォールを設定します。

# ufw allow 8080

ネットワーク内の他のマシンから、http://<ビルド環境のIPアドレス>:8080/memdiskとhttp://<ビルド環境のIPアドレス>:8080/disk.gzにアクセスできればOKです。

 

次はiPXEの起動メディアを準備します。

まずは手軽にやるために、ここからiPXEイメージをダウンロードしてきます。iPXE起動用のメディアとしてUSBメモリを使う場合はipxe.usbを、CDを使う場合はipxe.isoを使いましょう。

ipxe.usbはディスクイメージなので、ddコマンドで書き込んでください。ISOファイルは煮るなり焼くなりお好きにどうぞ

# dd if=ipxe.usb of=/dev/sdx(対象のデバイス)

テスト環境はとりあえず有線LANを繋いでおきます。無線LANの話はまたあとで。

メディアが焼けたら、そこからブートします。

 

iPXEが起動し始めたら、Ctrl+Bを連打してコンソールに入りましょう。Ctrl+Bを受け付けるタイミングが一瞬あった後、dhcpの初期化を行い、そしてまた一瞬Ctrl+Bを受け付けるタイミングがあるので、そのどちらかでコンソールに飛べればOKです。

p_20161212_065008

(光沢液晶に顔が映らないようにするの、大変)

上のようにiPXEのコンソールが出たら、以下のコマンドを打ち込んで行きます。

iPXE> dhcp

iPXE> kernel http://<ビルド環境のIPアドレス>:8080/memdisk

iPXE> initrd http://<ビルド環境のIPアドレス>:8080/disk.gz

iPXE> boot

ディスクイメージがきちんと構築されていれば、これで起動するはずです。disk.gzのサイズが大きい場合、ダウンロードに少し時間が掛かるかもしれません。

今回テストしたディスクイメージは、grub2からmemtest86+や自作のOSカーネルを起動する、というものでしたが、問題なく起動できました。

 

iPXEイメージのconfigure

起動のたびにキーボードからコマンドを打ち込むのはダルいので、上述のコマンドをスクリプト化して自動的に読み込ませるiPXEイメージを作ります。

今度はiPXEのソースコードをgitからダウンロードします。依存ライブラリがあるので、インストールしておきます。

git clone git://git.ipxe.org/ipxe.git

$ aptitude install build-essential binutils-dev zlib1g-dev libiberty-dev

 

次に以下のようなconfigファイル(load.cfg)を作ります。

#!ipxe

dhcp

kernel http://<ビルドマシンのIPアドレス>:8080/memdisk

initrd http://<ビルドマシンのIPアドレス>:8080/disk.gz

boot

 

テスト環境が無線LANの場合は、以下を2行目に記述しましょう(未検証です)

set net0/ssid WIFISSID
set net0/key WIFIKEY

無線LANはデバドラの都合で動かないケースもあり得るので、テスト環境は有線LANを使う事をオススメします。(ビルド環境は無線か有線かは関係なし)

configファイルが書けたら、iPXEをビルドします。

 $ cd ipxe/src

$ make bin-x86_64-pcbios/ipxe.usb EMBED=../../load.cfg

ISOファイルを作る場合はipxe.usbをipxe.isoに変えてください。メディアに焼く部分は先程の通り。

 

最後に

今回、iPXEとHTTPサーバーを使ってネットブート環境を構築しましたが、先程も書いたとおり、イメージ配信サーバーのIPアドレスが変わるたびにiPXEイメージの再構築が必要になります。OSのデバッグをしている上では、デバッグを始めようと思ったときに毎回イメージ作成作業をすれば良い(スクリプトで自動化すれば、コマンド一つでメディアまで焼けるので)のですが、もしそれすらも面倒になった場合は、サーバー(ローカル、グローバル問わず)を使うか、ビルドマシンのIPを固定するかして、IPアドレスが変わらないようにしてあげれば良いと思います。

僕はデバッグを始めようと思ったときに一回USBメモリに焼く作業くらいは許容範囲かなー、って思ってます。今のところは。たぶんUSBメモリを焼くよりも、テスト環境の起動のために電源ボタンないしリセットボタンを押す事の方が苦になると思うので、先にそっちをなんとかしなきゃいけなさそうです。WoLとか使えばいけそうですね。make叩いたら一発で隣にあるマシンが勝手に起動して、OSの起動までできるようになれば、デバッグがもの凄く捗りそうですよね。

 

おまけ(TFTPサーバーを立てる)

HTTPサーバーは環境構築が楽なのですが、重いです。(気になる程ではないのだけども)

というわけで、TFTPサーバーをビルド環境に立ててみましょう。PyPXEが良さげなので、今回はこれを使いました。

 git clone https://github.com/psychomario/PyPXE

PyPXE/netboot/がルートディレクトリとなります。memdiskはすでにあるので、ここにディスクイメージをおいてあげればよろし。

TFTPはUDPの69番ポートを使用するので、ファイヤーウォールを開けておきます。起動は以下のコマンドで。

# python -m pypxe.server

 

iPXEのconfigは以下の通りです。

#!ipxe

set next-server <ビルド環境のIPアドレス>

kernel memdisk

initrd disk.gz

dhcp

autoboot

iPXEメディアを焼いて、起動しましょう。

 

おまけ(grubを使っている場合)

今回はmemdiskを挟んでディスクイメージを丸々起動させましたが、ぶっちゃけ、PXEブートできるカーネルバイナリを作れるなら、直接iPXEから起動しちゃえば良いと思います。また、grub2から起動している場合はgrub2をPXEブートできるので、それを作ってあげるのが良いかと。この方法はたぶんTFTPプロトコルでしかできないと思います。調べた限りでは。

これだとカーネルバイナリのみをダウンロードしてくるので、ディスクイメージまるごとよりかは軽くなります。まあディスクイメージは圧縮してるので、たぶんそんなに変わらないけども。

 

まずビルド環境で以下のコマンドを叩きます。

$ grub-mknetdir -d /usr/lib/grub/i386-pc/ –net-directory=(TFTPサーバーのルートディレクトリ)

ルートディレクトリ以下にboot/grub/i386-pc/core.0というファイルができるので、iPXEの設定ファイルに以下のように記述します。

#!ipxe

dhcp

set filename /boot/grub/i386-pc/core.0

set next-server <ビルド環境のIPアドレス>

dhcp

autoboot

/boot/grub/grub.cfgの先頭に以下を追記して、後は普通のgrubのconfigを書きましょう。

set root=(pxe)

あとはイメージをビルドして起動するだけですね。

livaの雑記帳(2016/12)

12/31

  • 逆アセした時にファイルオフセットを出すには-Fをつけると良い
    • objdump -F -d build/testmodule.elf

    • みたいな
  • ノートPCの備え付けキーボードは今でもi8042で動いてるっぽい
    • ThinkPadだけかもしれないが
  • LinuxのvirtualboxでUSBメモリを認識しない場合の解決策
    • これ
    • 以前も調べたけども忘れてたので

12/30

  • 結構以前から使ってたコマンドだけど、ここに載せてなかったので、記録にのせておく
    • gcc -I./ -D_KERNEL -MG -M sys/hoge.h

    • freebsd/sysの中で打つとヘッダーの依存関係が出る幸せな呪文

12/27

  • あとで読む>ELFの動的リンク(1)

12/26

  • ネットワーク方面も設備増強している
    • p_20161226_111820
    • めっちゃうるさいんだが・・・・

12/25

  • せっかくブログという物がありながら、あまり活用してこなかったと反省中
    • 今後もコンスタントに月に2記事くらいは書きたいなぁ。クオリティは落とさずに
  • 自分へのクリスマスプレゼント
    • p_20161225_164518
    • OS開発とは関係ない

12/24

12/23

  • 現実逃避のために記事を書きまくってる

12/22

  • バグをひたすら修正してる
    • カーネルタスクが上手く実行されないケースのバグ修正
      • ただのロジックミス
      • 赤いマシンのNICのLinkが上がらない問題も解決
        • 通信はなぜかまだできないので、スイッチで切り離した(ルーターがネットワーク上にいない)テストを早急に行う
    • 時折起動時に#GPが発生する問題の修正
      • ハードウェア側のエラー、カーネルのコーディングミス
      • ついでに記事も書いた

12/20

  • pull-request元のコードってどうやって手元でcheckoutすればいいんだろう、というのを悩んでいたら、ぴったしな物を見つけた

12/17

  • hikaliumさんがelfを実装しているのが楽しそうで、僕も触ってみたいと思った結果、進捗を産んでしまった
    • ほら、いろいろ目を向けたくない時ってあるじゃん

12/16

  • アンパンマン!新しいマシンよ!
    • p_20161216_160901
    • Phiちゃん(青)も、仲間(赤)ができて喜んでるわね!
    • 新しいマシンが来たのは良いのだけど、僕の机がどんどんマシンによって占拠されていく。。。。
  • ifconfigってMacとlinuxだと結構出力が違うのね
    • というわけでネットワークブート絡みのコマンドをいろいろ修正した
  • qemu上でAHCIからブートすると遅い問題、とりあえずAHCI上から起動しないようにした
    • しばらくはAHCIを触る事もあるまい
  • 開発機を変えて、どうやってfreebsdのソースコードをダウンロードするか、悩んでしまった
    • たぶんgitからダウンロードするのが早いのだけど、gitのmasterからダウンロードすると、ダウンロードタイミングによって別のコードが手に入る可能性がある
    • release/11.0.1のbranchからzipをダウンロードするのが一番良さそう

12/15

  • 以前qemuの起動が遅いという苦情が入っていたので、調査
    • ついでにCPUが荒ぶってるのでは疑惑も
    • grubから呼び出されるエントリポイント直後にhltを仕掛ける
      • 結果、普通に遅い
      • つまり、grubの問題
    • grubから呼び出されるまで10秒くらい待たされる
      • この間CPUが荒ぶってる
    • grubの設定をいろいろ戻してみる
      • 結果、ディスクをAHCIにしているのが問題と判明
      • う〜ん、設定を戻すべきか、戻さざるべきか

12/14

  • 寸劇
    • アンパンマン!新しいマシンよ!p_20161214_024033
    • でもドラえもん、このマシンUSBメモリ挿せないからOSのデバッグができないよ!p_20161214_024046
    • 大丈夫だ。問題ない
  • ここ数日悩んでいた問題についての記録
    • 症状:一部実機において、iPXEからgrub経由でRaph Kernelが起動しない
      • CPUリセットが掛かる
      • 起動する実機もある
    • grubからmemtestは起動する
      • multiboot kernelの問題かと疑う
        • これはフェイク
    • multiboot kernelがcrashするのであればgrubとiPXEの相性の問題かと思ってgrubを2.02~beta3にアップデートする
      • iPXEではなく、ディスクからのブートもcrashするようになる
    • このタイミングでは新しいgrubでクラッシュする問題とiPXEでクラッシュする問題を切り分けて考えていた
      • これもフェイク
    • とりあえずgrub2.02~beta3環境でqemuデバッグ
    • 落ちる場所はCPUID命令の存在判定の所。具体的にはEFLAGSのID bitが立たない
    • この直前で止めるようにしたらiPXEでもCPUリセットしなくなる
      • 実は2つの問題が繋がってるのではないか疑惑が浮上
    • EFLAGS==2なのが不明で、これまたgrubを疑う
      • 2.02~beta3で変な初期化をするのではないかという予想
        • これもフェイク
    • なんとなく、 grubからカーネルが呼び出された直後で止めるようにしてみる
      • なんかEFLAGSの値が正常
    • このあたりでスタックの値がなんか変だなと思い始める
      • とりあえず検証は後回し
    • どこでEFLAGSの値が不正になってるのかを調べる
      • どうやらスタックを設定する(espに値をセット)あたりっぽい
    • デバッグしていると、スタックをセットした後あたりで止める場合、止まるまでにCPUが荒ぶってるっぽい事がわかる
      • プロテクトモードになった後、リアルモードになってたりとか
    • スタックを設定する部分を眺めていると、ミスに気づく
      • add 0x1000, %espでスタックを4Kずらしているコード
      • add $0x1000, %espが正しい。上のコードでは0x1000というメモリから値を取ってきてespに足す事になってしまう
    • コード修正。すべてが上手く動くようになる
      • 2.02~beta3環境もiPXEブートも
    • すべての謎は解明できないものの、とりあえず終戦
      • スタックずらした直後で止めてるのに、なんでCPUが荒ぶるの
        • espを変にずらして、そのespを元にstackを読みにいって落ちたならまだしも、0x1000を読むだけで荒ぶるのは謎
        • CPUが荒ぶってるのは別の問題では説
          • CPUは荒ぶっていなかった→12/15
      • grubをupdateしてなぜ落ちた?
        • たぶん0x1000に別の値を書き込むようになったんだろう
      • iPXEブートでなぜ落ちた?
        • iPXEが別の値を書き込んだんだろう
    • 未だにこんなやばいバグが残ってたのかと思うと、割と背筋が凍った
  • CPUID命令によるモデル識別について
    • Intel技術資料Vol2 のCPUID命令の項目、INPUT EAX = 01H: Returns Model, Family, Stepping Informationに、Figure 3-6. Version Information Returned by CPUID in EAXがある
    • その下にあるアルゴリズムを用いて、DisplayDamilyとDisplayModelを計算する
    • これとIntel技術資料Vol3のCHAPTER 35 MODEL-SPECIFIC REGISTERS (MSRS)内、

      Table 35-1. CPUID Signature Values of DisplayFamily_DisplayModelを参照

12/13

12/10

  • メモ。そのうちカーネルのメモリの使い方について解説するスライドを作ろう
  • hikaliumさんがelf実装を頑張ってくれている。とりあえず動く感じの物が出来上がってきていて、素晴らしい(もちろん、まだまだ道のりは遠いけど)
    • というわけで、凄く期待しています
    • elf対応はLinux互換という目標への大きな一歩になるので、割と大事なのです
  • 最近ブログに表立って書ける事がないので、記事を書いてみた→gdbによるqemuデバッグ

12/07

  • デバイスドライバを書いていると、よく分からなくなるので、linuxでlspci等を叩きたくなる
    • というわけでqemu上で動くlinuxイメージを探す
    • こんなのがあるが、流石にカーネルだけだと使い勝手が悪い
    • 仕方がないのでubuntuのイメージを探す
    • 結局pre-buildイメージは使わず、LiveCDからインストール
  • 久々にVagrantでの環境構築を行った。途中で落ちた
    • 落ちたらVagrantの旨味半減である。大いに反省
    • 修正ついでにVagrant環境の高速化を図る。参考

12/05

  • Xeon Phiの方はハードウェア構成を元にアルゴリズムを弄ると順調に性能改善ができる
    • 楽しい
    • 一応書いておくと(これくらいなら書いてもいいでしょ)OS屋なので、並列計算アルゴリズムの高速化をやっているわけではない。まあ最終的にはそこに行き着く必要はあるのだけども

12/04

  • 参考コードがあるにも関わらず、怠惰が祟って進捗が生まれない
    • ようやくセーブポイント(?)まで来たけど、QEMUですら動かなかった
    • やっぱ一筋縄ではいきませんよねー