OSb: Code Reading (Boot Process of OSv)

【注意】こちら OSv/BitVisor 軍事境界線(BitVisor側)の記事です.

こちら OSv Advent Calender 2014 6 日目の記事です.

OSv はハイパーバイザ上で動作する事を前提として設計された軽量 OS です.そこで,BitVisor という軽量ハイパーバイザ上で,OSv を動かす試みをしています.

OSv を BitVisor 上で動かすにあたって,まずは味方を知るという事で OSv のコードを読んでみました.その時に読んだ内容の一部をここで紹介します.

(こちら OSv 初心者です.色々間違っているかもしれません.重ねてご注意ください.

まずは,コードを入手して(2014 年 12 月頃),

とっかかりとしてブートシグニチャを探すと,boot16.S が見つかるので,

調べると Makefile の中で boot16.ld でリンクされているので,

boot16.ld を眺めると,ブートセクタっぽい.

以上から arch/x64/boot16.S が最初のコード(ブートセクタ)かも,ということで読み始めます.boot16.S では,コードは start: から始まり,セグメントを整えて,A20 ラインを有効化してから,さっそく何かをディスクから読んでいるようです.

boot16.S のコードを見ながら説明すると,start: が最初で,

セグメントを整えて,

BIOS コールで A20 有効化.

そして,BIOS コールでディスクから何かを読み込みます.

ここで,ディスクのどこから何を読み込んでいるかですが,読み込みについての情報(int1342_boot_struct)を見ると,

第 2 セクタ (LBA = 1) から 63 セクタ分(31.5KiB)をメモリ cmdline (=0x7e00) へ読んでいると分かります.実際に OSv のイメージを dd しても分かるのですが,第 2 セクタに OSv のコマンドラインが入っているようです.

その後,さらにディスクからの読み込みを繰り返しています.主に以下の処理を繰り返して,32 KiB 単位で読み込んでいます.

  1. BIOS コールでディスクから 32KiB 読み込む.
  2. 32-bit Protected Mode へ遷移.
  3. 読み込んだ 32KiB を高いアドレスのメモリ領域へコピーする.
  4. 16-bit Real Mode へ遷移.
  5. 読み込み元の LBA と読み込み先のメモリポインタをインクリメントして 1. へ戻る.

これを 4096 回繰り返して,計 128 MiB 分ディスクからメモリへ読み込んでいるようです.(ちなみに後々触れますが,ここで読み込んでいるのはどうも (1) 圧縮された OSv カーネルと,それを展開する (2) 展開ルーチンのようです.)

それでは,この繰り返し処理について説明します.

まず,繰り返し処理の始まりはここからです.BIOS コールでディスクを読み込みます.

ディスクアクセスの内容はこちらから分かります.

LBA = 128 から 64 セクタ (32KiB) 分読み込んで,tmp (=0x80000) から始まるメモリ領域にロードしているようです.

ディスクアクセス後,Real Mode (16 bit) のアドレス上限を解除するために,一旦 Protected Mode (32 bit) に遷移します.そこで遷移の前準備として,まず GDT を設定します.

GDT の設定内容はこちらです.(32-bit のセグメントが用意されています.前述の通り,後々 16-bit にも戻るので 16-bit のセグメントもあります.)

GDT の準備ができたので,Protected Mode に遷移し,32 bit セグメントに切替えます.

16-bit のアドレス上限解除されたので,先ほど読み込んだ 32 KiB を,上限を氣にしなくていい広々とした領域 (つまり BIOS 領域より高い領域) xfer (=0x1800000) へコピーします.(コピー後,読み込み先ポインタ xfer を 32 KiB 分加算して,次の 32 KiB 分の読み込みに備えます.)

次の 32KiB 分を読み込むためには,また BIOS コール使うので,使えるようにするために再び Real Mode に帰ります.

最後に,読み込むべき LBA を 32KiB 分加算して,繰り返し処理の最初に戻って,次の読み込みを行ないます.毎回,カウンタ (count32=初期値4096) をデクリメントしていって 0 まで減った時は,繰り返し処理を抜けます.

繰り返し処理を終えて,ディスクをすべて読み込んだら,メモリマップを取得して,メモリ e820data (=0x2000) へ保存しています.これは後々,カーネルのためのメモリ領域を初期化する際に利用します.

これらの処理が終わった後,ついに OSv カーネルを展開&実行していくようです.C言語の関数を実行するために,ここで再び 32-bit Protected Mode に遷移します.

そして,シンボル lzentry (= 0x1800000 + 0x18) に格納されているアドレスへジャンプします.この +0x18 はELF のエントリポイントを示しています.

では,このエントリポイントがコード的にどこなのか探します.0x1800000 ,これは元々は LBA = 128 から読み込まれてきたものが入っています.そこで正体を突き止めるべく,Makefile を探ってみると,

どうも lzloader.elf のようです(dd で 128 セクタ目から書かれています).その正体は,

fastlz/* にあるコードっぽい.ということで fastlz/* のコードを少し眺めると,この ELF の正体は軽量な圧縮ライブラリの fastlz ということが分かります.

では,実際のジャンプ先,つまり 0x1800000 + 0x18 に入っている値(エントリポイント)はどこをさしているか? 上記で利用されているっぽい lzloader.ld 内を探ってみると,

uncompress_loader というシンボルがエントリポイントのよう.シンボルを探してみると,

それは fastlz/lzloader.cc にある関数のようです.そして,この uncompress_loader() という関数では, Binary として objcopy で結合された loader-stripped.elf.lz (loader-stripped.elf の圧縮版) を展開しているようです.(これは変数名 binary_loader_stripped_elf_lz{start, size} から分かります.)

そして,loader-stripped.elf.lz は何かと言うと,

loader-stripped.elf から来ている.では loader-stripped.elf の正体は,

loader.elf がストリップされたもの(設定に依ってはされないかも)のようですが,この loader.elf とは,

OSv 本体っぽい.drivers/*, core/*, java/* など色々ここでリンクされているようです.

ゆえに uncompress_loader() で展開された正体は OSv 本体になります.さて,では展開された OSv 本体はどこに吐き出されたかと言うと,再度 fastlz/loader.cc を見てみると,

吐き出し先は BUFFER_OUT (fastlz/fastlz.h の fastlz_decompress() の宣言にある void *output という引数に相当しますので).

これはマクロ定義されてて元々 OSV_KERNEL_BASE.つまり,展開先のアドレスはどうも OSV_KERNEL_BASE らしく,OSV_KERNEL_BASE で示されるメモリ領域に loader.elf が展開されて格納されるようです.

さて,展開後,return して呼び出し元の boot16.S に処理が戻ります.

lzentry から戻ってきた後,下記のように,また,すぐに entry (= OSV_KERNEL_BASE + 0x18) で示されるアドレスへジャンプします.(+0x18 は lzentry の時と同様に ELF のエントリポイントです.)

ということは,この call *entry では OSV_KERNEL_BASE で示される ELF ,つまり loader.elf のエントリポイントに飛ぶことになります.

では loader.elf のエントリポイントは? loader.elf 生成時に参照されていた loader.ld を見ると,

start32 というシンボルのようです.探してみると,

arch/x64/boot.S にあるっぽいので,読んでいきます.(実は arch/x64/boot32.S:18:start32 も引っ掛りますが Makefile 的に違いそう.)

先ほど呼び出し元の boot16.S にて OSv カーネルの先頭のアドレスを EAX に格納していましたが,それを EBP に控えておくようです.

そして 64-bit Long Mode へ切り替わる処理に入っていきます.まず,GDT を設定し直して,その後,データセグメントを 64-bit にします.

設定された GDT は以下の通りです.(今回の GDT は 64-bit のテキスト・コードセグメントを持っています.)

そして,64-bit Long Mode へ.

64-bit Long Mode へ遷移すると,ついに OSv 本体の C/C++ のコードへ突入していきます.

premain() は loader.cc にあります.以下,loader.cc を読んでみます.以降,ほぼ C/C++ なので関数名から推測しやすいかと思いますので説明はザックリと行きます.

109 行目で *init が呼び出されている部分で,ここで基本的なオブジェクトの初期化処理が行なわれます.マルチコア,メモリ管理,タイマー,ACPI などなどを操作する色々なオブジェクトの初期化処理を行ないます.以下のようにして,初期化処理とその呼び出し順番が追えそうです.それぞれの詳細は割愛します.

OSv を BitVisor 上で動かす目的で,ここまで読んで思ったことは以下 2 点.

  1. ごく普通の x86/64 系ブートプロセスに見える.
  2. デバッグメッセージ “OSv ” OSV_VERSION “\n” が出れば,初期のブートプロセスは一応突破できたと判断できる.

BitVisor は基本的にデバイスを仮想化しないので,BitVisor 上で動作する OSv は普通に物理マシンでブートするのと同じことになります.このとき, 1. より, OSv はここまでは起動しそうという予想が立ちます.また,2. より,シリアル見ればここまでちゃんと起動したかを簡単に確かめられそう.ゆえに,さっそく OSv を BitVisor 上でブートして確かめてみたくなるところですが,ぐっとこらえてもう少しコードリーディング.

premain() が終わると,一旦 arch/x64/boot.S へ帰りますが,

またすぐに main() へジャンプするようです.この時参照されている __argc, __argv という変数は, core/mempool.cc の初期化関数でメモリ初期化した後に呼び出される parse_cmdline() で,boot16.S で読んだ第 2 セクタ以降のコマンドラインをパースしたものが格納されます.

main() は同じく loader.cc にあります.

そして,同じファイル内の関数 main_cont() に処理が続きます.ここで色々と残りの初期化処理が行なわれます.

ACPI 周りの初期化をしたり,

cmdline をパースしたり,

IPI で複数コアを動かしたり,

BSD 系のルーチンを初期化したり,VFS 初期化したり,ネットワーク系(プロトコルスタック)を初期化したり,

そして,do_main_thread() に処理が続きます.

そこでやっとドライバの初期化(Virtio など),オプションに応じて,ローカルディスク(ZFS)のマウント,ネットワークへの接続などを試みます.

割愛した部分も含め,やはりここまで眺めた感じ,ブートプロセス自体は普通っぽい印象を受けます.BitVisor 上(= 物理マシンのインターフェース上)で OSv を動作させた場合,ここまでブートして来て,「Virtio がない…,期待しているデバイス見つからない…」という展開になればひとまず万々歳と思われます.

(BitVisor 上でこれ以降もまともに動作させたければ,BitVisor で Virtio を見せる等工夫が必要ということになります.本当の楽しみはここからです.)

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">