1. 程式人生 > >【系列分享】探索QEMU-KVM中PIO處理的奧祕

【系列分享】探索QEMU-KVM中PIO處理的奧祕

投稿方式:傳送郵件至linwei#360.cn,或登陸網頁版線上投稿

我們都知道在kvm/qemu的虛擬機器中向埠讀寫輸入會陷入kvm中(絕大部分埠)。但是其具體過程是怎麼樣的,虛擬機器、kvm和qemu這三者的關係在這個過程中又是如何相互聯絡來完成這一模擬過程的。本文就是對這一問題的探索,通過對kvm進行除錯來了解其中的奧祕。

零.  準備工作

工欲善其事,必先利其器。為了瞭解kvm如何對PIO進行截獲處理,首先需要除錯kvm,這需要 配置雙機除錯環境,網上很多例子,需要注意的是,4.x核心清除kernel text的可防寫有點問題。 所以本文還是用的3.x核心,具體為3.10.105。所以我們的環境是target為3.10.105的核心,debugger隨意。

如果我們直接用kvm/qemu除錯,由於一個完整的環境會有非常多的vm exit,會干擾我們的分析。 這裡我們只需要建立一個使用kvm api建立起一個最簡易虛擬機器的例子,在虛擬機器中執行in/out 指令即可。網上也有很多這種例子。比如使用KVM API實現Emulator Demo, Linux KVM as a Learning Tool.

這裡我們使用第一個例子,首先從

把程式碼clone下來,直接make,如果載入了kvm應該就可以看到輸出了,kvm的api用法這裡不表,仔細看看 前兩篇文章之一就可以了,qemu雖然複雜,本質上也是這樣執行的。這個例子中的guest是向埠輸出資料。

一.  IO埠在KVM中的註冊

首先我們需要明確的一點是,IO port 這個東西是CPU用來與外設進行資料互動的,也不是所有CPU都有。 在虛擬機器看來是沒有IO port這個概念的,所以是一定要在vm exit中捕獲的。

對於是否截獲IO指令,是由vmcs中的VM-Execution controls中的兩個域決定的。 參考intel SDM 24.6.2:

http://p4.qhimg.com/t01c3805bdbf1fd509d.png

我們可以看到如果設定了Use I/O bitmpas這一位,Unconditional I/O exiting就無效了,如果在IO bitmap 中某一位被設定為1,則訪問該埠就會發生vm exit,否則客戶機可以直接訪問。 IO bitmap的地址存在vmcs中的I/O-Bitmap Addresses域中,事實上,有兩個IO bitmap,我們叫做A和B。 再來看看SDM

http://p6.qhimg.com/t016316aedffae19b8e.png

每一個bitmap包含4kb,也就是一個頁,bitmap A包含了埠0000H到7FFFFH(4*1024*8),第二個埠包含了8000H到 FFFFH。

好了,我們已經從理論上對IO port有了瞭解了,下面看看kvm中的程式碼。

首先我們看到arch/x86/kvm/vmx.c中,定義了兩個全域性變量表示bitmap A和B的地址。 在vmx_init函式中這兩個指標都被分配了一個頁大小的空間,之後所有位都置1,然後在bitmap A中對第 80位進行了清零,也就是客戶機訪

這個0x80埠不會發生vm exit。

static unsigned long *vmx_io_bitmap_a;
static unsigned long *vmx_io_bitmap_b;
static int __init vmx_init(void)
{
    vmx_io_bitmap_a = (unsigned long *)__get_free_page(GFP_KERNEL);
    vmx_io_bitmap_b = (unsigned long *)__get_free_page(GFP_KERNEL);
    
   
    /*
    * Allow direct access to the PC debug port (it is often used for I/O
    * delays, but the vmexits simply slow things down).
    */
    memset(vmx_io_bitmap_a, 0xff, PAGE_SIZE);
    clear_bit(0x80, vmx_io_bitmap_a);
    memset(vmx_io_bitmap_b, 0xff, PAGE_SIZE);
    ...
}

在同一個檔案中,我們看到在對vcpu進行初始化的時候會把這個bitmap A和B的地址寫入到vmcs中去,這樣 就建立了對IO port的訪問的截獲。

