During recent research in developing NoirVisor, I find it exceedingly difficult to develop without using sufficient debugging facilities (i.e.: It is very unstable to print debug messages using DbgPrint
routines provided by Windows Kernel). As such, certain measures must be taken in order to help debugging. QEMU provides a very neat peripheral device, called the ISA-DebugCon
, dedicated for debugging purpose. This blog goes over how to setup QEMU+Linux-KVM for hypervisor development.
You will need to do the following preparations:
- Setup a dedicated Linux host machine on which QEMU and KVM will be running.
- Choose a suitable distribution for your Linux host. I recommend using Linux with kernel of latest major version. (e.g.: In 2024, I would suggest 6.x version of Linux kernel like Ubuntu 24.04 LTS)
- Development machine that will run your code editor, terminal (e.g.: PuTTY) and debugger (e.g.: WinDbg).
- You may try WSL2 and WSLg in order to merge the Linux host and development machine. This is optional. However, your virtual machine hosted under KVM will be running on L2.
Create the Virtual Machine
This blog uses Debian as example. Installation of QEMU might differ among distributions but the way of running it should be the same.
First of all, install QEMU.
sudo apt install qemu-system ovmf
Then create a disk image for your virtual machine.
qemu-img create -f raw disk.img 40G
You may customize the size of your image. The command I provided will create a 40G image with raw format, meaning that you may analyze the disk by directly using binary viewer tools. You may also choose other formats if you want your disk image to be compatible for other virtual machine products (e.g.: specify vhdx
for Hyper-V or vmdk
for VMware) but you will have to decode their formats or even decrypt them in order to read the contents of the disk image.
Next, create a bash script file so that running QEMU will be easier. The base code of the script should be:
qemu-system-x86_64 -accel kvm -cpu host,hypervisor=off,svm=on -smp 2 -m 4096 -bios OVMF.fd -drive format=raw,file=disk.img -vga qxl -audio pa,model=hda -nic model=e1000e -device usb-ehci -device usb-mouse -device usb-tablet
qemu-system-x86_64
is the program that QEMU emulates an x86_64 system.-accel kvm
specifies the KVM accelerator.-cpu host,hypervisor=off,svm=on
will let the guest know that the CPU model is the same to the host, that hypervisor does not appear in CPUID, and nested AMD-V is available.
Note that if you use Intel machines, thesvm=on
should be replaced withvmx=on
.-smp 2
specifies two cores for the VM.-m 4096
specifies 4096 MB of RAM for the VM.-bios OVMF.fd
specifies OVMF (Open Virtual Machine Firmware, an open-source UEFI firmware implementation by TianoCore) as the firmware for the guest.-drive format=raw,file=disk.img
specifies the virtual disk for the guest.-vga qxl
specifies the QXL graphics card for the guest.-audio pa,model=hda
specifies the Intel HD Audio card for the guest.-nic e1000e
specifies the Intel E1000e NIC for the guest. This NIC supports KDNET. You may make use of it to debug your Windows Driver.-device usb-ehci
specifies an EHCI USB Bus Controller.-device usb-mouse
specifies a USB mouse device.-device usb-tablet
specifies a USB tablet device.
At this point, the virtual machine is created successfully. Assuming the script file is saved to runvm.sh
, you may execute the script to start it:
bash runvm.sh
Install Windows
This blog will not talk about the fool-proof method of installing Windows. It is reader’s responsibility to acquire the installation image and do the installation.
You will need to add your installation image to your guest by adding the following parameter:
-cdrom Windows-Installer.iso
After you installed Windows, install the SPICE-Guest Tools. This is similar to the VMware Tools so that the experience of using QEMU is significantly improved.
You may also follow Microsoft’s guide to setup KDNET.
Remote Access to QEMU
You can use the virt-viewer to remotely access VM in QEMU if your QEMU is running on a remote Linux host. You will have to append the following arguments to QEMU:
-spice addr=0.0.0.0,port=3001,disable-ticketing=on,disable-agent-file-xfer=off -device virtio-serial -chardev spicevmc,id=vdagent,debug=0,name=vdagent -device virtserialport,chardev=vdagent,name=com.redhat.spice.0 -chardev spiceport,id=spicechannel1,name=org.spice-space.webdav.0 -device virtserialport,nr=2,chardev=spicechannel1,name=org.spice-space.webdav.0
Change the port=3001
part if you need to do so (e.g.: port is occupied by other software).
Also, change the audio part:
-audio spice,model=hda
Or otherwise the sound from your guest will come out from your Linux host’s machine, rather than your workstation.
Then run the virt-viewer you just installed on your workstation. Input spice://kvmhost:3001
to connect to your QEMU VM. If you are using a different port, adjust accordingly. After you installed the SPICE guest tools in QEMU VM, you will be able to drag’n’drop files into the VM. Unfortunately, all files go to the desktop, and you can’t drag files out of your VM. The clipboard also works.
For some strange reasons that I don’t know, Windows Update can sometimes kill the SPICE guest tools so that your mouse will disappear and keyboard won’t react. Therefore, you must disable Automatic Updates and never update your guest system.
Debug Your UEFI Program
After you compiled your UEFI program, you may pack it into a disk image. You may use GNU mtools to create a disk image. You may download my precompiled mtools
for Windows and add them into PATH
. But in Linux, you may install mtools
directly. For example, you may use apt
in Debian or Ubuntu:
sudo apt install mtools
Then create the disk images:
dd if=/dev/zero of=disk.img bs=1k count=1440
mformat -i disk.img -f 1440 ::
mmd -i disk.img ::/EFI
mmd -i disk.img ::/EFI/BOOT
mcopy -i disk.img bootx64.efi ::/EFI/BOOT
You may use mcopy
command to copy more files into the disk image. Note that this image is a 1.44M floppy disk image. You may want to extend this size as situation requires.
If you are using Windows (not WSL), please replace dd
command with fsutil
:
fsutil file createNew disk.img 1474560
mformat -i disk.img -f 1440 ::
mmd -i disk.img ::/EFI
mmd -i disk.img ::/EFI/BOOT
mcopy -i disk.img bootx64.efi ::/EFI/BOOT
You may use any method to send your disk image to your remote Linux host (e.g.: the scp
method). Then run your UEFI image:
qemu-system-x86_64 -accel kvm -bios OVMF.fd -drive format=raw,file=disk.img
You can add extra arguments as needed. For example, you may add extra arguments specified in the following chapters to help debugging.
The ISA-DebugCon Peripheral Device
As we mentioned, the reason we use QEMU is the ISA-DebugCon
device it provides. The principle of this device is very simple: as you read from or write to this device, the request will be transferred to a specific stream device (e.g.: files, standard I/O, TCP connection, named pipe, etc.) It is even easier to use than the serial port (you don’t have to configure baud rate, parity, etc.) In addition, this device won’t occupied by the operating systems (in contrast, you can’t directly use I/O instructions to operate serial port after Windows is booted.)
To use ISA-DebugCon
in QEMU, I recommend to append the following parameters:
-chardev socket,id=debugger,port=23456,host=0.0.0.0,server=on,telnet=on -device isa-debugcon,iobase=0x402,chardev=debugger
This will make the ISA-DebugCon
using TCP connection through Telnet protocol and listen on 23456 port. You may use PuTTY on your development machine to see the output. As you start QEMU, it will hang in order to wait for the TCP connection to be established. After you connect PuTTY to QEMU, the VM will start booting. Considering that we are using 23456 port and Telnet protocol, you will have to specify them on PuTTY Session configurations. In addition, go to Terminal configurations and check on “Implicit CR in every LF” so that line-feeds will make the cursor jump to the start of the next line. It is also recommended to configure your Linux host with a static IP address on your router so that you can enter the session without repeating the configuration efforts. By the way, if you use the default firmware (SeaBIOS) of QEMU and the I/O address of it is on 0x402, you can view SeaBIOS’ debug output.
If you are using WSL2 but that’s not your development machine, you will have to configure network bridging since servers in WSL2 can only respond inside the Hyper-V network. Therefore, you don’t have to configure network bridging if you are using WSL2 in your development machine.
Using ISA-DebugCon
There is no standard method to detect this device. Therefore, you will have to save the I/O address somewhere else (it can be Registry or even hard-code into your program). To operate this device, we should use the I/O instructions. See the __outbyte
documentations from MSDN.
To output some data to the device, it’s pretty simple:
void DebugWriteUnsafe(IN PBYTE Buffer,IN SIZE_T Length)
{
for(SIZE_T i=0;i<Length;i++)
__outbyte(0x402,Buffer[i]); // Use your actual port number!
}
The reason it’s unsafe is because there are race conditions among different cores. Without a lock, simultaneous output will render your output being mixed together, causing the outputted content chaotic and hard to read. As such, a lock is required. Considering the extreme context we will be using this device, a spin-lock is required. However, you can also be lazy: just add as many ISA-DebugCon devices as the CPU cores of the VM so that race condition is mitigated.
However, this device can only be used as a debug-printer. Albeit you can write to it in order to output your debug messages, any reads from it will only return byte 0xE9
. This is this device’s signature. In other words, this device can’t be used as an interactive debugger. This characteristic can assist detecting ISA-DebugCon device, but it’s only necessary – not sufficient – to prove a port is used for ISA-DebugCon. In other words, if a byte read from the port is 0xE9, you can’t confirm it must be ISA-DebugCon device (not a sufficient condition), but if that byte is not 0xE9, you can confirm it’s absolutely not ISA-DebugCon device (a necessary condition).
You may use __outbytestring
macro in order to remove the for-loop. However, please note that the future x86-S architecture will deprecate the string I/O instructions, so it’s not recommended for the sake of compatibility.
Using GDB
It is recommended to enable QEMU monitor regardless of whether you will be using GDB or not. If you will be using SPICE so that you can operate QEMU VM screen from a remote Windows workstation, you can append the following parameters to enable QEMU Monitor:
-monitor tcp:0.0.0.0:3002,server
You will need to use a telnet client (e.g.: PuTTY) to connect to QEMU Monitor console. In QEMU Monitor, you can operate the VM state. To enable GDB server, enter the following command:
gdbserver tcp:0.0.0.0:3003
The best way to use GDB in Windows is probably via IDA. However, that’s a feature from IDA Pro version. IDA Freeware cannot attach to a remote GDB session. VisualGDB is not free either. So, let’s just use GDB itself. To use GDB on Windows, we may run it on WSL, or msys2 if your Windows is too old to run WSL. Simply type gdb
to run GDB. In GDB’s console, run the command:
tar rem kvmhost:3003
Replace “kvmhost
” with the address of your KVM host machine. Adjust the port number if you are using a different one. Listed here are some simple commands you should know when you debug VM in QEMU.
c
command can continue the guest if it is suspended due to debug event.- While the VM is running, press
Ctrl+C
to break. b
command can add a software breakpoint. You should usehb
command to add a hardware breakpoint instead, because OS in the guest will catch the software breakpoint.
If you are using an address directly, make sure you put an asterisk before your address (e.g.:hb *0x12345678
).x
command can view memory contents. The format is “x /fmt addr
“. For example:x /4i addr
can view 4 instructions specified ataddr
.x /8x addr
can view 8 integers in hexadecimal specified ataddr
.- You can check GDB’s documentation about the format specifier.
thread n
command can switch the CPU you are running on. Then
is the index of which you are switching to the target CPU.set disassembly-flavor intel
command will let GDB disassemble instructions into Intel’s assembly syntax. In my opinion, the AT&T’s assembly syntax is too horrible to read.- GDB has no innate command to view memory via physical address. However, QEMU provides a way to toggle how GDB uses the address. Please note the letter cases in the following commands.
maintenance packet qqemu.PhyMemMode
command can check what mode you are using.maintenance packet Qqemu.PhyMemMode:1
command will let GDB use physical address. You should seereceived: "OK"
as reply.maintenance packet Qqemu.PhyMemMode:0
command will let GDB use virtual/normal address. You should seereceived: "OK"
as reply.- These commands remain in effect until the VM is terminated. Therefore, you must check what mode you are using before you are checking memory.
tui enable
command will let GDB use Text User Interface (TUI) so that you will be able to do source-level debugging intuitively. You may usegdb -tui
command in system console to let GDB enter TUI mode directly. GDB cannot use PDB symbols, unfortunately. It would be nice if there are PDB to DWARF converters.
You may write your own debugger by using GDB protocol so that PDB symbols can be supported. The specification of remote GDB protocol is available online.
Using GDB could be particularly useful when you are going to debug or even reverse engineer proprietary software components such as PatchGuard.
Summary
This blog introduced the method of using QEMU+Linux-KVM and installing debugging environment for Windows Kernel and most importantly – the usage of ISA-DebugCon
peripheral device. This blog also briefly described about using GDB to debug QEMU guest.