QEMU如何pcie_host将物理地址转换为pcie地址

QEMU how pcie_host converts physical address to pcie address

我正在学习 QEMU 的实现。这里有一个问题:我们知道,在真实的硬件中,当cpu读取到虚拟地址即pci设备的地址时,pci主机会负责将其转换为pci的地址。而QEMU,提供了pcie_host.c来模拟pcie主机。在这个文件中,实现了pcie_mmcfg_data_write,但是没有实现物理地址到pci地址的转换。

我使用 gdb 在 QEMU 中进行测试:

static void pcie_mmcfg_data_write(void *opaque, hwaddr mmcfg_addr,
                                  uint64_t val, unsigned len)
{
    PCIExpressHost *e = opaque;
    PCIBus *s = e->pci.bus;
    PCIDevice *pci_dev = pcie_dev_find_by_mmcfg_addr(s, mmcfg_addr);
    uint32_t addr;
    uint32_t limit;

    if (!pci_dev) {
        return;
    }
    addr = PCIE_MMCFG_CONFOFFSET(mmcfg_addr);
    limit = pci_config_size(pci_dev);
    pci_host_config_write_common(pci_dev, addr, limit, val, len);
}

很明显pcie host就是用这个函数来找设备做的事情。 使用bt可以得到:

#0  pcie_mmcfg_data_write
    (opaque=0xaaaaac573f10, mmcfg_addr=65540, val=2, len=1)
    at hw/pci/pcie_host.c:39
#1  0x0000aaaaaae4e8a8 in memory_region_write_accessor
    (mr=0xaaaaac574520, addr=65540, value=0xffffe14703e8, size=1, shift=0, mask=255, attrs=...) 
    at /home/mrzleo/Desktop/qemu/memory.c:483
#2  0x0000aaaaaae4eb14 in access_with_adjusted_size
    (addr=65540, value=0xffffe14703e8, size=1, access_size_min=1, access_size_max=4, access_fn=
    0xaaaaaae4e7c0 <memory_region_write_accessor>, mr=0xaaaaac574520, attrs=...) at /home/mrzleo/Desktop/qemu/memory.c:544
#3  0x0000aaaaaae51898 in memory_region_dispatch_write
    (mr=0xaaaaac574520, addr=65540, data=2, op=MO_8, attrs=...)
    at /home/mrzleo/Desktop/qemu/memory.c:1465
#4  0x0000aaaaaae72410 in io_writex
    (env=0xaaaaac6924e0, iotlbentry=0xffff000e9b00, mmu_idx=2, val=2, 
    addr=18446603336758132740, retaddr=281473269319356, op=MO_8)
    at /home/mrzleo/Desktop/qemu/accel/tcg/cputlb.c:1084
#5  0x0000aaaaaae74854 in store_helper
    (env=0xaaaaac6924e0, addr=18446603336758132740, val=2, oi=2, retaddr=281473269319356, op=MO_8) 
    at /home/mrzleo/Desktop/qemu/accel/tcg/cputlb.c:1954
#6  0x0000aaaaaae74d78 in helper_ret_stb_mmu
    (env=0xaaaaac6924e0, addr=18446603336758132740, val=2 '[=12=]2', oi=2, retaddr=281473269319356) 
    at /home/mrzleo/Desktop/qemu/accel/tcg/cputlb.c:2056
#7  0x0000ffff9a3b47cc in code_gen_buffer ()
#8  0x0000aaaaaae8d484 in cpu_tb_exec
    (cpu=0xaaaaac688c00, itb=0xffff945691c0 <code_gen_buffer+5673332>)
    at /home/mrzleo/Desktop/qemu/accel/tcg/cpu-exec.c:172
#9  0x0000aaaaaae8e4ec in cpu_loop_exec_tb
    (cpu=0xaaaaac688c00, tb=0xffff945691c0 <code_gen_buffer+5673332>, 
    last_tb=0xffffe1470b78, tb_exit=0xffffe1470b70)
    at /home/mrzleo/Desktop/qemu/accel/tcg/cpu-exec.c:619
#10 0x0000aaaaaae8e830 in cpu_exec (cpu=0xaaaaac688c00)
    at /home/mrzleo/Desktop/qemu/accel/tcg/cpu-exec.c:732
