关于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的处理过程为:

  1. 利用OUT指令对0xcfc和0xcf8两个北桥地址进行操作,从而得到该网卡的bdf+vendor:device_id+header_type。这一步一定是从QEMU维护的模拟PCI配置空间中进行读取的。
  2. 分组计算PCI总线上所有MMIO BAR和IO BAR的大小,得到MMIO BAR总大小和IO BAR总大小。
  3. 根据这两个size为MMIO和IO BAR分配空间,seaBIOS提供了几个预分配方案。
  4. 分别在这两个空间中逐一放置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处理框架

int kvm_cpu_exec(CPUState *cpu) { ... kvm_arch_pre_run(cpu, run); run_ret = kvm_vcpu_ioctl(cpu, KVM_RUN, 0); kvm_arch_post_run(cpu, run); switch (run->exit_reason) { case KVM_EXIT_IO: kvm_handle_io case KVM_EXIT_MMIO: address_space_rw ... } }

可以看到在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的运行过程。

kvm_vcpu_iotcl => kvm_arch_vcpu_ioctl_run => vcpu_run => vcpu_enter_guest => kvm_x86_ops->run => vmx_vcpu_run => __vmx_vcpu_run => vcpu进入Guest

最后的__vmx_vcpu_run会运行汇编代码使vcpu进入VMX Guest mode。

在Guest中遇到IO指令,会导致vmexit,而回退到vmx_vcpu_run。

Guest中执行IO指令 => vmx_vcpu_run(记录退出原因vmx->exit_reason) => kvm_x86_ops->handle_exit => vmx_handle_exit => kvm_vmx_exit_handlers[exit_reason](vcpu) => handle_io

可以看到,在Guest中执行IO指令后,会vmexit到kvm的handle_io中,暂时不看kvm的handle_io中执行了什么,有个问题需要在之后看kvm的handle_io时理解,即,kvm的io处理是怎样进入到qemu的io处理中的。

QEMU-KVM的IO处理

KVM

