外部記憶装置

脳みそ小さすぎるからメモしとく

64bitUEFIでxv6を動かす方法(ブートローダー編)

セキュリティキャンプ2018で32bitのxv6を64bitUEFIから起動したPiBVTです。(長い)

セキュリティキャンプの参戦記録ではなく、どのようにしてxv6を64bitUEFIから起動したのか書いていきたいと思います。 ブートローダー編とxv6編の2本立てで、今日はブートローダー編です。

x86x86-64(IA32e)について

本記事では、x86の64bit拡張、いわゆるIA32eをx86-64と呼称します。

x86

x86では、以下の動作モードが存在します。

  • Real mode
  • Protected mode
  • Virtual 8086 mode

一般的にBIOS起動時はReal modeで動作しているため、OSは起動シーケンス内でGDTやIDT等を設定してProtected modeに移行します。
Protected modeに移行することでOSにとって必要不可欠なページングが利用できるようになるので、適宜ページテーブルを設定します。 今回はココらへんの機構にとてつもなく悩まされました。

x86-64

x86-64では以上のx86の動作モードに加えて次の動作モードが存在します。

  • Long mode
  • Compatibility mode

Long modeはx86-64のバイナリのみしか走らせることができません。そのため、x86のバイナリを走らせるためのモードとして Compatibility mode があります。
Compatibility modeではx86と完全互換なモードとなり、x86のバイナリがそのまま走ります。しかし、32bitOSを走らせるための手段としては一般的では無いようです。

64bitUEFIについて

x86-64が搭載されたPC、というか、Atomタブレットなどのごく一部を除いてUEFI搭載のPCのほとんどが64bitUEFIを搭載しています。
64bitUEFIではUEFI起動時、すでにCPUはLong modeで動作しています。通常のLinuxWindowsの最新版はすでに64bitに対応しているため問題ないのですが、xv6などのレガシーなOSはUEFIからは起動させることが出来ません。

そのため、UEFI上で動作するブートローダーでCPUの設定を変える必要があります。今回はこのブートローダーを書きました。

Long mode から Protected mode へ移行する手順

Long mode から Protected mode へ移行する手順は、Intel SDM vol3に記載されています。 f:id:PiBVT:20180915003533p:plain Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3 (3A, 3B, 3C & 3D): System Programming Guide より

この手順通りに一言一句間違うことなく設定すれば、Long mode から Protected mode に移行することが出来ます。

ブートローダー側では、コンパイラの関係上x86-64のバイナリしか吐けないので、Compatibility modeへの移行までを行います。

64bitUEFIからxv6が起動するまでの手順(ブートローダー編)

ブートローダーはこんな感じでxv6のカーネルを起動します。

f:id:PiBVT:20180915010402p:plain

xv6のカーネルをロード

UEFIの便利な点の1つとして、HDD,USBメモリ上のFATファイルシステムを認識できることが挙げられます。
今回はこの利点を利用して、簡単にカーネルをメモリー上に展開することが出来ました。

まずは、ELF展開部分のソースコードを見ていきます。(かなり汚いですが...)

RelocateELF関数はKernelPathで指定されたELFファイルをメモリー上に展開し、エントリーアドレスをRelocateAddrとして代入します。

20行目のLoadFile関数はKernelPathで指定されたファイルを一時的に確保した領域に展開する関数です。(file_loader.c参照)

26~55行目でELFヘッダーやプログラムヘッダーを読み込み、実行可能バイナリのアドレスの最小値と最大値を求めています。
本来はプログラムヘッダーごとにメモリー領域を確保しバイナリを展開すべきなのですが、メモリーページが重複することを恐れた結果、一括で領域確保することにしました。(たぶんプログラムヘッダーごとに確保しても大丈夫な気はする)

62~72行目でバイナリを展開する領域を確保しています。

75~89行目でバイナリを展開しています。

90行目でエントリーポイントのアドレスをRelocateAddrに代入しています。

以上のように、UEFIのおかげで構造体に代入したり、メモリーコピーをするだけでカーネルの展開が完了してしまいます。

Long mode から Compatibility mode への移行

Long mode から Compatibility mode へ移行するには、GDTに32bitのコード・セグメント・ディスクリプタを書き込めばできます。

実際のソースコードはこんな感じです。

47行目以降がGDTの再設定をしている部分です。

boot_paramは0x50000から1ページ分確保された領域で、OSとのやり取りとGDTを設定するために利用されています。

50~55行目は、NULL Segmentを設定しています。

57~58行目は、実際に32bitのコード・セグメントとデータ・セグメントをそれぞれ設定しています。 ここらへんの設定は、xv6のbootasm.Sを参考にしています。

83~86行目で、設定したGDTを読み込み、展開したカーネルのエントリーポイントへ飛んでいます。far jumpをすると同時に読み込んだGDTが有効になるようです。今回は、lretqを使った、いわゆるret trickによって、GDTを有効化しています。 (ネット上のサンプルコードではうまくいかなかったので、最終的にはLinuxソースコードを参考にしました。)

以上で、ブートローダー側の作業は終了です。

あとがき

UEFIって何?の状態から1,2ヶ月あがくことで何とかUEFI上で動くブートローダーを作ることが出来ました。キャンプ中も、完成していたと思っていたローダーのバグによって意味不明な現象に悩まされたりすることは有りましたが、何とか完成させることができました。

このローダーのソースコードはここに有ります。

github.com

xv6本体はこちらです。

github.com

参考にさせていただいたソースコードです。(uchanさんありがとうございました!)

github.com