时间轴
时间轴
2025-11-21
- init
本文介绍了QEMU中地址空间抽象的核心机制,详细阐述了AddressSpace和MemoryRegion两个关键概念。文章从CPU访存流程出发,说明了QEMU如何通过地址空间管理区分不同设备、实现离散映射和重映射。文中展示了虚拟机地址空间的布局示例,解释了memory-region的重叠处理方式及alias机制的作用,并探讨了初始化流程中关键字段的赋值过程。通过本文的讨论,读者可以深入理解QEMU模拟内存与外设的底层实现原理。
环境
源码
123456 | wget https://download.qemu.org/qemu-10.1.2.tar.xztar xvJf qemu-10.1.2.tar.xzcd qemu-10.1.2mkdir -p output./configure --prefix=$PWD/output --target-list=aarch64-softmmu,riscv64-softmmu --enable-debugbear -- make -j$(nproc) |
创建.clangd
123 | 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访存流程
- CPU 在计算(通过算术逻辑单元 ALU)出目标地址以后,将其发送到地址总线上,同时 CPU 还会给出读写的控制信号;
- **地址对应的设备,可能是一块普通内存,也可能是一个 I/O 设备(**这里特指外设),会对地址总线的信号进行响应;
- 如果是读操作,则将该地址对应的数据,按照 CPU 指定的位宽大小,通过总线传输回去,一般是存放到 CPU 访存指令给出的寄存器内;
- 如果是写操作,则会把总线传递过来的数据,按照 CPU 指定的位宽大小,写入指定地址里面,如果是 I/O 设备,一般是更新了这个地址对应的寄存器,并可能产生副作用。
Qemu模拟内存外设
为了能够模拟内存/外设的行为,QEMU 至少要实现以下机制:
- 基本的地址空间管理,能够根据 CPU 投递过来的地址,区分是什么设备;
- 实现地址的离散映射,有些外设的地址不一定是连续的;
- 实现地址的重映射,比如 MCS-51 的 RAM、XRAM 都是从 0 地址开始的;
为此,QEMU 提供了两个概念:address-space 和 memory-region(下文简称为 mr)。前者用于描述整个地址空间的映射关系(不同部件看到的地址空间可能不同),后者用于描述地址空间中某个地址范围内的映射规则。
地址空间布局
执行:
1 | $ ./build/qemu-system-riscv64 -M virt -monitor stdio -s -S -display none |
然后我们输入info mtree命令可以看到地址空间的布局:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475 | (qemu) info mtreeaddress-space: cpu-memory-0address-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-00000007ffffffffaddress-space: gpex-root 0000000000000000-ffffffffffffffff (prio 0, i/o): bus master containeraddress-space: I/O 0000000000000000-000000000000ffff (prio 0, i/o): iomemory-region: pcie-mmcfg-mmio 0000000000000000-000000000fffffff (prio 0, i/o): pcie-mmcfg-mmiomemory-region: gpex_mmio_window 0000000000000000-ffffffffffffffff (prio 0, i/o): gpex_mmio_window 0000000000000000-ffffffffffffffff (prio 0, i/o): gpex_mmiomemory-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/O 和 memory。
- 每个 address-space 对应一个 mr 树,比如 address-space: memory 对应的 mr 的根节点是 system,子节点按照地址大小顺序排列。
由于 mr 描述的是某个具体地址范围内的映射规则,因此可以很方便地实现设备的离散映射。
举例:
12345 | 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-spacesystem是顶层 memory-region(整个虚拟系统的容器)riscv_virt_board.mrom、sifive.test等都是 子 memory-region
每个 memory-region 都挂在 address-space 下,并提供访问 handler
memory_region地址重叠
mr 支持同一级之间地址范围重叠,重叠的部分按照优先级呈现,高优先级的重叠部分作为访问目标。(prio 0, type) 中的 prio 后跟着的是优先级,virt 的外设之间没有地址重叠,因此优先级都是 0。
这里举例说明:
123456 | 0x8000 0x70000 0x60000 0x50000 0x40000 0x30000 0x20000 0x10000 0 |--------|--------|--------|--------|--------|--------|--------|--------|A:[-----------------------------------------------------------------------] prio:0B:[-----------------------------------------------------] prio:1C:[-----------------------------------] prio:2D:[-----------------] prio:3 |
对于 mr A 来说,它的地址范围可以看成:
123 | 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
12345678910111213141516171819202122232425262728293031323334 | /** * 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
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152 | /** 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 的关系:
12345678910 | 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
123456789101112 | 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
12345678910111213141516171819202122232425262728293031323334353637 | 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 一些关键字段的初始化。
123456789101112131415 | /** 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,命令和流程如下:

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

可以看到memory_region_initfn()是在object_init_with_type()中被调用的,这是 QEMU 的 QOM 模块,可以简单理解为是对 mr 对象的初始化,这个初始化方法是一个注册的函数指针。
以此类推,我们可以得到 system_memory->subregions 是在哪里被初始化的:

结点关系
进一步watchsystem_memory->subregions

memory_region_update_container_subregions()的过程很简单,最终执行的结果如下:
123456789101112131415 | struct MemoryRegion +------------------------+ |subregions | | QTAILQ_HEAD() | +------------------------+ | +-------------------+---------------------+ | | | | struct MemoryRegion struct MemoryRegion+------------------------+ +------------------------+|subregions | |subregions || QTAILQ_HEAD() | | QTAILQ_HEAD() |+------------------------+ +------------------------+ ... ... |
QEMU 的 memory-region 组织成树形结构,而地址查找时使用红黑树(Interval Tree)来高效管理地址区间。
address-space 内有一个 root 字段,指向 memory-region 的根节点,这样就实现了一个 address-space 对应一个 memory-region 树,如下:
123456789101112131415161718 | 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 分布和对应的外设:

memory-region (mr) 都表示一个可访问的地址块,比如:
- RAM/ROM → 存储内容
- I/O → 设备寄存器
- alias → 地址映射
- container → “容器”,管理子 region
每个 mr 都有 起始偏移(相对于父 region)和 大小。
对于 memory-region container 类型,它包含了其他的 mr,记录每个 mr 的 offset。
container 是一种特殊类型的 memory-region
它自身 不存储数据,也没有直接的读写 handler
作用:
- 管理子 memory-region
- 提供地址偏移映射
- 建立层级化结构
在实际应用场景中,我们可以利用 mr container 创建不同的地址层级关系,可以在地址空间层面清晰地描述不同子系统的关系,对于实现模块化有很好的帮助。
参考:
