时间轴

2025-11-21

  1. init

环境

源码

1
2
3
4
5
6
wget https://download.qemu.org/qemu-10.1.2.tar.xz
tar xvJf qemu-10.1.2.tar.xz
cd qemu-10.1.2
mkdir -p output
./configure --prefix=$PWD/output --target-list=aarch64-softmmu,riscv64-softmmu --enable-debug
bear -- make -j$(nproc)

创建.clangd

1
2
3
CompileFlags:
Add: -Wno-unknown-warning-option
Remove: [-m*, -f*]

gdb

1
gdb -args ./build/qemu-system-riscv64 -M virt -device edu,id=edu1 -nographic

基本介绍

从 CPU 的角度来说,一切访存行为都是对地址进行操作的(load/store),CPU 并不关心这个地址背后对应的是什么设备,只要能读写到正确结果即可。

CPU访存流程

  1. CPU 在计算(通过算术逻辑单元 ALU)出目标地址以后,将其发送到地址总线上,同时 CPU 还会给出读写的控制信号;
  2. 地址对应的设备,可能是一块普通内存,也可能是一个 I/O 设备(这里特指外设),会对地址总线的信号进行响应;
  3. 如果是读操作,则将该地址对应的数据,按照 CPU 指定的位宽大小,通过总线传输回去,一般是存放到 CPU 访存指令给出的寄存器内;
  4. 如果是写操作,则会把总线传递过来的数据,按照 CPU 指定的位宽大小,写入指定地址里面,如果是 I/O 设备,一般是更新了这个地址对应的寄存器,并可能产生副作用。

Qemu模拟内存外设

为了能够模拟内存/外设的行为,QEMU 至少要实现以下机制:

  1. 基本的地址空间管理,能够根据 CPU 投递过来的地址,区分是什么设备
  2. 实现地址的离散映射,有些外设的地址不一定是连续的
  3. 实现地址的重映射,比如 MCS-51 的 RAM、XRAM 都是从 0 地址开始的;

为此,QEMU 提供了两个概念,address-spacememory-region(下文简称为 mr),前者用于描述整个地址空间的映射关系(不同部件看到的地址空间可能不同),后者用于描述地址空间中某个地址范围内的映射规则。

地址空间布局

执行:

1
$ ./build/qemu-system-riscv64 -M virt -monitor stdio -s -S -display none

然后我们输入 info mtree 命令可以看到地址空间的布局:

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
(qemu) info mtree
address-space: cpu-memory-0
address-space: memory
0000000000000000-ffffffffffffffff (prio 0, i/o): system
0000000000001000-000000000000ffff (prio 0, rom): riscv_virt_board.mrom
0000000000100000-0000000000100fff (prio 0, i/o): riscv.sifive.test
0000000000101000-0000000000101023 (prio 0, i/o): goldfish_rtc
0000000002000000-0000000002003fff (prio 0, i/o): riscv.aclint.swi
0000000002004000-000000000200bfff (prio 0, i/o): riscv.aclint.mtimer
0000000003000000-000000000300ffff (prio 0, i/o): gpex_ioport_window
0000000003000000-000000000300ffff (prio 0, i/o): gpex_ioport
0000000004000000-0000000005ffffff (prio 0, i/o): platform bus
000000000c000000-000000000c5fffff (prio 0, i/o): riscv.sifive.plic
0000000010000000-0000000010000007 (prio 0, i/o): serial
0000000010001000-00000000100011ff (prio 0, i/o): virtio-mmio
0000000010002000-00000000100021ff (prio 0, i/o): virtio-mmio
0000000010003000-00000000100031ff (prio 0, i/o): virtio-mmio
0000000010004000-00000000100041ff (prio 0, i/o): virtio-mmio
0000000010005000-00000000100051ff (prio 0, i/o): virtio-mmio
0000000010006000-00000000100061ff (prio 0, i/o): virtio-mmio
0000000010007000-00000000100071ff (prio 0, i/o): virtio-mmio
0000000010008000-00000000100081ff (prio 0, i/o): virtio-mmio
0000000010100000-0000000010100007 (prio 0, i/o): fwcfg.data
0000000010100008-0000000010100009 (prio 0, i/o): fwcfg.ctl
0000000010100010-0000000010100017 (prio 0, i/o): fwcfg.dma
0000000020000000-0000000021ffffff (prio 0, romd): virt.flash0
0000000022000000-0000000023ffffff (prio 0, romd): virt.flash1
0000000030000000-000000003fffffff (prio 0, i/o): alias pcie-ecam @pcie-mmcfg-mmio 0000000000000000-000000000fffffff
0000000040000000-000000007fffffff (prio 0, i/o): alias pcie-mmio @gpex_mmio_window 0000000040000000-000000007fffffff
0000000080000000-0000000087ffffff (prio 0, ram): riscv_virt_board.ram
0000000400000000-00000007ffffffff (prio 0, i/o): alias pcie-mmio-high @gpex_mmio_window 0000000400000000-00000007ffffffff

