OSp: Bare-metal OSv

In this post, I’d like to summarize what I did for bare-metal OSv, or OSp.

Fortunately, if you give up using some functions that rely on Virtio devices (e.g., networking), OSv is already capable of running on bare-metal with only two small fixes.

First, you need to skip AcpiReallocateRootTable() in drivers/acpi.cc. In this function, something causes triple fault and prevents OSv from booting up.

Second, you had better insert sleep() to wait for AHCI port linkup. Without it, OSv often mistakenly believes that AHCI ports won’t get ready.

The remaining tasks for bare-metal OSv are straightforward.

First, build the OSv’s disk image.

Then, you will get the image, build/release.x64/usr.img, which is in the QCOW2 format. So, next, convert it to the raw image (e.g., named bare.img).

Then, start your favorite OS on the target physical machine, on which bare-metal OSv is supposed to run, without mounting the primary local disk (e.g., you can use USB booting or network booting), and simply dd the bare.img you’ve just created onto the primary local disk.

Finally, restart the target physical machine from the local disk. OSp will show up :)


Since I’m tired of dd-ing OSv’s kernel image to the local disk each time I modified the source code of OSv, I make OSv kernel network-bootable (as shown in the figure).


First, apply the patch. Then, try git submodule update because the patch adds iPXE as a submodule.

Next, make the network-bootable image of OSv.

Then, use the script, scripts/embed, to specify the command parameters you want to pass to OSv.

Now, you have two important files: build/release.x64/loader.bin (network-bootable OSv kernel iage) and build/release.x64/loader.ipxe (network-bootable iPXE image). The remaining steps are for allowing PXE/iPXE chain-loading using the two files.

Setup TFTP server and put the two files (loader.bin and loader.ipxe) on the TFTP directory. Also, setup DHCP server so that the target client machine can be properly instructed during PXE booting. In the configuration file of DHCP (typically dhcp.conf), specify loader.ipxe as the boot file name for the client machine.

(How to setup TFTP/DHCP servers are well described in this article, for example.)

Here is the example of DHCP/TFTP configuration:

Finally, starts the client machine enabling PXE booting. Flying OSp will show up :)


In this movie, OSv performs network booting and lands on a physical machine (my HP server). I also applied the patch, which is for the physical 10GbE driver for OSv, so the OSv can also use network functions.

(Firmware initialization of my HP server takes more than a minute in that movie. Skip it if you are not interested in. Sorry…)


Because OSp runs on physical machines, things can be easily changed depending on underlying hardware. So, here I’ll note my server specification where I confirmed OSp ran. My HP server is ProLiant ML310e Gen8 v2 (Intel Xeon E3-1241 v3 @ 3.50GHz, PC3-12800E-11 4GB * 4, SAMSUNG HD103UJ 7200RPM 1T). Here I put the output of lspci, /proc/cpuinfo and hdparm -I on the server as a reference.

Also note that OSv has the AHCI driver but does not have drivers for other physical block devices such as SCSI or RAID controllers.


Practicing C++: Hello World

For some reasons, I need to get used to C++ including C++11, even C++14 possibly.

I’ve read some books of C++ and learnt basic things such as class, template, inheritance, capsulation, polymorphism, virtual functions, collections and some new features introduced in C++11 such as type reference, initializer lists, lambda expressions, smart pointers, rvalue references. Now I’m reading and modifying some open source programs written in C++ for practice.

Through the practice, I’ve added a simple piece of code to OSv, which is an Intel 10GbE driver. With this code, OSv can directly handle a physical 10GbE NIC if the host allows NIC pass-through. I’ve confirmed DHCP and ping succeed. (It is a kind of “Hello World” example program in C++, in which OSv can directly say “Hello” to physical LAN “World”.)

I put my dirty patch here.
(Revised ones: Intel 10GbE Driver and Pass-through Script)


(Assuming that you have already compiled the patched OSv on KVM)

