环境 源码 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.2mkdir -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访存流程
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 命令可以看到地址空间的布局:
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/O 和 memory 。
每个 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.mrom、sifive.test 等都是 子 memory-region
每个 memory-region 都挂在 address-space 下,并提供访问 handler
memory_region地址重叠 mr 支持同一级之间地址范围重叠 ,重叠的部分按照优先级呈现,高优先级的重叠部分作为访问目标。(prio 0, type) 中的 prio 后跟着的是优先级,virt 的外设之间没有地址重叠,因此优先级都是 0。
这里举例说明:
1 2 3 4 5 6 -------- -------- -------- -------- -------- -------- -------- -------- [ ----------------------------------------------------------------------- ] [ ----------------------------------------------------- ] [ ----------------------------------- ] [ ----------------- ]
对于 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 -000000003 fffffff (prio 0 , i/o): alias pcie-ecam @pcie-mmcfg-mmio 0000000000000000 -000000000 fffffff
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 { struct rcu_head rcu ; char *name; MemoryRegion *root; struct FlatView *current_map ; int ioeventfd_nb; int ioeventfd_notifiers; struct MemoryRegionIoeventfd *ioeventfds ; QTAILQ_HEAD(, MemoryListener) listeners; QTAILQ_ENTRY(AddressSpace) address_spaces_link; size_t max_bounce_buffer_size; size_t bounce_buffer_size; 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 struct MemoryRegion { Object parent_obj; bool romd_mode; bool ram; bool subpage; bool readonly; bool nonvolatile; bool rom_device; bool flush_coalesced_mmio; bool unmergeable; uint8_t dirty_log_mask; bool is_iommu; RAMBlock *ram_block; Object *owner; DeviceState *dev; const MemoryRegionOps *ops; void *opaque; MemoryRegion *container; int mapped_via_alias; 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; 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 struct MemoryRegion { Object parent_obj; 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 是在哪里被初始化的:
结点关系 进一步watch system_memory->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 分布和对应的外设:
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 创建不同的地址层级关系 ,可以在地址空间层面,清晰的描述不同子系统的关系,对于实现模块化有很好的帮助。
参考: