どうも、PiBVTです。xv6後編では、entry.Sから呼び出されたmain関数で、BIOS依存部分をUEFI用に書き換えたところを解説していきます。
main関数の中身
main関数の中身はこのようになっています。
gista76d564e81beff0f6c7008544829e720
具体的には、mpinit()は全面的にBIOSに依存し、consoleinit()は一部BIOSに依存しているためUEFI用に書き換える必要が有ります。
ちなみにどうやって書き換えが必要な部分をチェックしたかと言うと、一行ごとにHLT命令をはさんで暴走しないことを確認しながら割り出し作業をしていました。
なぜHLT命令を使っていたかというと、GDBがバグって使えなかったためです。このことについては後日記事にしたいと思います。
mpinit()の役割
xv6のmpinit()では、BDA(BIOS Data Area)やEBDA(Extended BIOS Data Area)を探してその中にあるMP Floating Point Structureを読み込んでいます。 mpinit()はxv6のマルチプロセッサ対応の要であり必要不可欠なものなのですが、UEFIにはMP Floating Point Structureはありません。
そのため、MP Floating Point Structureの代わりとなるものでmpinit()を書き換えなければなりません。
mpinit()は以下のようになっています。
gistfa0c45ba07143838c5fde7b08bb118ce
ざっと見る限りでは、 LAPIC Address と IOAPIC Address,そして各プロセッサのAPIC IDを取得して設定すれば良さそうです。 UEFIでは同等な情報をMADT(Multiple APIC Description Table)から取得できるので、MADTを利用して書き直すことにしました。
MADTを取得する方法
UEFIでMADTを取得する場合、以下のような手順でMADTのアドレスを取得します。
RSDPアドレスの取得はUEFIブートローダー側でしか行うことができないためUEFIブートローダーで取得します。
64bitのOSだとRSDPのアドレスさえあれば順番にたどることでMADTを取得することが出来ますが、 32bitOSであるxv6ではRSDP,XSDT,MADTのアドレスが32bitの範囲を超えた場所に存在する可能性があり、実際にメモリーを増やすと32bitの範囲を超えたところに配置されてしまいます。 そのため、今回はUEFIローダー側でMADTを32bitアドレス空間に収まる場所にコピーして、それをxv6が利用する形で問題を回避しました。
UEFIブートローダーのMADT処理部分
UEFIローダーのMADT処理部分は以下のようになっています。
gist0f629aafac3cb9652b72d365d5fdfe75
RSDP等の各種構造体です。
gist89783e2163a30f264e4a0eec50f660d5
1~3行目でedk2の関数を利用して一気にRSDPまで取得し、構造体に入れています。
12~15行目で、xv6側に引き渡すMADTを格納するためのメモリー領域を確保しています。
16行目で、RSDPの構造体からXSDTのアドレスを取り出し、構造体に入れています。 XSDTはヘッダー部のSDTとその他のテーブルへのポインタの配列pointer_othersに分かれています。 MADTは、そのpointer_othersのテーブルを全て探索することで見つけることが出来ます。 pointer_othersのエントリー数は
(XSDTのサイズ - SDTヘッダーのサイズ) / (ポインタのサイズ = 8)
によって求めることが出来るため、エントリー数を求め、for文で各テーブルのSignatureを調べます。
21~36行目で、MADTを見つけboot_paramにコピーしています。各テーブルはSDTをヘッダーとして持ち、SDTのSignatureを調べることで種類を区別することが出来ます。今回はMADTなので、SignatureがAPICであるテーブルを探し、MADT全体をboot_param内にコピーしています。
xv6のMADT処理部分
mpinit()をUEFI用に書き直したmpinit_uefi()は以下のようになっています。
gist91a2acccc14a2b710f3e27773b5d0f7e
構造体は以下のようになっています
gist27a35478dc6b0b2c99932edef99ba60b
MADTはヘッダーのSDTの後から
- LAPIC(Entry Type 0)
- IOAPIC(Entry Type 1)
- Interrupt Source Override(Entry Type 2)
- Non-maskable Interrupts(Entry Type 4)
- LAPIC Address Override(Entry Type 5)
の5種類のテーブルが続きます。
各テーブルを参照するには、順番に先頭からポインタを移動させることでできます。
16~19行目で、UEFIブートローダー側から持ってきたMADTのアドレスを元に構造体を取得しています。さらに、テーブルを探索するためのポインタiを用意しています。
25行目で、SDTヘッダーに含まれるLAPIC Addressを取得し、lapicに設定しています。
28~60行目で、テーブル群全体を探索しています。Entry Typeを読み込むことでそれぞれのテーブルの種類を区別し、LAPICやIOAPICならばmpinit()と同様にcpusやioapicidを設定しています。それ以外のテーブルは無視し、ポインタiをそのテーブルの分だけ進めています。
以上でmpinit()の代替となるmpinit_uefi()の完成です。もとのmpinit()に比べてかなりコンパクトになりました。
consoleinit()の改修
mpinit()を書き換えたことでBIOS依存部分は殆どなくなったのですが、残るBIOS依存部分としてテキストモードを利用したコンソールがあります。UEFIではテキストモードを利用できないため、このままではレジスタやメモリーに対して不正なアクセスをしてしまい、強制リセットがかかってしまいます。
そこで、xv6ではテキストモードでの画面出力と同時にUART経由での出力も行っているため、画面表示は切り捨て、UART経由で操作をすることにしました。
改変した内容としては以下のとおりです。バッサリCGA系を殺しています。
以上で、BIOS依存部分の改修は全て終了し、xv6が起動できるようになります。 セキュリティキャンプでは3日目(集中講義2日目)の昼にここまでの改修を終え、xv6が起動するようになりました。
64bit UEFIから32bit xv6を起動することに成功しました!#seccamp #seccamp2018 pic.twitter.com/GhWgjGa30q
— PiBVT (@PiBVT) August 17, 2018
まとめ
xv6のソースコードの約1割程度を書き換える程度でUEFIから起動することが出来ました。が、64bitから32bitへとCPUのモードを変更しているためPCIなどの周辺機器のアドレスは64bit空間のままであり、実機で動かすことはほぼ不可能といったところです。当初の目標である簡単なWeb Serverの実装を終えたらxv6を完全64bit化はしたいと考えています。
参考文献
ソースコード等
xv6本体