#11 0x0000aaaaaae3d43c in tcg_cpu_exec (cpu=0xaaaaac688c00)
    at /home/mrzleo/Desktop/qemu/cpus.c:1405
#12 0x0000aaaaaae3dd4c in qemu_tcg_cpu_thread_fn (arg=0xaaaaac688c00)
    at /home/mrzleo/Desktop/qemu/cpus.c:1713
#13 0x0000aaaaab722c70 in qemu_thread_start (args=0xaaaaac715be0)
    at util/qemu-thread-posix.c:519
#14 0x0000fffff5af84fc in start_thread (arg=0xffffffffe3ff)
    at pthread_create.c:477
#15 0x0000fffff5a5167c in thread_start ()
    at ../sysdeps/unix/sysv/linux/aarch64/clone.S:78
(gdb) bt
#0  edu_mmio_read 
    (opaque=0xaaaaae71c560, addr=0, size=4) 
        at hw/misc/edu.c:187
#1  0x0000aaaaaae4e5b4 in memory_region_read_accessor
    (mr=0xaaaaae71ce50, addr=0, value=0xffffe2472438, size=4, shift=0, mask=4294967295, attrs=...)
    at /home/mrzleo/Desktop/qemu/memory.c:434
#2  0x0000aaaaaae4eb14 in access_with_adjusted_size
    (addr=0, value=0xffffe2472438, size=4, access_size_min=4, access_size_max=8, access_fn=
    0xaaaaaae4e570 <memory_region_read_accessor>, mr=0xaaaaae71ce50, attrs=...) 
    at /home/mrzleo/Desktop/qemu/memory.c:544
#3  0x0000aaaaaae51524 in memory_region_dispatch_read1 
(mr=0xaaaaae71ce50, addr=0, pval=0xffffe2472438, size=4, attrs=...)
    at /home/mrzleo/Desktop/qemu/memory.c:1385
#4  0x0000aaaaaae51600 in memory_region_dispatch_read 
(mr=0xaaaaae71ce50, addr=0, pval=0xffffe2472438, op=MO_32, attrs=...)
    at /home/mrzleo/Desktop/qemu/memory.c:1413
#5  0x0000aaaaaae72218 in io_readx
    (env=0xaaaaac6be0f0, iotlbentry=0xffff04282ec0, mmu_idx=0, 
    addr=281472901758976, retaddr=281473196263360, access_type=MMU_DATA_LOAD, op=MO_32) 
    at /home/mrzleo/Desktop/qemu/accel/tcg/cputlb.c:1045
#6  0x0000aaaaaae738b0 in load_helper
    (env=0xaaaaac6be0f0, addr=281472901758976, oi=32, retaddr=281473196263360, 
    op=MO_32, code_read=false, full_load=0xaaaaaae73c68 <full_le_ldul_mmu>) 
    at /home/mrzleo/Desktop/qemu/accel/tcg/cputlb.c:1566
#7  0x0000aaaaaae73ca4 in full_le_ldul_mmu 
(env=0xaaaaac6be0f0, addr=281472901758976, oi=32, retaddr=281473196263360)
    at /home/mrzleo/Desktop/qemu/accel/tcg/cputlb.c:1662
#8  0x0000aaaaaae73cd8 in helper_le_ldul_mmu 
(env=0xaaaaac6be0f0, addr=281472901758976, oi=32, retaddr=281473196263360)
    at /home/mrzleo/Desktop/qemu/accel/tcg/cputlb.c:1669
#9  0x0000ffff95e08824 in code_gen_buffer 
()
#10 0x0000aaaaaae8d484 in cpu_tb_exec 
(cpu=0xaaaaac6b4810, itb=0xffff95e086c0 <code_gen_buffer+31491700>)
    at /home/mrzleo/Desktop/qemu/accel/tcg/cpu-exec.c:172
#11 0x0000aaaaaae8e4ec in cpu_loop_exec_tb
    (cpu=0xaaaaac6b4810, tb=0xffff95e086c0 <code_gen_buffer+31491700>, 
    last_tb=0xffffe2472b78, tb_exit=0xffffe2472b70)
    at /home/mrzleo/Desktop/qemu/accel/tcg/cpu-exec.c:619