static int vmx_vcpu_setup(struct vcpu_vmx *vmx)
{
    /* I/O */
    vmcs_write64(IO_BITMAP_A, __pa(vmx_io_bitmap_a));
    vmcs_write64(IO_BITMAP_B, __pa(vmx_io_bitmap_b));
    return 0;
}

二.  PIO中out的處理流程

本節我們來探討一下kvm中out指令的處理流程。首先,將上一節中的test.S程式碼改一下,只out一次。

.globl _start
    .code16
_start:
    xorw %ax, %ax
    mov  $0x0a,%al
    out %ax, $0x10
    inc %ax
    hlt

kvm中guest傳送vm exit之後會根據傳送exit的原因呼叫各種handler。這也在vmx.c中

static int (*const kvm_vmx_exit_handlers[])(struct kvm_vcpu *vcpu) = {
    [EXIT_REASON_EXCEPTION_NMI]           = handle_exception,
    [EXIT_REASON_EXTERNAL_INTERRUPT]      = handle_external_interrupt,
    [EXIT_REASON_TRIPLE_FAULT]            = handle_triple_fault,
    [EXIT_REASON_NMI_WINDOW]          = handle_nmi_window,
    [EXIT_REASON_IO_INSTRUCTION]          = handle_io,
    ...
}

對應這裡,處理IO的回撥是handle_io。我們在target中執行:

[email protected]:/home/test# echo g >/proc/sysrq-trigger

這樣除錯機中的gdb會斷下來,給handle_io下個斷點:

(gdb) b handle_io
Breakpoint 1 at 0xffffffff81037dca: file arch/x86/kvm/vmx.c, line 4816.
(gdb) c
接著,我們用gdb啟動target中的kvmsample,並且在main.c的84行下個斷點。
[email protected]:~/kvmsample$ gdb ./kvmsample 
...
Reading symbols from ./kvmsample...done.
(gdb) b ma
main        main.c      malloc      [email protected]  
(gdb) b main.c:84
Breakpoint 1 at 0x400cac: file main.c, line 84.

第84行恰好是從ioctl KVM_RUN中返回回來的時候。

http://p7.qhimg.com/t01624359c58c735857.png

好了,開始r,會發現debugger已經斷下來了:

