SHV (Small HyperVisor) is a small hypervisor for learning and testing purpose.
SHV is a monolithic system-level program. It runs on Intel CPUs and uses VMX to perform hardware virtualization. It can be booted using multiboot. It only supports BIOS (i.e. no support for UEFI). It supports multiple CPUs (SMP).
SHV is open source and is released under GPLv3.
SHV is ported from LHV. The boot process of LHV based on XMHF64 and is complicated. SHV rewrites the booting process to make it easy to debug.
LHV is used during the development of XMHF64 to test XMHF64's nested virtualization functionality. LHV is written by lxylxy123456, based on XMHF64. Some of the interrupt handling code is based on open source projects that are used used by CMU's 15-410. See 15-410 Software License Statements.
XMHF64 is a research project by lxylxy123456. XMHF64 is based on XMHF. XMHF only supports 32-bit OSes, but XMHF64 also supports 64-bit OSes. XMHF does not support nested virtualizatoin, but XMHF64 supports nested virtualization.
XMHF (eXtensible Micro-Hypervisor Framework) is a micro-hypervisor research project by Vasudevan et al. XMHF uses multiple pieces of open source software. Please see https://github.com/uberspark/uberxmhf/blob/master/xmhf/COPYING.md.
As of the writing of this section, the git commit sha of the projects above are:
- LHV:
ed8d59e1bac6d439946c1e18d896a4b7f6a14b27
- XMHF64:
3c22712f5de7b60eedac3483112a69d647d9e86c
- XMHF:
b0365e5cd7df78414d3df8553a98bcce5ce6d217
Hint: looking at CI section may be helpful when something does not work.
Install dependencies (qemu-system-x86
is only needed for testing), in this
example we demonstrate installing on Debian-based Linux:
sudo apt-get update && \
sudo apt-get install -y \
build-essential crossbuild-essential-i386 autoconf libtool xorriso \
mtools grub-pc git qemu-system-x86
tools/build.sh is intended to make the build process easy.
To build in directory build/
:
mkdir build
cd build
../tools/build.sh
To specify the platform to build for:
- i386, 32-bit paging:
--i386
or-i
- i386, PAE paging:
--pae
or-p
- amd64, 4-level paging:
--amd64
or-a
To view the command build.sh
passes to configure
:
../tools/build.sh -n
To view arguments to build.sh
:
../tools/build.sh -h
SHV uses Autoconf and Automake. In this example we build in a different
directory build/
:
autoreconf --install
mkdir build
cd build
../configure
make -j `nproc`
To specify the platform to build for:
- i386, 32-bit paging:
--host=i686-linux-gnu
- i386, PAE paging:
--host=i686-linux-gnu --enable-i386-pae
- amd64, 4-level paging:
--host=x86_64-linux-gnu
To change optimization level (default is O2, due to autoconf?):
- O0:
CFLAGS='-g -O0'
- O3:
CFLAGS='-g -O3'
To view other configuration options:
../configure -h
shv.bin
: multiboot ELF image for SHV.grub.iso
: grub ISO image that boots SHV.
There are two ways to load SHV to QEMU / KVM.
- To load SHV image directly, use
-kernel shv.bin
.- To add arguments to multiboot commandline, use
-append "arg1 arg2 ..."
.
- To add arguments to multiboot commandline, use
- To boot SHV using GRUB, use
-cdrom grub.iso
.- To add arguments to multiboot commandline, edit
grub.cfg
in the build directory and then rebuild usingmake
.
- To add arguments to multiboot commandline, edit
Note that SHV requires hardware virtualization (VMX), so QEMU must enable KVM.
Sample command line is -cpu Haswell,vmx=yes -enable-kvm
.
By default SHV prints output to serial port. Use -serial stdio
to let QEMU
print the serial port message to stdout.
tools/qemu.sh is intended to make running SHV easy. For
example, the following command tests SHV with 4 Haswell CPUs and 1G RAM.
g_shv_opt
is set to 0xdfd and g_nmi_opt
is set to 0.
../tools/qemu.sh -kernel shv.bin -append 'shv_opt=0xdfd nmi_opt=0'
To view arguments to qemu.sh
:
../tools/qemu.sh -h
Pass -s
to QEMU to enable GDB server. Optionally pass -S
to let QEMU wait
for the GDB server after initializing.
To connect GDB to QEMU and load debug info in shv.bin
:
gdb --ex 'target remote localhost:1234' --ex 'symbol-file shv.bin'
The approximate boot sequence is:
_start (boot.S, BSP only, protected mode without paging)
kernel_main (kernel.c, BSP only)
smp_init (smp.c, BSP only)
(BSP calls wakeupAPs() to wake up APs)
0x10000 (was _ap_bootstrap_start, smp-asm.S, AP only, real mode)
0x10??? (was _ap_clear_pipe, smp-asm.S, AP only, protected mode without paging)
init_core_lowlevel_setup_32 (smp-asm.S, AP only, protected mode without paging)
init_core_lowlevel_setup (smp-asm.S)
kernel_main_smp (kernel.c)
shv_main (shv.c)
shv_vmx_main (shv-vmx.c)
vmlaunch_asm (shv-asm.S)
shv_guest_entry (shv-guest-asm.S, VMX guest mode)
shv_guest_main (shv-guest.c, VMX guest mode)
The interrupt / exception call stack is approximately:
idt_stub_{host,guest}_h...h1...1 (idt-asm.S, e.g. idt_stub_host_hh11)
idt_stub_common (idt-asm.S)
handle_idt (idt.c)
handle_..._interrupt
idt_stub_common()
calls handle_idt()
with the ESP/RSP containing the
EIP/RIP of interrupted code. This allows GDB to back trace to interrupted code.
A similar trick is implemented when vmexit_asm()
calls vmexit_handler()
.
This allows GDB to back trace from host mode to guest mode.
Note that back trace works the best when compiled with -O0
. When SHV is
compiled in 32-bits, QEMU must also be 32-bits (use qemu-system-i386
or
./tools/qemu.sh --qb32
).
For example, GDB's bt
command may output something like:
(gdb) bt
#0 update_screen (..., guest=false) // callee of #1
#1 0x00110099 in handle_timer_interrupt (...) // callee of #2
#2 0x0010676c in handle_idt (...) // C interrupt handler
#3 0x00104aec in idt_stub_common () // assembly interrupt handler
#4 0x0010a7bf in shv_guest_wait_int_vmexit_handler (...)
// interrupted code
#5 0x0011da8e in vmexit_handler (...) // C VMEXIT handler
#6 0x0011c993 in vmexit_asm () // assembly VMEXIT handler
#7 0x0010a817 in shv_guest_wait_int (...) // guest code that causes VMEXIT
#8 0x0010b377 in shv_guest_main (...) // caller of #7
#9 0x00108fe9 in shv_guest_entry () // caller of #8
(gdb) x/3i 0x0010a7bf - 2
0x10a7bd <shv_guest_wait_int_vmexit_handler+78>: sti
0x10a7be <shv_guest_wait_int_vmexit_handler+79>: hlt
0x10a7bf <shv_guest_wait_int_vmexit_handler+80>: cli
(gdb) x/i 0x0010a817
0x10a817 <shv_guest_wait_int+34>: vmcall
(gdb)
Running SHV on real hardware requires a machine with Intel CPU that supports BIOS booting. Note that computers manufactures after around 2020 no longer supports BIOS booting (i.e. only supports UEFI). See technical advisory by Intel.
If the real hardware has GRUB installed in BIOS mode, then it can add SHV as a menuentry in GRUB. Otherwise, it is probably easier to boot SHV via a CD or USB stick.
If the real hardware does not have a serial port, you can let SHV print output
to VGA (e.g. ./tools/build.sh -s 0x2000 --vga
). If the serial port does not
use the standard 0x3f8 port and 115200 baud rate etc, unfortunately you need to
manually edit src/debug-uart.c.
First, put shv.bin
to /boot
of the machine.
Second, add the following menuentry block to /etc/grub.d/40_custom
. It is
similar to the one in grub.cfg, but (hd0,msdos3)
refers to the
/
partition in GRUB. In my case it is /dev/sda3
in Linux.
menuentry "shv" {
multiboot (hd0,msdos3)/boot/shv.bin
}
Third, update grub.cfg
. On Debian this is sudo update-grub
. On Fedora this
is sudo grub2-mkconfig -o /boot/grub2/grub.cfg
.
Fourth, restart the machine and select SHV in GRUB.
First, write grub.iso to the USB or CD (suppose the USB is /dev/sdX
):
dd if=grub.iso of=/dev/sdX
Then boot the real machine with the USB or CD.
TODO
Create a new virtual machine using VirtualBox, select grub.iso
as the disk
to boot the OS. Make sure to allocate enough memory. VirtualBox supports serial
port through VM settings.
Since SHV requires nested virtualization, make sure to check "Enable Nested VT-x/AMD-V" in VM settings. If the checkbox is grayed out, try the commands from https://stackoverflow.com/questions/54251855/:
VBoxManage modifyvm <VirtualMachineName> --nested-hw-virt on
Due to problems of VirtualBox or SHV, currently these workarounds are required:
- Only configure one CPU in VirtualBox (i.e. SMP not supported).
- Do not let SHV access VGA mmio. That is, make sure
g_shv_opt
sets bit0x2000
(e.g../tools/build.sh -s 0x2000
) and make sure not to enable VGA (i.e. do not use./tools/build.sh --vga
). - Do not set any bits in
0x421
ofg_shv_opt
(e.g../tools/build.sh -s 0x29dc
).
The problems are:
- VirtualBox does not support MTRR.
- Guest mode of SHV cannot access LAPIC memory of VirtualBox.
- Guest mode of SHV cannot access VGA memory of VirtualBox.
- VirtualBox does not seem to support MSR bitmaps.
- For some reason
SHV_USE_UNRESTRICTED_GUEST
does not work on VirtualBox.- See #GP(0) when moving 0x80000000 to CR0? Check VMX CR0 shadow
TODO
The latest version of Bochs is probably on GitHub: https://github.com/bochs-emu/Bochs
First, compile Bochs from source code. We need to make sure Bochs is compiled
with VMX support. Suppose you want to clone Bochs to /PATH/TO/BOCHS
:
cd /PATH/TO/BOCHS
git clone https://github.com/bochs-emu/Bochs
# I got commit 253882589d65ab17e23420a416fbd2d6e7591642
cd Bochs/bochs
./configure \
--enable-all-optimizations \
--enable-cpu-level=6 \
--enable-x86-64 \
--enable-vmx=2 \
--enable-clgd54xx \
--enable-busmouse \
--enable-show-ips \
\
--enable-smp \
--disable-docbook \
--with-x --with-x11 --with-term --with-sdl2 \
\
--prefix="$PWD"
make -j `nproc`
make install
Then go back to SHV build directory, create bochsrc
that can boot SHV:
cpu: model=corei7_sandy_bridge_2600k, count=2, ips=50000000, reset_on_triple_fault=1, ignore_bad_msrs=1, msrs="msrs.def"
memory: guest=512, host=256
romimage: file=$BXSHARE/BIOS-bochs-latest, options=fastboot
vgaromimage: file=$BXSHARE/VGABIOS-lgpl-latest
ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata1: enabled=1, ioaddr1=0x170, ioaddr2=0x370, irq=15
ata0-master: type=cdrom, path="grub.iso", status=inserted
boot: cdrom, disk
log: log.txt
panic: action=ask
error: action=report
info: action=report
debug: action=ignore, pci=report # report BX_DEBUG from module 'pci'
debugger_log: -
com1: enabled=1, mode=file, dev=/dev/stdout
Then start Bochs
/PATH/TO/BOCHS/Bochs/bochs/bochs
Select "Begin simulation" to run SHV.
SHV's coding style is similar to Linux's, but it uses 4 spaces as tab width.
indent
is used to automatically format the code. The options to indent are
-linux -ts4 -i4
. tools/indent.sh is used to automatically
indent most source files.
Dashboard: https://github.com/lxylxy123456/shv/actions
Configuration directory is .github/workflows/.
build.yml
: Compile SHV in multiple configurations.indent.yml
: Check indentation of most source files. Fail iftools/indent.sh
is not happy for all of the last 10 commits.vmcs.yml
: Check the format ofinclude/_vmx_vmcs_fields.h
.
Dashboard: https://app.circleci.com/pipelines/github/lxylxy123456/shv
Configuration directory is .circleci/.
Circle CI is used to compile SHV and test using QEMU / KVM. Circle CI is used because it supports nested virtualization, unlike other online CI services.
Jenkins is hosted locally, using a computer with Intel CPU.
Configuration directory is tools/ci/.
Jenkinsfile
: Compile and test SHV, similar to Circle CIJenkinsfile_xmhf
: Compile and test running SHV in XMHF64.
- https://sourceforge.net/p/bochs/bugs/1460/
- Fixed by Stanislav Shwartsman in Bochs commit
6b48d6e3
(Jan 2023) - Before, around
EXP_BOCHS_MASK = 0x27d0b75f
is stable - After, around
EXP_BOCHS_MASK = 0x7ff3fffe
is stable - At least fixes experiments 7, 16, 28 (consistently fails before fix) TODO
- Fixed by Stanislav Shwartsman in Bochs commit
- Complete README
- Report "#### VirtualBox Problems"
- Report Bochs bug on experiment 18, 19