The Virtual Machine Monitor Basic

[0x00] Concept

Virtualization Technology(VT) 는 매우 오래된 기술이며 현재까지 매우 발전되어 왔습니다. 해당 기술에 대해 공부하면 낯선 용어들과 개념을 매우 많이 마주치게 됩니다.

컴퓨터 공학에 대해 자세하게 공부한 사람이 아니라면 해당 부분에서 막히게 됩니다. 본인도 마찬가지로 해당 부분에서 여러 번 좌절하며 해당 포스팅을 작성하였습니다.

하나의 원시적인 기술에 대해 접근하기 위해서는 왕도가 없습니다. 천천히 집중하길 권장드립니다. 예제 코드의 경우 지속적으로 변경되므로 최종장에 있는 내용을 참조 바랍니다.

Intel64 and IA-32 Architectures Software Developer Manuals 의 문서와 Hypervisor From Scratch 자습서를 참조하였습니다.

이번 내용에서는 코드와 함께 간단히 구현해보고 이에 대해 이해하는 내용으로 채워집니다.

해당 문서의 Vol.3C(page 31-4) 에 있는 VMM Setup & Tear Down 의 내용을 보면 아래와 같은 내용이 존재하며 이는 해당 글에서 다루게 될 내용의 요약입니다.(2022.04 업데이트 문서에서는 제거됨)

[0x01] Term

[-] Intel Virtualization Extensions(VMX)

인텔의 가상화 기술은 가상화를 지원하는 Extension 을 제공합니다. 이 확장 기술은 VMX(Virtual Machine Extensions) 이라고도 합니다. VMX 는 가상 머신을 관리하는 데 사용되는 새로운 시스템 소프트웨어인 VMM(Virtual Machine Monitor) 에 대한 프로그래밍 인터페이스를 제공합니다.

[-] Virtual Machine Monitor(VMM)

일반적으로 VMM 을 이해하기 가장 쉬운 것은 현재 출시되어 있는 다양한 가상화 소프트웨어에서 Host 라는 개념으로 접근할 수 있습니다. 이에 대해 VMM 또는 HyperVisor 라고 합니다.

VMM 은 Host 역할을 하고, 프로세서와 다른 플랫폼 하드웨어를 모두 제어합니다. VMM 은 가상 프로세서의 추상화와 함께 Guest Software 를 제공하고 논리적 프로세서에서 직접 실행할 수 있도록 합니다.

VMM은 프로세서 리소스, 물리 메모리, 인터럽트 관리 및 I/O 에 대해 선택적 제어가 가능합니다.

[-] Guest Software

일반적으로 VMWare 에서 Guest 라는 개념으로 이해할 수 있습니다. 즉, 각각의 가상 머신(VM)이 Guest Software 환경이라고 말할 수 있습니다.

[-] Model-Specific Register

이 레지스터는 RDMSR, WRMSR 명령에 의해 처리됩니다. 해당 명령에 전달되는 피연산자는 Intel SDM 에 자세하게 정의되어 있습니다. 이러한 값을 이용하여 현재 프로세서에서 지원되는 기능을 확인할 수 있으며 또한 이를 설정할 수도 있습니다. 그 외에도 매우 다양한 용도로 활용됩니다.

[0x02] Virtualization Technology

가상화 기술이란, 말 그대로 해석하면 물리적으로 존재하지 않지만 논리적으로 존재하도록 만드는 기술이라고 이해하였습니다.

우리가 흔하게 접할 수 있는 VMWare, VirtualBox, Xen 등이 이러한 가상화 기술을 이용한 가상화 소프트웨어(VMM or HyperVisor)라고 할 수 있습니다.

이러한 VMM 의 가장 핵심이되는 목표는 Guest Software(VM) 이 마치 물리적인 하드웨어에서 동작하고 있다는 착각을 할 수 있도록 구현하는 것 입니다.

해당 목표를 이루고 나면 가상화 기술의 가장 큰 장점을 확인할 수 있습니다. 바로 단일의 VMM 안에서 여러 운영 체재가 동작하고, 하드웨어 및 리소스를 공유가 가능해집니다.

이러한 VMM 에는 두 가지의 형태가 존재합니다.

[-] Type 1(Native or Bare-Metal)

  • 호스트 하드웨어에서 직접 동작하고, 하드웨어를 제어하고 가상 머신을 관리합니다.
  • Native 또는 Bare-Metal 이라고 부릅니다.
  • Xen, Microsoft Hyper-V, VMware ESX/ESXi

[-] Type 2(Hosted)

  • 다른 소프트웨어와 같이 OS 내부에서 로드됩니다. VMM 은 운영체제를 거쳐야하고, OS에 의해 관리되기 때문에 퍼포먼스가 낮습니다.
  • Hosted 라고 부릅니다.
  • VMware WorkStation, VirtualBox

가장 많이 접할 수 있는 형태가 Type 2 의 형태입니다. Hosted 라는 의미에 대해서 깊이 찾아보진 않았지만 아마 다음과 같은 이유 때문이라고 생각합니다.

  • 실제 하드웨어와 연결된 호스트가 별도로 존재
  • 호스트(하드웨어와 직접 연결된)에서 VMM(Guest Software와 연결된 호스트) 동작을 하기 때문에 호스팅되었다는 의미

[0x03] VMX(Virtual Machine Extensions) Operation

VMXIntel 에서 가상화 지원을 위해 사용되는 확장 기술입니다. VMX Operation 이란 말 그대로, 가상화를 지원하기 위한 프로세서 작업 이라고 이해할 수 있습니다.

VMX OperationVMX Root OperationVMX Non-Root Operation 이라는 두 가지 작업 형태로 분류됩니다.

일반적으로 VMMVMX Root Operation 으로, Guest SoftwareVMX Non-Root Operation 으로 실행됩니다.

특징은 아래와 같습니다.

  • VMX Root Operation : 새로운 명령어 세트(VMX Instructions) 를 사용 할 수 있고, 특정 CR(Control Register) 에 로드할 수 있는 값이 제한적
  • VMX Non-Root Operation : 가상화를 용이하게 하기 위해 제한됩니다. 일반적인 동작 대신 특정 명령(VMCALL 등)과 이벤트로 인해 VMMVM Exit 가 발생.

현재 논리 프로세서가 VMX Non-Root Operation 에 있는지 여부를 확인할 수 있는 비트는 별도로 존재하지 않습니다. 그렇기 때문에 VMMGuest SoftwareVM 에서 실행 중인지 확인하지 못하도록 할 수 있습니다.

이 때 VMX Root Operation , VMX Non-Root Operation 간의 전환을 VMX Transitions 라고 합니다.

VMX Transitions 에는 전환되는 방향에 따른 두 가지의 종류의 명령이 존재합니다.

  • VM Entry : VMM(VMX Root Operation) 에서 Guest Software(VMX Non-Root Operation) 으로 전환
  • VM Exit : Guest Software(VMX Non-Root Operation) 에서 VMM(VMX Root Operation) 으로 전환

위의 내용을 토대로 기본적인 VMM 의 Life Cycle을 표현하면 아래와 같습니다.

  • VMXON 명령을 통해 VMM에 진입하고 VMX Operation 을 진행합니다.
  • VMMVMEntry 를 이용하여 Guest Software 에 진입할 수 있습니다. 이 때 한 번에 하나씩만 가능합니다.
  • VM ExitVMM에서 지정한 진입점(Exit Handler)으로 제어를 전달합니다. VMMVM Exit 의 원인에 따라 적절한 조치를 취하고, VM Entry를 통해 다시 가상머신으로 돌아갈 수 있습니다.
  • VMXOFF 명령을 통해 스스로 종료하고 VMX Operation 을 종료할 수 있습니다.

[-] Virtual Machine Control Structure(VMCS)

VMX Non-Root OperationVMX TransitionVMCS 라는 데이터 구조에 의해 제어됩니다.

VMCS에 대한 접근은 VMCS 포인터를 통해 관리됩니다.(논리 프로세서당 하나씩)

VMPTRSTVMPTRLD 명령을 이용하면 VMCS 포인터를 저장하고 로드할 수 있습니다. (VMPTR = VMCS Pointer, ST = Store, LD = Load)

VMREAD, VMWRITE, VMCLEAR 명령을 이용하면 VMCS 의 필드를 구성할 수 있습니다. 아래에서 좀 더 자세하게 다룹니다.

[0x04] Check Support For VMX & Enabling VMX Operation

[-] VMX Check

현재 시스템이 VMX 지원이 가능한지 확인이 필요합니다. CPUID 명령을 사용하여 VMX Operation 을 지원하는지 확인할 수 있습니다.

CPUID.1:ECX.VMX[bit 5] = 1 인 경우 지원이 가능합니다.

해당 값을 가져오기 위한 방법은 두 가지가 있습니다. 하나는 Asm 을 이용하는 것이고, 하나는 내장함수를 이용하는 방법입니다.