Thread 434 hit Breakpoint 1, handle_io (vcpu=0xffff8800ac528000)
at arch/x86/kvm/vmx.c:4816
4816    {
(gdb)

從handle_io的程式碼我們可以看出,首先會從vmcs中讀取exit的一些資訊,包括訪問這個埠是in還是out, 大小,以及埠號port等。

static int handle_io(struct kvm_vcpu *vcpu)
{
    unsigned long exit_qualification;
    int size, in, string;
    unsigned port;
    exit_qualification = vmcs_readl(EXIT_QUALIFICATION);
    string = (exit_qualification & 16) != 0;
    in = (exit_qualification & 8) != 0;
    ++vcpu->stat.io_exits;
    if (string || in)
        return emulate_instruction(vcpu, 0) == EMULATE_DONE;
    port = exit_qualification >> 16;
    size = (exit_qualification & 7) + 1;
    skip_emulated_instruction(vcpu);
    return kvm_fast_pio_out(vcpu, size, port);
}

之後通過skip_emulated_instruction增加guest的rip之後呼叫kvm_fast_pio_out,在該函式中, 我們可以看到首先讀取guest的rax,這個值放的是向埠寫入的資料,這裡是,0xa

int kvm_fast_pio_out(struct kvm_vcpu *vcpu, int size, unsigned short port)
{
    unsigned long val = kvm_register_read(vcpu, VCPU_REGS_RAX);
    int ret = emulator_pio_out_emulated(&vcpu->arch.emulate_ctxt,
                        size, port, &val, 1);
    /* do not return to emulator after return from userspace */
    vcpu->arch.pio.count = 0;
    return ret;
}

我們可以對比gdb中看看資料:

Thread 434 hit Breakpoint 1, handle_io (vcpu=0xffff8800ac528000)
    at arch/x86/kvm/vmx.c:4816
4816    {
(gdb) n
4821        exit_qualification = vmcs_readl(EXIT_QUALIFICATION);
(gdb) n
4825        ++vcpu->stat.io_exits;
(gdb) n
4827        if (string || in)
(gdb) n
4832        skip_emulated_instruction(vcpu);
(gdb) n
[New Thread 3654]
4834        return kvm_fast_pio_out(vcpu, size, port);
(gdb) s
kvm_fast_pio_out (vcpu=0xffff8800ac528000, size=16, port=16)
    at arch/x86/kvm/x86.c:5086
5086    {
(gdb) n
[New Thread 3656]
5087        unsigned long val = kvm_register_read(vcpu, VCPU_REGS_RAX);
(gdb) n
[New Thread 3657]
5088        int ret = emulator_pio_out_emulated(&vcpu->arch.emulate_ctxt,
(gdb) p /x val
$1 = 0xa
(gdb)

再往下,我們看到在emulator_pio_out_emulated,把值拷貝到了vcpu->arch.pio_data中,接著呼叫 emulator_pio_in_out。

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);
    return emulator_pio_in_out(vcpu, size, port, (void *)val, count, false);
}
static int emulator_pio_in_out(struct kvm_vcpu *vcpu, int size,
                unsigned short port, void *val,
                unsigned int count, bool in)
{
    trace_kvm_pio(!in, port, size, count);
    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;
}

在後一個函式中,我們可以看到vcpu->run->io.data_offset設定為4096了,我們可以看到之前已經把我們 向埠寫的值通過memcpy拷貝到了vpuc->arch.pio_data中去了,通過除錯我們可以看出其中的端倪。 vcpu->arch.pio_data就在kvm_run後面一個頁的位置。這也可以從kvm_vcpu_init中看出來。

4405        vcpu->run->io.size = size;
(gdb) n
[New Thread 3667]
4406        vcpu->run->io.data_offset = KVM_PIO_PAGE_OFFSET * PAGE_SIZE;
(gdb) n
4407        vcpu->run->io.count = count;
(gdb) n
4408        vcpu->run->io.port = port;
(gdb) p count
$7 = 1
(gdb) n
4410        return 0;
(gdb) x /2b 0xffff88002a2a2000+0x1000
0xffff88002a2a3000: 0x0a    0x00
(gdb) p vcpu->run
$9 = (struct kvm_run *) 0xffff88002a2a2000
(gdb) p vcpu->arch.pio_data
$10 = (void *) 0xffff88002a2a3000
(gdb)

這樣,我們看到vcpu->run->io儲存了一些PIO的基本資訊,比如大小,埠號等,run後面的一個頁 vcpu->arch.pio_data則儲存了實際out出來的資料。讓target繼續執行,這個時候我們斷回了kvmsample 程式中。

(gdb) p kvm->vcpus->kvm_run->io 
$2 = {direction = 1 '01', size = 2 '02', port = 16, count = 1, 
data_offset = 4096}
(gdb)

這裡簡單說一下kvm_run,這是用於vcpu和應用層的程式(典型如qemu)通訊的一個結構,user space的 程式通過KVM__VCPU_MMAP_SIZE這個ioctl得到大小得到大小,然後對映到使用者空間。

(gdb) x /2b 0x7ffff7ff4000+0x1000
0x7ffff7ff5000: 10

我們通過gdb可以看到,我們在guest向埠寫入的資料以及埠都能夠從user space讀出來。在這個示例程式中, 僅僅是把資料輸出來,qemu中會根據埠去尋找對應的裝置,然後執行對應的回撥。

整體而言,out指令的流程是非常簡單的,guest寫埠,陷入kvm, kvm回到user space處理。

三.  PIO中in的處理流程

雖然我們說guest訪問埠包含了讀寫,都會導致vm exit。但是如果我們細想一下會發現,out和in肯定是不一樣 的。out只需要guest寫一個數據就好了,但是in還需要讀回來資料。所以流程應該是guest發起一個in操作, 然後kvm處理,返回到user space之中,把資料填到kvm_run結構中,這樣,kvm得到資料了再vm entry,這樣 in的資料就能夠到guest中了。

我們隊例項程式做簡單修改。在test.S中首先從0x10埠讀入一個值,這個值為0xbeff,然後寫到埠0x10。

test.S
# A test code for kvmsample
.globl _start
    .code16
_start:
    xorw %ax, %ax
    mov  $0x0a,%al
    in $0x10,%ax
    out %ax, $0x10
    hlt

對main.c做如下修改:

http://p7.qhimg.com/t01c206cdde382981bd.png

在處理KVM_EXIT_IO的時候區分了一下in/out,對in我們拷貝一個0xbeff過去。然後用在guest中用out向 埠0x10輸出這個值。

執行in指令的第一次仍然是陷入kvm handle_io處理,只是這次走另一條路:

Thread 486 hit Breakpoint 1, handle_io (vcpu=0xffff88011d428000)
    at arch/x86/kvm/vmx.c:4816
4816    {
(gdb) n
4821        exit_qualification = vmcs_readl(EXIT_QUALIFICATION);
(gdb) 
4825        ++vcpu->stat.io_exits;
(gdb) 
4827        if (string || in)
(gdb) 
4828            return emulate_instruction(vcpu, 0) == EMULATE_DONE;
(gdb) s
emulate_instruction (emulation_type=<optimized out>, vcpu=<optimized out>)
    at /home/test/linux-3.10.105/arch/x86/include/asm/kvm_host.h:811
811     return x86_emulate_instruction(vcpu, 0, emulation_type, NULL, 0);
(gdb) s

呼叫x86_emulate_instruction,這之中呼叫的最重要的兩個函式時x86_decode_insn, x86_emulate_insn。

int x86_emulate_instruction(struct kvm_vcpu *vcpu,
            unsigned long cr2,
            int emulation_type,
            void *insn,
            int insn_len)
{
    int r;
    struct x86_emulate_ctxt *ctxt = &vcpu->arch.emulate_ctxt;
    bool writeback = true;
    bool write_fault_to_spt = vcpu->arch.write_fault_to_shadow_pgtable;
    /*
    * Clear write_fault_to_shadow_pgtable here to ensure it is
    * never reused.
    */
    vcpu->arch.write_fault_to_shadow_pgtable = false;
    kvm_clear_exception_queue(vcpu);
    if (!(emulation_type & EMULTYPE_NO_DECODE)) {
        init_emulate_ctxt(vcpu);
        
        r = x86_decode_insn(ctxt, insn, insn_len);
    }
restart:
    r = x86_emulate_insn(ctxt);
    if (ctxt->have_exception) {
        inject_emulated_exception(vcpu);
        r = EMULATE_DONE;
    } else if (vcpu->arch.pio.count) {
        if (!vcpu->arch.pio.in)
            vcpu->arch.pio.count = 0;
        else {
            writeback = false;
            vcpu->arch.complete_userspace_io = complete_emulated_pio;
        }
        r = EMULATE_DO_MMIO;
    
    if (writeback) {
        toggle_interruptibility(vcpu, ctxt->interruptibility);
        kvm_set_rflags(vcpu, ctxt->eflags);
        kvm_make_request(KVM_REQ_EVENT, vcpu);
        vcpu->arch.emulate_regs_need_sync_to_vcpu = false;
        kvm_rip_write(vcpu, ctxt->eip);
    } else
        vcpu->arch.emulate_regs_need_sync_to_vcpu = true;
    return r;
}
EXPORT_SYMBOL_GPL(x86_emulate_instruction);

第一個函式,x86_decode_insn,顧名思義,就是解碼當前的指令。

int x86_decode_insn(struct x86_emulate_ctxt *ctxt, void *insn, int insn_len)
{
    
    /* Legacy prefixes. */
    for (;;) {
        switch (ctxt->b = insn_fetch(u8, ctxt)) {
    
    }
    /* Opcode byte(s). */
    opcode = opcode_table[ctxt->b];
    /* Two-byte opcode? */
    if (ctxt->b == 0x0f) {
        ctxt->twobyte = 1;
        ctxt->b = insn_fetch(u8, ctxt);
        opcode = twobyte_table[ctxt->b];
    }
    ctxt->d = opcode.flags;
    ctxt->execute = opcode.u.execute;
    ctxt->check_perm = opcode.check_perm;
    ctxt->intercept = opcode.intercept;
    rc = decode_operand(ctxt, &ctxt->src, (ctxt->d >> SrcShift) & OpMask);
    if (rc != X86EMUL_CONTINUE)
        goto done;
    /*
    * Decode and fetch the second source operand: register, memory
    * or immediate.
    */
    rc = decode_operand(ctxt, &ctxt->src2, (ctxt->d >> Src2Shift) & OpMask);
    if (rc != X86EMUL_CONTINUE)
        goto done;
    /* Decode and fetch the destination operand: register or memory. */
    rc = decode_operand(ctxt, &ctxt->dst, (ctxt->d >> DstShift) & OpMask);
}

首先通過insn_fetch獲取指令,從下面的除錯可以看到取到的指令正好是我們的in指令的機器碼:

(gdb) 
4366            switch (ctxt->b = insn_fetch(u8, ctxt)) {
(gdb) 
4414        if (ctxt->rex_prefix & 8)
(gdb) p ctxt->b
$38 = 229 '345'
(gdb) p /x ctxt->b
$39 = 0xe5

之後根據指令,查表opcode_table找到對應的回撥函式,將回調賦值給ctxt->execute.對於我們的in指令 來說這個回撥是em_in函式。

4472        ctxt->execute = opcode.u.execute;
(gdb) 
4473        ctxt->check_perm = opcode.check_perm;
(gdb) p ctxt->execute 
$41 = (int (*)(struct x86_emulate_ctxt *)) 0xffffffff81027238 <em_in>
(gdb) n

接下來就是呼叫三次decode_operand取出對應指令的操作數了。從下面的除錯結果我們看出,源運算元 的值為ctxt->src->val=16,需要寫到的暫存器是RAX,即ctxt->dst->addr.reg

(gdb) n
4528        rc = decode_operand(ctxt, &ctxt->src2, (ctxt->d >> Src2Shift) & OpMask);
(gdb) n
4529        if (rc != X86EMUL_CONTINUE)
(gdb) p ctxt->src->val
$42 = 16
(gdb) n
4533        rc = decode_operand(ctxt, &ctxt->dst, (ctxt->d >> DstShift) & OpMask);
(gdb) s
...
(gdb) p op->addr.reg
$46 = (unsigned long *) 0xffff88011d4296c8
(gdb) p ctxt->_regs[0]
$47 = 10
(gdb) p &ctxt->_regs[0]
$48 = (unsigned long *) 0xffff88011d4296c8

繼續回到x86_emulate_instruction函式中,指令解碼之後就是執行了,這是通過呼叫x86_emulate_insn 實現的。

int x86_emulate_insn(struct x86_emulate_ctxt *ctxt)
{
    const struct x86_emulate_ops *ops = ctxt->ops;
    int rc = X86EMUL_CONTINUE;
    int saved_dst_type = ctxt->dst.type;
    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;
    }
writeback:
    rc = writeback(ctxt);
    if (rc != X86EMUL_CONTINUE)
        goto done;
done:
    if (rc == X86EMUL_PROPAGATE_FAULT)
        ctxt->have_exception = true;
    if (rc == X86EMUL_INTERCEPTED)
        return EMULATION_INTERCEPTED;
    if (rc == X86EMUL_CONTINUE)
        writeback_registers(ctxt);
    return (rc == X86EMUL_UNHANDLEABLE) ? EMULATION_FAILED : EMULATION_OK;
}

最重要的當然是呼叫回撥函數了

rc = ctxt->execute(ctxt);

從之前的解碼中,我們已經知道這是em_in了,相關呼叫函式如下:

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 pio_in_emulated(struct x86_emulate_ctxt *ctxt,
            unsigned int size, unsigned short port,
            void *dest)
{
    struct read_cache *rc = &ctxt->io_read;
    if (rc->pos == rc->end) { /* refill pio read ahead */
        ...
        rc->pos = rc->end = 0;
        if (!ctxt->ops->pio_in_emulated(ctxt, size, port, rc->data, n))
            return 0;
        rc->end = n * size;
    }
    if (ctxt->rep_prefix && !(ctxt->eflags & EFLG_DF)) {
        ctxt->dst.data = rc->data + rc->pos;
        ctxt->dst.type = OP_MEM_STR;
        ctxt->dst.count = (rc->end - rc->pos) / size;
        rc->pos = rc->end;
    } else {
        memcpy(dest, rc->data + rc->pos, size);
        rc->pos += size;
    }
    return 1;
}
static int emulator_pio_in_emulated(struct x86_emulate_ctxt *ctxt,
                    int size, unsigned short port, void *val,
                    unsigned int count)
{
    struct kvm_vcpu *vcpu = emul_to_vcpu(ctxt);
    int ret;
    if (vcpu->arch.pio.count)
        goto data_avail;
    ret = emulator_pio_in_out(vcpu, size, port, val, count, true);
    if (ret) {
data_avail:
        memcpy(val, vcpu->arch.pio_data, size * count);
        vcpu->arch.pio.count = 0;
        return 1;
    }
    return 0;
}

在最後一個函式中,由於vcpu->arch.pio.count此時還沒有資料(需要user spaces提供),所以會執行 emulator_pio_in_out,這在之前已經看過這個函數了,這就是設定kvm_run的相關資料,然後user spaces來 填充。

執行完了x86_emulate_insn,流程再次回到x86_emulate_instruction,最重要的是設定 vcpu->arch.complete_userspace_io這樣一個回撥。

if (ctxt->have_exception) {
    inject_emulated_exception(vcpu);
    r = EMULATE_DONE;
} else if (vcpu->arch.pio.count) {
    if (!vcpu->arch.pio.in)
        vcpu->arch.pio.count = 0;
    else {
        writeback = false;
        vcpu->arch.complete_userspace_io = complete_emulated_pio;
    }

之後這一次vm exit就算完事了。這樣就會退到user space的ioctl KVM_RUN處。user space發現是一個 KVM_EXIT_IO,並且方向是KVM_EXIT_IO_IN,於是向kvm_run填入資料0xbeff。

    case KVM_EXIT_IO:
        printf("KVM_EXIT_IOn");
        if(kvm->vcpus->kvm_run->io.direction == KVM_EXIT_IO_OUT)
            printf("out port: %d, data: 0x%xn", 
                kvm->vcpus->kvm_run->io.port,  
                *(int *)((char *)(kvm->vcpus->kvm_run) + kvm->vcpus->kvm_run->io.data_offset)
                );
        else if(kvm->vcpus->kvm_run->io.direction == KVM_EXIT_IO_IN)
        {
            printf("in port: %dn",kvm->vcpus->kvm_run->io.port);
            *(short*)((char*)(kvm->vcpus->kvm_run)+kvm->vcpus->kvm_run->io.data_offset) = 0xbeff;
        }

由於user space的ioctl一般都是執行在一個迴圈中(如果不這樣,guest也就不可能一直執行著了)。所以接著呼叫 KVM_RUN ioctl。在進入non-root的模式前,有一個工作就是判斷vcpu->arch.complete_userspace_io 是否設定,如果設定就會呼叫。

int kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu, struct kvm_run *kvm_run)
{
    int r;
    sigset_t sigsaved;
    if (unlikely(vcpu->arch.complete_userspace_io)) {
        int (*cui)(struct kvm_vcpu *) = vcpu->arch.complete_userspace_io;
        vcpu->arch.complete_userspace_io = NULL;
        r = cui(vcpu);
        if (r <= 0)
            goto out;
    } else
        WARN_ON(vcpu->arch.pio.count || vcpu->mmio_needed);
    r = __vcpu_run(vcpu);
    return r;
}

從之前的分之知道

vcpu->arch.complete_userspace_io = complete_emulated_pio;

看看相應的程式碼

static int complete_emulated_pio(struct kvm_vcpu *vcpu)
{
    BUG_ON(!vcpu->arch.pio.count);
    return complete_emulated_io(vcpu);
}
static inline int complete_emulated_io(struct kvm_vcpu *vcpu)
{
    int r;
    vcpu->srcu_idx = srcu_read_lock(&vcpu->kvm->srcu);
    r = emulate_instruction(vcpu, EMULTYPE_NO_DECODE);
    srcu_read_unlock(&vcpu->kvm->srcu, vcpu->srcu_idx);
    if (r != EMULATE_DONE)
        return 0;
    return 1;
}
static inline int emulate_instruction(struct kvm_vcpu *vcpu,
        int emulation_type)
{
    return x86_emulate_instruction(vcpu, 0, emulation_type, NULL, 0);
}

最終也是呼叫了x86_emulate_instruction,值得注意的是用了引數EMULTYPE_NO_DECODE,這就不會再次 解碼。而是直接執行我們之前的em_in函式。

static int emulator_pio_in_emulated(struct x86_emulate_ctxt *ctxt,
                    int size, unsigned short port, void *val,
                    unsigned int count)
{
    struct kvm_vcpu *vcpu = emul_to_vcpu(ctxt);
    int ret;
    if (vcpu->arch.pio.count)
        goto data_avail;
    ret = emulator_pio_in_out(vcpu, size, port, val, count, true);
    if (ret) {
data_avail:
        memcpy(val, vcpu->arch.pio_data, size * count);
        vcpu->arch.pio.count = 0;
        return 1;
    }
    return 0;
}

在最終的emulator_pio_in_emulated中,由於這個時候vcpu->arch.pio.count已經有值了,表示資料可用了。 最終會把資料拷貝到ctx->dst.val中。

(gdb) n
em_in (ctxt=0xffff88011d429550) at arch/x86/kvm/emulate.c:3440
3440        return X86EMUL_CONTINUE;
(gdb) n
3441    }
(gdb) p ctxt->dst.val
$58 = 48895
(gdb) p /x ctxt->dst.val
$59 = 0xbeff
(gdb) n

回到x86_emulate_insn,執行完了指令回撥之後,會調到writeback函式去:

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;
    }
writeback:
    rc = writeback(ctxt);
    if (rc != X86EMUL_CONTINUE)
        goto done;

我們之前解碼得到ctxt->dst.type是一個暫存器,所以會執行write_register_operand

static int writeback(struct x86_emulate_ctxt *ctxt)
{
    int rc;
    if (ctxt->d & NoWrite)
        return X86EMUL_CONTINUE;
    switch (ctxt->dst.type) {
    case OP_REG:
        write_register_operand(&ctxt->dst);
        break;
    
    return X86EMUL_CONTINUE;
}
static void write_register_operand(struct operand *op)
{
    /* The 4-byte case *is* correct: in 64-bit mode we zero-extend. */
    switch (op->bytes) {
    case 1:
        *(u8 *)op->addr.reg = (u8)op->val;
        break;
    case 2:
        *(u16 *)op->addr.reg = (u16)op->val;
        break;
    case 4:
        *op->addr.reg = (u32)op->val;
        break;  /* 64b: zero-extend */
    case 8:
        *op->addr.reg = op->val;
        break;
    }
}

最後一個函式op->addr.reg是解碼過程中的目的運算元的暫存器,由之前知道是rax(&ctxt->_regs[0]),這樣 就把資料(0xbeff)寫到了暫存器了。但是這裡是ctxt的暫存器,最後還需要寫到vmcs中去,通過呼叫如下函式 實現

if (rc == X86EMUL_CONTINUE)
    writeback_registers(ctxt);
static void writeback_registers(struct x86_emulate_ctxt *ctxt)
{
    unsigned reg;
    for_each_set_bit(reg, (ulong *)&ctxt->regs_dirty, 16)
        ctxt->ops->write_gpr(ctxt, reg, ctxt->_regs[reg]);
}
static void emulator_write_gpr(struct x86_emulate_ctxt *ctxt, unsigned reg, ulong val)
{
    kvm_register_write(emul_to_vcpu(ctxt), reg, val);
}
static inline void kvm_register_write(struct kvm_vcpu *vcpu,
                    enum kvm_reg reg,
                    unsigned long val)
{
    vcpu->arch.regs[reg] = val;
    __set_bit(reg, (unsigned long *)&vcpu->arch.regs_dirty);
    __set_bit(reg, (unsigned long *)&vcpu->arch.regs_avail);
}

這樣,接著進入guest狀態的時候,guest得RAX就有了user space傳來的資料了。下面是一些除錯資料。

(gdb) n
x86_emulate_insn (ctxt=0xffff88011d429550) at arch/x86/kvm/emulate.c:4828
4828        ctxt->dst.type = saved_dst_type;
(gdb) p ctxt->dst.val
$64 = 48895
(gdb) p &ctxt->dst.val
$65 = (unsigned long *) 0xffff88011d429640
(gdb) p &op->val
No symbol "op" in current context.
(gdb) n
4830        if ((ctxt->d & SrcMask) == SrcSI)
(gdb) p ctxt->dst.type
$66 = OP_REG
(gdb) n
[New Thread 2976]
4833        if ((ctxt->d & DstMask) == DstDI)
(gdb) n
[New Thread 2978]
[New Thread 2977]
4836        if (ctxt->rep_prefix && (ctxt->d & String)) {
(gdb) n
4866        ctxt->eip = ctxt->_eip;
(gdb) n
4875            writeback_registers(ctxt);

四.  參考