ページングをONにした後に落ちる理由

そっかー。そりゃ落ちるわ。
リンク時に 0x40000000 用にコンパイルしてるのに、Grubから出た直後は0x100000にバイナリが読み込まれているんだから。

-----         0x100000         ----- <- EIP # binary is loaded here by grub boot loader.
   binary compiled for 0x40000000
---- size of binary + 0x100000 ----  # end of binary

せっかくなので、上記のようなメモリマップのとき、CPUの挙動がどうなるかシミュレートしてみる。
まず、とんだ先のバイナリ中に、以下のようなコードがあると仮定する。

 movl $page_dir_entry,%eax
 movl %eax,cr3
 movl cr0,%eax
 orl  0x80000000,%eax
 movl %eax,cr0

 .align 4096
page_table_entry:
 ...ページテーブルのことが色々書いてある...

page_dir_entry:
 ...ページディレクトリのことが色々書いてある。...

このときの様子を時系列に追ってみよう。

eaxレジスタには0x40000000+offset0x40100000+offset番地が代入される。だけどそこには uninitialized data しかない。これらのデータは、Qemu上ではゼロクリアされている。つまり、page_dir_entryの中身が全て0になっている状態である。この状態でページングONにするとページテーブルのベースアドレスは0x00000番地に設定される。すると、何が起きるか。

  1. ページフォールト発生
  2. ページディレクトリが回路に読まれる
  3. ページディレクトリを解釈して、ページテーブルの在処をつきとめる。それはなんと0x00000番地。
  4. 0クリアされているので、もちろんページディレクトリのアクセスビットも0。結果として、ページフォールトがページテーブルに対して発生する。もちろん行き先は0x00000番地。
  5. とってくる先も、cr3レジスタに入っている0x00000番地さ!
  6. ↑繰り返し(っていっても、triple faultしてクラッシュする)

ちなみに、Qemuのはこのエラーの原因を「triple fault」として吐き出してくれる。なんと優秀な。


なぜ原因が分かったかというと、Qemu Monitor でコントロールレジスタの値とインストラクションポインタの値をダンプしたから(w
ユーザランドのプログラムを書いていると、OSが専用に吐き出されたバイナリを、勝手に正しい位置にロケートしてくれるけれど、それに慣れてると「どこの番地用に吐き出されたバイナリか」なんて意識しないもんなぁ。OSがいない状態でプログラミングしてるんだから、それを意識してプログラムせにゃあかん。<-結論

というわけで、0x100000から0x40000000までプログラム本体をコピーするようなコードが必要ってわかっただけでも、本日の収穫にしときます。

追記と修正と注意

  1. elfバイナリとして0x100000に読み込まれているので、シンボルは0x40100000に配置される。
  2. 0x100000から0x40100000へプログラム本体をコピーするとき、物理アドレス上で行ってはならない(マシンにそこまでメモリが載っていない可能性があるため)。そのため、物理アドレス下で本体のコピーを行う代わりに、シンボルの配置場所から差分を減らす、というテクニックを使用するのが常套手段のようだ( FreeBSDでいうところの #define R(foo) (foor-VIRTADDRBASE) )。
  3. 当たり前だが、x86における仮想記憶の根っこにはページフォールトハンドラが絡んでいるので、ページングをONにする前にInterrupt Descripter Table を設定して割り込みを ON にしとくこと。