EAX에 가져올 CPU 정보의 값을 저장하여 cpuid 명령을 실행하게 되면, eax, ebx, ecx, edx 4개의 레지스터에 반환된 정보들의 값이 저장됩니다.

Intel SDM 에 따르면, eax == 1 인 경우 ecx 에 반환된 값의 5번째 비트를 확인하면 된다고 나와있습니다.


BOOLEAN ShHvSupport::IsVmxSupport()
{
	bool Result = false;
	CPUID CpuId = { 0, };
	CPU_FEATURES FeaturesFlags = { 0, };

	__cpuid((int*)&CpuId, 1);
	
	FeaturesFlags.Feature = CpuId.ecx;
	Result = _bittest64((LONG64*)&FeaturesFlags, 5);

	return Result;
}
; bool ShHvSupport::IsVmxSupportAsm()
xor    eax, eax
inc    eax
cpuid
bt     ecx, 5
xor    eax, eax
mov    eax, ecx
ret

[-] Enabling VMX

VMX Operation 을 시작하기 전에 CR4.VMXE[bit 13] = 1 으로 설정하여 VMX 를 활성화 할 수 있습니다. 위의 Life Cycle에 나와있는 것 처럼, VMXON 명령을 실행하면 VMX Operation 이 진행됩니다.

