CTF-虚拟机-QEMU-前置知识-操作流程与源码阅读

CTF-虚拟机-QEMU-前置知识-操作流程与源码阅读将 parent class interfaces 的一些信息添加到 ti class interfaces 列表上面 ti interfaces i typename 对应的 type 的信息

大家好,欢迎来到IT知识分享网。

吹爆这篇博客,写得巨好

总览

KVM是在内核中运行的,让QEMU启动的虚拟机能直接在host的CPU上安全地执行guest的代码,作用为负责虚拟机的创建,虚拟内存的分配,虚拟CPU

// 第一步,获取到 KVM 句柄 kvmfd = open("/dev/kvm", O_RDWR); // 第二步,创建虚拟机,获取到虚拟机句柄。 vmfd = ioctl(kvmfd, KVM_CREATE_VM, 0); // 第三步,为虚拟机映射内存,还有其他的 PCI,信号处理的初始化。 ioctl(kvmfd, KVM_SET_USER_MEMORY_REGION, &mem); // 第四步,将虚拟机镜像映射到内存,相当于物理机的 boot 过程,把镜像映射到内存。 // 第五步,创建 vCPU,并为 vCPU 分配内存空间。 ioctl(kvmfd, KVM_CREATE_VCPU, vcpuid); vcpu->kvm_run_mmap_size = ioctl(kvm->dev_fd, KVM_GET_VCPU_MMAP_SIZE, 0); // 第五步,创建 vCPU 个数的线程并运行虚拟机。 ioctl(kvm->vcpus->vcpu_fd, KVM_RUN, 0); // 第六步,线程进入循环,并捕获虚拟机退出原因,做相应的处理。 for (;;) { 
     ioctl(KVM_RUN) switch (exit_reason) { 
     case KVM_EXIT_IO: /* ... */ case KVM_EXIT_HLT: /* ... */ } } // 这里的退出并不一定是虚拟机关机, // 虚拟机如果遇到 I/O 操作,访问硬件设备,缺页中断等都会退出执行, // 退出执行可以理解为将 CPU 执行上下文返回到 Qemu。 

退出时判断原因,可能由KVM执行也有可能由QEMU执行

内存

可以这么认为,guest所使用的物理内存,实际上是对应的启动它的那个QEMU的虚拟内存的一部分。即该部分可能是对应gust的物理内存是从0开始的(guest视角)

两层转换

  1. 从guest的虚拟地址转换到guest的物理地址
    相当于从页表得到物理地址
  2. 从guest的物理地址转换到host的QEMU进程中的虚拟地址
    该物理地址再加上guest对应在host的QEMU进程中的虚拟地址中起始地址的就是对应的host的虚拟地址了

第一层转换。用pagemap的页面映射文件来转换

  1. 虚拟地址对应的pagemap中的偏移(此时为pagemap中第几个)乘8可得到在pagemap中的偏移(此时为pagemap中对应的地址)

在这里插入图片描述

  1. 读取后判断内容是否存在并且判断最高位是否1,为1则代表页面存在,然后将读取的内容左移12位得到低52位(物理页的地址)再或上原虚拟地址的低12位的页内偏移就是guest的物理地址了

用QEMU运行下列代码

#include <stdio.h> #include <string.h> #include <stdint.h> #include <stdlib.h> #include <fcntl.h> #include <assert.h> #include <inttypes.h> #define PAGE_SHIFT 12 #define PAGE_SIZE (1 << PAGE_SHIFT) #define PFN_PRESENT (1ull << 63) #define PFN_PFN ((1ull << 55) - 1) int fd; // 获取页内偏移 uint32_t page_offset(uint32_t addr) { 
     // addr & 0xfff return addr & ((1 << PAGE_SHIFT) - 1); } uint64_t gva_to_gfn(void *addr) { 
     uint64_t pme, gfn; size_t offset; printf("pfn_item_offset : %p\n", (uintptr_t)addr >> 9); offset = ((uintptr_t)addr >> 9) & ~7; 下面是网上其他人的代码,只是为了理解上面的代码 //一开始除以 0x1000 (getpagesize=0x1000,4k对齐,而且本来低12位就是页内索引,需要去掉),即除以212, 这就获取了页号了, //pagemap中一个地址64位,即8字节,也即sizeof(uint64_t),所以有了页号后,我们需要乘以8去找到对应的偏移从而获得对应的物理地址 //最终 vir/2^12 * 8 = (vir / 2^9) & ~7  //这跟上面的右移9正好对应,但是为什么要 & ~7 ,因为你 vir >> 12 << 3 , 跟vir >> 9 是有区别的,vir >> 12 << 3低3位肯定是0,所以通过& ~7将低3位置0 // int page_size=getpagesize(); // unsigned long vir_page_idx = vir/page_size; // unsigned long pfn_item_offset = vir_page_idx*sizeof(uint64_t); lseek(fd, offset, SEEK_SET); read(fd, &pme, 8); // 确保页面存在——page is present. if (!(pme & PFN_PRESENT)) //同时判断 return -1; // physical frame number  gfn = pme & PFN_PFN; //取低52位 return gfn; } uint64_t gva_to_gpa(void *addr) { 
     uint64_t gfn = gva_to_gfn(addr); assert(gfn != -1); return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);//合并 } int main() { 
     uint8_t *ptr; uint64_t ptr_mem; fd = open("/proc/self/pagemap", O_RDONLY); if (fd < 0) { 
     perror("open"); exit(1); } ptr = malloc(256); strcpy(ptr, "Where am I?"); printf("%s\n", ptr); //此时ptr是guest中虚拟地址 ptr_mem = gva_to_gpa(ptr); //此时转换成了guest中物理地址 printf("Your physical address is at 0x%"PRIx64"\n", ptr_mem); getchar(); return 0; } 

此时

 printf("Your physical address is at 0x%"PRIx64"\n", ptr_mem); ptr_mem输出为0x68cf0010 0x7fcddc000000 为guest的物理地址在host视角下的起始地址 0x7fcddc000000+0x68cf0010即对应where am I? 

PCI设备

PCI是一个外部链接(Peripheral Component Interconnect)标准,PCI设备就是符合这个标准的设备,且连接到PCI总线上。而PCI总线是CPU与外部设备沟通的桥梁。

每个PCI设备对应备一个PCI配置空间(PCI Configuration Space),它记录了关于此设备的信息。PCI配置空间最大256个字节,其中前64字节都是预定义好的标准。

PCI配置空间前64个字节

在这里插入图片描述

对应源码

typedef struct { 
     WORD wBusNum; // Bus No. input field WORD wDeviceNum; // Device No. input field WORD wFunction; // Function No. input field WORD wVendorId; // Vendor ID input field WORD wDeviceId; // Device ID input field WORD wDeviceIndex; // Device Search No. input field WORD wCommand; // Command WORD wClassId; // Class ID BYTE byInterfaceId; // Interface ID BYTE byRevId; // Revision ID BYTE byCLS; // Cache Line Size BYTE byLatency; // Latency Timer DWORD dwBaseAddr[6]; // 6个Base Address Register为32位 DWORD dwCIS; WORD wSubSystemVendorId; WORD wSubSystemId; DWORD dwRomBaseAddr; // Extension ROM Base Address BYTE byIntLine; // Interrupt Line BYTE byIntPin; // Interrupt Pin  BYTE byMaxLatency; // Max Latency BYTE byMinGrant; // Min Grant } PCIDEV, *LPPCIDEV; 

6个BAR,每个BAR记录了该设备映射的一段地址空间,有Memorry空间和IO空间

Memorry空间的BAR

在这里插入图片描述
第0位为0,表示该为Memorry空间
第1位为0表示32位地址,为1表示64位地址
第2为为0表示区间大小超过1M,为0表示不超过1M
第3位表示是否支持可预读取



IO空间的BAR

在这里插入图片描述第0位为1,表示该为IO空间

MMIO

PMIO

端口映射io,内存和io设备有各自独立的地址空间,cpu需要通过专门的指令才能去访问。在intel的微处理器中使用的指令是IN和OUT。

通过I/O 空间访问设备I/O的方式称为port mapped I/O,即PMIO,这种情况下CPU需要使用专门的I/O指令如IN/OUT访问I/O端口

Ispci

在这里插入图片描述

在这里插入图片描述
每个设备的目录下resource0 对应MMIO空间。resource1 对应PMIO空间。(不是所有设备文件都有resource0或者resource1)
resource文件里面会记录相关的数据,第一行就是MIMO的信息,从左到右是:起始地址、结束地址、标识位。第二行是PMIO

访问PCI设备配置空间中的Memory空间和IO空间

MMIO

#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <errno.h> #include <signal.h> #include <fcntl.h> #include <ctype.h> #include <termios.h> #include <assert.h> #include <sys/types.h> #include <sys/mman.h> #include <sys/io.h> #define MAP_SIZE 4096UL #define MAP_MASK (MAP_SIZE - 1) char* pci_device_name = "/sys/devices/pci0000:00/0000:00:04.0/resource0"; unsigned char* mmio_base; unsigned char* getMMIOBase(){ 
     int fd; if((fd = open(pci_device_name, O_RDWR | O_SYNC)) == -1) { 
     perror("open pci device"); exit(-1); } mmio_base = mmap(0, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);//根据resource0中的文件内容来分配Memory空间 if(mmio_base == (void *) -1) { 
     perror("mmap"); exit(-1); } return mmio_base; } void mmio_write(uint64_t addr, uint64_t value) { 
     *((uint64_t*)(mmio_base + addr)) = value; } uint64_t mmio_read(uint64_t addr) { 
     return *((uint64_t*)(mmio_base + addr)); } int main(int argc, char const *argv[]) { 
     getMMIOBase(); printf("mmio_base Resource0Base: %p\n", mmio_base); mmio_write(144, val); mmio_read(144); return 0; } 

PMIO

#include <sys/io.h > iopl(3); inb(port); inw(port); inl(port); outb(val,port); outw(val,port); outl(val,port); 

M(qemu object model)

  1. 将 TypeInfo 注册 TypeImpl
  2. 实例化 ObjectClass
  3. 实例化 Object
  4. 添加 Property

简洁概要

将 TypeInfo 注册 TypeImpl:

ObjectClass的初始化:

调用链main->select_machine->object_class_get_list->object_class_foreach->object_class_foreach_tramp->type_initialize

将parent->class->interfaces的一些信息添加到ti->class->interfaces列表上面,ti->interfaces[i].typename对应的type的信息也添加到ti->class->interfaces列表,最后最重要的就是调用parent的class_base_init进行初始化,最后调用自己ti->class_init进行初始化。

实例化 Instance(Object)

调用链qemu_opts_foreach->device_init_func->qdev_device_add->object_new->object_new_with_type

object_new_with_type函数里面初始化了Object的一些成员,并通过object_init_with_type函数调用ti->instance_init函数(有parent就会先递归调用object_init_with_type,再调用自身的ti->instance_init函数),而最后就是通过object_post_init_with_type函数差不多,只不过先调用自身的ti->instance_post_init,再递归调用parent的ti->instance_post_init

准备自己写mini版QEMU吧,不然实在迷糊


免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/113679.html

(0)
上一篇 2025-12-13 07:46
下一篇 2025-12-13 08:10

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信