關於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的情況做特殊處理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM