Qemu学习-内存虚拟化

Qemu内存模拟

词汇约定

缩写 意义
VA Virtual Address 虚拟地址
PA Physical Address 物理地址
PT Page Table 页表(一级页表)
PDT Page Directory Table 页目录表(二级页表)
PDPT Page Directory Pointers Table 页目录指针表(三级页表)
PML4T Page Map Level 4 Table 四级页表
PGD Page Global Directory 页全局目录
PUD Page Upper Directory 页上级目录
PMD Page Middle Directory 页中间目录
GVA Guest Virtual Address 客户机虚拟地址
GPA Guest Physical Address 客户机物理地址
HVA Host Virtual Address 宿主机虚拟地址
HPA Host Physical Address 宿主机物理地址
GFN Guest Frame Number 虚拟机的页框号
PFN Host Page Frame Number 宿主机的页框号
SPT Shadow Page Table 影子页表

我们主要研究的是GVAGPAHVAHPA之间的关系和相互转化。

Qemu的内存模拟

Qemu利用mmap系统调用,在Qemu进程的虚拟地址空间中申请连续的大小的空间,作为Guest的物理内存,即内存的申请是在用户空间完成的。 通过kvm提供的APIQemuGuest内存的地址信息传递并注册到kvm中维护,即内存的管理是由内核空间的kvm实现的。

即:Qemu负责申请客户机物理内存,kvm负责管理客户机物理内存。

在这样的架构下,内存地址访问有四层映射:

GVA=>GPA=>HVA=>HPA

GVA=>GPA的映射由guest OS负责维护,而HVA =>HPAhost OS负责维护。
而内存虚拟化的关键是GPA=>HVA的映射。

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
              Guest's processes
+--------------------+
GVA | |
+--------------------+
| |
\__ Page Table \__
\ \
| | Guest kernel
+----+--------------------+----------------+
GHA | | | |
+----+--------------------+----------------+
| |
\__ \__
\ \
| QEMU process |
+----+------------------------------------------+
HVA | | |
+----+------------------------------------------+
| |
\__ Page Table \__
\ \
| |
+----+-----------------------------------------------++
HPA | | ||
+----+-----------------------------------------------++

为了提高从GVAHPA的转换效率,KVM常用的实现有SPT(Shadow Page Table)EPT/NPT,前者通过软件维护影子页表直接将GVA转换成HPA,省略中间的映射;后者通过硬件特性实现二级映射(two dimentional paging),将GPA转换成HPA

GVA=>GPA

我们先来分析一下GVA=>GPA,这是比较基础的计算机知识,也是写qemu漏洞利用经常用到的地方,例子如下:

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
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <fcntl.h>
#include <assert.h>
#include <inttypes.h>

#define PAGE_SHIFT 12
#define PAGE_SIZE (1 << PAGE_SHIFT)
#define PFN_PRESENT (1ull << 63)
#define PFN_PFN ((1ull << 55) - 1)

int fd;

uint32_t page_offset(uint32_t addr)
{
return addr & ((1 << PAGE_SHIFT) - 1);
}

uint64_t gva_to_gfn(void *addr)
{
uint64_t pme, gfn;
size_t offset;
offset = ((uintptr_t)addr >> 9) & ~7;
lseek(fd, offset, SEEK_SET);
read(fd, &pme, 8);
if (!(pme & PFN_PRESENT))
return -1;
gfn = pme & PFN_PFN;
return gfn;
}

uint64_t gva_to_gpa(void *addr)
{
uint64_t gfn = gva_to_gfn(addr);
assert(gfn != -1);
return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);
}

int main()
{
uint8_t *ptr;
uint64_t ptr_mem;

fd = open("/proc/self/pagemap", O_RDONLY);
if (fd < 0) {
perror("open");
exit(1);
}

ptr = malloc(256);
strcpy(ptr, "Where am I?");
printf("%s\n", ptr);
ptr_mem = gva_to_gpa(ptr);
printf("Your physical address is at 0x%"PRIx64"\n", ptr_mem);

getchar();
return 0;
}
//=========================================================================================
root@debian:~# ./mmu
Where am I?
Your physical address is at 0x78b0d010

(gdb) info proc mappings
process 14791
Mapped address spaces:

Start Addr End Addr Size Offset objfile
0x7fc314000000 0x7fc314022000 0x22000 0x0
0x7fc314022000 0x7fc318000000 0x3fde000 0x0
0x7fc319dde000 0x7fc31c000000 0x2222000 0x0
0x7fc31c000000 0x7fc39c000000 0x80000000 0x0
...

(gdb) x/s 0x7fc31c000000 + 0x78b0d010
0x7fc394b0d010: "Where am I?"

gva_to_gpa函数原理下方文章里解释地非常详细,这里不再多赘述:

通过/proc/self/pagemap文件

通过CR3寄存器

GPA=>HVA

这个是比较复杂的地方,也是Qemu内存模拟的核心,主要有两种方法,一种是SPT,一种是EPT(Extent Page Table),前者通过软件维护影子页表,后者通过硬件特性实现二级映射。

SPT(影子页表)

KVM通过维护记录GVA=>HPA影子页表SPT,减少了地址转换带来的开销,可以直接将GVA转换为HPA

在软件虚拟化的内存转换中,GVAGPA的转换通过查询CR3寄存器来完成,CR3中保存了Guest的页表基地址,然后载入MMU中进行地址转换。

在加入了SPT技术后,当Guest访问CR3时,KVM会捕获到这个操作EXIT_REASON_CR_ACCESS,之后KVM会载入特殊的CR3和影子页表,欺骗Guest这就是真实的CR3。之后就和传统的访问内存方式一致,当需要访问物理内存的时候,只会经过一层影子页表的转换。

img

影子页表由KVM维护,实际上就是一个Guest页表到Host 页表的映射KVM会将Guest的页表设置为只读,当Guest OS对页表进行修改时就会触发Page FaultVM-EXITKVM,之后KVM会对GVA对应的页表项进行访问权限检查,结合错误码进行判断:

  • 如果是Guest OS引起的,则将该异常注入回去,Guest OS将调用自己的缺页处理函数,申请一个Page,并将PageGPA填充到上级页表项中。
  • 如果是Guest OS的页表和SPT不一致引起的,则同步SPT,根据Guest页表和mmap映射找到GPAHVA的映射关系,然后在SPT中增加/更新GVA-HPA表项。

为了快速检索Guest页表对应的影子页表,KVM为每个客户机维护了一个hash表来进行客户机页表到影子页表之间的映射。
对于每一个Guest来说,其页目录和页表都有唯一的GPA,通过页目录/页表的GPA就可以在哈希链表中快速地找到对应的影子页目录/页表。

Guest切换进程时,会把带切换进程的页表基址载入到GuestCR3中,导致VM-EXITKVM中,KVM再通过哈希表找到对应的 SPT,然后加载到机器的CR3中。

优点:影子页表的引入,减少了GVA=>HPA的转换开销。

缺点:需要为Guest的每个进程都维护一个影子页表,这将带来很大的内存开销。同时影子页表的建立是很耗时的,如果Guest的进程过多,将导致影子页表频繁切换。因此IntelAMD在此基础上提供了基于硬件的虚拟化技术。

EPT/NPT

IntelEPT(Extent Page Table)技术和AMDNPT(Nest Page Table)技术都对内存虚拟化提供了硬件支持。
这两种技术原理类似,都是在硬件层面上实现GVAHPA之间的转换。
下面就以EPT为例分析一下KVM基于硬件辅助的内存虚拟化实现。

img

Intel EPT技术引入了EPT(Extended Page Table)EPTP(EPT base pointer EPT页表基址寄存器)的概念。EPT中维护着GPAHPA的映射,而EPTP负责指向EPT基址,类似于CR3指向Guest OS页表基址

Guest OS运行时,Guest对应的EPT地址被加载到EPTP,而Guest OS当前运行的进程页表基址被加载到CR3。于是在进行地址转换时,首先通过CR3指向的页表实现GVAGPA的转换,再通过EPTP指向的EPT完成GPAHPA的转换。当发生EPT Page Fault时,需要VM-EXITKVM,更新EPT

  • 优点:Guest的缺页在Guest OS内部处理,不会VM-EXITKVM中。地址转化基本由硬件(MMU)查页表来完成,大大提升了效率,且只需为Guest维护一份EPT页表,减少内存的开销。
  • 缺点:两级页表查询,只能寄望于TLB命中。

Qemu与KVM的分工合作