#12 0x0000aaaaaae8e830 in cpu_exec 
(cpu=0xaaaaac6b4810) at /home/mrzleo/Desktop/qemu/accel/tcg/cpu-exec.c:732
#13 0x0000aaaaaae3d43c in tcg_cpu_exec 
(cpu=0xaaaaac6b4810) at /home/mrzleo/Desktop/qemu/cpus.c:1405
#14 0x0000aaaaaae3dd4c in qemu_tcg_cpu_thread_fn 
(arg=0xaaaaac6b4810) 
    at /home/mrzleo/Desktop/qemu/cpus.c:1713
#15 0x0000aaaaab722c70 in qemu_thread_start (args=0xaaaaac541610) at util/qemu-thread-posix.c:519
#16 0x0000fffff5af84fc in start_thread (arg=0xffffffffe36f) at pthread_create.c:477
#17 0x0000fffff5a5167c in thread_start () at ../sysdeps/unix/sysv/linux/aarch64/clone.S:78

好像qemu只是直接定位到edu设备,pcie host在这个过程中什么都没做。请问qemu这里是不是没有实现转换,只是用memoryRegion来实现多态?如果没有,QEMU的pcie host在这个程序中是怎么做的?

QEMU 使用一组称为 MemoryRegions 的数据结构来模拟 CPU 看到的地址 space(详细的 API 记录在 in part in the developer docs 中)。

MemoryRegions 可以构建成一棵树,在“根”处有一个 'container' 覆盖整个 64 位地址的 MR space guest CPU 可以请参阅,然后将 RAM 块、设备等的 MR 以适当的偏移量放置到该根 MR 中。子 MR 也可以是包含更多 MR 的容器。然后,您可以通过遍历 MR 树找到与给定访客物理地址对应的 MR。

MemoryRegions 树主要是在 QEMU 启动时静态构建的(因为大多数设备不会四处移动),但它也可以响应来宾软件操作而动态更改。特别是,PCI 以这种方式工作。当客户机 OS 写入 PCI 设备 BAR(在 PCI 配置 space 中)时,这会导致 QEMU 的 PCI 主机控制器仿真代码将与设备寄存器对应的 MR 放入 MemoryRegion 层次结构中正确的位置位置和偏移量(取决于访客写入 BAR 的地址,即它要求将其映射到的位置)。完成此操作后,PCI 设备的 MR 就像树中的任何其他 MR 一样,PCI 主机控制器代码不需要参与来宾访问它。

作为性能优化,QEMU 实际上并没有为每次访问都沿着 MR 树走下去。相反,我们首先将树“扁平化”成一个数据结构(一个 FlatView),它直接表示“对于这个地址范围,它将是这个 MR;对于这个范围;这个 MR”,等等。其次,QEMU的TLB结构可以直接缓存从“guest virtual address”到“specific memory region”的映射。在第一次访问时,它将执行模拟的访客 MMU 页面 table 遍历以从访客虚拟地址到访客物理地址,然后它将在 FlatView 中向上查找该物理地址以找到真正的主机 RAM 或映射到那里的 MemoryRegion,它会将“guest VA -> this MR”映射添加到 TLB 缓存。未来的访问将命中 TLB,无需重复转换为 physaddr 然后在 flatmap 中找到 MR 的工作。这就是你的回溯中发生的事情—— io_readx() 函数被传递给客户虚拟地址以及 TLB 数据结构的相关部分,然后它可以直接找到目标 MR 和其中的偏移量,因此它可以调用 memory_region_dispatch_read() 将读取请求分派给该 MR 的读取回调函数。 (如果这是第一次访问,初始的“MMU walk + FlatView lookup”工作将在调用 io_readx() 之前在 load_helper() 中完成。)

显然,所有这些缓存还意味着 QEMU 跟踪事件,这意味着缓存数据不再有效,因此我们可以将其丢弃(例如,如果来宾再次写入 BAR 以取消映射或将其映射到其他地方;或者如果更改 MMU 设置或页面 tables 以更改来宾虚拟到物理映射)。