address-space: gpex-root
0000000000000000-ffffffffffffffff (prio 0, i/o): bus master container

address-space: I/O
0000000000000000-000000000000ffff (prio 0, i/o): io

memory-region: pcie-mmcfg-mmio
0000000000000000-000000000fffffff (prio 0, i/o): pcie-mmcfg-mmio

memory-region: gpex_mmio_window
0000000000000000-ffffffffffffffff (prio 0, i/o): gpex_mmio_window
0000000000000000-ffffffffffffffff (prio 0, i/o): gpex_mmio

memory-region: system
0000000000000000-ffffffffffffffff (prio 0, i/o): system
0000000000001000-000000000000ffff (prio 0, rom): riscv_virt_board.mrom
0000000000100000-0000000000100fff (prio 0, i/o): riscv.sifive.test
0000000000101000-0000000000101023 (prio 0, i/o): goldfish_rtc
0000000002000000-0000000002003fff (prio 0, i/o): riscv.aclint.swi
0000000002004000-000000000200bfff (prio 0, i/o): riscv.aclint.mtimer
0000000003000000-000000000300ffff (prio 0, i/o): gpex_ioport_window
0000000003000000-000000000300ffff (prio 0, i/o): gpex_ioport
0000000004000000-0000000005ffffff (prio 0, i/o): platform bus
000000000c000000-000000000c5fffff (prio 0, i/o): riscv.sifive.plic
0000000010000000-0000000010000007 (prio 0, i/o): serial
0000000010001000-00000000100011ff (prio 0, i/o): virtio-mmio
0000000010002000-00000000100021ff (prio 0, i/o): virtio-mmio
0000000010003000-00000000100031ff (prio 0, i/o): virtio-mmio
0000000010004000-00000000100041ff (prio 0, i/o): virtio-mmio
0000000010005000-00000000100051ff (prio 0, i/o): virtio-mmio
0000000010006000-00000000100061ff (prio 0, i/o): virtio-mmio
0000000010007000-00000000100071ff (prio 0, i/o): virtio-mmio
0000000010008000-00000000100081ff (prio 0, i/o): virtio-mmio
0000000010100000-0000000010100007 (prio 0, i/o): fwcfg.data
0000000010100008-0000000010100009 (prio 0, i/o): fwcfg.ctl
0000000010100010-0000000010100017 (prio 0, i/o): fwcfg.dma
0000000020000000-0000000021ffffff (prio 0, romd): virt.flash0
0000000022000000-0000000023ffffff (prio 0, romd): virt.flash1
0000000030000000-000000003fffffff (prio 0, i/o): alias pcie-ecam @pcie-mmcfg-mmio 0000000000000000-000000000fffffff
0000000040000000-000000007fffffff (prio 0, i/o): alias pcie-mmio @gpex_mmio_window 0000000040000000-000000007fffffff
0000000080000000-0000000087ffffff (prio 0, ram): riscv_virt_board.ram
0000000400000000-00000007ffffffff (prio 0, i/o): alias pcie-mmio-high @gpex_mmio_window 0000000400000000-00000007ffffffff

可以看出:

地址范围 类型 对应设备
0x00001000~0xFFFF ROM 板载固件
0x00100000~0x00101023 I/O 测试设备 + RTC
0x02000000~0x020BFFFF I/O CLINT(软件/时钟中断)
0x03000000~0x030FFFFF I/O PCI I/O window
0x04000000~0x05FFFFFF I/O Platform Bus 设备
0x0C000000~0x0C5FFFFF I/O PLIC
0x10000000~0x100081FF I/O UART + Virtio-MMIO
0x20000000~0x23FFFFFF ROMD flash0 / flash1
0x80000000~0x87FFFFFF RAM Guest 内存
0x300000000~0x7FFFFFFFF I/O PCI MMIO alias
  • 一个 Guest(表示被模拟的对象,这里指 virt machine)可以有多个 address-space每个 address-space 描述的地址映射关系不一定相同,典型的是 I/Omemory
  • 每个 address-space 对应一个 mr 树,比如 address-space: memory 对应的 mr 的根节点是 system,子节点按照地址大小顺序排列。

由于 mr 描述的是某个具体地址范围内的映射规则,因此可以很方便地实现设备的离散映射。

举例:

1
2
3
4
5
address-space: cpu-memory-0
0000000000000000-ffffffffffffffff (prio 0, i/o): system
0000000000001000-000000000000ffff (prio 0, rom): riscv_virt_board.mrom
0000000000100000-0000000000100fff (prio 0, i/o): riscv.sifive.test
...
  • cpu-memory-0 是 address-space
  • system 是顶层 memory-region(整个虚拟系统的容器)
  • riscv_virt_board.mromsifive.test 等都是 子 memory-region

每个 memory-region 都挂在 address-space 下,并提供访问 handler

memory_region地址重叠

mr 支持同一级之间地址范围重叠,重叠的部分按照优先级呈现,高优先级的重叠部分作为访问目标。(prio 0, type) 中的 prio 后跟着的是优先级,virt 的外设之间没有地址重叠,因此优先级都是 0。

这里举例说明:

1
2
3
4
5
6
0x8000   0x70000  0x60000  0x50000  0x40000  0x30000  0x20000  0x10000    0
|--------|--------|--------|--------|--------|--------|--------|--------|
A:[-----------------------------------------------------------------------] prio:0
B:[-----------------------------------------------------] prio:1
C:[-----------------------------------] prio:2
D:[-----------------] prio:3

对于 mr A 来说,它的地址范围可以看成:

1
2
3
0x8000   0x70000  0x60000  0x50000  0x40000  0x30000  0x20000  0x10000    0
|--------|--------|--------|--------|--------|--------|--------|--------|
A:[DDDDDDDDDDDDDDDDD|CCCCCCCCCCCCCCCCC|BBBBBBBBBBBBBBBBB|AAAAAAAAAAAAAAAAA]

为了实现以上机制,QEMU 使用 alias 来描述 mr 中重叠的部分,使用 alias 可以将一个 mr 的一部分放到另外一个 mr 上,以此来简化内存模拟的复杂度(可以类比 mmap)

alias 示例

1
0000000030000000-000000003fffffff (prio 0, i/o): alias pcie-ecam @pcie-mmcfg-mmio 0000000000000000-000000000fffffff
  • alias memory-region 将一个 region 映射到 address-space 的另一个地址范围
  • 方便不同总线访问同一物理设备
  • alias 也是 memory-region,只是内部引用了另一个 region

AddressSpace

  • 表示 CPU 或总线看到的完整地址空间
  • 包括:
    • 所有被映射的 memory-region
    • 每个 region 的优先级(prio)
    • 类型(RAM / ROM / I/O / alias)
  • 可以理解为 虚拟机的“物理地址空间视图”

一个 CPU 可以有多个 address-space(比如 risc-v CPU 有 cpu-memory-0,还有其他 I/O space 或 PCI 总线 space)。