First, make sure that IOMMU is enabled by your BIOS and host OS.

Second, use the shell script (bindctrl), which is added by the patch, in order to remove the target 10GbE NIC from your host OS and make it ready for OSv. If your 10GbE NIC is located at Function #0 of Device #00 in Bus #07 with vendor ID 8086h and device ID 1528h, then the command will be:

Finally, run the python script with the special option (-a 07:00.0), which is also added by the patch, to start OSv with PCI pass-through.

The output will be:


  • Use interrupts
  • Use offloading
  • Use header write back
  • Use advanced descriptors


  • pf class describes a physical function of PCI device, which consists of ioreg, phyreg, tx_queue, rx_queue classes.
  • ioreg class describes a full set of I/O registers of the pf
  • phyreg class describes a full set of PHY registers of the pf, which can be accessed through MSCA/MSRWD registers.
  • tx_desc_layout struct is exactly a data structure of a TX descriptor.
  • rx_desc_layout struct is exactly a data structure of a RX descriptor.
  • desc template abstracts a descriptor, which is a template that depends on the descriptor types (tx_desc_layout or rx_desc_layout).
  • tx_desc class abstracts a TX descriptor, which is derived from desc with TX descriptor type, tx_desc_layout.
  • rx_desc class abstracts a RX descriptor, which is derived from desc with RX descriptor type, rx_desc_layout.
  • queue template describes a queue, descriptors of which depends on the descriptor types (tx_desc or rx_desc).
  • tx_queue class behaves as a queue with RX descriptors but also supports transmission operations that manipulate the queue.
  • rx_queue class behaves as a queue with TX descriptors but also supports reception operations that manipulate the queue.

3 日で作る OS の断片

こちら 自作 OS Advent Calendar 2014 の 21 日目の記事です.

ハードウェアにいくつかの適当な仮定をおいて簡単な OS の断片(以下,OScrap (おぉスクラップ)と呼びます)を作りました.コンセプトもぼんやりしておりますが,とりあえず OScrap はサーバ用途 x86 OS です.アプリケーション側にはメモリ管理とネットワーク送受信の API だけを提供します.OScrap はアプリケーション含めシングルアドレス空間,シングル CPU モード (Ring 0) で,完全にポーリングベースで動作し,NIC はごく一部の高性能なもの 10GbE, 40GbE だけを仮定して VMDq などの NIC 側の多重化機能を前提にしてたくさんあるコアがあまり協調せず並列動作することを考えています.今後,OScrap 上に何らかのサーバアプリケーションを実装して遊ぶ予定です(試しに In-memory database とか.).今のところ KVM 上と実機(Xeon E3-1241 / 16GB) で動作確認をしております.



以下,現時点での OScrap の特徴について説明します.

  • 特徴 1) ローカルディスクなんてなかった.

PXE/iPXE での起動を前提にします.BIOS 環境でローカルディスクから起動すると通常,一番最初のコードは MBR 512 バイトに制限されますが, PXE/iPXE だと 512 バイトより多く読み込んでくれるので(少なくとも 480 KiB ぐらいはいいのかな?),いきなり OScrap カーネル本体がネットワーク越しに起動します.よって,OScrap には,一般的な OS のブートローダでよくある INT13h でカーネルを段階的に読み込むフェーズはありません.

  • 特徴 2) 32-bit Protected Mode なんてなかった.

OScrap は起動すると Real-mode から大して何も確かめず一氣に Long mode へ入ります.

  • 特徴 3) 例外・割り込みなんてなかった.

OScrap の IDT はからっぽです.NMI はごめんなさい.デバイスはすべてポーリングで処理する氣です.

  • 特徴 4) ページサイズとは 1 GB だった.

