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處理框架
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指令分成兩類:
- 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的處理方式。
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的情況做特殊處理。