关于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的情况做特殊处理。

posted @ 2021-02-24 12:44  EwanHai  阅读(1029)  评论(0编辑  收藏  举报