QEMUKVM之间是通过KVM提供的ioctl()接口进行交互的。在linux kernel 2.6kvm_vm_ioctl()函数中,设置虚拟机内存的系统调用为KVM_SET_USER_MEMORY_REGION

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// /virt/kvm/kvm_main.c
static long kvm_vm_ioctl(struct file *filp,
unsigned int ioctl, unsigned long arg)
{
/* ... */
case KVM_SET_USER_MEMORY_REGION: { // 在 KVM 中注册用户空间传入的内存信息
struct kvm_userspace_memory_region kvm_userspace_mem;

r = -EFAULT;
// 将传入的数据结构复制到内核空间
if (copy_from_user(&kvm_userspace_mem, argp, sizeof kvm_userspace_mem))
goto out;

// 实际进行处理的函数
r = kvm_vm_ioctl_set_memory_region(kvm, &kvm_userspace_mem, 1);
if (r)
goto out;
break;
}
/* ... */
}

参数类型为kvm_userspace_memory_region

1
2
3
4
5
6
7
8
/* for KVM_SET_USER_MEMORY_REGION */
struct kvm_userspace_memory_region {
__u32 slot; // slot 编号
__u32 flags; // 标志位,例如是否追踪脏页、是否可用等
__u64 guest_phys_addr; // Guest 物理地址,即 GPA
__u64 memory_size; // 内存大小,单位 bytes
__u64 userspace_addr; // 从 QEMU 进程地址空间中分配内存的起始地址,即 HVA
};

KVM_SET_USER_MEMORY_REGION这个ioctl主要目的就是设置GPA=>HVA的映射关系,KVM会继续调用kvm_vm_ioctl_set_memory_region(),在内核空间维护并管理Guest的内存。

核心数据结构

AddressSpace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// include/exec/memory.h

/**
* AddressSpace: describes a mapping of addresses to #MemoryRegion objects
*/
struct AddressSpace {
/* All fields are private. */
struct rcu_head rcu;
char *name;
MemoryRegion *root; //root级MemoryRegion

/* Accessed via RCU. */
struct FlatView *current_map; //相对应的FlatView平面展开视图

int ioeventfd_nb;
struct MemoryRegionIoeventfd *ioeventfds;
QTAILQ_HEAD(memory_listeners_as, MemoryListener) listeners;
QTAILQ_ENTRY(AddressSpace) address_spaces_link;
};

QEMUAddressSpace结构体表示Guest中CPU/设备看到的内存,类似于物理机中地址空间的概念,但在这里表示的是Guest的一段地址空间,如内存地址空间address_space_memory、I/O 地址空间address_space_io

每个AddressSpace一般包含一系列的MemoryRegionroot指针指向根级MemoryRegion,而root可能有自己的若干个subregions,于是形成树状结构。这些MemoryRegion通过树连接起来,树的根即为AddressSpaceroot域。

AddressSpace有两个静态全局变量address_space_memoryaddress_space_io,这两个变量的root域分别指向后面要说的system_memorysyetem_io

1
2
3
// exec.c 可能不同版本的qemu源码位置不一样
static AddressSpace address_space_memory; // 内存地址空间
static AddressSpace address_space_io; // I/O 地址空间

MemoryRegion

MemoryRegion表示在Guest Memory Layout中的一段内存区域,它是联系GPARAMBlocks(描述真实内存)之间的桥梁。

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
// include/exec/memory.h
struct MemoryRegion {
Object parent_obj;

/* All fields are private - violators will be prosecuted */

/* The following fields should fit in a cache line */
bool romd_mode;
bool ram; //表示是否为RAM
bool subpage;
bool readonly; /* For RAM regions */
bool nonvolatile;
bool rom_device;
bool flush_coalesced_mmio;
bool global_locking;
uint8_t dirty_log_mask;
bool is_iommu;
RAMBlock *ram_block; //指向对应的RAMBlock
Object *owner;

const MemoryRegionOps *ops; //回调函数集合
void *opaque;
MemoryRegion *container;
Int128 size; //该区域内存大小
hwaddr addr;
void (*destructor)(MemoryRegion *mr);
uint64_t align;
bool terminates;
bool ram_device;
bool enabled;
bool warning_printed; /* For reservations */
uint8_t vga_logging_count;
MemoryRegion *alias; //若不为0,则代表自己为alias MemoryRegion,并指向实体的MemoryRegion
hwaddr alias_offset; //若为alias,则为实体MemoryRegion中RAMBlock的host指向地址中的offset
int32_t priority;
QTAILQ_HEAD(subregions, MemoryRegion) subregions; //子区域链表头
QTAILQ_ENTRY(MemoryRegion) subregions_link; //子区域链表节点
QTAILQ_HEAD(coalesced_ranges, CoalescedMemoryRange) coalesced;
const char *name; //此MemoryRegion的name,调试时使用
unsigned ioeventfd_nb;
MemoryRegionIoeventfd *ioeventfds;
};

