下一站 - Ihcblog!

远方的风景与脚下的路 | 子站点:ihc.im

0%

用 Rust 实现极简 VMM 2 - 模式切换

This article also has an English version.

本系列文章主要记录我在尝试用 Rust 实现一个 Hypervisor 的过程。目录:

  1. 用 Rust 实现极简 VMM - 基础
  2. 用 Rust 实现极简 VMM - 模式切换
  3. 用 Rust 实现极简 VMM - 运行真实的 Linux Kernel
  4. 用 Rust 实现极简 VMM - 实现 Virtio 设备

本文是系列的第二篇,主要科普几种常见的运行模式,并切换它们。

Chap 2: Real Mode to Protected/Long Mode

2.1. 运行模式

本节主要简单科普一下常见的几种 x86 运行模式,以及其中的机制。

保护模式与实模式

8086 CPU 运行于 16 位,其寻址方式为段式寻址。通过 segment << 4 + offset 得到线性地址,利用这种机制做到了 20 位寻址空间(支持的最大范围从 64K 提升到了 1M)。CS、DS 等寄存器内存储的是具体的 segment 值。

80286 后引入保护模式;80386 后真正进入了 32 位时代(同时也支持了分页,这个后面会提到)。但是为了兼容性,启动后仍旧先进去实模式,之后需要手动切换到保护模式。

分段机制、GDT 与 LDT

保护模式下,分段仍是必选项,但是该设计有较大变化。对一个段的描述变为 Base、Limit 和权限位。对段的引用不再是直接写一个 base 然后 <<4 再 add,而是存储段描述符。既然有段描述符,那么要么这是个指针,要么这是个 index。

实际上,段描述符包含了 “index” 和请求权限级别两个信息;其中 “index” 包括了索引号和一个用于选择表的 bit。 既然是 index,那么就要需要有存储一张表,并把表的地址存储在某个寄存器。这里其实有两张描述表,分别叫 GDT(G for Global) 和 LDT(L for Local),前面说到的特殊 bit 会用来选择用哪张表。

这并不是独立的两张表,实际上 LDT 是 GDT 的次级表,GDT 表中可以存储 LDT 表的描述符。

当选择使用 LDT 时,会根据 LDTR 寄存器中的索引值,在 GDT 中找到对应的 LDT 描述符得到该 LDT 的 base 和 limit,之后根据段选择器中的索引值查该 LDT 即可;当选择使用 GDT 时更为简单,直接通过 GDTR 找到 GDT 表所在位置,然后根据段选择器中的索引值查 GDT。

GDT and LDT

GDT and LDT2

所以设计上,操作系统会将每个进程的段描述表存储为一堆 LDT,之后将每个 LDT 记录在 GDT 中,进程切换是更换 LDTR 寄存器。但实际中这套东西并没有用到(那怎么做权限控制呢?靠的是分页机制),Linux 的 GDT 只有 user code、user data、kernel code、kernel data 四个有效段(user 和 kernel 的区别在于权限位)。

在启用了分页机制后,这套东西基本上除了做很小一部分权限检查外就没作用了。所以我们往往直接将 GDT 里的几个段的 base 都设置为 0,这样无论是使用 cs 还是 ds 最终计算出的地址是一致的,也就是所谓的 flat model。

Long Mode

到了 64 位时代,又引入了一种模式叫 Long Mode(Intel 的也叫 IA-32e)。有点晕是不是?我们看看图:

Differet Modes

Differet Modes2

整体来说有两种模式:Legacy Mode 和 Long Mode,这两种模式各有几种子模式。Legacy 是过去一些老古董的统称,其中常见的两种模式我们基本已经清楚了,我们主要来看下 Long Mode 的 Compatibility mode 和 64-bit mode。

实模式支持分段,不支持分页;80386 之后在支持分段的同时也支持了分页,但分段是必选,而分页是可选;到了 Long Mode 后,分页机制成为必选项,而分段机制逐渐形同虚设。

如果想充分利用硬件(如新增的寄存器、超过 4GB 的内存),就要求程序本身是 64 位的,这时就要求运行于 64-bit mode。如果目标是不加修改地运行 32 位程序,那么就要使用 Compatibility mode。

那么这几种模式怎么切换呢?毕竟我们启动之后 CPU 是处于实模式的;并且我们常见的情况是,在 64 位机器上想同时运行 64bit 和 32bit 的程序。

Mode Switch

Mode Switch

通过设置某些特殊寄存器的位,我们就可以控制 CPU 完成切换。

详细的参数可以参考这里:https://wiki.osdev.org/CPU_Registers_x86-64#IA32_EFER