include/system/memory.h

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
/**
* struct AddressSpace: describes a mapping of addresses to #MemoryRegion objects
*/
struct AddressSpace {
/* private: */
struct rcu_head rcu;
char *name;
MemoryRegion *root;

/* Accessed via RCU. */
struct FlatView *current_map;

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

/*
* Maximum DMA bounce buffer size used for indirect memory map requests.
* This limits the total size of bounce buffer allocations made for
* DMA requests to indirect memory regions within this AddressSpace. DMA
* requests that exceed the limit (e.g. due to overly large requested size
* or concurrent DMA requests having claimed too much buffer space) will be
* rejected and left to the caller to handle.
*/
size_t max_bounce_buffer_size;
/* Total size of bounce buffers currently allocated, atomically accessed */
size_t bounce_buffer_size;
/* List of callbacks to invoke when buffers free up */
QemuMutex map_client_list_lock;
QLIST_HEAD(, AddressSpaceMapClient) map_client_list;
};

MemoryRegion

  • 内存或 I/O 的 具体块
  • 包括:
    • 起始地址范围(相对 address-space 的偏移)
    • 大小
    • 类型(RAM / ROM / I/O/ alias / container)
    • 子 memory-region(支持嵌套)
    • 对应的 read/write handler 或对象指针

可以理解为 address-space 中的“单个区块”,它可以是物理内存、设备寄存器、PCI BAR 等。

include/system/memory.h

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
/** MemoryRegion:
*
* A struct representing a memory region.
*/
struct MemoryRegion {
Object parent_obj;

/* private: */

/* The following fields should fit in a cache line */
bool romd_mode;
bool ram;
bool subpage;
bool readonly; /* For RAM regions */
bool nonvolatile;
bool rom_device;
bool flush_coalesced_mmio;
bool unmergeable;
uint8_t dirty_log_mask;
bool is_iommu;
RAMBlock *ram_block;
Object *owner;
/* owner as TYPE_DEVICE. Used for re-entrancy checks in MR access hotpath */
DeviceState *dev;

const MemoryRegionOps *ops;
void *opaque;
MemoryRegion *container;
int mapped_via_alias; /* Mapped via an alias, container might be NULL */
Int128 size;
hwaddr addr;
void (*destructor)(MemoryRegion *mr);
uint64_t align;
bool terminates;
bool ram_device;
bool enabled;
uint8_t vga_logging_count;
MemoryRegion *alias;
hwaddr alias_offset;
int32_t priority;
QTAILQ_HEAD(, MemoryRegion) subregions;
QTAILQ_ENTRY(MemoryRegion) subregions_link;
QTAILQ_HEAD(, CoalescedMemoryRange) coalesced;
const char *name;
unsigned ioeventfd_nb;
MemoryRegionIoeventfd *ioeventfds;
RamDiscardManager *rdm; /* Only for RAM */

/* For devices designed to perform re-entrant IO into their own IO MRs */
bool disable_reentrancy_guard;
};

初始化流程

我们从 QEMU 初始化过程,来理解 mr 和 address-space 的关系:

1
2
3
4
5
6
7
8
9
10
main() // system/main.c
|--qemu_init(argc, argv) // system/vlc.c
| |--cpu_exec_init_all() // system/physmem.c
| | |--io_mem_init()
| | | |--memory_region_init_io(&io_mem_unassigned, NULL, &unassigned_mem_ops, NULL, NULL, UINT64_MAX)
| | |--memory_map_init()
| | | |--memory_region_init(system_memory, NULL, "system", UINT64_MAX)
| | | |--address_space_init(&address_space_memory, system_memory, "memory")
| | | |--memory_region_init_io(system_io, NULL, &unassigned_io_ops, NULL, "io", 65536)
| | | |--address_space_init(&address_space_io, system_io, "I/O")

memory_map_init

system/physmem.c

1
2
3
4
5
6
7
8
9
10
11
12
static void memory_map_init(void)
{
system_memory = g_malloc(sizeof(*system_memory));

memory_region_init(system_memory, NULL, "system", UINT64_MAX);
address_space_init(&address_space_memory, system_memory, "memory");

system_io = g_malloc(sizeof(*system_io));
memory_region_init_io(system_io, NULL, &unassigned_io_ops, NULL, "io",
65536);
address_space_init(&address_space_io, system_io, "I/O");
}

对于 memory_region_init() ,最终调用到 memory_region_do_init()