1 GB ページング前提で動作し,起動時に 12 KiB のページテーブルを構築し 512 GB のメモリ領域を恒等写像でマップします.また,MMIO 用途として,仮想アドレス 512 + X GB は,物理アドレス X GB へキャッシュしない設定でマップします.ページフォルトなんてなかった.

  • 特徴 5) APIC なんて最初から x2APIC だった.

x2APIC 前提で IPI 周りの処理をします.

  • 特徴 6) ネットワークデバイスは Intel x540 10GbE しかなかった.

Intel x540 10GbE のドライバだけを持っています.VMDq サポート前提で VMDq 有効化し,64 個ペアの送受信インターフェースを提供します.(将来的には 40GbE のドライバを持つ予定です.)

  • 特徴 7) 外部へのインターフェースはネットワークしかなかった.



  • 現状,OScrap はメモリ管理とネットワーク送受信(L2)の API を持っているだけです.今後,main.c/main(), init.c/init_ap() 以下に処理を書き加えて何か役に立ちそうなアプリケーションを作ろうと思います.
  • メモリ管理は liballoc を利用させて頂きました.また,実はページのアロケーションが適当で空いたページを解放しません(コード参照).
  • バグバグしているかもしれません.


コードは こちら になります(いずれはレポジトリへ).KVM 上で実行するにはダウンロードしたものを展開して make run するだけです.前述の通り,CPU が 1GB ページング,x2APIC などに対応していないとちゃんと動作しません.ちなみに KVM 上では Intel X540 をパススルーにしないと OScrap はネットワーク通信できません.

KVM 上での実行結果は以下のようになります.現在デフォルトの OScrap アプリケーションは 3 秒数えて終了します.

OScrap を実機で動作させたい場合は,OScrap をネットワークブートさせます.そこでサーバ側でまず PXE/iPXE の設定を行ないます.

はじめに PXE のための DHCP の設定をします.ファイル “undionly.kpxe” を PXE ブートするように設定します.よくある BOOTP の設定です.

次に PXE のための TFTP の設定をします.よくある TFTP の設定です.

続いて iPXE の準備です.

最後に OScrap のバイナリを TFTP ディレクトリへ.

以上,準備ができたら OScrap を動作させる実機を TFTP/DHCP サーバとネットワーク的につないだ状態で再起動します.これで実機上で OS が実行されます.

x540 10GbE 搭載実機上で Debug 出力を全部有効にして実行すると以下のようになります.


  • acpi.{ch} : ACPI テーブルを見つけてコアの数を数えたり,タイマーを見つけたりします.
  • alloc.{ch} : liballoc を使います.
  • apic.{ch} : x2APIC で IPI を飛ばして AP を起動させるコードです.
  • init.{ch} : 各種初期化処理を順番に呼び出すコードです.
  • kvi.ld : 0x7c00 から実行されるバイナリを生成するリンカスクリプトです.
  • loader.S : 最初に実行されるアセンブラのコードです.OScrap の IDT, GDT の設定,ページングの設定,CPU モード遷移はここで完結します.
  • main.c : アプリケーションの始まりとなる関数 main() が入っています.
  • Makefile : OScrap のコンパイルや,QEMU/KVM 上での実行のためのコマンドが入っています.
  • page.{ch} : ページを割当・解放するコードです.(解放は実は TODO.)
  • printf.{ch} : printf です.
  • putchar.{ch} : シリアルポートから文字をデバッグ用に出力します.
  • timer.{ch} : デバイスを初期化する際の sleep を実装するためのタイマーの実装が入っています.HPET を使います.
  • drivers/hpet.{ch} : HPET を初期化して使うコードです.割り込みは設定せずに,ただのカウンタとして使います.
  • drivers/ixgbe.{ch} : Intel X540 10GbE Controller を初期化します.VMDq を有効化し,64 個の送受信機能を有効化します.
  • drivers/pci.{ch} : PCI デバイスをスキャンするコードです.
  • drivers/pmt.{ch} : ACPI Power Management を使うコードです.(結局,未使用)
  • include/asm.h : C 言語からアセンブラ命令を呼ぶ必要がある時に使います.IO 命令や RDMSR, WRMSR などのインラインアセンブラです.
  • include/common.h : よく使うヘッダファイルを束ねています.
  • include/lock.h : ロックの処理です.
  • include/msr.h : MSR の番号のマクロです.
  • include/panic.h : panic 用関数ですが,ただ busy ループに突入するだけのスタブです.
  • include/string.h : 文字列操作系関数です.(簡易実装)
  • include/types.h : OScrap 全体で使われる型の定義です.
  • include/mmio.h : MMIO のための関数です.(512GB + X へアクセスするだけ.)

