- 文档
- 网络安全
- 教程
【免费下载链接】ctf-wiki
Come and join us, we need you!
本篇基于 ctf-wiki 仓库中 QEMU 虚拟化基础篇的内存管理章节,系统讲解 QEMU 如何从 Guest 视角(GPA)与 Host VMM 视角(HVA)管理一台虚拟机的内存:Guest 侧的MemoryRegion树、FlatView扁平化视图与AddressSpace统一访问入口,以及 Host 侧承载实际内存的RAMBlock。读完本文,你将理解 QEMU 内存子系统各核心结构体的字段含义与相互关系,并能解释为什么 CTF QEMU Pwn 题目普遍围绕“自定义设备 + MMIO/PMIO 回调”设计,为后续阅读 QEMU 设备模拟 与 QEMU 逃逸入门 打下基础。
总体模型:GPA 与 HVA 的双视角
QEMU 内存管理的核心任务,是同时维护两套“地图”:
- Guest VM 视角(GPA,Guest Physical Address):Guest 操作系统看到的物理地址空间。QEMU 用一棵嵌套的
MemoryRegion(MR)树来描述它,并通过AddressSpace+FlatView将其转换为线性的、可直接做地址翻译的结构。 - Host VMM 视角(HVA,Host Virtual Address):QEMU 进程在宿主机上实际占用的虚拟内存。每个实体 MR 背后的真实内存由一个
RAMBlock描述,通常通过mmap()获得。
下面先深入 Guest 侧的四个核心结构体,再切换到 Host 侧的RAMBlock,最后讨论这套模型在 CTF QEMU Pwn 中的意义。
Guest VM 视角(GPA)
MemoryRegion:Guest 视角的一块“内存”
在 QEMU 当中使用MemoryRegion结构体类型来表示一块具体的 Guest 物理内存区域,该结构体定义于 QEMU 源码include/exec/memory.h当中:
/** MemoryRegion: * * 表示一块内存区域的一个结构体. */ 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 global_locking; uint8_t dirty_log_mask; bool is_iommu; RAMBlock *ram_block; Object *owner; const MemoryRegionOps *ops; void *opaque; MemoryRegion *container; // 指向父 MemoryRegion Int128 size; // 内存区域大小 hwaddr addr; // 在父 MR 中的偏移量 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; // 仅在 alias MR 中,指向实际的 MR 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; };几个对理解后续内容至关重要的成员:
container/subregions/subregions_link:三者共同构成 MR 树的父子关系。container指向父 MR,subregions是该节点的子 MR 双向链表头;size与addr:size是该 MR 覆盖的范围大小,addr是它在父 MR 坐标系内的偏移量,二者是后续地址翻译的基本参数;ram、readonly、rom_device、is_iommu等布尔标志:区分这块区域是实体 RAM、只读区域、设备寄存器还是 IOMMU;ops与opaque:回调函数表指针及其“this 指针”,MMIO 设备读写的实现入口;alias与alias_offset:仅在别名(alias)MR 中非空,指向它真正代理的那块实体 MR;ram_block:实体 RAM MR 指向其背后的RAMBlock,是连接 Guest 视角与 Host 视角的桥梁字段。
在 QEMU 当中有三类最基本的 MemoryRegion:
- MemoryRegion 根(container):通过
memory_region_init()进行初始化,其用以表示与管理由多个 sub-MemoryRegion 组成的一段内存区域,并不实际指向一块内存区域,例如system_memory; - MemoryRegion 实体:通过
memory_region_init_ram()初始化,表示具体的一块大小为size的内存空间,指向一块真实的内存; - MemoryRegion 别名(alias):通过
memory_region_init_alias()初始化,作为另一个 MemoryRegion 实体的别名而存在,不指向一块实际内存。
MR 容器与 MR 实体间构成树形结构,其中容器为根节点而实体为子节点:
struct MemoryRegion +------------------------+ |name | | (const char *) | +------------------------+ |addr | | (hwaddr) | +------------------------+ |size | | (Int128) | +------------------------+ |subregions | | QTAILQ_HEAD() | +------------------------+ | | ----+-------------------+---------------------+---- | | | | | | struct MemoryRegion struct MemoryRegion +------------------------+ +------------------------+ |name | |name | | (const char *) | | (const char *) | +------------------------+ +------------------------+ |addr | |addr | | (hwaddr) | | (hwaddr) | +------------------------+ +------------------------+ |size | |size | | (Int128) | | (Int128) | +------------------------+ +------------------------+ |subregions | |subregions | | QTAILQ_HEAD() | | QTAILQ_HEAD() | +------------------------+ +------------------------+补充一点:若按 QEMU 官方文档的分类口径(仓库中简体中文版 QEMU 内存管理 亦收录了该分类),上述三类可以进一步细分为八种,基本覆盖 Guest 常见的内存访问需求:RAM MR(memory_region_init_ram(),代表分配给 Guest 的 Host 内存)、ROM MR(memory_region_init_rom(),只读、禁止写操作)、MMIO MR(memory_region_init_io(),每次读写都会调用 QEMU 中注册的回调)、ROM device MR(memory_region_init_rom_device(),读走 RAM 式的直接内存访问、写走回调)、IOMMU MR(memory_region_init_iommu(),访问时进行地址转换并转发到其他目标区域)、container MR(将多个 MR 组合为一个单元管理,如 PCI BAR 可能由一个 RAM MR 和一个 MMIO MR 组成)、alias MR(另一 MR 的别名)、以及 reservation MR(向memory_region_init_io()传NULL回调,占用的 I/O 空间不由 QEMU 本身处理,典型用途是跟踪启用 KVM 时由主机内核处理的地址空间部分)。对做 QEMU Pwn 的读者而言,最常打交道的是 MMIO MR 与 container MR:前者是设备寄存器的载体,后者是 PCI BAR 这类“组合区域”的载体。
MemoryRegionOps:以函数表实现的“成员函数”
相应地,基于 OOP 的思想,MemoryRegion 的“成员函数”被封装在函数表MemoryRegionOps当中:
/* * Memory region callbacks */ struct MemoryRegionOps { /* 从内存区域上读. @addr 与 @mr 有关; @size 单位为字节. */ uint64_t (*read)(void *opaque, hwaddr addr, unsigned size); /* 往内存区域上写. @addr 与 @mr 有关; @size 单位为字节. */ void (*write)(void *opaque, hwaddr addr, uint64_t data, unsigned size); MemTxResult (*read_with_attrs)(void *opaque, hwaddr addr, uint64_t *data, unsigned size, MemTxAttrs attrs); MemTxResult (*write_with_attrs)(void *opaque, hwaddr addr, uint64_t data, unsigned size, MemTxAttrs attrs); enum device_endian endianness; /* Guest可见约束: */ struct { /* 若非 0,则指定了超出机器检查范围的访问大小界限 */ unsigned min_access_size; unsigned max_access_size; /* If true, unaligned accesses are supported. Otherwise unaligned * accesses throw machine checks. */ bool unaligned; /* * 若存在且 #false, 则该事务不会被设备所接受 * (并导致机器的相关行为,例如机器检查异常). */ bool (*accepts)(void *opaque, hwaddr addr, unsigned size, bool is_write, MemTxAttrs attrs); } valid; /* 内部应用约束: */ struct { /* 若非 0,则决定了最小的实现的 size . * 更小的 size 将被向上回绕,且将返回部分结果. */ unsigned min_access_size; /* 若非 0,则决定了最大的实现的 size . * 更大的 size 将被作为一系列的更小的 size 的访问而完成. */ unsigned max_access_size; /* 若为 true, 支持非对齐的访问. * 否则所有的访问都将被转换为(可能多种)对齐的访问. */ bool unaligned; } impl; };这张函数表里有两组很容易混淆的约束字段,值得单独说明:
valid是Guest 可见约束:描述该设备“对外宣称”的访问能力,例如min_access_size/max_access_size指定了超出机器检查范围的访问大小界限,unaligned表明是否支持非对齐访问,accepts回调若存在且返回false,则该事务不会被设备接受并触发机器检查异常等行为。设备若违反自己声明的约束(比如声明了unaligned = false却收到了非对齐访问),QEMU 会主动报错;impl是内部实现约束:描述回调实现自身能处理的最小/最大 size。更大的访问会被 QEMU 自动拆分为一系列更小的访问来完成,因此impl与valid不一致时是允许且常见的(例如设备按字节实现回调,但对 Guest 宣称只支持 4 字节对齐访问)。
endianness字段则决定设备寄存器采用大端还是小端解释。对逆向与漏洞分析而言,read/write或read_with_attrs/write_with_attrs这组函数指针就是设备寄存器逻辑的落点——绝大多数 MMIO 漏洞(越界读、UAF、堆溢出等)都发生在这些回调内部。
统一访问入口:address_space_rw()
当 Guest 要读写虚拟机上的内存时,在 QEMU 内部实际上会调用address_space_rw():对于一般的 RAM 内存而言,直接对 MR 对应的 Host 内存进行读写;对于 MMIO 而言,则最终调用到对应的MR->ops->read()或MR->ops->write()。
同一仓库的 QEMU 设备模拟 章节给出了这条调用链在 KVM 路径下的具体形态:kvm_cpu_exec()中kvm_vcpu_ioctl(cpu, KVM_RUN, 0)返回后,根据 VM-exit 原因分发——KVM_EXIT_MMIO直接调用address_space_rw(&address_space_memory, ...);KVM_EXIT_IO则调用kvm_handle_io()。而address_space_rw()的实现(QEMU 源码softmmu/physmem.c)会先在 RCU 读保护下把地址空间展开为FlatView,再由flatview_read()/flatview_write()通过flatview_translate()定位到目标 MR,随后检查flatview_access_allowed()并继续执行读写:
MemTxResult address_space_rw(AddressSpace *as, hwaddr addr, MemTxAttrs attrs, void *buf, hwaddr len, bool is_write) { if (is_write) { return address_space_write(as, addr, attrs, buf, len); } else { return address_space_read_full(as, addr, attrs, buf, len); } }/* Called from RCU critical section. */ static MemTxResult flatview_write(FlatView *fv, hwaddr addr, MemTxAttrs attrs, const void *buf, hwaddr len) { hwaddr l; hwaddr addr1; MemoryRegion *mr; l = len; mr = flatview_translate(fv, addr, &addr1, &l, true, attrs); if (!flatview_access_allowed(mr, attrs, addr, len)) { return MEMTX_ACCESS_ERROR; } return flatview_write_continue(fv, addr, attrs, buf, len, addr1, l, mr); }注意两个关键细节:其一,每次访问都要经过flatview_translate()这一步“GPA → 具体 MR + MR 内偏移”的翻译,addr1就是翻译后的 MR 内地址;其二,翻译结果还要通过flatview_access_allowed()的权限检查,不通过会返回MEMTX_ACCESS_ERROR。
同样地,为了统一接口,在 QEMU 当中PMIO(端口 I/O)的实现也是通过 MemoryRegion 来完成的——可以把一组端口理解为 QEMU 视角的一块 Guest 内存:kvm_handle_io()只是对address_space_rw()的封装,只不过用的是端口地址空间address_space_io,最终同样落到对应 MR 函数表中的读写函数。
几乎所有的 CTF QEMU Pwn 题都是自定义一个设备并定义相应的 MMIO/PMIO 操作。理解上面的调用链后就能明白:题目的可利用面,本质上就是这些
ops回调及其背后的状态结构。
FlatView:MR 树对应的 Guest 视角物理地址空间
QEMU 通过树状结构的 MemoryRegion 管理 Guest 的物理地址空间,天然支持动态调整(如热插拔设备、动态修改 BAR)。但这种嵌套、重叠的复杂结构不适合直接与 KVM 等内核模块交互——内核需要明确的线性物理内存布局来配置 EPT(Extended Page Table)。因此 QEMU 使用FlatView表示一棵 MemoryRegion 树所展平后的 Guest 地址空间:将树状结构“展平”为有序列表,每个条目记录连续内存区域的起始地址(GPA)、大小与属性(如 RAM/MMIO),消除嵌套关系。FlatView使用一个FlatRange结构体数组来存储不同MemoryRegion对应的地址信息,每个FlatRange表示单个MemoryRegion在 Guest 视角的一块物理地址空间,以及只读等特性信息;FlatRange之间所表示的地址范围不会重叠。
/* Range of memory in the global map. Addresses are absolute. */ struct FlatRange { MemoryRegion *mr; hwaddr offset_in_region; AddrRange addr; uint8_t dirty_log_mask; bool romd_mode; bool readonly; bool nonvolatile; }; //... /* Flattened global view of current active memory hierarchy. Kept in sorted * order. */ struct FlatView { struct rcu_head rcu; unsigned ref; FlatRange *ranges; unsigned nr; unsigned nr_allocated; struct AddressSpaceDispatch *dispatch; MemoryRegion *root; };字段解读:
FlatRange.mr+offset_in_region:该段地址最终归属的 MR 及其在 MR 内部的偏移,两者配合即可把绝对 GPA 还原为(mr, mr 内偏移);FlatRange.addr:该段在全局地址空间中的绝对范围(AddrRange为“基址 + 长度”类型);readonly、dirty_log_mask、nonvolatile:该段的访问属性快照,readonly即上文权限检查的依据之一;FlatView.ranges/nr:按地址排序的FlatRange数组与条目数,“sorted”特性使地址翻译可以二分查找;rcu/ref:FlatView采用 RCU 机制发布——MR 树变化时重建新的 FlatView,读侧(address_space_to_flatview())在 RCU 读锁内安全取用当前版本,这正是前面调用链中RCU_READ_LOCK_GUARD()的原因;root:指向本视图所展平的 MR 树根节点,用于检测“树是否已变化、是否需要重新展平”。
可以推断,FlatView 是一个“按需重建、多版本共存”的缓存:任何 MR 增删(如设备 realize/unrealize)都会触发对应地址空间的重新展平,而 Guest 正在执行的翻译操作不会被打断。
AddressSpace:不同类型的 Guest 地址空间
AddressSpace结构体用以表示Guest 视角不同类型的地址空间。在 x86 下其实只有两种:address_space_memory(内存空间,MMIO 走这里)与address_space_io(端口空间,PMIO 走这里)。
单个AddressSpace结构体与一棵 MemoryRegion 树的根节点相关联,并使用一个FlatView结构体建立该树的平坦化内存空间:
/** * 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; struct MemoryRegionIoeventfd *ioeventfds; QTAILQ_HEAD(, MemoryListener) listeners; QTAILQ_ENTRY(AddressSpace) address_spaces_link; };其中current_map就是当前生效的FlatView(“Accessed via RCU”即要求读侧持 RCU 读锁访问);listeners链表挂载MemoryListener回调,当 MR 拓扑变化时逐一通知(KVM 等加速器正是借此同步内存布局);ioeventfds则用于 ioeventfd 优化——Guest 对特定 MMIO 地址的写可以直接通过事件 fd 通知后端(典型如 virtio 的 kick 队列),省去一次 VM-exit 到用户态的完整路径。
至此,Guest 视角的三层抽象关系可以概括为:
AddressSpace(地址空间类型)→ 1 棵 MR 树的root;- MR 树(嵌套、可动态变化)→ 通过展平生成
FlatView(线性、有序、不可变快照); - 每次 Guest 访问:
AddressSpace→ 取当前FlatView→flatview_translate()定位到具体 MR → RAM 直读直写 / MMIO 走ops回调。
Host VMM 视角(HVA)
RAMBlock:MR 对应的 Host 虚拟内存
前述的 RAM MR 只是 Guest 侧的“描述”,真正的内存存放在 Host 上。RAMBlock结构体用来表示单个实体 MemoryRegion 所占用的 Host 虚拟内存信息,多个RAMBlock结构体之间构成单向链表:
struct RAMBlock { struct rcu_head rcu; struct MemoryRegion *mr; uint8_t *host; uint8_t *colo_cache; /* For colo, VM's ram cache */ ram_addr_t offset; 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]; /* RCU-enabled, writes protected by the ramlist lock */ QLIST_ENTRY(RAMBlock) next; QLIST_HEAD(, RAMBlockNotifier) ramblock_notifiers; int fd; size_t page_size; /* dirty bitmap used during migration */ unsigned long *bmap; /* bitmap of already received pages in postcopy */ unsigned long *receivedmap; /* * bitmap to track already cleared dirty bitmap. When the bit is * set, it means the corresponding memory chunk needs a log-clear. * Set this up to non-NULL to enable the capability to postpone * and split clearing of dirty bitmap on the remote node (e.g., * KVM). The bitmap will be set only when doing global sync. * * It is only used during src side of ram migration, and it is * protected by the global ram_state.bitmap_mutex. * * NOTE: this bitmap is different comparing to the other bitmaps * in that one bit can represent multiple guest pages (which is * decided by the `clear_bmap_shift' variable below). On * destination side, this should always be NULL, and the variable * `clear_bmap_shift' is meaningless. */ unsigned long *clear_bmap; uint8_t clear_bmap_shift; /* * RAM block length that corresponds to the used_length on the migration * source (after RAM block sizes were synchronized). Especially, after * starting to run the guest, used_length and postcopy_length can differ. * Used to register/unregister uffd handlers and as the size of the received * bitmap. Receiving any page beyond this length will bail out, as it * could not have been valid on the source. */ ram_addr_t postcopy_length; };比较重要的成员:
mr:该 RAMBlock 对应的 MemoryRegion,即建立 HVA → GPA 的反向索引(MemoryRegion.ram_block与RAMBlock.mr互指);host:该块在 Host 上的虚拟地址基址,通常由 QEMU 通过mmap()获得(若未使用 KVM);offset/used_length/max_length:该块在 QEMU 内部 RAM 地址空间中的偏移、当前使用长度与最大长度,支持运行时 resize;idstr:块的名字(256 字节),与 QOM 对象命名一致,便于按名称查找;fd:块对应的文件描述符(如内存后端文件、hugetlbfs 等);bmap/receivedmap/clear_bmap:热迁移相关的脏页位图、postcopy 已接收页位图、延迟清除脏位图,与 CTF 单题场景关系不大,但说明 RAMBlock 同时是迁移子系统的记账单元;next:所有 RAMBlock 串成的全局单向链表(ramlist)。
RAM 访问的完整路径因此是:Guest 物理地址 GPA →FlatView翻译到某个 RAM MR 的offset_in_region→ram_block->host + offset_in_region得到 HVA → 直接内存访问(KVM 场景下这一步由 EPT 在硬件上完成,QEMU 只在缺页/迁移等场景介入)。MR、子区域与 RAMBlock 的对应关系如下所示:
一个容易踩坑的细节是:一个 RAMBlock 通常对应一整块连续 Host 内存,而 MR 树上的 RAM 实体 MR 可以是这块内存的一个子区域(subregion)。因此在 Host 侧做内存取证或理解内存布局时,不能假设“MR 边界 == RAMBlock 边界”,而要以ram_block指针 + 偏移为准。
对 CTF QEMU Pwn 的意义
把上面的模型放回 CTF 场景,整条攻击面链条就非常清晰了:
- 题目通过
-device xxx加载一个自定义设备(如 QEMU 逃逸入门 中的 BlizzzardCTF2017 Strng 设备,其启动命令为./qemu-system-x86_64 -m 1G -device strng ...); - 设备 realize 阶段调用
memory_region_init_io()注册 MMIO/PMIO 区域,ops函数表指向回调;MMIO MR 通过pci_register_bar()挂到 PCI BAR(一个 container MR)下,最终并入address_space_memory的 MR 树; - Guest 内程序对 BAR 地址的每次读写触发 VM-exit,经
address_space_rw()→flatview_translate()落到这些回调; - 利用点就在回调内部:对设备状态结构(
opaque指向的对象)的越界读写、UAF 等,最终效果是攻击 QEMU 进程自身的堆内存——这正是 越界读写 等逃逸章节讨论的前提。
也就是说,读题时“找设备 → 找memory_region_init_io→ 读ops回调”的三步法,本质上就是在 MR 树上定位可利用的叶子节点,并用 FlatView 视角确认其 GPA 映射。
延伸阅读(仓库内)
- QEMU 内存管理(本文源文档)
- QEMU 设备模拟:IO 处理与 PCI 设备
- QEMU 逃逸入门:以 Strng 为例
- 简体中文对照版:QEMU 内存管理(含按 QEMU 官方文档细化的八类 MemoryRegion 说明)
源文档另列有 QEMU 官方文档(memory 篇)、understanding qemu 系列、《QEMU内存分析》系列博客等参考资料作为扩展阅读,本文按仓库规范不再外链。
- 文档
- 网络安全
- 教程
【免费下载链接】ctf-wiki
Come and join us, we need you!
相关推荐
QEMU 内存模型深度解析:MemoryRegion 与 AddressSpace 内存 API 完全指南
QEMU 内存模型深度解析:MemoryRegion 与 AddressSpace 内存 API 完全指南 导读 QEMU 的 memory API(内存 AP
虚拟化硬件仿真QEMU 設備模擬原理解析:從 VM-exit 到 MemoryRegion 的 MMIO/PMIO 處理鏈路(ctf-wiki)
QEMU 設備模擬原理解析:從 VM exit 到 MemoryRegion 的 MMIO/PMIO 處理鏈路(ctf wiki) 本篇基於 ctf wiki
文档网络安全教程mercur内存模型:JavaScript内存管理深度解析
mercur内存模型:JavaScript内存管理深度解析 前言:为什么需要关注内存管理? 在现代JavaScript应用中,内存管理往往是开发者最容易忽视却又
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考