文章目录
- 总览
- 内存
- PCI设备
- PCI配置空间前64个字节
- 对应源码
- Memorry空间的BAR
- IO空间的BAR
- MMIO
- PMIO
- Ispci
- 访问PCI设备配置空间中的Memory空间和IO空间
- MMIO
- PMIO
- QQM(qemu object model)
- 简洁概要
- 将 TypeInfo 注册 TypeImpl:
- ObjectClass的初始化:
- 实例化 Instance(Object)
- 准备自己写mini版QEMU吧,不然实在迷糊
吹爆这篇博客,写得巨好
总览
QEMU能够为用户进程进行CPU仿真提供环境
一个QEMU进程提供一种环境可启动一个虚拟机
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视角)
两层转换
- 从guest的虚拟地址转换到guest的物理地址
相当于从页表得到物理地址 - 从guest的物理地址转换到host的QEMU进程中的虚拟地址
该物理地址再加上guest对应在host的QEMU进程中的虚拟地址中起始地址的就是对应的host的虚拟地址了
第一层转换。用pagemap的页面映射文件来转换
- 虚拟地址对应的pagemap中的偏移(此时为pagemap中第几个)乘8可得到在pagemap中的偏移(此时为pagemap中对应的地址)
- 读取后判断内容是否存在并且判断最高位是否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 & 0xfffreturn 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位就是页内索引,需要去掉),即除以2**12, 这就获取了页号了,//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输出为0x68cf00100x7fcddc000000 为guest的物理地址在host视角下的起始地址0x7fcddc000000+0x68cf0010即对应where am I?
PCI设备
PCI是一个外部链接(Peripheral Component Interconnect)标准,PCI设备就是符合这个标准的设备,且连接到PCI总线上。而PCI总线是CPU与外部设备沟通的桥梁。
符合 PCI 总线标准的设备就被称为 PCI 设备
PCI 设备同时也分为主设备和目标设备两种,主设备是一次访问操作的发起者,而目标设备则是被访问者。
每个PCI设备对应备一个PCI配置空间(PCI Configuration Space),它记录了关于此设备的信息。PCI配置空间最大256个字节,其中前64字节都是预定义好的标准。
PCI配置空间前64个字节
对应源码
typedef struct {WORD wBusNum; // Bus No. input fieldWORD wDeviceNum; // Device No. input fieldWORD wFunction; // Function No. input fieldWORD wVendorId; // Vendor ID input fieldWORD wDeviceId; // Device ID input fieldWORD wDeviceIndex; // Device Search No. input fieldWORD wCommand; // CommandWORD wClassId; // Class IDBYTE byInterfaceId; // Interface IDBYTE byRevId; // Revision IDBYTE byCLS; // Cache Line SizeBYTE byLatency; // Latency TimerDWORD dwBaseAddr[6]; // 6个Base Address Register为32位DWORD dwCIS;WORD wSubSystemVendorId;WORD wSubSystemId;DWORD dwRomBaseAddr; // Extension ROM Base AddressBYTE byIntLine; // Interrupt LineBYTE byIntPin; // Interrupt Pin BYTE byMaxLatency; // Max LatencyBYTE 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
内存映射io,和内存共享一个地址空间。可以和像读写内存一样读写其内容。
通过Memory 空间访问设备I/O的方式称为memory mapped I/O,即MMIO,这种情况下,CPU直接使用普通访存指令即可访问设备I/O。
PMIO
端口映射io,内存和io设备有各自独立的地址空间,cpu需要通过专门的指令才能去访问。在intel的微处理器中使用的指令是IN和OUT。
通过I/O 空间访问设备I/O的方式称为port mapped I/O,即PMIO,这种情况下CPU需要使用专门的I/O指令如IN/OUT访问I/O端口
Ispci
pci外设地址,形如0000:00:1f.1。第一个部分16位表示域;第二个部分8位表示总线编号;第三个部分5位表示设备号;最后一个部分3位表示功能号。
lspci 命令可以显示当前的pci设备
lspci -v可以显示当前的pci设备的详细信息,如mmio的地址,pmio的端口号
lspci -v -m -n -s 设备可以显示头部的一些信息
/sys/bus/pci/devices可以找到pci设备相关的文件。
/sys/devices/pci0000:00也可以找到pci设备的相关的文件
查看设备id是device文件
cat /sys/devices/pci0000:00/0000:00:03.0/device
随便进入一个pci设备文件用ls查看
每个设备的目录下resource0 对应MMIO空间。resource1 对应PMIO空间。(不是所有设备文件都有resource0或者resource1)
resource文件里面会记录相关的数据,第一行就是MIMO的信息,从左到右是:起始地址、结束地址、标识位。第二行是PMIO
I/O 内存:/proc/iomem
I/O 端口:/proc/ioports
使用cat /proc/iomem可查看当前PCI设备的映射内存空间
使用cat /proc/ioports可查看当前PCI设备的映射端口空间
访问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
需要权限才能访问端口
0x000-0x3ff端口可以用ioperm(from, num, turn_on)获得权限
比如ioperm(0x300,5,1); 获得 0x300 到 0x304 端口的访问权限
更高端口需要iopl(3)获得权限,这个可以获得范围所有端口权限
in,out系列函数如下,分别是写入/读取一个字节(b结尾),两个字节(w结尾),四个字节(l结尾)
#include <sys/io.h >iopl(3);
inb(port);
inw(port);
inl(port);outb(val,port);
outw(val,port);
outl(val,port);
QQM(qemu object model)
QEMU提供了一套面向对象编程的模型——QOM,即QEMU Object Module,几乎所有的设备如CPU、内存、总线等都是利用这一面向对象的模型来实现的。
而对象的初始化分为四步:
- 将 TypeInfo 注册 TypeImpl
- 实例化 ObjectClass
- 实例化 Object
- 添加 Property
ObjectClass: 是所有类对象的基类,仅仅保存了一个整数 type 。
Object: 是所有对象的 基类Base Object , 第一个成员变量为指向 ObjectClass 的指针。
TypeInfo:是用户用来定义一个 Type 的工具型的数据结构。
TypeImpl:对数据类型的抽象数据结构,TypeInfo的属性与TypeImpl的属性对应。
简洁概要
将 TypeInfo 注册 TypeImpl:
1、首先__attribute__((constructor))的修饰让type_init在main之前执行,type_init的参数是XXX_register_types函数指针,将函数指针传递到ModuleEntry的init函数指针,最后就是将这个ModuleEntry插入到ModuleTypeList
2、main函数中的module_call_init(MODULE_INIT_QOM);调用了MODULE_INIT_QOM类型的ModuleTypeList中的所有ModuleEntry中的init()函数,也就是第一步type_init的第一个参数XXX_register_types函数指针
3、那就下了就是XXX_register_types函数的操作了,就是创建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