INT13h network redirection patch for BitVisor

This patch enables BitVisor to redirect BIOS call 13h/42h (Disk Read) to a server over ethernet. This can perform network booting of tiny guest OSs (which rely on only BIOS call 13h/42h to read disks lol) on BitVisor over ethernet.

I’ll explain how to use. Before everything, prepare comparatively-new two x86 machines: a server machine (that provides OS images) and a client machine (that network-boots OSs on it). Now, follow the steps for both machines.

1. Preparing client

First of all, get the latest version of BitVisor from the repository and apply this patch to it.

Then, configure the BitVisor by modifying defconfig. All you have to do is modify .pci parameter and insert .netboot_id parameter just next to the .pci parameter like this. (0x5252 is the id number for the server. You can use any number you like. Note that BitVisor supports Intel PRO/1000, X540 10GbE, RealTek 816x and Broadcom NetXtreme.)

Make bitvisor.elf and install it on the target client machine (see its manual for details).

2. Preparing server

Then, get the server program from the repository and compile it.

Prepare an OS image (that reads disks only via BIOS call 13h/42h) that you want to boot over ethernet. (For example, patched OSv with I/O-less applications in its bootfs with “–nomount” options works fine lol)

Then, start the server program, setting the OS image as its argument. (0x5252 is the id number of the server that provides a disk image, which should be the same number with the one you used in defconfig of BitVisor.)

3. Fire!

Confirm that the server machine (where the server program is running) is connected to the target client machine (where your OS runs on BitVisor) over ethernet.

Then, start BitVisor on the target client machine, then you will see the guest OS (OSv in this example) boots on the target client machine from the disk image provided by the server instead of the local disk.

The example output of OSv on BitVisor is like this (I used “–verbose” for OSv’s boot options). BitVisor does not virtualize devices, and OSv directly runs on physical hardware interface. Note that the firmware vendor printed by OSv is “HP”, which indicates the actual firmware of the physical machine (not the one on QEMU you usually see when running OSv on KVM).

(If you’re interested in running OSv on BitVisor, see OSb.)

Enabling SR-IOV on Linux

First of all, you need to enable SR-IOV in firmware settings (The way depends on firmware).

Screen Shot 2014-12-16 at 8.29.41 PM

There are two methods to enable SR-IOV on Linux: the deprecated method and the new method.

The deprecated method is to use max_vfs option on modprobe. When you want to allocate 8 virtual functions, you reload your SR-IOV-capable driver with the option, max_vfs=8.

The new method is to use sysfs. To allocate 8 virtual functions, you can redirect 8 to /sys/bus/pci/devices/{SR-IOV-capable device}/sriov_numvfs.

Note that your device needs to be installed to a PCIe slot with enough bandwidth. You will see the message below without enough bandwidth.

OSb: Network Booting OSv

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

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

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

結論から言うと,OSv はデバイスのサポートを氣にしなければ,コードを約 1 箇所コメントアウトすることで BitVisor (= デバイスを仮想化しないハイパーバイザ = 物理マシン) 上でブートします.しかし,コンパイルする度に物理ディスクに OSv をインストールのは大変面倒だったので,BitVisor を使って OSv をネットワークブートさせることにしました.