其同样有两个全局变量system_memorysystem_io

1
2
3
// exec.c
static MemoryRegion *system_memory; // 内存 MemoryRegion,对应 address_space_memory
static MemoryRegion *system_io; // I/O MemoryRegion,对应 address_space_io`

MemoryRegion有多种类型,可以表示一段RAMROMMMIOalias

若为alias则表示一个MemoryRegion的部分区域,例如QEMU会为pc.ram这个表示RAMMemoryRegion添加两个aliasram-below-4gram-above-4g,之后会有代码解释。

另外,MemoryRegion也可以表示一个container,这就表示它只是其他若干个MemoryRegion的容器。

那么要如何创建不同类型的MemoryRegion呢?在QEMU中实际上是通过调用不同的初始化函数区分的。根据不同的初始化函数及其功能,可以将MemoryRegion划分为以下三种类型:

root MemoryRegion

直接通过memory_region_init初始化,没有自己的内存,用于管理subregion,例如之前说的system_memory

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
void memory_region_init(MemoryRegion *mr,
Object *owner,
const char *name,
uint64_t size)
{
object_initialize(mr, sizeof(*mr), TYPE_MEMORY_REGION);
memory_region_do_init(mr, owner, name, size);
}

static void memory_region_do_init(MemoryRegion *mr,
Object *owner,
const char *name,
uint64_t size)
{
mr->size = int128_make64(size);
if (size == UINT64_MAX) {
mr->size = int128_2_64();
}
mr->name = g_strdup(name);
mr->owner = owner;
mr->ram_block = NULL; //无自己的RAMBloack,即没有自己的内存

if (name) {
char *escaped_name = memory_region_escape_name(name);
char *name_array = g_strdup_printf("%s[*]", escaped_name);

if (!owner) {
owner = container_get(qdev_get_machine(), "/unattached");
}

object_property_add_child(owner, name_array, OBJECT(mr), &error_abort);
object_unref(OBJECT(mr));
g_free(name_array);
g_free(escaped_name);
}
}

实体 MemoryRegion

通过memory_region_init_ram初始化,有自己的内存(从qemu进程地址空间中分配),大小为size,例如ram_memorypci_memory

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
void *pc_memory_init(MemoryRegion *system_memory,
const char *kernel_filename,
const char *kernel_cmdline,
const char *initrd_filename,
ram_addr_t below_4g_mem_size,
ram_addr_t above_4g_mem_size,
MemoryRegion *rom_memory,
MemoryRegion **ram_memory)
{
MemoryRegion *ram, *option_rom_mr;
/* ...*/

option_rom_mr = g_malloc(sizeof(*option_rom_mr));
//调用memory_region_init_ram对ram_memory初始化
memory_region_init_ram(option_rom_mr, NULL, "pc.rom", PC_ROM_SIZE,
&error_fatal);
/* ... */
}

void memory_region_init_ram(MemoryRegion *mr,
struct Object *owner,
const char *name,
uint64_t size,
Error **errp)
{
DeviceState *owner_dev;
Error *err = NULL;

memory_region_init_ram_nomigrate(mr, owner, name, size, &err);
if (err) {
error_propagate(errp, err);
return;
}
/* This will assert if owner is neither NULL nor a DeviceState.
* We only want the owner here for the purposes of defining a
* unique name for migration. TODO: Ideally we should implement
* a naming scheme for Objects which are not DeviceStates, in
* which case we can relax this restriction.
*/
owner_dev = DEVICE(owner);
vmstate_register_ram(mr, owner_dev);
}

void memory_region_init_ram_nomigrate(MemoryRegion *mr,
Object *owner,
const char *name,
uint64_t size,
Error **errp)
{
memory_region_init_ram_shared_nomigrate(mr, owner, name, size, false, errp);
}

void memory_region_init_ram_shared_nomigrate(MemoryRegion *mr,
Object *owner,
const char *name,
uint64_t size,
bool share,
Error **errp)
{
Error *err = NULL;
memory_region_init(mr, owner, name, size);
mr->ram = true; //为RAM
mr->terminates = true;
mr->destructor = memory_region_destructor_ram;
mr->ram_block = qemu_ram_alloc(size, share, mr, &err);//从qemu的进程地址空间中申请此MemoryRegion的RAMBloack结构体内存并将指针填入ram_block,RAMBloack才是记录实际分配的内存地址信息的结构体,后面会分析到。
mr->dirty_log_mask = tcg_enabled() ? (1 << DIRTY_MEMORY_CODE) : 0;
if (err) {
mr->size = int128_zero();
object_unparent(OBJECT(mr));
error_propagate(errp, err);
}
}

alias MemoryRegion

通过memory_region_init_alias() 初始化,没有自己的内存,表示实体 MemoryRegion的一部分。通过alias成员指向实体 MemoryRegionalias_offset为在实体 MemoryRegion中的偏移量,例如ram_below_4gram_above_4g

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
void pc_memory_init(PCMachineState *pcms,
MemoryRegion *system_memory,
MemoryRegion *rom_memory,
MemoryRegion **ram_memory)
{
int linux_boot, i;
MemoryRegion *ram, *option_rom_mr;
MemoryRegion *ram_below_4g, *ram_above_4g;
FWCfgState *fw_cfg;
MachineState *machine = MACHINE(pcms);
PCMachineClass *pcmc = PC_MACHINE_GET_CLASS(pcms);

assert(machine->ram_size == pcms->below_4g_mem_size +
pcms->above_4g_mem_size);

linux_boot = (machine->kernel_filename != NULL);

/* Allocate RAM. We allocate it as a single memory region and use
* aliases to address portions of it, mostly for backwards compatibility
* with older qemus that used qemu_ram_alloc().
*/
ram = g_malloc(sizeof(*ram));
memory_region_allocate_system_memory(ram, NULL, "pc.ram",
machine->ram_size);
*ram_memory = ram;
ram_below_4g = g_malloc(sizeof(*ram_below_4g));
memory_region_init_alias(ram_below_4g, NULL, "ram-below-4g", ram,
0, pcms->below_4g_mem_size);
/*...*/
}

void memory_region_init_alias(MemoryRegion *mr,
Object *owner,
const char *name,
MemoryRegion *orig,
hwaddr offset,
uint64_t size)
{
memory_region_init(mr, owner, name, size);
mr->alias = orig; //指向实体MemoryRegion
mr->alias_offset = offset; //实体MemoryRegion的RAMBlock的host指向的地址中的偏移
}

RAMBlock

MemoryRegion用来描述一段逻辑层面上的内存区域,而记录实际分配的内存地址信息的结构体则是RAMBlock

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
// include/exec/ram_addr.h
struct RAMBlock {
struct rcu_head rcu;
struct MemoryRegion *mr; //唯一对应的MemoryRegion
uint8_t *host; //RAMBlock关联的内存,HVA
uint8_t *colo_cache; /* For colo, VM's ram cache */
ram_addr_t offset; //RAMBlock在VM物理内存中的偏移量,即GPA
ram_addr_t used_length;
ram_addr_t max_length;
void (*resized)(const char*, uint64_t length, void *host);
uint32_t flags;
/* Protected by iothread lock. */
char idstr[256]; //RAMBlock的id
/* RCU-enabled, writes protected by the ramlist lock */
QLIST_ENTRY(RAMBlock) next; //指向下一个RAMBlock
QLIST_HEAD(, RAMBlockNotifier) ramblock_notifiers;
int fd;
size_t page_size;
/* dirty bitmap used during migration */
unsigned long *bmap;
/* bitmap of pages that haven't been sent even once
* only maintained and used in postcopy at the moment
* where it's used to send the dirtymap at the start
* of the postcopy phase
*/
unsigned long *unsentmap;
/* bitmap of already received pages in postcopy */
unsigned long *receivedmap;
};

可以看到在RAMBlockhostoffset域分别对应了HVAGPA,因此也可以说RAMBlock 中存储了GPA->HVA的映射关系,另外每一个RAMBlock都会指向其所属的MemoryRegion

全局变量ramlist以单链表的形式管理所有的RAMBlock

1
2
3
4
5
6
7
8
9
10
11
// include/exec/ramlist.h
typedef struct RAMList {
QemuMutex mutex;
RAMBlock *mru_block;
/* RCU-enabled, writes protected by the ramlist lock. */
QLIST_HEAD(, RAMBlock) blocks;
DirtyMemoryBlocks *dirty_memory[DIRTY_MEMORY_NUM];
uint32_t version;
QLIST_HEAD(, RAMBlockNotifier) ramblock_notifiers;
} RAMList;
extern RAMList ram_list;