当然更多细节可以看 SDM。

2.2. 进入保护模式

进入保护模式需要正确配置 GDT 和一些特殊寄存器,之后将 CR0 的最低位置 1 即可。

配置段寄存器

段寄存器对应 kvm_segment 结构,它包括 base、limit、selector 等成员。本质上它是对 GDT entry 的一层抽象,毕竟除了 Intel 还有一些其他厂的处理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const fn seg_with_st(selector_index: u16, type_: u8) -> kvm_segment {
kvm_segment {
base: 0,
limit: 0x000fffff,
selector: selector_index << 3,
// 0b1011: Code, Executed/Read, accessed
// 0b0011: Data, Read/Write, accessed
type_,
present: 1,
dpl: 0,
db: 1,
s: 1,
l: 0,
g: 1,
avl: 0,
unusable: 0,
padding: 0,
}
}

我们定义一个 const fn 来快速创建它。

由于我们的目标是平台模型,所以这里 base 一定是 0,limit 一定是最大值,即 0xfffff

Segment Selector

selector 的低 0-1 和 2 bit 分别表示 RPL 和 TI,所以我们需要将传入的 index 左移 3 位。

Segement Type

Type 有 4 个 bit,当它表示数据段时第一位为 0,后 3 位表示 E(增长方向)、W(可写)、A(写标记,访问后会被硬件置 1);表示代码段时第一位为 1,后三位表示 C(是否允许低权限调用)、R(可读)、A(写标记)。所以对于代码段我们可以使用 1011,对于数据段我们使用 0011。

present 代表是否存在,对应 GDT entry 里的 P,我们写 1 即可。S 置 1 表示这个是代码或数据段;L 表示是否为 Long Mode;G 表示 limit 的单位,置 1 表示以 4k 为单位(此时最大表示 4k*0xfffff 字节,置 0 表示 byte 为单位,最大表示 2^20 字节)。dpl、db 等同样可以参考 GDT 定义做对应赋值。

1
2
3
4
5
6
7
8
9
10
11
let mut sregs = vcpu.get_sregs().unwrap();
const CODE_SEG: kvm_segment = seg_with_st(1, 0b1011);
const DATA_SEG: kvm_segment = seg_with_st(2, 0b0011);

// construct kvm_segment and set to segment registers
sregs.cs = CODE_SEG;
sregs.ds = DATA_SEG;
sregs.es = DATA_SEG;
sregs.fs = DATA_SEG;
sregs.gs = DATA_SEG;
sregs.ss = DATA_SEG;

这里已经正确处理段寄存器。

配置 GDT

GDT 是一张表,每个 entry 存储了一个段的描述。

Segment Descriptor

每个 entry 是一个 64bit 值,其每个 bit 含义可以参考上图。简单来说,每个 entry 对应一个段,主要包括它的 base、limit 和权限位等信息。我们需要构建这张表。

每个 entry 中字段的含义和 kvm_segment 中非常类似。我们可以写一个转换函数,从 kvm_segment 得到 entry。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Ref: <https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html> 3-10 Vol. 3A
const fn to_gdt_entry(seg: &kvm_segment) -> u64 {
let base = seg.base;
let limit = seg.limit as u64;
// flags: G, DB, L, AVL
let flags = (seg.g as u64 & 0x1) << 3
| (seg.db as u64 & 0x1) << 2
| (seg.l as u64 & 0x1) << 1
| (seg.avl as u64 & 0x1);
// access: P, DPL, S, Type
let access = (seg.present as u64 & 0x1) << 7
| (seg.dpl as u64 & 0x11) << 5
| (seg.s as u64 & 0x1) << 4
| (seg.type_ as u64 & 0x1111);
((base & 0xff00_0000u64) << 32)
| ((base & 0x00ff_ffffu64) << 16)
| (limit & 0x0000_ffffu64)
| ((limit & 0x000f_0000u64) << 32)
| (flags << 52)
| (access << 40)
}

之后我们只需要构建 GDT table 并将其拷贝至用户内存中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// construct gdt table, write to memory and set it to register
let gdt_table: [u64; 3] = [
0, // NULL
to_gdt_entry(&CODE_SEG), // CODE
to_gdt_entry(&DATA_SEG), // DATA
];
let boot_gdt_addr = GuestAddress(BOOT_GDT_OFFSET);
for (index, entry) in gdt_table.iter().enumerate() {
let addr = guest_mem
.checked_offset(boot_gdt_addr, index * std::mem::size_of::<u64>())
.unwrap();
guest_mem.write_obj(*entry, addr).unwrap();
}
sregs.gdt.base = BOOT_GDT_OFFSET;
sregs.gdt.limit = std::mem::size_of_val(&gdt_table) as u16 - 1;