しかし,BitVisor 自体は KVM などと違ってゲストをネットワークブートさせる機能はありません(デバイスを仮想化しませんので).そこで今回は OSv を BitVisor 上でネットワークブートさせる実装について説明します.必要なコードを全部公開したいところですが,政治的理由のため今回は控えて,コードは限定的に触れていきます.

※ いずれ全部公開できると思います.

前回の記事 の通り,OSv のブートプロセスをザックリ読んだところ,BIOS コールでのディスクアクセス(INT13/42) だけで OSv がブートしている感じです.そこで,INT13/42 を BitVisor で捕まえてディスクアクセスをサーバへ転送すれば,OSv がネットワークブートしそうです.ということで,挑戦してみます.

※ OSv はブートした後,ファイルシステム ZFS をマウントするために,Virtio Block Device を使いにきます.BitVisor 側で Virtio Block Device を仮想化してあげないと ZFS マウントできないことになりますが,これは今後の課題として,今回はひとまず OSv カーネル (& bootfs) をブートさせるところまでを目的とします.SATA もいけるようなオプションドライバ を見かけましたが,どうせ Virtio にしたいので試していません.

BIOS コールは OS 起動初期の Real Mode 環境下で,INT 命令を叩いてソフトウェア割り込みを発生させると,BIOS がそれをハンドリングして色々リッチな処理をしてくれるという仕組みです.ゆえに BIOS コールは,BIOS が用意した 割込みベクタテーブル で割込みとしてハンドリングされています.

割込みベクタテーブルは 4 バイト幅のポインタのテーブルになっており,各ポインタは割込みハンドラのアドレスを示しています.それらポインタを差し替えることで,BIOS コールにお好みの処理を挿入できます.今回は,OSv がディスクアクセスに使っている INT13 のポインタを BitVisor で差し替えて,その中でディスクアクセスをサーバの仮想ディスクに対して行なうことで,ネットワークブートを目指します.

今回 BitVisor で差し替えた割込みハンドラ内では,VMCALL を発行するようにして,BitVisor に遷移します.そこで BIOS コールの引数を解釈して,INT13/42 だった場合,要求されたセクタをサーバから読み込んでくる処理をします.それ以外(INT13/XX, XX は 42 以外)だったらもともと BIOS が用意していた割込みハンドラにジャンプします.

Unrestricted Guest 前提で考えています.

INT13 の割込みハンドラを以下のコードに差し替えます.まず,INT13 で本来 BIOS が用意した割り込みハンドラジャンプするところを,こちらの guest_int13h_guest へジャンプさせます.


まず,レジスタを退避します.BIOS コールの引数になっている AX, BX レジスタはBitVisor 内で VMCALL ハンドラを区別する際に使われてしまいますので,使われていない ECX, EDX の上位 16 ビットを活用して退避します.

そして,さっそく VMCALL を発行します.BitVisor には, BitVisor 内であらかじめ登録されたハンドラを,ゲストから VMCALL を使って呼び出す仕組みが既に実装されています.ゲストから BitVisor 内のハンドラを呼び出すには以下の手順で VMCALL を 2 回発行します.

  1. EAX には 0,EBX にはハンドラ名を表す文字列を示すポインタを格納してから,VMCALL を実行します.VMCALL 実行し終えると,EAX にはハンドラ番号(0 以外)が返って来ます.
  2. EAX にハンドラ番号を格納して VMCALL をもう一度実行します.すると,BitVisor に遷移(VMExit)して該当のハンドラが呼び出されます.ハンドラの処理が終わると VMCALL の次の命令からゲストの処理が再開(VMEntry)されます.

BitVisor ではあらかじめ “int13h” という名前でハンドラを登録しておきます.上記の手順に則って,”int13h” を EBX に指定して 1 回目の VMCALL を発行します.その直後に 2 回目の VMCALL を発行します.1 回目の VMCALL で EAX にハンドラ番号が入っているので,これで目的のハンドラが実行できます.