system/memory.c

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
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->dev = (DeviceState *) object_dynamic_cast(mr->owner, TYPE_DEVICE);
mr->ram_block = NULL;

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

if (!owner) {
owner = machine_get_container("unattached");
}

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

在这段代码会完成 mr 一些关键字段的初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** MemoryRegion:
*
* A struct representing a memory region.
*/
struct MemoryRegion {
Object parent_obj;

/* private: */
Object *owner;
const MemoryRegionOps *ops;
Int128 size;
QTAILQ_HEAD(, MemoryRegion) subregions;
QTAILQ_ENTRY(MemoryRegion) subregions_link;
...
};
  • ops 指向 mr 访存的实际接口;
  • subregions 指向其他 mr,通过 subregions,可以将所有关联的 mr 串起来。

这部分初始化代码,有一些是注册的函数回调,静态 review 代码不太方便理清中间的逻辑,可以借助 gdb 来操作。

system_memory 是一个全局变量指针指向 mr 的根节点,我们可以对 system_memory->ops 和 system_memory->subregions 进行监视,看看是在哪个函数内被初始化的。

首先观察 system_memory->ops,命令和流程如下:

watch system_memory->ops

第一次和第二次命中监视点,是对 ops 进行 reset 操作,第三次命中,是真正初始化的地方,我们可以观察一下调用栈:

ops初始化

可以看到 memory_region_initfn()是在 object_init_with_type()中被调用,这是 QEMU 的 QOM 模块,可以简单理解为是对 mr 对象的初始化,这个初始化方法是注册的一个函数指针

以此类推,我们可以得到 system_memory->subregions 是在哪里被初始化的:

system_memory->subregions

结点关系

进一步watch system_memory->subregions

其他subregions是如何被添加进来的

memory_region_update_container_subregions() 的过程很简单,最终执行的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
                      struct MemoryRegion
+------------------------+
|subregions |
| QTAILQ_HEAD() |
+------------------------+
|
+-------------------+---------------------+
| |
| |
struct MemoryRegion struct MemoryRegion
+------------------------+ +------------------------+
|subregions | |subregions |
| QTAILQ_HEAD() | | QTAILQ_HEAD() |
+------------------------+ +------------------------+
... ...

是不是很像一个树形结构?其实这就是红黑树

address-space 内有一个 root 字段,指向 memory-region 的根节点,这样就实现了一个 address-space 对应一个 memory-region 树,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
     AddressSpace
+-------------------------+
|name |
| (char *) |
| | MemoryRegion(system_memory/system_io)
+-------------------------+ +------------------------+
|root | |subregions |
| (MemoryRegion *) | -------->| QTAILQ_HEAD() |
+-------------------------+ +------------------------+
|
|
+-------------------+---------------------+
| |
struct MemoryRegion struct MemoryRegion
+------------------------+ +------------------------+
|subregions | |subregions |
| QTAILQ_HEAD() | | QTAILQ_HEAD() |
+------------------------+ +------------------------+

每个 mr 会对应到具体的内存块 RAMBlock这个内存块从 Host 申请,作为 Guest 外围设备的存储。

mr 提供了一些类型,用于描述存储设备,常见的有 RAM、ROM、IOMMU、container。

回到 QEMU 的交互终端,使用如下命令,我们可以打印 virt 的 memory-region 分布和对应的外设:

qom-tree

memory-region (mr) 都表示一个可访问的地址块,比如:

  • RAM/ROM → 存储内容
  • I/O → 设备寄存器
  • alias → 地址映射
  • container → “容器”,管理子 region

每个 mr 都有 起始偏移(相对于父 region)和 大小

对于 memory-region container 类型,它包含了其他的 mr,记录每个 mr 的 offset。

container 是一种特殊类型的 memory-region

它自身 不存储数据,也没有直接的读写 handler

作用:

  1. 管理子 memory-region
  2. 提供地址偏移映射
  3. 建立层级化结构

在实际应用场景,我们可以利用 mr container 创建不同的地址层级关系,可以在地址空间层面,清晰的描述不同子系统的关系,对于实现模块化有很好的帮助。

参考: