Vamos construir um hipervisor com KVM

Vamos construir um hipervisor com KVM


28/08/2025

Vamos construir um hipervisor com KVM

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:

  1. abrir /dev/kvm e compare a versão
  2. unidade de processamento: criar VM com vcpu
  3. obter estrutura de status de execução
  4. alocar região de memória e carregar imagem inicializável
  5. inicializar registros e segmentos
  6. execute VM e responda às solicitações de convidados (como IO ou desligamento)
  7. 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/kvm existe, o módulo do kernel kvm existe (lsmod|grep kvm)
    • ambientes virtualizados, como servidores em nuvem, provavelmente não suportam isso
  • anfitrião x86
  • conjunto de ferramentas go (testado com go versão 1.24.0 e plataforma linux_amd64)
  • Cópia ioctl.go do apêndice. É minha versão portada do include/asm-generic/ioctl.h diretivas usadas para todos os consts KVM definidos em include/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_VERSION o 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_VM e KVM_CREATE_VCPU para 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_REGION adiciona 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(&registers)))
  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 R registros
  • 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 convidado
  • RFlags: 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 2 indique uma solicitação io do convidado
  • estender o KVMRun estrutura com details contendo detalhes sobre a operação io solicitada
    • direction: indica se é uma entrada 0 ou saída 1 operação
    • size: tamanho do buffer para leitura/gravação
    • port: cada componente de hardware possui sua própria porta
    • count: ??
    • 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(&registers)))
	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



Source link

Postagens Similares

Deixe um comentário

O seu endereço de email não será publicado. Campos obrigatórios marcados com *