最后打开保护模式开关,并提交 sreg。

1
2
3
// enable protected mode
sregs.cr0 |= X86_CR0_PE;
vcpu.set_sregs(&sregs).unwrap();

至此,我们完成了保护模式必备组件的初始化。

生成对应架构代码

我们先前的实模式章节中,我们手动将一段代码通过 nasm 翻译成了二进制。我们现在需要一段能够跑在保护模式下的代码。我们仍旧使用 nasm 汇编下面这段代码:

1
2
3
4
bits 32
mov eax, 0x42
mov ds:[0x10000], eax
hlt

ndisasm -b32 demo 得到:

1
2
3
00000000  B842000000        mov eax,0x42
00000005 3EA300000100 mov [ds:0x10000],eax
0000000B F4 hlt

除了手写或者 nasm 外,我们也可以用 pwntools 完成这个汇编操作(打过 CTF 的同学一定很熟悉了):

1
2
3
from pwn import context, asm
context.update(arch = 'i386')
asm('mov eax,0x42; mov [ds:0x10000],eax; hlt')

于是我们得到 [0xb8, 0x42, 0x00, 0x00, 0x00, 0x3e, 0xa3, 0x00, 0x00, 0x01, 0x00, 0xf4

拷贝并运行代码

这部分和我们在实模式下做的事情是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// copy code
// B842000000 mov eax,0x42
// 3EA300000100 mov [ds:0x10000],eax
// F4 hlt
let code = [
0xb8, 0x42, 0x00, 0x00, 0x00, 0x3e, 0xa3, 0x00, 0x00, 0x01, 0x00, 0xf4,
];
guest_mem.write_slice(&code, GuestAddress(0x0)).unwrap();
let reason = vcpu.run().unwrap();
let regs = vcpu.get_regs().unwrap();
println!("exit reason: {:?}", reason);
println!("rax: {:x}, rip: 0x{:X?}", regs.rax, regs.rip);
println!(
"memory at 0x10000: 0x{:X}",
guest_mem.read_obj::<u32>(GuestAddress(0x10000)).unwrap()
);

我们可以得到:

1
2
3
exit reason: Hlt
rax: 42, rip: 0xC
memory at 0x10000: 0x42

至此我们在保护模式下(未开启分页)正确完成了计算和内存操作。当然,你也可以尝试将 DS 的 GDT 表项写成 CS 对应的,这时会遇到错误,因为段没有写权限。

本小节完整的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
use kvm_bindings::{
kvm_segment, kvm_userspace_memory_region, KVM_MAX_CPUID_ENTRIES, KVM_MEM_LOG_DIRTY_PAGES,
};
use kvm_ioctls::Kvm;
use vm_memory::{Bytes, GuestAddress, GuestMemory, GuestMemoryMmap};

const MEMORY_SIZE: usize = 0x30000;

const KVM_TSS_ADDRESS: usize = 0xfffb_d000;
const X86_CR0_PE: u64 = 0x1;
const BOOT_GDT_OFFSET: u64 = 0x500;

fn main() {
// create vm
let kvm = Kvm::new().expect("open kvm device failed");
let vm = kvm.create_vm().expect("create vm failed");

// create memory
let guest_addr = GuestAddress(0x0);
let guest_mem = GuestMemoryMmap::<()>::from_ranges(&[(guest_addr, MEMORY_SIZE)]).unwrap();
let host_addr = guest_mem.get_host_address(guest_addr).unwrap();
let mem_region = kvm_userspace_memory_region {
slot: 0,
guest_phys_addr: 0,
memory_size: MEMORY_SIZE as u64,
userspace_addr: host_addr as u64,
flags: KVM_MEM_LOG_DIRTY_PAGES,
};
unsafe {
vm.set_user_memory_region(mem_region)
.expect("set user memory region failed")
};
vm.set_tss_address(KVM_TSS_ADDRESS as usize)
.expect("set tss failed");

// create vcpu and set cpuid
let vcpu = vm.create_vcpu(0).expect("create vcpu failed");
let kvm_cpuid = kvm.get_supported_cpuid(KVM_MAX_CPUID_ENTRIES).unwrap();
vcpu.set_cpuid2(&kvm_cpuid).unwrap();

// set regs
let mut regs = vcpu.get_regs().unwrap();
regs.rip = 0;
regs.rflags = 2;
vcpu.set_regs(&regs).unwrap();

// set sregs
let mut sregs = vcpu.get_sregs().unwrap();
const CODE_SEG: kvm_segment = seg_with_st(1, 0b1011);
const DATA_SEG: kvm_segment = seg_with_st(2, 0b0011);

// construct kvm_segment and set to segment registers
sregs.cs = CODE_SEG;
sregs.ds = DATA_SEG;
sregs.es = DATA_SEG;
sregs.fs = DATA_SEG;
sregs.gs = DATA_SEG;
sregs.ss = DATA_SEG;

// construct gdt table, write to memory and set it to register
let gdt_table: [u64; 3] = [
0, // NULL
to_gdt_entry(&CODE_SEG), // CODE
to_gdt_entry(&DATA_SEG), // DATA
];
let boot_gdt_addr = GuestAddress(BOOT_GDT_OFFSET);
for (index, entry) in gdt_table.iter().enumerate() {
let addr = guest_mem
.checked_offset(boot_gdt_addr, index * std::mem::size_of::<u64>())
.unwrap();
guest_mem.write_obj(*entry, addr).unwrap();
}
sregs.gdt.base = BOOT_GDT_OFFSET;
sregs.gdt.limit = std::mem::size_of_val(&gdt_table) as u16 - 1;

// enable protected mode
sregs.cr0 |= X86_CR0_PE;
vcpu.set_sregs(&sregs).unwrap();

// copy code
// B842000000 mov eax,0x42
// 3EA300000100 mov [ds:0x10000],eax
// F4 hlt
let code = [
0xb8, 0x42, 0x00, 0x00, 0x00, 0x3e, 0xa3, 0x00, 0x00, 0x01, 0x00, 0xf4,
];
guest_mem.write_slice(&code, GuestAddress(0x0)).unwrap();
let reason = vcpu.run().unwrap();
let regs = vcpu.get_regs().unwrap();
println!("exit reason: {:?}", reason);
println!("rax: {:x}, rip: 0x{:X?}", regs.rax, regs.rip);
println!(
"memory at 0x10000: 0x{:X}",
guest_mem.read_obj::<u32>(GuestAddress(0x10000)).unwrap()
);
}

const fn seg_with_st(selector_index: u16, type_: u8) -> kvm_segment {
kvm_segment {
base: 0,
limit: 0x000fffff,
selector: selector_index << 3,
// 0b1011: Code, Executed/Read, accessed
// 0b0011: Data, Read/Write, accessed
type_,
present: 1,
dpl: 0,
db: 1,
s: 1,
l: 0,
g: 1,
avl: 0,
unusable: 0,
padding: 0,
}
}

// Ref: <https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html> 3-10 Vol. 3A
const fn to_gdt_entry(seg: &kvm_segment) -> u64 {
let base = seg.base;
let limit = seg.limit as u64;
// flags: G, DB, L, AVL
let flags = (seg.g as u64 & 0x1) << 3
| (seg.db as u64 & 0x1) << 2
| (seg.l as u64 & 0x1) << 1
| (seg.avl as u64 & 0x1);
// access: P, DPL, S, Type
let access = (seg.present as u64 & 0x1) << 7
| (seg.dpl as u64 & 0x11) << 5
| (seg.s as u64 & 0x1) << 4
| (seg.type_ as u64 & 0x1111);
((base & 0xff00_0000u64) << 32)
| ((base & 0x00ff_ffffu64) << 16)
| (limit & 0x0000_ffffu64)
| ((limit & 0x000f_0000u64) << 32)
| (flags << 52)
| (access << 40)
}

2.3. 开启分页

启用 PAE

No PAE

With PAE

通常,x86 下页表是 3 级的(4K 页)。但是如果你使用了更大的页(如 4M),或者启用了 PAE,那么可能会影响这个级数。启用 PAE 可以使 32 位机器最大支持 64 GB 内存,但受限于地址空间,平坦模型下单个进程的可用内存上线仍为 4GB。

要开启 PAE,需要设置 CR4 的第 5bit 为 1,同时确保分页功能开启(CR0.PG)以及 LME 为 0(IA32_EFER.LME)。

通过这两张图我们可以对比出 PAE 开启前后的页表结构差异。注意,除了多了一层 PDPTE 外,PDE 也由 32bit 变为了 64bit(所以才能支持更大的物理内存)。

初始化页表

这里我们选用最常见配置:32 位,开启 PAE,4K 页(其实这里用 2M 页更简单、更高效,因为往往这个过程并不涉及 context 切换,且全局就这么一个页表)。在后面 64bit 实验中我们会尝试使用更大的页面。

现在我们要初始化开启 PAE 后的页表。参考上图,我们需要构造 PDPTE Registers、Page Directory、Page Table 和 Page,并为其初始化一些 element(否则 page fault 了我们处理比较烦,当前阶段只是一个临时启动环境,真正的 page fault 处理交给之后启动的操作系统来做)。

更多选项可以参考 SDM Volume 3, Chapter 4.4。

对照上表,我们需要:

  1. 向 CR3 寄存器写入 PDPTE Register 地址
  2. 向 PDPTE Register 初始化成员(该表的大小是 4,但实际上我们填充一个元素即可,对应低 1GB 地址空间),指向 Page Directory(低 12 位全部置 0,最低位置 1)。
  3. 向 Page Directory 初始化成员,该表大小为 512,全部填充,对应 2M,指向物理内存地址(后续的 4K 为该页面)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// set page table
let boot_pdpte_addr = GuestAddress(0xa000);
let boot_pde_addr = GuestAddress(0xb000);
let boot_pte_addr = GuestAddress(0xc000);

guest_mem
.write_slice(
&(boot_pde_addr.raw_value() as u64 | 1).to_le_bytes(),
boot_pdpte_addr,
)
.unwrap();

guest_mem
.write_slice(
&(boot_pte_addr.raw_value() as u64 | 0b11).to_le_bytes(),
boot_pde_addr,
)
.unwrap();

for i in 0..512 {
guest_mem
.write_slice(
&((i << 12) + 0b11u64).to_le_bytes(),
boot_pte_addr.unchecked_add(i * 8),
)
.unwrap();
}

配置寄存器

我们将 CR3 设置为 PDPTE 表的地址,并开启分页和 PAE,然后设置寄存器。

1
2
3
4
sregs.cr3 = boot_pdpte_addr.raw_value() as u64;
sregs.cr4 |= X86_CR4_PAE;
sregs.cr0 |= X86_CR0_PG;
vcpu.set_sregs(&sregs).unwrap();

代码生成与运行

这里我们不再依赖段寄存器做内存访问。

1
2
3
4
bits 32
mov eax, 0x42
mov [0x10000], eax
hlt

生成对应机器码:

1
2
3
00000000  B842000000        mov eax,0x42
00000005 A300100000 mov [0x1000],eax
0000000A F4 hlt

运行:

1
2
3
exit reason: Hlt
rax: 42, rip: 0xB
memory at 0x10000: 0x42

完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
use kvm_bindings::{
kvm_segment, kvm_userspace_memory_region, KVM_MAX_CPUID_ENTRIES, KVM_MEM_LOG_DIRTY_PAGES,
};
use kvm_ioctls::Kvm;
use vm_memory::{Address, Bytes, GuestAddress, GuestMemory, GuestMemoryMmap};

const MEMORY_SIZE: usize = 0x30000;

const KVM_TSS_ADDRESS: usize = 0xfffb_d000;
const X86_CR0_PE: u64 = 0x1;
const X86_CR4_PAE: u64 = 0x20;
const X86_CR0_PG: u64 = 0x80000000;
const BOOT_GDT_OFFSET: u64 = 0x500;

fn main() {
// create vm
let kvm = Kvm::new().expect("open kvm device failed");
let vm = kvm.create_vm().expect("create vm failed");

// create memory
let guest_addr = GuestAddress(0x0);
let guest_mem = GuestMemoryMmap::<()>::from_ranges(&[(guest_addr, MEMORY_SIZE)]).unwrap();
let host_addr = guest_mem.get_host_address(guest_addr).unwrap();
let mem_region = kvm_userspace_memory_region {
slot: 0,
guest_phys_addr: 0,
memory_size: MEMORY_SIZE as u64,
userspace_addr: host_addr as u64,
flags: KVM_MEM_LOG_DIRTY_PAGES,
};
unsafe {
vm.set_user_memory_region(mem_region)
.expect("set user memory region failed")
};
vm.set_tss_address(KVM_TSS_ADDRESS as usize)
.expect("set tss failed");

// create vcpu and set cpuid
let vcpu = vm.create_vcpu(0).expect("create vcpu failed");
let kvm_cpuid = kvm.get_supported_cpuid(KVM_MAX_CPUID_ENTRIES).unwrap();
vcpu.set_cpuid2(&kvm_cpuid).unwrap();

// set regs
let mut regs = vcpu.get_regs().unwrap();
regs.rip = 0;
regs.rflags = 2;
vcpu.set_regs(&regs).unwrap();

// set sregs
let mut sregs = vcpu.get_sregs().unwrap();
const CODE_SEG: kvm_segment = seg_with_st(1, 0b1011);
const DATA_SEG: kvm_segment = seg_with_st(2, 0b0011);

// construct kvm_segment and set to segment registers
sregs.cs = CODE_SEG;
sregs.ds = DATA_SEG;
sregs.es = DATA_SEG;
sregs.fs = DATA_SEG;
sregs.gs = DATA_SEG;
sregs.ss = DATA_SEG;

// construct gdt table, write to memory and set it to register
let gdt_table: [u64; 3] = [
0, // NULL
to_gdt_entry(&CODE_SEG), // CODE
to_gdt_entry(&DATA_SEG), // DATA
];
let boot_gdt_addr = GuestAddress(BOOT_GDT_OFFSET);
for (index, entry) in gdt_table.iter().enumerate() {
let addr = guest_mem
.checked_offset(boot_gdt_addr, index * std::mem::size_of::<u64>())
.unwrap();
guest_mem.write_obj(*entry, addr).unwrap();
}
sregs.gdt.base = BOOT_GDT_OFFSET;
sregs.gdt.limit = std::mem::size_of_val(&gdt_table) as u16 - 1;

// enable protected mode
sregs.cr0 |= X86_CR0_PE;

// set page table
let boot_pdpte_addr = GuestAddress(0xa000);
let boot_pde_addr = GuestAddress(0xb000);
let boot_pte_addr = GuestAddress(0xc000);

guest_mem
.write_slice(
&(boot_pde_addr.raw_value() as u64 | 1).to_le_bytes(),
boot_pdpte_addr,
)
.unwrap();

guest_mem
.write_slice(
&(boot_pte_addr.raw_value() as u64 | 0b11).to_le_bytes(),
boot_pde_addr,
)
.unwrap();

for i in 0..512 {
guest_mem
.write_slice(
&((i << 12) + 0b11u64).to_le_bytes(),
boot_pte_addr.unchecked_add(i * 8),
)
.unwrap();
}
sregs.cr3 = boot_pdpte_addr.raw_value() as u64;
sregs.cr4 |= X86_CR4_PAE;
sregs.cr0 |= X86_CR0_PG;
vcpu.set_sregs(&sregs).unwrap();

// copy code
// B842000000 mov eax,0x42
// A300000100 mov [0x10000],eax
// F4 hlt
let code = [
0xb8, 0x42, 0x00, 0x00, 0x00, 0xa3, 0x00, 0x00, 0x01, 0x00, 0xf4,
];
guest_mem.write_slice(&code, GuestAddress(0x0)).unwrap();
let reason = vcpu.run().unwrap();
let regs = vcpu.get_regs().unwrap();
println!("exit reason: {:?}", reason);
println!("rax: {:x}, rip: 0x{:X?}", regs.rax, regs.rip);
println!(
"memory at 0x10000: 0x{:X}",
guest_mem.read_obj::<u32>(GuestAddress(0x10000)).unwrap()
);
}

const fn seg_with_st(selector_index: u16, type_: u8) -> kvm_segment {
kvm_segment {
base: 0,
limit: 0x000fffff,
selector: selector_index << 3,
// 0b1011: Code, Executed/Read, accessed
// 0b0011: Data, Read/Write, accessed
type_,
present: 1,
dpl: 0,
db: 1,
s: 1,
l: 0,
g: 1,
avl: 0,
unusable: 0,
padding: 0,
}
}

// Ref: <https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html> 3-10 Vol. 3A
const fn to_gdt_entry(seg: &kvm_segment) -> u64 {
let base = seg.base;
let limit = seg.limit as u64;
// flags: G, DB, L, AVL
let flags = (seg.g as u64 & 0x1) << 3
| (seg.db as u64 & 0x1) << 2
| (seg.l as u64 & 0x1) << 1
| (seg.avl as u64 & 0x1);
// access: P, DPL, S, Type
let access = (seg.present as u64 & 0x1) << 7
| (seg.dpl as u64 & 0x11) << 5
| (seg.s as u64 & 0x1) << 4
| (seg.type_ as u64 & 0x1111);
((base & 0xff00_0000u64) << 32)
| ((base & 0x00ff_ffffu64) << 16)
| (limit & 0x0000_ffffu64)
| ((limit & 0x000f_0000u64) << 32)
| (flags << 52)
| (access << 40)
}

2.4. 进入 64bit 模式

要进入 64bit 模式,我们需要开启 PE、PG 和 LME。如果操作正确,我们就可以通过 LMA 读到 1。

LME、LMA 是 IA32_EFER(EFER means extended feature enable register)的两个 bit。

仅仅是修改寄存器十分简单,这里比较复杂的还是页表。进入 64bit 后页表结构也会变化。

结合 Figure 4-1,这里我们需要 4-level paging。我们可以选择 4K、2M 和 1G 的页面。

这里我们参考上图,选用 2M 页(对应 Figure 4-9)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// set page table
let boot_pml4_addr = GuestAddress(0xa000);
let boot_pdpte_addr = GuestAddress(0xb000);
let boot_pde_addr = GuestAddress(0xc000);

guest_mem
.write_slice(
&(boot_pdpte_addr.raw_value() as u64 | 0b11).to_le_bytes(),
boot_pml4_addr,
)
.unwrap();
guest_mem
.write_slice(
&(boot_pde_addr.raw_value() as u64 | 0b11).to_le_bytes(),
boot_pdpte_addr,
)
.unwrap();

for i in 0..512 {
guest_mem
.write_slice(
&((i << 21) | 0b10000011u64).to_le_bytes(),
boot_pde_addr.unchecked_add(i * 8),
)
.unwrap();
}
sregs.cr3 = boot_pml4_addr.raw_value() as u64;
sregs.cr4 |= X86_CR4_PAE;
sregs.cr0 |= X86_CR0_PG;
sregs.efer |= EFER_LMA | EFER_LME;

GDT 适配

我们在 GDT Entry 中设置了 L 位(Long Mode)为 0,这里需要改为 1;并且按照手册的要求,当 L 位设置时,D 位必须清零。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const fn seg_with_st(selector_index: u16, type_: u8) -> kvm_segment {
kvm_segment {
base: 0,
limit: 0x000fffff,
selector: selector_index << 3,
// 0b1011: Code, Executed/Read, accessed
// 0b0011: Data, Read/Write, accessed
type_,
present: 1,
dpl: 0,
// If L-bit is set, then D-bit must be cleared.
db: 0,
s: 1,
l: 1,
g: 1,
avl: 0,
unusable: 0,
padding: 0,
}
}

代码生成与运行

1
2
3
4
bits 64
mov rax, 0x4200000042
mov [0x10000], rax
hlt

得到:

1
2
3
00000000  48B84200000042000000  mov rax,0x4200000042
0000000A 4889042500000100 mov [0x10000],rax
00000012 F4 hlt

运行:

1
2
3
exit reason: Hlt
rax: 42, rip: 0xE
memory at 0x10000: 0x42

完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
use kvm_bindings::{
kvm_segment, kvm_userspace_memory_region, KVM_MAX_CPUID_ENTRIES, KVM_MEM_LOG_DIRTY_PAGES,
};
use kvm_ioctls::Kvm;
use vm_memory::{Address, Bytes, GuestAddress, GuestMemory, GuestMemoryMmap};

const MEMORY_SIZE: usize = 0x30000;

const KVM_TSS_ADDRESS: usize = 0xfffb_d000;
const X86_CR0_PE: u64 = 0x1;
const X86_CR4_PAE: u64 = 0x20;
const X86_CR0_PG: u64 = 0x80000000;
const BOOT_GDT_OFFSET: u64 = 0x500;
const EFER_LME: u64 = 0x100;
const EFER_LMA: u64 = 0x400;

fn main() {
// create vm
let kvm = Kvm::new().expect("open kvm device failed");
let vm = kvm.create_vm().expect("create vm failed");

// create memory
let guest_addr = GuestAddress(0x0);
let guest_mem = GuestMemoryMmap::<()>::from_ranges(&[(guest_addr, MEMORY_SIZE)]).unwrap();
let host_addr = guest_mem.get_host_address(guest_addr).unwrap();
let mem_region = kvm_userspace_memory_region {
slot: 0,
guest_phys_addr: 0,
memory_size: MEMORY_SIZE as u64,
userspace_addr: host_addr as u64,
flags: KVM_MEM_LOG_DIRTY_PAGES,
};
unsafe {
vm.set_user_memory_region(mem_region)
.expect("set user memory region failed")
};
vm.set_tss_address(KVM_TSS_ADDRESS as usize)
.expect("set tss failed");

// create vcpu and set cpuid
let vcpu = vm.create_vcpu(0).expect("create vcpu failed");
let kvm_cpuid = kvm.get_supported_cpuid(KVM_MAX_CPUID_ENTRIES).unwrap();
vcpu.set_cpuid2(&kvm_cpuid).unwrap();

// set regs
let mut regs = vcpu.get_regs().unwrap();
regs.rip = 0;
regs.rflags = 2;
vcpu.set_regs(&regs).unwrap();

// set sregs
let mut sregs = vcpu.get_sregs().unwrap();
const CODE_SEG: kvm_segment = seg_with_st(1, 0b1011);
const DATA_SEG: kvm_segment = seg_with_st(2, 0b0011);

// construct kvm_segment and set to segment registers
sregs.cs = CODE_SEG;
sregs.ds = DATA_SEG;
sregs.es = DATA_SEG;
sregs.fs = DATA_SEG;
sregs.gs = DATA_SEG;
sregs.ss = DATA_SEG;

// construct gdt table, write to memory and set it to register
let gdt_table: [u64; 3] = [
0, // NULL
to_gdt_entry(&CODE_SEG), // CODE
to_gdt_entry(&DATA_SEG), // DATA
];
let boot_gdt_addr = GuestAddress(BOOT_GDT_OFFSET);
for (index, entry) in gdt_table.iter().enumerate() {
let addr = guest_mem
.checked_offset(boot_gdt_addr, index * std::mem::size_of::<u64>())
.unwrap();
guest_mem.write_obj(*entry, addr).unwrap();
}
sregs.gdt.base = BOOT_GDT_OFFSET;
sregs.gdt.limit = std::mem::size_of_val(&gdt_table) as u16 - 1;

// enable protected mode
sregs.cr0 |= X86_CR0_PE;

// set page table
let boot_pml4_addr = GuestAddress(0xa000);
let boot_pdpte_addr = GuestAddress(0xb000);
let boot_pde_addr = GuestAddress(0xc000);

guest_mem
.write_slice(
&(boot_pdpte_addr.raw_value() as u64 | 0b11).to_le_bytes(),
boot_pml4_addr,
)
.unwrap();
guest_mem
.write_slice(
&(boot_pde_addr.raw_value() as u64 | 0b11).to_le_bytes(),
boot_pdpte_addr,
)
.unwrap();

for i in 0..512 {
guest_mem
.write_slice(
&((i << 21) | 0b10000011u64).to_le_bytes(),
boot_pde_addr.unchecked_add(i * 8),
)
.unwrap();
}
sregs.cr3 = boot_pml4_addr.raw_value() as u64;
sregs.cr4 |= X86_CR4_PAE;
sregs.cr0 |= X86_CR0_PG;
sregs.efer |= EFER_LMA | EFER_LME;
vcpu.set_sregs(&sregs).unwrap();

// copy code
// B842000000 mov eax,0x42
// 4889042500000100 mov [0x10000],rax
// F4 hlt
let code = [
0xB8, 0x42, 0x00, 0x00, 0x00, 0x48, 0x89, 0x04, 0x25, 0x00, 0x00, 0x01, 0x00, 0xf4,
];
guest_mem.write_slice(&code, GuestAddress(0x0)).unwrap();
let reason = vcpu.run().unwrap();
let regs = vcpu.get_regs().unwrap();
println!("exit reason: {:?}", reason);
println!("rax: {:x}, rip: 0x{:X?}", regs.rax, regs.rip);
println!(
"memory at 0x10000: 0x{:X}",
guest_mem.read_obj::<u32>(GuestAddress(0x10000)).unwrap()
);
}

const fn seg_with_st(selector_index: u16, type_: u8) -> kvm_segment {
kvm_segment {
base: 0,
limit: 0x000fffff,
selector: selector_index << 3,
// 0b1011: Code, Executed/Read, accessed
// 0b0011: Data, Read/Write, accessed
type_,
present: 1,
dpl: 0,
// If L-bit is set, then D-bit must be cleared.
db: 0,
s: 1,
l: 1,
g: 1,
avl: 0,
unusable: 0,
padding: 0,
}
}

// Ref: <https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html> 3-10 Vol. 3A
const fn to_gdt_entry(seg: &kvm_segment) -> u64 {
let base = seg.base;
let limit = seg.limit as u64;
// flags: G, DB, L, AVL
let flags = (seg.g as u64 & 0x1) << 3
| (seg.db as u64 & 0x1) << 2
| (seg.l as u64 & 0x1) << 1
| (seg.avl as u64 & 0x1);
// access: P, DPL, S, Type
let access = (seg.present as u64 & 0x1) << 7
| (seg.dpl as u64 & 0x11) << 5
| (seg.s as u64 & 0x1) << 4
| (seg.type_ as u64 & 0x1111);
((base & 0xff00_0000u64) << 32)
| ((base & 0x00ff_ffffu64) << 16)
| (limit & 0x0000_ffffu64)
| ((limit & 0x000f_0000u64) << 32)
| (flags << 52)
| (access << 40)
}

欢迎关注我的其它发布渠道