对于qemu来说读写内存涉及到内存模拟模块,qemu还模拟了tlb,因此读写一块Guest OS的虚拟内存地址(Guest Virtual Address -> GVA)首先会查询tlb,如果tlb不命中的话会走tlb慢路径。tlb慢路径要经由guest的mmu经页表转换为物理内存地址(Guest Physics Address -> GPA),再经过qemu内存管理模块转换为qemu进程的虚拟地址(Host Virtual Address -> HVA)。
那么读写GVA的arm指令编译成X86_64指令就是读写对应的HVA即可。
tlb相应的数据结构在include/exec/cpu-defs.h文件中定义,其中结构体CPUTLB由ArchCPU中的CPUNegativeOffsetState neg所引用。
TLB命中时对应CPUTLBEntry对象的addend + GVA = HVA。
code_gen_buffer
-》helper_le_ldq_mmu
-》load_helper
-》tlb_fill
-》arm_cpu_tlb_fill
-》get_phys_addr (&cpu->env, address, access_type, core_to_arm_mmu_idx(&cpu->env, mmu_idx)),
&phys_addr, &attrs, &prot, &page_size,&fi, &cacheattrs); // phys_addr 中存放返回的GPA: 0X4XXXXXXX地址
//根据 ttbr_el1 寄存器中取得页表基地址,将GVA转成GPA
-》get_phys_addr_lpae() //进行页表转换
-》ttbr = regime_ttbr(env, mmu_idx, param.select)
-》return env->cp15.ttbr1_el //从 ttbr_el1 中获得一级页表的基地址
-》tlb_set_page_with_attrs(cs, address, phys_addr, attrs, prot, mmu_idx, page_size) // Add a new TLB entry,设置页属性
-》addend = (uintptr_t)memory_region_get_ram_ptr(section->mr) + xlat; //通过GPA得到HVA地址
-》tn.addend = addend - vaddr_page; //HVA – GVA 得到 addend
get_phys_addr()函数会遍历页表并(如果映射存在)将页面添加到 TLB。成功时返回 true。否则,如果正在探测,返回 false。否则使用 ARM DFSR/IFSR 故障寄存器格式填充 fsr,并发出故障信号。
假设已经知道了 GPA 地址,接下来,又该如何读写到里面的数据呢?
如果QEMU已经知道了Guest OS中的一个 GPA 地址,需要向GPA写数据,可以调用如下2个函数进行读写:
//addr: 虚拟机物理地址
//buf: 要写的数据
//len: 要写的数据长度
static inline void cpu_physical_memory_write(hwaddr addr,
const void *buf, hwaddr len)
{
cpu_physical_memory_rw(addr, (void *)buf, len, true);
}
static inline void cpu_physical_memory_read(hwaddr addr,
void *buf, hwaddr len)
{
cpu_physical_memory_rw(addr, buf, len, false);
}
调用流程如下:
cpu_physical_memory_read
-》cpu_physical_memory_rw(addr, buf, len, false)
-》address_space_rw(&address_space_memory, addr, MEMTXATTRS_UNSPECIFIED, buf, len, is_write)
-》address_space_read_full(as, addr, attrs, buf, len)
-》fv = address_space_to_flatview(as);
-》return qatomic_rcu_read(&as->current_map)
-》flatview_read(fv, addr, attrs, buf, len);
-》mr = flatview_translate(fv, addr, &addr1, &l, false, attrs)
-》section = flatview_do_translate(fv, addr, xlat, plen, NULL, is_write, true, &as, attrs);
-》section = address_space_translate_internal(flatview_to_dispatch(fv), addr, xlat, plen_out, is_mmio)
section = address_space_lookup_region(d, addr, resolve_subpage);
addr -= section->offset_within_address_space;
*xlat = addr + section->offset_within_region;
-》mr = section.mr
-》flatview_read_continue(fv, addr, attrs, buf, len, addr1, l, mr)
-》ram_ptr = qemu_ram_ptr_length(mr->ram_block, addr1, &l, false);
-》ramblock_ptr(block, addr)
-》return (char *)block->host + offset //哈哈,到这里,算是跟踪到底了,例如一个GPA地址是val:0x42a68000, 而虚拟机的内存起始地址是 0x40000000,那么,这里的 offset 就是 0x2a68000,而 block->host 指向为虚拟机分配2GB 空间时,mmap() 返回的HVA地址,所以,两者相加,就是该 GPA 对应的HVA的地址
-》memcpy(buf, ram_ptr, l);
===[qemu_anon_ram_alloc ]—[248 ] ptr:0x7fa793e00000, size:0x80000000
===block->host:0x7fa793e00000
总结一下:
假如现在有一个GPA地址0x42a68000,Guest的物理内存区间是[0x40000000,+2GB],这2GB空间,是由 HOST通过 mmap分配得来的,位于HVA: 0x7fa793e00000处,那么,
• section->offset_within_address_space:0x40000000
• section->offset_within_region:0
• block->host:0x7fa793e00000
QEMU根据GPA转换HVA的大致过程是这样的,首先,通过全局变量address_space_memory,找到它里面的as->current_map,得到 flatview,接着,得到fv-> dispatch,通过address_space_lookup_region(),在 AddressSpaceDispatch中遍历查找包含该GPA地址0x42a68000的那个section,接着再用 GPA 地址 0x42a68000减去 section->offset_within_address_space,这个值是0x40000000, 得到 0x2a68000,再加上section->offset_within_region,这个值是0,所以得到 0x2a68000
接着,再通过 section.mr,得到MemoryRegion, 再通过 mr->ram_block,得到 ram_block,再 ram_block->host + 0x2a68000,而 ram_block->host 是0x7fa793e00000,所以最终得到的HVA就是( 0x7fa793e00000 + 0x2a68000 )