static int handle_io(struct kvm_vcpu *vcpu) { unsigned long exit_qualification; int size, in, string; unsigned port; exit_qualification = vmcs_readl(EXIT_QUALIFICATION); // exit qualification的bit4 string = (exit_qualification & 16) != 0; ++vcpu->stat.io_exits; /* 传送的是字符串 */ if (string) return kvm_emulate_instruction(vcpu, 0); /* 传送的不是字符串 */ port = exit_qualification >> 16; // bit31:16 操作端口 size = (exit_qualification & 7) + 1; // bit2:0 传送size in = (exit_qualification & 8) != 0; // bit3 传送方向 return kvm_fast_pio(vcpu, size, port, in); }

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指令分成两类:

  1. Those that transfer a single item (byte, word, or doubleword) located in a register.

传送寄存器中的单个项目(字节、字或双字)。代表指令IN、OUT。

  1. 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的处理方式。

kvm_emulate_instruction => x86_emulate_instruction(vcpu, 0, 0, NULL, 0)

x86_emulate_instruction函数的代码太长了,找关键部分吧。

CR2是页故障线性地址寄存器,保存最后一次出现页故障的全32位线性地址。

int x86_emulate_instruction(struct kvm_vcpu *vcpu, unsigned long cr2, int emulation_type, void *insn, int insn_len) { // 传入的参数中,除了vcpu有值之外,其余参数全部为0 ... if (!(emulation_type & EMULTYPE_NO_DECODE)) { init_emulate_ctxt(vcpu); // 初始化模拟指令时的寄存器环境(EFlags,EIP,CPU运行模式(实模式、保护模式等)等) ... r = x86_decode_insn(ctxt, insn, insn_len); // 对指令进行解码 } }

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指令的内容.

// arch/x86/kvm/emulate.c static const struct opcode opcode_table[256] = { I2bvIP(DstDI | SrcDX | Mov | String | Unaligned, em_in, ins, check_perm_in), /* insb, insw/insd */ I2bvIP(SrcSI | DstDX | String, em_out, outs, check_perm_out), /* outsb, outsw/outsd */ I2bvIP(SrcImmUByte | DstAcc, em_in, in, check_perm_in), I2bvIP(SrcAcc | DstImmUByte, em_out, out, check_perm_out), I2bvIP(SrcDX | DstAcc, em_in, in, check_perm_in), I2bvIP(SrcAcc | DstDX, em_out, out, check_perm_out), ... }

在opcode_table中, 根据源操作数和目标操作数的类型,分了几种IO指令,其最终的回调函数只有2种,要么em_in,要么em_out.

指令解码之后开始模拟指令.

int x86_emulate_instruction(struct kvm_vcpu *vcpu, unsigned long cr2, int emulation_type, void *insn, int insn_len) { // 对指令解码之后 r = x86_emulate_insn(ctxt); // 进行指令模拟 }
int x86_emulate_insn(struct x86_emulate_ctxt *ctxt){ if (ctxt->execute) { if (ctxt->d & Fastop) { void (*fop)(struct fastop *) = (void *)ctxt->execute; rc = fastop(ctxt, fop); if (rc != X86EMUL_CONTINUE) goto done; goto writeback; } rc = ctxt->execute(ctxt); if (rc != X86EMUL_CONTINUE) goto done; goto writeback; } }

调用 rc = ctxt->execute(ctxt);进而调用em_in/em_out进行指令模拟.

static int em_in(struct x86_emulate_ctxt *ctxt) { if (!pio_in_emulated(ctxt, ctxt->dst.bytes, ctxt->src.val, &ctxt->dst.val)) return X86EMUL_IO_NEEDED; return X86EMUL_CONTINUE; }
static int em_out(struct x86_emulate_ctxt *ctxt) { ctxt->ops->pio_out_emulated(ctxt, ctxt->src.bytes, ctxt->dst.val, &ctxt->src.val, 1); /* Disable writeback. */ ctxt->dst.type = OP_NONE; return X86EMUL_CONTINUE; }

这两个函数的核心都是pio_out_emulated.

.pio_out_emulated = emulator_pio_out_emulated static int emulator_pio_out_emulated(struct x86_emulate_ctxt *ctxt, int size, unsigned short port, const void *val, unsigned int count) { struct kvm_vcpu *vcpu = emul_to_vcpu(ctxt); memcpy(vcpu->arch.pio_data, val, size * count); trace_kvm_pio(KVM_PIO_OUT, port, size, count, vcpu->arch.pio_data); return emulator_pio_in_out(vcpu, size, port, (void *)val, count, false); }

ctxt->ops->pio_out_emulated实际调用的函数为pio_out_emulated,后者将IO指令包含的value拷贝到vcpu->arch.pio_data中,然后调用emulator_pio_in_out.

static int emulator_pio_in_out(struct kvm_vcpu *vcpu, int size, unsigned short port, void *val, unsigned int count, bool in) { vcpu->arch.pio.port = port; vcpu->arch.pio.in = in; vcpu->arch.pio.count = count; vcpu->arch.pio.size = size; if (!kernel_pio(vcpu, vcpu->arch.pio_data)) { vcpu->arch.pio.count = 0; return 1; } vcpu->run->exit_reason = KVM_EXIT_IO; vcpu->run->io.direction = in ? KVM_EXIT_IO_IN : KVM_EXIT_IO_OUT; vcpu->run->io.size = size; vcpu->run->io.data_offset = KVM_PIO_PAGE_OFFSET * PAGE_SIZE; vcpu->run->io.count = count; vcpu->run->io.port = port; return 0; }

在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指令时的方式.

static int kernel_pio(struct kvm_vcpu *vcpu, void *pd) { int r = 0, i; for (i = 0; i < vcpu->arch.pio.count; i++) { if (vcpu->arch.pio.in) r = kvm_io_bus_read(vcpu, KVM_PIO_BUS, vcpu->arch.pio.port, vcpu->arch.pio.size, pd); else r = kvm_io_bus_write(vcpu, KVM_PIO_BUS, vcpu->arch.pio.port, vcpu->arch.pio.size, pd); if (r) break; pd += vcpu->arch.pio.size; } return r; }

kvm处理IO指令时,对IO指令进行了分类,IN指令调用kvm_io_bus_read,OUT指令调用kvm_io_bus_write.以kvm_io_bus_write为例.

int kvm_io_bus_write(struct kvm_vcpu *vcpu, enum kvm_bus bus_idx, gpa_t addr, int len, const void *val) { struct kvm_io_bus *bus; struct kvm_io_range range; int r; range = (struct kvm_io_range) { .addr = addr, .len = len, }; bus = srcu_dereference(vcpu->kvm->buses[bus_idx], &vcpu->kvm->srcu); if (!bus) return -ENOMEM; r = __kvm_io_bus_write(vcpu, bus, &range, val); return r < 0 ? r : 0; }

首先用传入的IO指令信息构造一个IO范围结构,然后调用__kvm_io_bus_write.

static int __kvm_io_bus_write(struct kvm_vcpu *vcpu, struct kvm_io_bus *bus, struct kvm_io_range *range, const void *val) { int idx; idx = kvm_io_bus_get_first_dev(bus, range->addr, range->len); if (idx < 0) return -EOPNOTSUPP; while (idx < bus->dev_count && kvm_io_bus_cmp(range, &bus->range[idx]) == 0) { if (!kvm_iodevice_write(vcpu, bus->range[idx].dev, range->addr, range->len, val)) return idx; idx++; } return -EOPNOTSUPP; }

__kvm_io_bus_write中,首先利用IO范围信息找到kvm中注册的总线上的相关设备,然后调用kvm_iodevice_write进行IO write操作.这个kvm_iodevice_write只是简单调用了kvm中注册的IO设备的ops的write方法.即:

static inline int kvm_iodevice_write(struct kvm_vcpu *vcpu, struct kvm_io_device *dev, gpa_t addr, int l, const void *v) { return dev->ops->write ? dev->ops->write(vcpu, dev, addr, l, v) : -EOPNOTSUPP; }

IO read的代码路径也是类似,设备的读写方法因设备而异,在设备注册到kvm中时初始化.

非string类IO的处理

static int handle_io(struct kvm_vcpu *vcpu) { unsigned long exit_qualification; int size, in, string; unsigned port; exit_qualification = vmcs_readl(EXIT_QUALIFICATION); // exit qualification的bit4 string = (exit_qualification & 16) != 0; ++vcpu->stat.io_exits; /* 传送的是字符串 */ if (string) return kvm_emulate_instruction(vcpu, 0); /* 传送的不是字符串 */ port = exit_qualification >> 16; // bit31:16 操作端口 size = (exit_qualification & 7) + 1; // bit2:0 传送size in = (exit_qualification & 8) != 0; // bit3 传送方向 return kvm_fast_pio(vcpu, size, port, in); }

handle_io对于非字符串类IO,会首先获得此次IO指令的port,访问size,传送方向,然后调用kvm_fast_pio.

int kvm_fast_pio(struct kvm_vcpu *vcpu, int size, unsigned short port, int in) { int ret; if (in) ret = kvm_fast_pio_in(vcpu, size, port); else ret = kvm_fast_pio_out(vcpu, size, port); return ret && kvm_skip_emulated_instruction(vcpu); }

kvm_fast_pio会根据IO指令的方向调用kvm_fast_pio_in/kvm_fast_pio_out.

  • kvm_fast_pio_in
static int kvm_fast_pio_in(struct kvm_vcpu *vcpu, int size, unsigned short port) { unsigned long val; int ret; /* For size less than 4 we merge, else we zero extend */ val = (size < 4) ? kvm_rax_read(vcpu) : 0; ret = emulator_pio_in_emulated(&vcpu->arch.emulate_ctxt, size, port, &val, 1); if (ret) { kvm_rax_write(vcpu, val); return ret; } vcpu->arch.pio.linear_rip = kvm_get_linear_rip(vcpu); vcpu->arch.complete_userspace_io = complete_fast_pio_in; return 0; }

在kvm_fast_pio_in中,首先查看本次IN指令中需要向port写入的值的size(1,2,4)为多少,如果为1或2字节,那么就直接读取vcpu中的RAX寄存器的值,该寄存器用于存放IN/OUT指令中需要向port写入的值。如果为4字节,就将val置0,在后续处理中会对val为0的情况做特殊处理。


__EOF__

本文作者EwanHai
本文链接https://www.cnblogs.com/haiyonghao/p/14440716.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   EwanHai  阅读(1230)  评论(0编辑  收藏  举报
编辑推荐:
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示