※ EBX に “int13h” のポインタを入れる際ですが,上記の割り込みハンドラのコードを配置するメモリ領域を動的に確保しているので,”int13h” のアドレスが実行時まで分かりません.そこで実際のコードの位置が決まってから “mov $0, %ebx” の “$0″ の部分を書き換えて,正しいアドレスが EBX に入るように命令を書き換えています.”.set guest_int13h_vmmcall_offset, . – 4″ して,guest_int13h_vmmcall_offset に 4 バイトのアドレスの値を入れます.

そして,2回目の VMCALL 内では INT13/42 かどうかを,ゲストのレジスタの値を覗き込んで判断します.もし,INT13/42 でなかった場合は ZF を立ててゲストへ帰ってきます.もし,INT13/42 で転送処理をした結果失敗したら CF を立てて帰ってきます.ZF が立っていたらもともとの BIOS コールへジャンプします.CF が立っていたらエラー状態をエミュレートしてゲストへ帰ります.いずれでもない場合は正常完了をエミュレートしてゲストへ帰ります.

では続いて,BitVisor 内の VMCALL ハンドラで何が行なわれるかについて説明します.2 回目の VMCALL 時に,BitVisor では以下の int13hook_vmmcall() が実行されます.

int13hook_vmmcall() の中では,まず,先のアセンブラの方で ECX, EDX に退避した AX, BX を取り戻します.

続いて,AH, DL をチェックして,INT13/42 か判断して,そうであればディスクアクセスの転送処理に移っていきます.

AH が 0x42,DL が 0x80 の場合,INT13/42 でローカルディスク(ブートディスク)を読み込んでいるので,転送処理を行ないます(それ以外は基本スルーで,もともとの BIOS コールを呼ぶようにします).INT13/42 では,ディスクアクセスに関する情報(LBA, セクタ数など)は DS:SI へ格納されるので,DS:SI のアドレス値を取得し,引数として渡しながら int13hook_readlba() を呼び出します.

INT13/42 では DS:SI で指定された下記のような構造体の情報に基づいて読み込みを行ないます.サーバの仮想ディスクから blocks で指定されたセクタ数だけ,lba で指定された位置からデータを読み込んで,segment:offset で指定された位置にロードすればよいということになります.

int13hook_readlba() 内で,実際にサーバディスクから読み込みに当たる部分は, nd_read() 付近になります.

cont フラグを false にしてから nd_read () を呼びます.nd_read () ではネットワーク経由でサーバの仮想ディスクを読みます.実際に,自前プロトコルでサーバへリクエストを飛ばします.(ここで Ethernet でフレームが 1 個飛びます.)続いて nd_poll(), nd_check() を繰り返して,受信された Ethernet フレームをポーリングします.そして,サーバからレスポンスがあれば受信処理をした後, nd_read() で指定した関数ポインタ int13hook_cb() が呼び出されます.

int13hook_cb() では,cont フラグを true にするだけの処理が入っていて,nd_poll(), nd_check() のポーリングを終了します.この時点で,既に nd_read() に指定したバッファ p (= 構造体で指定された読み込み先メモリアドレス)にサーバから読み込んだデータが入っています.

以上でサーバからセクタ読み込みが完了したので,あとはただ呼び出し元に戻るだけです.VMCALL で呼ばれたこのハンドラから抜けると,VMEntry して先のアセンブラのコードに戻ります.そして最後はゲストへ帰ります.

以上のように実装していくと,INT13/42 が BitVisor によって全部サーバへ転送されて,OSv がネットワークブートします.BitVisor 自体も PXE/iPXE 経由でネットワークブート可能なので,ローカルディスクに何も入れない状態で,OSv + BitVisor (= OSb) が降ってくることになります.

※ 自前プロトコル含めコードはいずれ公開してみようと思います.

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 コールでディスクから何かを読み込みます.


第 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 セクタ以降のコマンド