关于PCI-BAR是如何映射到Guest_RAM的一些探索
BAR寄存器内容被BIOS修改
通过trace Intel网卡的VFIO透传过程,发现在透传到虚拟机之后,该网卡的BAR0中的内容从0xdf200000变为了0xfdba0000,这说明一定在透传的某个环节中,改变了该网卡的虚拟配置空间中的BAR0的内容。
为什么改变的不是该网卡的实际配置空间中的内容呢?因为从lspci选项发现,在透传前和透传后,Host上的该网卡的实际配置空间中的内容没有变化。
在QEMU初始化该网卡的过程中,会对QEMU维护的该网卡的模拟配置空间中的内容进行修改,其中就包括对BAR0的内容修改,不过不是从从0xdf200000变为了0xfdba0000,而是从从0xdf200000变为了0x00000000.
那么问题一定出在进入到Guest之后,查询资料后发现,BIOS会在PCI设备枚举阶段对各设备的BAR的内容进行修改,使PCIBUS空间中的BAR的内容不冲突。
QEMU使用的默认BIOS为开源的seaBIOS,该BIOS对该网卡的BAR0的处理过程为:
- 利用OUT指令对0xcfc和0xcf8两个北桥地址进行操作,从而得到该网卡的bdf+vendor:device_id+header_type。这一步一定是从QEMU维护的模拟PCI配置空间中进行读取的。
- 分组计算PCI总线上所有MMIO BAR和IO BAR的大小,得到MMIO BAR总大小和IO BAR总大小。
- 根据这两个size为MMIO和IO BAR分配空间,seaBIOS提供了几个预分配方案。
- 分别在这两个空间中逐一放置MMIO BAR和IO BAR,并将每个BAR空间的首地址写入到对应的PCI配置空间中的对应位置,使用的也是OUT指令对0xcfc和cf8的操作。
所以接下来需要明确在CPU执行step1和step4中的OUT指令时,是如何与QEMU模拟的配置空间进行交互的。
QEMU-KVM中的IO处理框架
在看IO处理之前需要直到QEMU-KVM启动一个虚拟机的过程。
下面这段话简单描述了qemu从创建vcpu到退出的整个过程。
qemu通过调用kvm提供的一系列接口来启动kvm。qemu的入口为vl.c中的main函数,main函数通过调用kvm_init 和 machine->init来初始化kvm。 其中, machine->init会创建vcpu, 用一个线程去模拟vcpu, 该线程执行的函数为qemu_kvm_cpu_thread_fn, 并且该线程调用kvm_cpu_exec,该函数调用kvm_vcpu_ioctl切换到kvm中,下次从kvm中返回时,会接着执行kvm_vcpu_ioctl之后的代码,判断exit_reason,然后进行相应处理。
qemu的IO exit处理框架
可以看到在qemu的vcpu执行循环中,通过pre、ioctl(KVM_RUN)、post对vcpu运行前、运行时、运行后进行一些处理,最后根据exit reason进行特定处理。
对于IN/OUT指令,属于KVM_EXIT_IO类型,所以会调用kvm_handle_io.
暂且搁置kvm_handle_io,只需要知道最后qemu对IO exit的处理会落到这个函数上,继续看看kvm中的处理,因为IO exit会首先从Guest退出到kvm而不是qemu。
kvm的IO exit处理框架
上面提到,qemu运行vcpu是通过向kvm发送ioctl(KVM_RUN)实现的,在该ioctl中实现了vcpu的运行过程。
最后的__vmx_vcpu_run
会运行汇编代码使vcpu进入VMX Guest mode。
在Guest中遇到IO指令,会导致vmexit,而回退到vmx_vcpu_run。
可以看到,在Guest中执行IO指令后,会vmexit到kvm的handle_io中,暂时不看kvm的handle_io中执行了什么,有个问题需要在之后看kvm的handle_io时理解,即,kvm的io处理是怎样进入到qemu的io处理中的。
QEMU-KVM的IO处理
KVM
EXIT_QUALIFICATION是VMCS的一个field:
- bit2:0 IO指令的访问size,0表示1字节访问,1表示2字节访问,3表示4字节访问
- bit3 IO指令的访问方向,0表示OUT,1表示IN
- bit4 IO指令为字符串传送还是单元素传送,0表示单元素传送,1表示字符串传送
80386IO指令用来访问处理器的IO端口,来和外围设备之间传送数据。这些指令有一个位于IO地址空间的端口地址作为操作数。IO指令分成两类:
- Those that transfer a single item (byte, word, or doubleword) located in a register.
传送寄存器中的单个项目(字节、字或双字)。代表指令IN、OUT。
- Those that transfer strings of items (strings of bytes, words, or doublewords) located in memory. These are known as "string I/O instructions" or "block I/O instructions".
传送内存中的字符串项目(字节、字或双字构成的字符串)。这些指令也被叫做“字符串IO指令”或“块传送IO指令”。代表指令INS、OUTS。
- bit5 IO指令前是否包含REP循环指令 0表示不包含 1表示包含
- bit6 IO指令的指令编码情况,0表示操作数在DX寄存器中,1表示操作数为立即数,在内存中
- bit31:16 IO指令指定的操作端口,存放于DX寄存器或立即数中
通过上面的handle_io可以看出,kvm遇到IO_EXIT时,对该IO指令进行分类,如果是string类的IO指令,则调用kvm_emulate_instruction,如果是非string类的IO,则调用kvm_fast_pio。
seaBIOS中对被trace网卡的配置使用的是非string类指令,因为其使用的汇编指令为OUT而非OUTS。
string类IO的处理
虽然本次trace的重点不是kvm对string类IO指令的处理,但还是应该看一下kvm的处理方式。
x86_emulate_instruction函数的代码太长了,找关键部分吧。
CR2是页故障线性地址寄存器,保存最后一次出现页故障的全32位线性地址。
x86_decode_insn的作用就是将ctxt中存储的指令信息逐个解码, 解码的过程涉及到指令编码和解码,这里暂不详细追究,只需要知道经过解码之后,指令的信息存储在ctxt变量中,其中,ctxt->execute存储指令的回调函数,ctxt->b存储指令的机器码,ctxt.src->val存储指令的源操作数,ctxt.dst->addr.reg存储的是需要写入的目标寄存器(也就是port).
我们本次追溯IO指令有2个,IN/OUT.指令模拟时需要执行具体的函数才能模拟,这里说的具体的函数就是ctxt->execute, 这个execute是在解码指令时,通过指令的机器码在opcode_table中找到对应的回调函数.这里展示opcode_table中关于IN和OUT指令的内容.
在opcode_table中, 根据源操作数和目标操作数的类型,分了几种IO指令,其最终的回调函数只有2种,要么em_in,要么em_out.
指令解码之后开始模拟指令.
调用 rc = ctxt->execute(ctxt);
进而调用em_in/em_out进行指令模拟.
这两个函数的核心都是pio_out_emulated
.
ctxt->ops->pio_out_emulated实际调用的函数为pio_out_emulated,后者将IO指令包含的value拷贝到vcpu->arch.pio_data
中,然后调用emulator_pio_in_out.
在emulator_pio_in_out中, 向vcpu->arch.pio
填充IO指令需要的具体信息.
填充完IO指令需要的信息之后, kernel_pio
负责在kvm(内核)内部处理这个IO指令,如果内核无法处理,即kernel_pio
返回非0值,则将IO指令的各种信息记录到vcpu->run
结构中,vcpu->run
结构是qemu和kvm的一个共享数据结构,回到qemu中之后,在kvm_cpu_exec函数中对KVM_EXIT_IO这种情况进行处理. qemu对这种情况的处理之后再讨论.先来看另一种情况,即kvm能够处理本次IO指令,那么就会返回1,即exit_handler返回1,会重新进入Guest继续运行.
接下来看看kvm处理IO指令时的方式.
kvm处理IO指令时,对IO指令进行了分类,IN指令调用kvm_io_bus_read
,OUT指令调用kvm_io_bus_write
.以kvm_io_bus_write为例.
首先用传入的IO指令信息构造一个IO范围结构,然后调用__kvm_io_bus_write
.
__kvm_io_bus_write中,首先利用IO范围信息找到kvm中注册的总线上的相关设备,然后调用kvm_iodevice_write进行IO write操作.这个kvm_iodevice_write只是简单调用了kvm中注册的IO设备的ops的write方法.即:
IO read的代码路径也是类似,设备的读写方法因设备而异,在设备注册到kvm中时初始化.
非string类IO的处理
handle_io对于非字符串类IO,会首先获得此次IO指令的port,访问size,传送方向,然后调用kvm_fast_pio
.
kvm_fast_pio
会根据IO指令的方向调用kvm_fast_pio_in/kvm_fast_pio_out.
- kvm_fast_pio_in
在kvm_fast_pio_in中,首先查看本次IN指令中需要向port写入的值的size(1,2,4)为多少,如果为1或2字节,那么就直接读取vcpu中的RAX寄存器的值,该寄存器用于存放IN/OUT指令中需要向port写入的值。如果为4字节,就将val置0,在后续处理中会对val为0的情况做特殊处理。
__EOF__

本文链接:https://www.cnblogs.com/haiyonghao/p/14440716.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律