苏州做公司网站设计的公司,深圳有做公司网站,中山网站建设公司哪家好,中小型网站建设怎么样文章目录 总览内存PCI设备PCI配置空间前64个字节对应源码Memorry空间的BARIO空间的BAR MMIOPMIOIspci访问PCI设备配置空间中的Memory空间和IO空间MMIOPMIO QQM#xff08;qemu object model#xff09;简洁概要将 TypeInfo 注册 TypeImpl#xff1a;ObjectClass的初始化qemu object model简洁概要将 TypeInfo 注册 TypeImplObjectClass的初始化实例化 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 getpagesize0x10004k对齐而且本来低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_sizegetpagesize();// 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视角下的起始地址0x7fcddc0000000x68cf0010即对应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);QQMqemu 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
准备自己写mini版QEMU吧不然实在迷糊