CR4.VMXE = 0 에서 실행되는 경우 Invalid-opcode exception(#UD) 가 발생합니다. 이는 VMXE 가 활성화되지 않으면 VMX 명령어 세트가 추가되지 않아서 발생되는 것으로 보입니다.

VMX Operation 중에는 CR4.VMXE 를 지울 수 없습니다. VMXOFF 명령을 이용하여 VMX Operation 을 종료할 수 있습니다.

VMXON 명령은 IA32_FEATURE_CONTROL_MSR(0x3A) 구조에 의해 제어됩니다. 이 MSR 이란 Model-Specific Register 로 CPU에서 제공하는 특수한 레지스터 입니다.

typedef struct _IA32_FEATURE_CONTROL
{
	union
	{
		ULONG64 Features;
		struct
		{
			ULONG64 Lock : 1;                    // 0
			ULONG64 EnableVMXinSMX : 1;          // 1
			ULONG64 EnableVMXoutSMX : 1;         // 2
			ULONG64 Reserved : 5;                // 3 - 7
			ULONG64 EnableLocalSENTER : 7;       // 8 - 14
			ULONG64 EnableGlobalSENTER : 1;      // 15
			ULONG64 Reserved_2 : 1;              // 16
			ULONG64 EnableSGXLaunchControl : 1;  // 17
			ULONG64 EnableGlobalSGX : 1;         // 18
			ULONG64 Reserved_3 : 1;              // 19
			ULONG64 LMCEOn : 1;                  // 20
			ULONG64 Reserved_4 : 43;             // 21 - 63
		}Fields;
	};
}IA32_FEATURE_CONTROL, *PIA32_FEATURE_CONTROL;

3개의 비트가 VMX 와 관련된 비트로 확인됩니다. (SMXSafe Mode Extensions 를 의미합니다.)

  • Bit 0 (Lock)
    • 해당 비트가 0인 상태에서 VMXON 명령을 실행하게 되는 경우와 GETSEC[SENTER] 가 실행되는 경우 General-Protection Exception(#GP) 가 발생합니다.
    • 만약 해당 비트가 설정된 상태에서 WRMSR 이 실행되면 #GP 가 발생합니다.
    • 해당 비트가 설정되면, 재설정 될 때까지 MSR 을 수정할 수 없습니다.
    • 해당 비트를 이용하여 BIOSVMX, SMX 또는 VMX, SMX 에 대한 지원을 비활성화하도록 설정 옵션을 선택 가능합니다.
  • Bit 1(Enable VMX in SMX)
    • 해당 비트는 SMX Operation(GETSEC의 SENTER 및 SEXIT 실행 사이) 에서 VMX를 활성화 합니다.
    • 해당 비트가 0이면, SMX Operation 에서 VMXON 명령을 실행하게 되는 경우와 VMX Operation , SMX Operation 모두를 지원하지 않는 프로세서에서 해당 비트를 설정하려고 하면 #GP 가 발생합니다.
  • Bit 2(Enable VMX outside SMX)
    • 해당 비트는 SMX Operation 외부에서 VMX 를 활성화 합니다.
    • 해당 비트가 0인 상태이고 SMX Operation 외부에서 VMXON 명령을 실행하게 되는 경우와 VMX operation 을 지원하지 않는 프로세서에서 해당 비트를 설정하려고 하면 #GP 가 발생합니다.

VMX 지원을 활성화하기 위해서는 우선 Lock 비트가 0이어야 합니다. MSR 수정을 할 수 없기 때문입니다. 마찬가지로 VMX in SMX 비트 또는 VMX out SMX 둘 중 하나는 반드시 설정되어야 합니다.

위의 내용들을 토대로 구현을 하면 아래와 같습니다.

BOOLEAN ShHvSupport::IsVmxSupport()
{
	bool Result = false;
	CPUID CpuId = { 0, };
	CPU_FEATURES FeaturesFlags = { 0, };
	IA32_FEATURE_CONTROL FeaturesControl = { 0, };
	
	__cpuid((int*)&CpuId, 1);
	
	FeaturesFlags.Features = CpuId.ecx;
	Result = _bittest64((LONG64*)&FeaturesFlags, 5);
	
	if (Result == false)
	{
		Log("Not supported cpu\n");
		return Result;
	}
	
	else
	{
		FeaturesControl.Features = __readmsr(IA32_FEATURE_CONTROL_MSR);
		if (_bittest64((LONG64*)&FeaturesControl, 0) == false)
		{
			FeaturesControl.Fields.Lock = 1;
			FeaturesControl.Fields.EnableVMXoutSMX = 1;
			__writemsr(IA32_FEATURE_CONTROL_MSR, FeaturesControl.Features);
			Result = true;
		}
		else if (FeaturesControl.Fields.EnableVMXoutSMX == 0)
		{
			Log("VMX locked in bios\n");
			Result = false;
		}
	}

	return Result;
}

BOOLEAN ShHvSupport::EnableVmx()
{
	bool Result = false;
	CR4 Cr4 = { 0, };
	Cr4.Features = __readcr4();
	if (Cr4.Fields.VMXEnable == 1)
	{
		Log("Already ready for VMX Operation\n");
	}
	else
	{
		Log("Enable VMXE\n");
		Cr4.Fields.VMXEnable = 1;
		__writecr4(Cr4.Features);
	}
	return Result;
}

추가로 VMXON 명령을 실행하기 전에 논리 프로세서가 VMX Operation 을 지원하는데 사용할 수 있는 정렬된 4kb 메모리 영역이 할당되어야 합니다. 이 영역은 VMXON Region 이라고 합니다.

해당 영역에 대해 알아보기 전에, 실제로 Intel SDM 에서 말하는 VMX Operation 의 제약 사항에 대한 내용 중에는 CR0 , CR4 에 대한 내용이 존재합니다.

VMXON(Enter VMX Operation) 명령에 대한 설명(Intel SDM Vol.3C 30-28 page) 에서 동작을 확인하면 CR0 값에 PE 비트가 0이거나 CR4 값에 VMXE 비트가 0인 경우 #UD 가 발생하게 되어 있습니다.

이에 따라 해당 비트를 설정하는 방법으로 IA32_VMX_CR0_FIXED0, IA32_VMX_CR0_FIXED1, IA32_VMX_CR4_FIXED0, IA32_VMX_CR4_FIXED1 에 대한 내용이 존재합니다.

#define IA32_VMX_CR0_FIXED0 0x486
#define IA32_VMX_CR0_FIXED1 0x487
#define IA32_VMX_CR4_FIXED0 0x488
#define IA32_VMX_CR4_FIXED1 0x489

위와 같은 형태로, 해당 MSR 을 읽으면 VMX Operation 에서 CR0, CR4 에 필요한 비트에 대한 정보를 읽을 수 있습니다.

IA32_VMX_CR0_FIXED0 을 읽었을 때, 해당 값의 Bit x 가 1인 경우, VMX Operation 에서는 CR0 의 해당 비트가 1로 고정됩니다.

IA32_VMX_CR0_FIXED1 을 읽었을 때, 해당 값의 Bit x 가 0인 경우, VMX Operation 에서는 CR0 의 해당 비트가 0으로 고정됩니다.

해당 의미를 쉽게 정리하면 아래와 같습니다.

  • IA32_VMX_CR0_FIXED0 의 값은 CR0 비트에 설정되어야 하는 값이다.
  • IA32_VMX_CR0_FIXED1 의 값은 CR0 에서 0으로 설정되어야 하는 값이 포함된다. 다만 이 때 0으로 설정되어야 하는 비트는 0으로 설정되어 있다.
NTSTATUS ShHvSupport::SetVMXEnable()
{
	CR0 Cr0 = { 0, };
	CR0 Cr0Origin = { 0, };
	CR4 Cr4 = { 0, };
	CR4 Cr4Origin = { 0, };
	ULONG CurrentProcessor = KeGetCurrentProcessorNumber();

	Cr0.Features = __readcr0();
	Cr4.Features = __readcr4();

	Cr0Origin.Features = Cr0.Features;
	Cr4Origin.Features = Cr4.Features;

	Cr0.Features |= __readmsr(IA32_VMX_CR0_FIXED0_MSR);
	Cr0.Features &= __readmsr(IA32_VMX_CR0_FIXED1_MSR);

	Cr4.Features |= __readmsr(IA32_VMX_CR4_FIXED0_MSR);
	Cr4.Features &= __readmsr(IA32_VMX_CR4_FIXED1_MSR);

	__writecr0(Cr0.Features);
	__writecr4(Cr4.Features);

	PlainLog("\n\t======================== VMX Enable Information(%d) ============================\n",CurrentProcessor);
	PlainLog("\t\tFixed CR0 : %Xh => %Xh\n",Cr0Origin.Features, Cr0.Features);
	PlainLog("\t\tFixed CR4 : %Xh => %Xh\n", Cr4Origin.Features, Cr4.Features);

	return STATUS_SUCCESS;
}

[0x05] Virtual Machine Control Structures(VMCS)

논리 프로세서는 VMX Operation 중에 VMCS(Virtual Machine Control Structures) 를 사용합니다. VMX Non-Root Operation 으로 들어오고 나가는 전환(VM Entry, VM Exit)과 VMX Non-Root Operation 의 프로세서 동작을 관리합니다.

이 구조는 새로운 명령어 VMCLEAR , VMPTRLD, VMREAD, VMWRITE 에 의해 제어할 수 있습니다.

논리 프로세서는 메모리 영역을 각 VMCS 와 연결합니다. 이를 VMCS Region 이라고 부릅니다. VMCS Region 의 64bit 물리 주소를 이용하여 특정 VMCS 를 참조합니다.(VMCS Pointer)

  • VMCS Pointer 는 4kb로 정렬되어야 하며, 이 때 bit 11:0 은 0이어야 합니다.
  • 프로세서의 물리 주소 범위를 초과하는 비트를 설정해서는 안됩니다.(CPUID 호출 시, EAX 를 0x80000008 로 실행 시 EAXbit 7:0 으로 범위가 반환됨)

VMM 은 가상 머신에 대해 각기 다른 VMCS를 사용할 수 있습니다. 여러 개의 가상 논리 프로세서가 있는 가상 머신의 경우 VMM 은 각 가상 프로세서에 대해 서로 다른 VMCS를 사용할 수 있습니다.

논리 프로세서는 Active 상태 인 여러 개의 VMCS를 유지할 수 있습니다. 프로세서는 메모리, 프로세서 또는 둘 다에서 VMCSActive 상태를 유지하여 VMX Operation 을 최적화 할 수 있습니다.

대부분의 내용이 그렇듯, 해당 내용 또한 Intel SDM 으로 학습하였습니다. 해당 내용을 본인이 이해한 바는 아래와 같습니다.

  • VMX Non-Root Operation 에서의 VMX Transition 행위가 발생할 때 사용되는 구조가 VMCS 이다.
  • 이 때 사용 가능한 새로운 명령어는 VMCLEAR, VMPTRLD, VMREAD, VMWRITE 이다.
  • 여러 개의 VMCS가 존재하는 경우 해당 VMCS 를 모두 Active 상태로 유지하여 최적화가 가능하다.(Active, Inactive 상태의 전환에 대한 소비를 줄여 최적화)

위와 같이 이해하였습니다. VMCS 상태에는 현재 확인 된 3가지의 상태가 존재합니다.(활성화 여부 : Active, Inactive, 현재 상태 여부 : Current, Not Current, 시작 상태 : Clear, Launched)

아래 내용은 논리 프로세서가 VMCS 의 상태를 결정하는 방법을 설명합니다.

  • VMPTRLD 명령의 피연산자는 VMCS의 주소 입니다. 명령 실행 후 해당 VMCS는 논리 프로세서에서 Active , Current 상태로 설정됩니다. 다른 VMCSCurrent 상태였다면 해당 VMCSNot Current 상태가 됩니다.(Active 상태는 유지)
  • VMCLEAR 명령의 피연산자는 VMCS 의 주소 입니다. 명령 실행 후 해당 VMCSInactive, Not Current, Clear 상태입니다.
  • VMLAUNCH 명령은 현재 상태가 Clear 상태인 VMCS 인 경우에만 가능하며, 성공하는 경우 Launched 상태가 됩니다.
  • VMPTRST 명령의 피연산자는 Current 상태의 VMCS 주소를 저장할 포인터 입니다. Current 상태의 VMCS가 존재하지 않는 경우 0xffffffff'ffffffffh 이 저장됩니다. (상태 변경 X)

아래 그림은 VMCS 의 상태에 대한 내용을 보여줍니다. VMCS X 와 다른 VMCSVMCS Y 로 표현됩니다.

  • VMPTRLD X 는 항상 XActive, Current 상태로 만듭니다.
  • VMPTRLD Y 는 항상 XNot Current 상태로 만듭니다. 이는 VMCS YCurrent 로 만들기 때문입니다.
  • VMLAUNCHVMCS XActive, Current, Clear 상태 일 때 시작 상태가 Launched 상태로 변경됩니다.
  • VMCLEAR X 는 항상 XInactive, Not Current, Clear 상태로 만듭니다.

[0x06] VMCS & VMXON Region

[-] VMCS Region

아래는 VMCS Region 의 형식입니다.

  • Byte Offset 0
    • VMCS Region 의 첫 4바이트(Offset 0)의 경우 Bit 30:0 에는 VMCS Revision Identifier 가 포함됩니다. 마지막 bit 31 의 경우 VMCSShadow VMCS 인지에 대한 여부를 나타냅니다. 여기서는 다루지 않습니다.

    VMMVMCS 에 대해 VMCS Region 을 사용하기 전에 VMCS Revision IdentifierVMCS Region 에 작성해야 합니다.(자동으로 프로세서가 작성해주지 않습니다)

    VMCS Revision Identifier 가 프로세서에서 사용하는 것과 다른 VMCS Region 을 참조하는 경우 VMPTRLD 명령은 실패합니다. IA32_VMX_BASIC_MSR(0x480) 을 이용하여 VMCS Revision Identifier 를 찾을 수 있습니다.

  • Byte Offset 1
    • VMX-abort Indicator 가 해당 4바이트를 차지합니다. 논리 프로세서는 VMX 중단이 발생하면 해당 비트에 0이 아닌 값으로 채웁니다. 해당 필드는 VMM 으로도 제어가 가능합니다.
  • Byte Offset 2
    • 나머지 영역은 VMCS Data 로 채워집니다.(VMX Non-Root Operation 및 VMX Transition 제어)
    • 해당 데이터 형식은 구현에 따라 다릅니다. VMCS Operation 에서 적절한 동작을 보장하기 위해서 VMMwriteback cacheable memory 에서 VMCS Region 및 관련된 구조를 유지해야 합니다.

VMCS Data 에 대해서는 조금 뒤에서 다루도록 하겠습니다.

[-] VMXON Region

VMXON 을 실행하기 전에 논리 프로세서가 VMX Operation 을 지원하는 데 사용하는 메모리 영역을 VMXON Region 이라고 합니다.

이 영역의 물리 주소(VMXON Pointer) 는 VMXON 명령에 대한 피연산자로 제공됩니다. VMXON Pointer 에는 VMCS Pointer 에 적용되는 제한 사항이 동일하게 적용됩니다.

  • 4kb로 정렬되어야 하며, 이 때 bit 11:0 은 0
  • 프로세서의 물리 주소 범위를 초과하는 비트를 설정하면 안됨

VMXON Region 에 필요한 메모리의 양은 VMCS Region 에 필요한 메모리 양과 동일합니다. (이러한 크기는 구현에 따라 다르며 IA32_VMX_BASIC_MSR(0x480) 을 참조하여 결정할 수 있습니다.)

VMXON 명령을 실행하기 전 VMCS Region 과 동일하게 VMCS Revision IdentifierVMXON Region 에 작성해야 합니다. VMM 은 각 논리 프로세서에 대해 별도의 영역을 사용해야 하며, VMXONVMXOFF 실행 사이에 VMXON Region 에 접근하거나 이를 수정해서는 안됩니다.

[0x07] Initialize Regions

위의 내용들을 토대로 보았을 때, VMX Operation 을 구현하기 위해선 우선 두 개의 영역이 필요합니다.(VMCS Region, VMXON Region)

우선 대부분의 오픈소스에서나 VT를 이용하는 여러가지 도구들에서는 특정 프로세서만 가상화하지 않고, 모든 프로세서들을 가상화 하는 것을 볼 수 있었습니다.

KeQueryActiveProcessorCount, KeQueryActiveProcessorCountEx, KeSetSystemGroupAffinityThread, KeSetSystemAffinityThread 와 같은 함수들을 이용하여 원하는 프로세서에서 루틴 호출을 구현할 수 있습니다.

[-] Allocate VMXON, VMCS Region

Intel SDM 순서 상 할당의 순서는 VMXON, VMCS 순 입니다. 다만 상세 설명의 경우 VMCS 에 초점이 맞춰져 있기 때문에 VMCS Region 에 대해 먼저 설명합니다.

VMCS Region 설명에 따르면 VMCS Region 의 구조는 아래와 같이 확인할 수 있습니다.

typedef struct _VMCS_REGION {

	struct
	{
		ULONG RevisionIdentifier : 31; // 00 - 30
		ULONG ShadowIndicator : 1;     // 31
	}Offset_0;

	ULONG VMXAbortIndicator;
	BYTE VMCSData[4088]; // A VMCS region comprises up to 4-KBytes <- 24.2 FORMAT OF THE VMCS REGION (4096 - 4 - 4)
}VMCS_REGION, * PVMCS_REGION;

우선은 4kb 로 정렬된 메모리어야 하며, 물리 주소 최대 범위를 초과해서는 안됩니다. 이러한 메모리를 할당하기 위해서는 MmAllocateContiguousMemory 루틴을 이용할 수 있습니다.

PVOID MmAllocateContiguousMemory(
  [in] SIZE_T           NumberOfBytes,
  [in] PHYSICAL_ADDRESS HighestAcceptableAddress
);

설명을 보면 연속된 Non-Paged 물리 메모리 범위를 할당하고 이를 시스템 주소에 매핑한다고 되어 있습니다. 추가로 MmAllocateContiguousMemory 가 호출되는 경우 조각화 된 물리 메모리에서 연속적인 메모리 블록을 검색해야 하기 때문에 성능이 심각하게 저하될 수 있다는 내용이 있습니다.

위의 참조 내용을 토대로 조건을 살펴보겠습니다.

  1. 4kb 로 정렬 된 메모리
    1. MmAllocatedContiguousMemory 루틴 사용
  2. 물리 주소 최대 범위 초과 X
    1. MmAllocatedContiguousMemory 루틴 사용
  3. 할당 된 영역의 Revision Identifier 의 값을 IA32_VMX_BASIC MSRRevision Identifier 으로 설정
    1. IA32_VMX_BASIC_MSR(0x480) 으로 __readmsr 을 호출하고, 해당 값을 할당 된 영역에 복사

위와 같은 로직이면 모든 조건이 충족됩니다.


4,096 vs 8,192

몇 가지 참조한 오픈소스코드를 보면 모두 4kb * 2 만큼을 VMCS Region 할당에 사용합니다.

Intel SDM Vol 3D A-1 Basic VMX Information 의 내용을 보면 Bits 44:32VMCS, VMXON Region 에 필요한 할당 크기를 나타낸다고 하였으며, 해당 비트가 0인 경우에만 bit 44 가 1로 설정된다고 되어 있습니다.

다음은 본인이 추측한 내용입니다.

  1. SDM 설명에 대한 부족
    1. 0부터 최대 4096 만큼의 범위를 지닙니다. 즉, VMCS, VMXON Region 에 필요한 할당 크기가 1024, 2048 도 될 수 있음을 의미합니다.
    2. 때문에 Bits 44:32 는 이를 표기하기 위함이고 최대 값 4096 의 할당이 필요한 경우 Bit 44 가 설정된다.
    3. 다만 메뉴얼에는 bits 43:32 가 0인 경우에 bit 44 가 설정된다라고만 되어 있음. 이를 unsigned long long 으로 출력하면 0x1000 값이 나오는 것을 확인
    4. bits 44:32 영역의 필요한 크기 인 것으로 추정
  2. 0x1000으로 정렬되지 않는 경우
    1. MmAllocateContiguousMemory 를 사용했을 때, 0xfffff678’91234000 같이 4kb 로 정렬되어 있지 않고, 0xfffff678’91234200 과 같이 할당 되는 경우?
    2. 4kb로 정렬되지 않았기 때문에 조건에 부적합
    3. 때문에 이를 정렬하여 0xfffff678'91235000 을 영역의 주소로 사용
    4. 위와 같이 되는 경우 4kb 사이즈가 안되기 때문에, 2개의 페이지 크기로 할당하는 것

명확한 이유에 대해 알려주시면 감사하겠습니다.


다시 돌아와서 설명을 하면, VMXON 영역의 경우에는 VMCS와 동일한 제약 사항을 가지고 있습니다. 때문에 동일하게 할당을 할 수 있습니다.

아래는 위의 내용들을 토대로 작성한 각 영역의 할당입니다.

NTSTATUS ShHvSupport::AllocateContiguousMemory(VIRTUAL_REGION_ADDRESS* RegionInformation)
{
	if (KeGetCurrentIrql() > DISPATCH_LEVEL) { KeRaiseIrqlToDpcLevel(); }

	NTSTATUS Status = STATUS_SUCCESS;
	PHYSICAL_ADDRESS PhysicalAddress = { 0, };
	IA32_VMX_BASIC VmxBasic = { 0, };

	PVOID Region = nullptr;
	ULONG64 RegionPhysicalAddress = 0;
	VMCS_REGION* AlignedRegion = 0;
	ULONG64 AlignedRegionPhy = 0;
	ULONG CurrentProcessor = KeGetCurrentProcessorNumber();

	VmxBasic.Features = __readmsr(IA32_VMX_BASIC_MSR);
	PhysicalAddress.QuadPart = MAXULONG64;

	while (true)
	{
		Region = MmAllocateContiguousMemory(VmxBasic.Fields.RequiredRegionSize, PhysicalAddress);

		if (Region == nullptr)
		{
			ErrLog("Can't allocted Physical memory\n");
			return STATUS_UNSUCCESSFUL;
		}

		RtlZeroMemory(Region, VmxBasic.Fields.RequiredRegionSize);

		RegionPhysicalAddress = ShDrvUtil::VirtualToPhysicalAddress(Region);
		AlignedRegion = (VMCS_REGION*)ShDrvUtil::SetAlignmentAddress((ULONG64)Region, REGION_ALIGNMENT);
		AlignedRegionPhy = ShDrvUtil::SetAlignmentAddress((ULONG64)RegionPhysicalAddress, REGION_ALIGNMENT);

		if (Region == AlignedRegion && RegionPhysicalAddress == AlignedRegionPhy)
		{
			break;
		}
		MmFreeContiguousMemory(Region);
	}

	AlignedRegion->Offset_0.RevisionIdentifier = VmxBasic.Fields.RevisionIdentifier;

	RegionInformation->RegionPhysicalAddress = (PVOID)RegionPhysicalAddress;
	RegionInformation->RegionVirtualAddress = Region;
	
	return Status;
}

기본적으로 각 영역에 대한 포인터가 의미하는 바는 각 영역의 물리주소 입니다. 이에 따라 각 명령어 세트를 이용하고, 종료 시에 해제할 수 있도록 전역 변수 또한 필요합니다.

하단에 g_VmmContext 는 이러한 용도로 사용되기 위한 전역 변수로 사용했습니다. 이로써 VMX Operation 에 진입하기 위한 조건이 갖추어졌습니다.

이후에 VMX Operation 으로 진입하기 위해서는 아래와 같이 __vmx_on 내장 함수를 이용하여 진입할 수 있습니다. VMXON 명령의 피연산자는 VMXON Pointer 로 이는 VMXON Region 의 물리 주소를 의미합니다.

NTSTATUS ShHvSupport::VmxEnterRootMode()
{
	VMXON_STATUS Status = VMXON_SUCCESS;
	ULONG CurrentProcessor = KeGetCurrentProcessorNumber();
	PlainLog("\t\n======================== [VMXON] (%d)============================\n", CurrentProcessor);

	Status = __vmx_on((ULONG64*)&g_VmmContext[CurrentProcessor].VmxonPhysical);
	if (Status != VMXON_SUCCESS)
	{
		ErrLog("VMXON Failed : %d\n", Status);
		Status = STATUS_UNSUCCESSFUL;
	}
	PlainLog("\t\t[%d Processor] Entered VMX Root Operation\n");
	return Status;
}

[0x08] Conclusion

이번 내용에서 대부분의 코드는 공개되지 않습니다. 아직 내 스스로 하이퍼바이저를 만들지 못했습니다.

추후에 필요한 코드들에 대해서 공개됩니다.

다음 챕터에서는 대부분 필요한 구조에 대해 설명하며 이에 대한 선언들과 설명이 게시됩니다.(매우 방대한 양)

[0x09] Reference

  1. Intel 64 and IA-32 Architectures Software Developer’s Manual
  2. Hypervisor From Scratch
  3. Tandasat DdiMon
  4. Gbps Gbhv