To compile and run lvisor, you need to set up the toolchain, as described in the tools guide.
You’ll find the Intel SDM, Vol 3 useful for exercises and projects, especially the virtualization part, Vol 3C.
The course Git repository is on the CSE GitLab. Follow the instructions there to set up your ssh keys if you haven’t.
You need to clone the lvisor repository, by running the commands below.
You have read access to this repository, but not write. We recommend creating a fork of this repo to keep any changes you make to lvisor.
Note that lvisor is a minimal hypervisor: it supports only one guest OS. You are encouraged to extend lvisor, such as to support running Linux as a guest OS or multiple guests at the same time.
From the root directory of lvisor, you will see the following subdirectories:
boot
: boot configuration & tools;firmware
: code for bootstrapping a guest kernel;include
: headers;kernel
: common code shared by lvisor and the lv6 kernel;lib
: utility functions for manipulating and printing strings;tests
: guest OSes, currently xv6, lv6, and Linux;vmm
: the main component of lvisor.The config.def
file contains the default configurations. You can either
edit these directly, or create a new file named config.mk
under the same directory
and override these variables.
Run make to build lvisor:
There are two commands to run lvisor and guest OS kernels:
make qemu will run lvisor in QEMU, along with the kernel specified
in your configuration as the guest OS. By default it will run the lv6 kernel.
To run xv6 instead, try make qemu KERNEL=tests/xv6/kernelmemfs or add
KERNEL=tests/xv6/kernelmemfs
to your config.mk
.
make qemu-kernel will run the guest OS directly in QEMU, without using the VMM. This is sometimes useful for debugging. Similarly, this will run lv6 by default.
Let’s start with lv6, a minimal x86-64 OS.
Browser the source code under tests/lv6/
:
head.S
: set up the 64-bit mode;main.c
: initialize the kernel and user space;syscall.c
: the system call table;user.S
: user space code invoking two system calls.First, let’s start the lv6 directly without lvisor:
You should see that lv6 prints out timestamps and two system calls. This is similar to OS kernels you have seen from CSE 451.
To quit QEMU, type Ctrl-a x.
What are the names of two system calls that print out “hey 481” and “bye 451”?
You may have noticed the timestamps printed out at the beginning of each line,
which represent the number of seconds (plus microseconds) since booting.
They are calculated by the uptime()
function in kernel/tsc.c
,
using the rdtsc
instruction.
Read the source code of uptime()
(no need to read the calibration code)
and briefly describe how it works.
Now, let’s run lv6 again, but this time as a guest OS of lvisor:
If you are running this in a terminal, the output consists of three parts, in three different colors.
Why do you think the two tsc:
lines appear twice?
Hint: check where and how many times the tsc_init()
function has
been called.
To run xv6 as the guest OS,
repeat the above two commands, but this time with KERNEL=tests/xv6/kernelmemfs
.
Make sure you see the following output:
Currently your lvisor is unable to run Linux as a guest OS yet. Check the project ideas if you’re interested in extending lvisor in this direction.
Recall that an OS kernel responds to user space requests through trap handlers: Upon system calls, exceptions, or interrupts, the CPU traps into the kernel from user space, the kernel then dispatches the request to specific handlers (e.g., invoking a system call or the page fault handler), and returns back to user space.
A hypervisor works in a similar way. The CPU normally executes the guest OS, such as lv6. Under certain conditions, the CPU traps into the hypervisor (in our case, lvisor), which we call a VM-exit. The hypervisor dispatches the request to a VM-exit handler, which can emulate instructions, inspect or modify the state of the guest OS, etc. Once the VM-exit handler is finished, the hypervisor returns to the guest OS, which we call a VM-entry.
We will explore this workflow by adding a VM handler to lvisor.
As a starting point, since lv6 uses rdtsc
to calculate timestamps,
let’s extend lvisor to “virtualize” time in lv6
by intercepting rdtsc
and modifying the result.
First, we need to configure lvisor to enable RDTSC exiting.
In other words, whenever the guest OS executes the rdtsc
instruction,
we want the CPU to perform a VM-exit to trap into the hypervisor.
Skim the sections 24.6.2: Processor-Based VM-Execution Controls
and 25.1.3 Instructions That Cause VM Exits Conditionally
of the Intel SDM.
Also take a look at the beginning of the include/asm/vmx.h
file.
You’ll get a basic idea of how to set up RDTSC exiting in a hypervisor.
Now set up RDTSC exiting in lvisor:
find setup_vmcs_config()
in vmm/vmx.c
,
and add the CPU_BASED_RDTSC_EXITING
flag to min
:
With this one-line change, run make qemu again. This time
the CPU will perform a VM-exit once it hits the rdtsc
instruction.
However, since we don’t have a VM-exit handler for RDTSC exiting yet in lvisor,
it will stop execution and dump the following information:
Notice “exit reason 16” in the output. Look it up in “Append C: VMX Basic Exit Reasons” of the Intel SDM. What is it? You don’t need to write down the answer for this question.
To handle RDTSC exiting, find vmx_exit_handlers
in vmm/vmx.c
,
and add a VM-exit handler as follows:
This change has two parts: a function called handle_rdtsc()
and an entry in vmx_exit_handlers
that dispatches to the function.
Read the source code of kvm_write_edx_eax()
and try to understand what it does.
Run make qemu again. Make sure your see the same output from lv6 as before adding RDTSC exiting.
Now you have a complete VM-exit handler! In summary, the workflow is the following:
Now let’s have some fun.
Add a negation to the result of rdtsc()
(i.e., uint64_t val = -rdtsc();
)
and run make qemu. Do you notice any difference in the output?
How about change it to val = rdtsc() * 10
?
Modify vmm/vmx.c
in lvisor to make the timestamps printed by lv6 100 times larger.
This completes the exercise. In answers.txt
,
write up your answers to the questions (no need to submit the code).
Upload the file through Canvas.
If you have done the challenge problem, include a diff in answers.txt
highlighting your changes to lvisor.