Vamos construir um hipervisor com KVM
28/08/2025

Existem vários desafios comuns na lista de desejos de um desenvolvedor de software:
- compilador/interpretador
- sistema operacional
- analisador de expressões regulares
- servidor HTTP
- Motor gráfico/jogo OpenGL (provavelmente Vulkan agora)
- deixe um LED piscar
Recentemente, recebi outro que merece mais atenção:
Construa seu próprio hipervisor e inicialize uma máquina virtual.
O que parece um projeto gigantesco é na verdade bastante viável usando a API KVM do Linux. No final deste post você estará executando seu próprio bootloader dentro de uma VM. Use matriz se quiser apenas brincar.
Conteúdo
Virtualização
Emule hardware usando software para executar sistemas operacionais como em hardware real. Uma instância de um sistema emulado é chamada de máquina virtual ou, abreviadamente, VM. A máquina real na qual ocorre a virtualização é chamada de host, enquanto o sistema emulado é chamado de convidado. Hypervisor é o software que fornece a máquina virtual.
Os sistemas operacionais podem dizer se estão apenas em uma máquina virtual? Teoricamente não, já que o hardware emulado deveria se comportar exatamente como sua contraparte real. Na prática o convidado pode usar cpuid ou dmi, por exemplo via systemd-detect-virt. Sua fonte mostra claramente como isso funciona para cada arquitetura.
O problema da emulação é a camada de tradução, que diminui o desempenho. É aqui que entra a paravirtualização.
Paravirtualização
Embora a emulação envolva a reconstrução de hardware dentro do software, a paravirtualização é exatamente o oposto: passar o hardware real para o convidado.
Isso também acelera muito o desempenho de uma máquina virtual, enquanto a camada de tradução não é mais necessária.
A máquina virtual ainda está isolada e o kernel concede a ela suas próprias regiões de memória e CPUs virtuais.
API KVM
A API Kernel Virtual Machine (KVM, abreviadamente) fornece um conjunto de funções para criar e operar em máquinas virtuais. A API pode ser acessada via /dev/kvm usando ioctl operações.
As principais etapas resumidas:
- abrir
/dev/kvme compare a versão - unidade de processamento: criar VM com vcpu
- obter estrutura de status de execução
- alocar região de memória e carregar imagem inicializável
- inicializar registros e segmentos
- execute VM e responda às solicitações de convidados (como IO ou desligamento)
- faça 6. repetidamente até que o convidado queira parar
Implementação
Como de costume hoje em dia, escolhi Golang. A implementação segue aproximadamente as etapas resumidas da seção anterior.
A programação completa você encontra no final.
Requisitos
- host deve suportar kvm:
- verificar:
/dev/kvmexiste, o módulo do kernel kvm existe (lsmod|grep kvm) - ambientes virtualizados, como servidores em nuvem, provavelmente não suportam isso
- verificar:
- anfitrião x86
- conjunto de ferramentas go (testado com go versão 1.24.0 e plataforma linux_amd64)
- Cópia
ioctl.godo apêndice. É minha versão portada doinclude/asm-generic/ioctl.hdiretivas usadas para todos os consts KVM definidos eminclude/linux/kvm.h.
Acesse a API KVM
Abra o dispositivo kvm e verifique a versão da ABI, que precisa ser 12 ou superior:
package main
import (
"fmt"
"os"
"syscall"
)
const (
// latest stable abi version
KVM_VERSION = 12
)
var (
// linux/kvm.h
KVMIO = uintptr(0xAE)
KVM_GET_API_VERSION = uintptr(_IO(KVMIO, 0x00)) // -> 0xAE00
)
func main() {
fd, err := syscall.Open("/dev/kvm", os.O_RDWR, 0600)
if err != nil {
fmt.Println("open:", err)
return
}
defer func() {
if err := syscall.Close(fd); err != nil {
fmt.Println("close:", err)
}
}()
kvmVersion, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(KVM_GET_API_VERSION), uintptr(0))
if errno != 0 {
fmt.Println("ioctl:", errno)
return
}
if kvmVersion == KVM_VERSION {
fmt.Printf("kvm version %d supported\n", kvmVersion)
} else {
fmt.Printf("kvm version %d is too old, need %d or newer\n", kvmVersion, KVM_VERSION)
}
}
- em caso de sucesso, o programa acima deve imprimir algo como
kvm version 12 supportd - a versão 12 é a primeira versão estável da ABI e não mudou desde 2007, isso pode ser ignorado em sistemas modernos
- o
KVM_GET_API_VERSIONo código da operação vem do arquivo de cabeçalho kvm, usaremos mais dele posteriormente - como a API é exposta na API do sistema de arquivos do Linux, usamos descritores de arquivo para referências. Este não é o caminho certo e precisamos interpretar o número inteiro como um ponteiro (
uintptr) sempre que fazemos um syscall. - o syscall ioctl leva três argumentos:
- descritor de arquivo do dispositivo no qual queremos operar
- o código de comando
- argumento específico do comando ou nil se nenhum for necessário.
Crie uma VM com VCPU
Usando a referência kvm, agora podemos criar uma VM seguida por um vcpu:
package main
import (
"fmt"
"os"
"syscall"
)
const (
KVM_VERSION = 12
)
var (
// linux/kvm.h
KVMIO = uintptr(0xAE)
KVM_GET_API_VERSION = uintptr(_IO(KVMIO, 0x00)) // -> 0xAE00
KVM_CREATE_VM = uintptr(_IO(KVMIO, 0x01))
KVM_CREATE_VCPU = uintptr(_IO(KVMIO, 0x41))
)
func main() {
kvm, err := syscall.Open("/dev/kvm", os.O_RDWR, 0600)
if err != nil {
fmt.Printf("can't open kvm device: %s\n", err)
return
}
defer syscall.Close(kvm)
// check for stable kvm abi, needs to be >=12
kvmVersion, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(kvm), KVM_GET_API_VERSION, 0)
if errno != 0 {
fmt.Printf(
"errror getting kvm api version %d\n",
errno,
)
return
}
if kvmVersion != KVM_VERSION {
fmt.Printf(
"kvm version %d is too old, need %d or newer\n",
kvmVersion,
KVM_VERSION,
)
return
}
vm, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(kvm), KVM_CREATE_VM, 0)
if errno != 0 {
fmt.Printf(
"errror creating vm %d\n",
errno,
)
return
}
defer syscall.Close(int(vm))
vcpu, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(vm), KVM_CREATE_VCPU, 0)
if errno != 0 {
fmt.Printf(
"errror creating vcpu %d\n",
errno,
)
return
}
defer syscall.Close(int(vm))
fmt.Printf("kvm-fd=%d vm-fd=%d vcpu-fd=%d\n", kvm, vm, vcpu)
}
- adicionamos mais dois códigos de operação
KVM_CREATE_VMeKVM_CREATE_VCPUpara criar um vm e vcpu respectivamente - a VM é criada usando a referência kvm, enquanto o vcpu é criado com a referência VM. Também poderíamos criar várias VMs ou vários vcpus por VM aqui.
- verificação: o programa gera algo como
kvm-fd=3 vm-fd=4 vcpu-fd=5
Estrutura de execução do mapa
O status da máquina virtual está contido no arquivo kvm_run estrutura. Isto é especialmente necessário quando a VM interrompe e solicita que o host faça algo (como IO). Antes de executar a VM, precisamos mapear a estrutura no espaço do usuário:
package main
import (
"fmt"
"os"
"syscall"
)
const (
KVM_VERSION = 12
)
var (
// linux/kvm.h
KVMIO = uintptr(0xAE)
KVM_GET_API_VERSION = uintptr(_IO(KVMIO, 0x00)) // -> 0xAE00
KVM_CREATE_VM = uintptr(_IO(KVMIO, 0x01))
KVM_GET_VCPU_MMAP_SIZE = uintptr(_IO(KVMIO, 0x04))
KVM_CREATE_VCPU = uintptr(_IO(KVMIO, 0x41))
)
type kvm_run struct {
_ (8)byte
exitReason uint32
}
func main() {
// open kvm and check version...
// create vm, vcpu
// map kvmrun into user-space memory
kvmRunSize, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(kvm), c, 0)
rawKvmRun, err := syscall.Mmap(
int(vcpu),
0,
int(kvmRunSize),
syscall.PROT_READ|syscall.PROT_WRITE,
syscall.MAP_SHARED,
)
if err != nil {
fmt.Printf("error mapping kvm run: %s\n", err)
return
}
kvmRun := (*(*kvm_run)(unsafe.Pointer(&rawKvmRun(0))))
fmt.Println(kvmRun.exitReason)
}
- embora o tamanho da estrutura de execução seja bastante constante, nós a consultamos via
KVM_GET_VCPU_MMAP_SIZE *(*goType)(unsafe.Pointer(&bytes(offset))): truque sujo para converter um buffer de bytes em um tipo go. As coisas C habituais…- verificação: nenhum erro e o programa imprime motivo de saída 0
Adicionar memória
A seguir alocamos alguma memória para o convidado. Neste exemplo reservamos 1Mib (ou 100000 hex) e carregue o arquivo de imagem fornecido nele:
package main
import (
"fmt"
"os"
"syscall"
"unsafe"
)
const (
GUEST_MEMORY = 0x100000 // 1Mib
KVM_VERSION = 12
)
var (
// linux/kvm.h
// KVM_...
KVM_SET_USER_MEMORY_REGION = uintptr(_IOW(KVMIO, 0x46, unsafe.Sizeof(KVMUserspaceMemoryRegion{})))
)
// kvm_run ...
// kvm_userspace_memory_region from linux/kvm.h
type KVMUserspaceMemoryRegion struct {
Slot uint32
Flags uint32
GuestPhysAddr uint64
MemorySize uint64 /* bytes */
UserspaceAddr uint64 /* start of the userspace allocated memory */
}
func main(){
// expect image file as first argument
if len(os.Args) != 2 {
fmt.Printf("usage: %s \n" , os.Args(0))
return
}
// open kvm and check version...
// create vm, vcpu
// map kvmrun into user-space memory
// load first argument
image, err := os.ReadFile(os.Args(1))
if err != nil {
fmt.Printf("error loading image: %s\n", err)
return
}
// allocate guest memory
guestMemory, err := syscall.Mmap(
-1, // ignored because of MAP_ANONYMOUS
0,
GUEST_MEMORY,
syscall.PROT_READ|syscall.PROT_WRITE,
syscall.MAP_ANONYMOUS|syscall.MAP_PRIVATE,
)
if err != nil {
fmt.Printf("error allocating guest memory: %s\n", err)
return
}
// copy embedded image
copy(guestMemory, image)
memoryRegion := &KVMUserspaceMemoryRegion{
Slot: 0,
Flags: 0,
GuestPhysAddr: 0,
MemorySize: uint64(GUEST_MEMORY),
UserspaceAddr: uint64(uintptr(unsafe.Pointer(&guestMemory(0)))),
}
_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, uintptr(vm), KVM_SET_USER_MEMORY_REGION, uintptr(unsafe.Pointer(memoryRegion)))
if errno != 0 {
fmt.Printf("error setting memory region: %d\n", err)
return
}
}
- KVMUserspaceMemoryRegion declara a relação entre o espaço de endereço de memória host e convidado
- a memória do host começa no ponteiro da memória alocada
- guest começa em zero, que é o início de seu espaço de endereço, se adicionarmos múltiplas regiões poderíamos definir isso com outro valor
- o tamanho da memória é igual para ambos os espaços de endereço, pois este é um mapeamento 1:1 e se refere ao tamanho da memória alocada
KVM_SET_USER_MEMORY_REGIONadiciona uma região de memória à VM fornecida- verificação: nenhum erro
Inicialize os registros e segmentos
Antes de podermos executar a VM, os registros e segmentos de nossa vcpu precisam ser configurados corretamente. Isso vem junto com um monte de estruturas que precisam ser redefinidas em nosso programa go. Para completar, defino todos os campos, embora não precisemos de muitos.
const (
GUEST_MEMORY = 0x100000 // 1Mib
KVM_VERSION = 12
// linux/kvm.h
// ...
KVM_GET_REGS = uintptr(_IOR(KVMIO, 0x81, unsafe.Sizeof(KVMRegs{})))
// not needed but for sake of completeness
// KVM_SET_REGS = uintptr(_IOW(KVMIO, 0x82, unsafe.Sizeof(KVMRegs{})))
KVM_GET_SREGS = uintptr(_IOR(KVMIO, 0x83, unsafe.Sizeof(KVMSRegs{})))
KVM_SET_SREGS = uintptr(_IOW(KVMIO, 0x84, unsafe.Sizeof(KVMSRegs{})))
)
// KVM Structs (asm/kvm.h)
type KVMRegs struct {
RAX, RBX, RCX, RDX uint64
RSI, RDI, RSP, RBP uint64
R8, R9, R10, R11 uint64
R12, R13, R14, R15 uint64
RIP, RFlags uint64
}
// needed for sregs
type KVMSegment struct {
Base uint64
Limit uint32
Selector uint16
Tyype uint8
Present, DPL, DB, S, L, G, AVL uint8
Unusable uint8
_ uint8 // padding
}
// needed for sregs
type KVMDTable struct {
Base uint64
Limit uint16
_ (3)uint16 //padding
}
type KVMSRegs struct {
CS, DS, ES, FS, GS, SS KVMSegment
TR, LDT KVMSegment
GDT, IDT KVMDTable
CR0, CR2, CR3, CR4, CR8 uint64
Efer uint64
ApicBase uint64
InterruptBitmap ((256 + 63) / 64)uint64
}
func main(){
registers := KVMRegs{
RIP: 0,
RFlags: 0x2,
}
_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, uintptr(vcpu), KVM_SET_REGS, uintptr(unsafe.Pointer(®isters)))
if errno != 0 {
fmt.Printf("KVM_SET_REGS: %s\n", errno)
return
}
// get initial state of regs
var sregs KVMSRegs
_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, uintptr(vcpu), KVM_GET_SREGS, uintptr(unsafe.Pointer(&sregs)))
if errno != 0 {
fmt.Printf("KVM_GET_SREGS: %s\n", errno)
return
}
sregs.CS.Base = 0
sregs.CS.Selector = 0
_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, uintptr(vcpu), KVM_SET_SREGS, uintptr(unsafe.Pointer(&sregs)))
if errno != 0 {
fmt.Printf("KVM_SET_REGS: %s\n", errno)
return
}
}
- usamos 64 bits, indicado pelo
Rregistros RIP: ponteiro de instrução iniciado originalmente no deslocamento da memória do convidado, mas não sei por que porque o bootloader é copiado para o início da memória do convidadoRFlags: o bit reservado deve ser definido pela definição da arquitetura x86, limpá-lo causa um comportamento indefinido- sregs: descrito no artigo LWN embora eu realmente não tenha entendido
- verificação: bem, não há outro senão executá-lo com sucesso
Menor bootloader do mundo
var smallestBootloader = ()byte{0xf4}
Este bootloader interrompe imediatamente a CPU com o hlt instrução com Opcode x86 f4.
Após nossa primeira execução, entrarei em mais detalhes sobre bootloaders.
Insira a matriz
É hora de executar nossa VM!
const (
// ...
KVM_RUN = uintptr(_IO(KVMIO, 0x80))
// ...
)
func main(){
// ...
_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, uintptr(vcpu), KVM_RUN, 0)
if errno != 0 {
fmt.Printf("KVM_RUN: %s\n", errno)
return
}
fmt.Println(kvmRun.exitReason)
}
5 é o resultado que ansiamos. É o motivo da interrupção da saída.
Parabéns! Você acabou de executar sua primeira VM \o/
Razões de saída
Agora vamos dar uma olhada em como o convidado e o anfitrião se comunicam. O KVM_RUN O comando não retorna apenas quando a VM é interrompida. Há um monte de outros razõesmais notavelmente KVM_EXIT_REASON_IO.
Cada vez que a VM deseja acessar hardware real, KVM_RUN retorna e precisamos fornecê-lo. É aqui que entra em ação a emulação de dispositivos, como um console UART serial, VGA, um relógio em tempo real ou uma interface de rede.
Escreva o bootloader com nasm
O programa halt era básico o suficiente para que pudéssemos codificá-lo com opcodes. Para sermos flexíveis e escrevermos um bootloader mais sofisticado, codificaremos todos os próximos exemplos com assembler.
Como sempre eu uso o assembler netwide com sintaxe Intel. Usar nasm -f bin boot.asm -o boot.bin montar boot.asm em boot.bin. Usar -O0 para evitar otimizações caso você queira ver o código binário.
Para testar se tudo funciona, o seguinte bootloader apenas faz um loop e deixa a VM travar:
loop:
jmp loop
Imprimir a saída do convidado
Vamos apimentar e imprimir algo que o convidado nos enviou.
Felizmente, controlamos ambas as extremidades: hipervisor e convidado. Isso torna a comunicação muito mais fácil. Jogue os dados e escolha o porto de sua preferência. Eu vou com 0x42:
mov al, "#" ; set accumulator to the character we wanna print
mov dx, 0x42 ; set data register to the desired port
out dx, al ; let's print
hlt ; halt and exit guest
Agora nós corremos KVM_RUN em um loop e verifique o motivo da saída. Na parada fazemos a saída usual e no IO imprimimos os dados dos convidados:
const (
/...
KVM_EXIT_REASON_HALT = 5
KVM_EXIT_REASON_IO = 2
KVM_IO_IN = 0
KVM_IO_OUT = 1
)
type KVMRun struct {
_ (8)byte
exitReason uint32
_ (20)byte
details (256)byte
}
type KVMExitIO struct {
Direction uint8
Size uint8
Port uint16
Count uint32
DataOffset uint64
}
main(){
// ...
for {
_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, uintptr(vcpu), KVM_RUN, 0)
if errno != 0 {
fmt.Printf("KVM_RUN: %s\n", errno)
return
}
fmt.Printf("exit-reason=%d\n", kvmRun.exitReason)
for {
_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, uintptr(vcpu), KVM_RUN, 0)
if errno != 0 {
fmt.Printf("KVM_RUN: %s\n", errno)
return
}
switch kvmRun.exitReason {
case 0:
fmt.Println("sometimes things get complicated")
return
case KVM_EXIT_REASON_HALT:
return
case KVM_EXIT_REASON_IO:
ioDetails := (*KVMExitIO)(unsafe.Pointer(&kvmRun.details))
start := *(*()byte)(unsafe.Pointer(&kvmRun))
if ioDetails.Direction == KVM_IO_OUT && ioDetails.Port == 0x42 {
fmt.Println(string(start(ioDetails.DataOffset)))
}
default:
fmt.Println("unhandeled exit reason", kvmRun.exitReason)
}
}
}
- código
2indique uma solicitação io do convidado - estender o
KVMRunestrutura comdetailscontendo detalhes sobre a operação io solicitadadirection: indica se é uma entrada0ou saída1operaçãosize: tamanho do buffer para leitura/gravaçãoport: cada componente de hardware possui sua própria portacount: ??data offset: deslocamento a partir do endereço da estrutura kvm_run até os dados reais
Matriz
Consolidei tudo em uma matriz repo. Ele vem com um shell interativo para percorrer as invocações KVM_RUN e despejar os registros.
Visão
- driver básico: console serial, relógio em tempo real
- depurador de etapa única
- apoiar mais motivos de saída
Resumo
Construímos um hipervisor!
Está longe de ser utilizável na produção, mas é real. É um ponto de partida para adicionar mais dispositivos, códigos de saída e outras funcionalidades.
Fiquei surpreso com as etapas simples para inicializar e executar minha primeira VM com kvm. A inicialização do registro e do segmento, como o ponteiro inseguro, é um pouco contra-intuitivo para esquilos, mas necessário para fazer uso do KVM. O mecanismo kvm-run que retorna para cada chamada de host com um motivo de saída é bastante inteligente e claro.
Essa missão secundária me ajudou em meu trabalho diário com o Qemu a entender um pouco melhor a camada de virtualização de baixo nível. Pode haver alguma exceção enigmática de VM sobre a qual agora posso raciocinar melhor.
Discussão sobre HN
Apêndice
programa completo
Consolidar todos os trechos pode ser qualquer coisa, pois eles podem divergir conforme eu reescrevo os programas enquanto escrevi esta postagem no blog. O programa independente a seguir inicializa qualquer imagem e grava solicitações io da porta 0x42 na tela.
package main
import (
"fmt"
"os"
"syscall"
"unsafe"
)
const (
GUEST_MEMORY = 0x100000 // 1Mib
KVM_VERSION = 12
// linux/kvm.h
KVM_BASE = 0xAE00
KVM_GET_API_VERSION = KVM_BASE + 0x00
KVM_CREATE_VM = KVM_BASE + 0x01
KVM_GET_VCPU_MMAP_SIZE = KVM_BASE + 0x04
KVM_CREATE_VCPU = KVM_BASE + 0x41
KVM_RUN = KVM_BASE + 0x80
KVM_SET_USER_MEMORY_REGION = 0x4020ae46
KVM_SET_REGS = 0x4090ae82
KVM_GET_SREGS = 0x8138ae83
KVM_SET_SREGS = 0x4138ae84
KVM_EXIT_REASON_HALT = 5
KVM_EXIT_REASON_IO = 2
KVM_IO_IN = 0
KVM_IO_OUT = 1
)
// start of kvm_run from linux/kvm.h
type KVMRun struct {
_ (8)byte
exitReason uint32
_ (20)byte
details (256)byte
}
type KVMExitIO struct {
Direction uint8
Size uint8
Port uint16
Count uint32
DataOffset uint64
}
// kvm_userspace_memory_region from linux/kvm.h
type KVMUserspaceMemoryRegion struct {
Slot uint32
Flags uint32
GuestPhysAddr uint64
MemorySize uint64 /* bytes */
UserspaceAddr uint64 /* start of the userspace allocated memory */
}
// KVM Structs (asm/kvm.h)
type KVMRegs struct {
RAX, RBX, RCX, RDX uint64
RSI, RDI, RSP, RBP uint64
R8, R9, R10, R11 uint64
R12, R13, R14, R15 uint64
RIP, RFlags uint64
}
// needed for sregs
type KVMSegment struct {
Base uint64
Limit uint32
Selector uint16
Tyype uint8
Present, DPL, DB, S, L, G, AVL uint8
Unusable uint8
_ uint8 // padding
}
// needed for sregs
type KVMDTable struct {
Base uint64
Limit uint16
_ (3)uint16 //padding
}
type KVMSRegs struct {
CS, DS, ES, FS, GS, SS KVMSegment
TR, LDT KVMSegment
GDT, IDT KVMDTable
CR0, CR2, CR3, CR4, CR8 uint64
Efer uint64
ApicBase uint64
InterruptBitmap ((256 + 63) / 64)uint64
}
func main() {
if len(os.Args) != 2 {
fmt.Printf("usage: %s \n" , os.Args(0))
return
}
kvm, err := syscall.Open("/dev/kvm", os.O_RDWR, 0600)
if err != nil {
fmt.Printf("can't open kvm device: %s\n", err)
return
}
defer syscall.Close(kvm)
// check for stable kvm abi, needs to be >=12
kvmVersion, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(kvm), KVM_GET_API_VERSION, 0)
if errno != 0 {
fmt.Printf(
"errror getting kvm api version %d\n",
errno,
)
return
}
if kvmVersion != KVM_VERSION {
fmt.Printf(
"kvm version %d is too old, need %d or newer\n",
kvmVersion,
KVM_VERSION,
)
return
}
vm, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(kvm), KVM_CREATE_VM, 0)
if errno != 0 {
fmt.Printf(
"errror creating vm %d\n",
errno,
)
return
}
defer syscall.Close(int(vm))
vcpu, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(vm), KVM_CREATE_VCPU, 0)
if errno != 0 {
fmt.Printf(
"errror creating vcpu %d\n",
errno,
)
return
}
defer syscall.Close(int(vm))
fmt.Printf("kvm-fd=%d vm-fd=%d vcpu-fd=%d\n", kvm, vm, vcpu)
// map kvmrun into user-space memory
kvmRunSize, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(kvm), KVM_GET_VCPU_MMAP_SIZE, 0)
if errno != 0 {
fmt.Printf(
"errror getting kvm_run size %d\n",
errno,
)
return
}
fmt.Printf("kvm_run size: %d\n", kvmRunSize)
rawKvmRun, err := syscall.Mmap(
int(vcpu),
0,
int(kvmRunSize),
syscall.PROT_READ|syscall.PROT_WRITE,
syscall.MAP_SHARED,
)
if err != nil {
fmt.Printf("error mapping kvm run: %s\n", err)
return
}
kvmRun := /* skip *, we want a pointer! */ (*KVMRun)(unsafe.Pointer(&rawKvmRun(0)))
// read image
image, err := os.ReadFile(os.Args(1))
if err != nil {
fmt.Printf("error loading image: %s\n", err)
return
}
// allocate guest memory
guestMemory, err := syscall.Mmap(
-1, // ignored because of MAP_ANONYMOUS
0,
GUEST_MEMORY,
syscall.PROT_READ|syscall.PROT_WRITE,
syscall.MAP_ANONYMOUS|syscall.MAP_PRIVATE,
)
if err != nil {
fmt.Printf("error allocating guest memory: %s\n", err)
return
}
fmt.Printf("image: %x\n", image)
// copy embeded image
copy(guestMemory, image)
memoryRegion := &KVMUserspaceMemoryRegion{
Slot: 0,
Flags: 0,
GuestPhysAddr: 0,
MemorySize: uint64(GUEST_MEMORY),
UserspaceAddr: uint64(uintptr(unsafe.Pointer(&guestMemory(0)))),
}
_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, uintptr(vm), KVM_SET_USER_MEMORY_REGION, uintptr(unsafe.Pointer(memoryRegion)))
if errno != 0 {
fmt.Printf("error setting memory region: %s\n", errno)
return
}
registers := KVMRegs{
RIP: 0,
RFlags: 0x2,
}
_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, uintptr(vcpu), KVM_SET_REGS, uintptr(unsafe.Pointer(®isters)))
if errno != 0 {
fmt.Printf("KVM_SET_REGS: %s\n", errno)
return
}
// get initial state of regs
var sregs KVMSRegs
_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, uintptr(vcpu), KVM_GET_SREGS, uintptr(unsafe.Pointer(&sregs)))
if errno != 0 {
fmt.Printf("KVM_GET_SREGS: %s\n", errno)
return
}
sregs.CS.Base = 0
sregs.CS.Selector = 0
_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, uintptr(vcpu), KVM_SET_SREGS, uintptr(unsafe.Pointer(&sregs)))
if errno != 0 {
fmt.Printf("KVM_SET_REGS: %s\n", errno)
return
}
for {
_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, uintptr(vcpu), KVM_RUN, 0)
if errno != 0 {
fmt.Printf("KVM_RUN: %s\n", errno)
return
}
switch kvmRun.exitReason {
case 0:
fmt.Println("sometimes things get complicated")
return
case KVM_EXIT_REASON_HALT:
return
case KVM_EXIT_REASON_IO:
ioDetails := (*KVMExitIO)(unsafe.Pointer(&kvmRun.details))
start := *(*()byte)(unsafe.Pointer(&kvmRun))
if ioDetails.Direction == KVM_IO_OUT && ioDetails.Port == 0x42 {
fmt.Println(string(start(ioDetails.DataOffset)))
}
default:
fmt.Println("unhandeled exit reason", kvmRun.exitReason)
}
}
}
ajudante ioctl
A lógica ioctl para definir um comando kvm é um pouco difícil de ler. Acabei de portá-lo sem muita razão:
package main
const (
_IOC_NRBITS = uintptr(8)
_IOC_TYPEBITS = uintptr(8)
_IOC_SIZEBITS = uintptr(14)
_IOC_DIRBITS = uintptr(2)
_IOC_NRMASK = ((1 << _IOC_NRBITS) - 1)
_IOC_TYPEMASK = ((1 << _IOC_TYPEBITS) - 1)
_IOC_SIZEMASK = ((1 << _IOC_SIZEBITS) - 1)
_IOC_DIRMASK = ((1 << _IOC_DIRBITS) - 1)
_IOC_NRSHIFT = uintptr(0)
_IOC_TYPESHIFT = (_IOC_NRSHIFT + _IOC_NRBITS)
_IOC_SIZESHIFT = (_IOC_TYPESHIFT + _IOC_TYPEBITS)
_IOC_DIRSHIFT = (_IOC_SIZESHIFT + _IOC_SIZEBITS)
_IOC_NONE = uintptr(0)
_IOC_WRITE = uintptr(1)
_IOC_READ = uintptr(2)
)
func _IOC(dir, tyype, nr, size uintptr) uintptr {
return (((dir) << _IOC_DIRSHIFT) |
((tyype) << _IOC_TYPESHIFT) |
((nr) << _IOC_NRSHIFT) |
((size) << _IOC_SIZESHIFT))
}
func _IO(tyype, nr uintptr) uintptr {
return _IOC(_IOC_NONE, (tyype), (nr), 0)
}
func _IOR(tyype, nr, size uintptr) uintptr {
return _IOC(_IOC_READ, (tyype), (nr), size)
}
func _IOW(tyype, nr, size uintptr) uintptr {
return _IOC(_IOC_WRITE, (tyype), (nr), size)
}
func _IOWR(tyype, nr, size uintptr) uintptr {
return _IOC(_IOC_READ|_IOC_WRITE, (tyype), (nr), size)
}
func _IOC_DIR(nr uintptr) uintptr {
return (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
}
func _IOC_TYPE(nr uintptr) uintptr {
return (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
}
func _IOC_NR(nr uintptr) uintptr {
return (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
}
func _IOC_SIZE(nr uintptr) uintptr {
return (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)
}
func IOC_IN() uintptr {
return (_IOC_WRITE << _IOC_DIRSHIFT)
}
func IOC_OUT() uintptr {
return (_IOC_READ << _IOC_DIRSHIFT)
}
func IOC_INOUT() uintptr {
return ((_IOC_WRITE | _IOC_READ) << _IOC_DIRSHIFT)
}
func IOCSIZE_MASK() uintptr {
return (_IOC_SIZEMASK << _IOC_SIZESHIFT)
}
func IOCSIZE_SHIFT() uintptr {
return (_IOC_SIZESHIFT)
}
Referências
Glossário
- VM: máquina virtual, plataforma de hardware isolada (para)virtualizada, convidado se comporta como se estivesse em hardware real
- hipervisor: gerencia e opera máquinas virtuais
- convidado: o sistema operacional dentro de uma máquina virtual
- host/bare-metal: máquina física
- vcpu: CPU virtual
- API KVM: API Kernel Virtual Machine do Linux para paravirtualizar sistemas
+rv=tch1b0,ai