每一个新分配的RAMBlock都会被插入到ram_list的头部。如需查找地址所对应的RAMBlock,则需要遍历ram_list,当目标地址落在当前RAMBlock的地址区间时,该RAMBlock即为查找目标。

img

关系小结

AddressSpaceMemoryRegionRAMBlock之间的关系如下所示:

img

可以看到AddressSpaceroot域指向根级MemoryRegionAddressSpace是由root域指向的MemoryRegion及其子树共同表示的。MemoryRegion作为一个逻辑层面的内存区域,还需借助分布在其中的RAMBlock来存储真实的地址映射关系

img

FlatView

AddressSpace 的root域及其子树共同构成了 Guest 的物理地址空间,但这些都是在QEMU侧定义的。要传入KVM进行设置时,复杂的树状结构是不利于内核进行处理的,因此需要将其转换为一个平坦的地址模型,也就是一个从零开始、只包含地址信息的数据结构,这在QEMU中通过FlatView来表示。每个AddressSpace都有一个与之对应的FlatView指针current_map,表示其对应的平面展开视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
// include/exec/memory.h
/* Flattened global view of current active memory hierarchy. Kept in sorted
* order.
*/
struct FlatView {
struct rcu_head rcu;
unsigned ref;
FlatRange *ranges; //对应的FlatRange数组
unsigned nr; //FlatRange的数目
unsigned nr_allocated; //当前数组的项数
struct AddressSpaceDispatch *dispatch;
MemoryRegion *root;
};

FlatView中,FlatRange表示在FlatView中的一段内存范围:

1
2
3
4
5
6
7
8
9
10
11
// memory.c
/* Range of memory in the global map. Addresses are absolute. */
struct FlatRange {
MemoryRegion *mr; //指向所属的MemoryRegion
hwaddr offset_in_region; //在全局MemoryRegion中的offset,对应GPA
AddrRange addr; //代表的地址区间,对应HVA
uint8_t dirty_log_mask;
bool romd_mode;
bool readonly;
bool nonvolatile;
};

每个FlatRange对应一段虚拟机物理地址区间,各个FlatRange不会重叠,按照地址的顺序保存在数组中,具体的地址范围由一个 AddrRange结构来描述:

1
2
3
4
5
6
7
8
9
10
11
// memory.c
typedef struct AddrRange AddrRange;

/*
* Note that signed integers are needed for negative offsetting in aliases
* (large MemoryRegion::alias_offset).
*/
struct AddrRange {
Int128 start;
Int128 size;
};

MemoryRegionSection

QEMU中,还有几个起到中介作用的结构体,MemoryRegionSection就是其中之一。

之前介绍的FlatRange代表一个物理地址空间的片段,偏向于描述在Host侧即AddressSpace中的分布,而MemoryRegionSection则代表在Guest侧即MemoryRegion中的片段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// include/exec/memory.h
/**
* MemoryRegionSection: describes a fragment of a #MemoryRegion
*
* @mr: the region, or %NULL if empty
* @fv: the flat view of the address space the region is mapped in
* @offset_within_region: the beginning of the section, relative to @mr's start
* @size: the size of the section; will not exceed @mr's boundaries
* @offset_within_address_space: the address of the first byte of the section
* relative to the region's address space
* @readonly: writes to this section are ignored
* @nonvolatile: this section is non-volatile
*/
struct MemoryRegionSection {
MemoryRegion *mr; //所属的MemoryRegion
FlatView *fv;
hwaddr offset_within_region; //在MemoryRegion内部的offset
Int128 size;
hwaddr offset_within_address_space; //在AddressSpace内部的offset
bool readonly;
bool nonvolatile;
};

img

  • AddressSpaceroot指向对应的根级MemoryRegioncurrent_map指向root通过generate_memory_topology()生成的FlatView
  • FlatView中的ranges数组表示该MemoryRegion所表示的Guest地址区间,并按照地址的顺序进行排列。
  • MemoryRegionSectionranges数组中的FlatRange对应生成,作为注册到KVM中的基本单位。

参考

https://abelsu7.top/2019/07/07/kvm-memory-virtualization/

https://www.binss.me/blog/qemu-note-of-memory/

[https://juniorprincewang.github.io/2018/07/20/qemu%E5%86%85%E5%AD%98%E8%99%9A%E6%8B%9F%E5%8C%96/

https://www.slideshare.net/HwanjuKim/4memory-virtualization-and-management?next_slideshow=1

打赏还是打残,这是个问题