Quantcast
Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

【系列分享】探索QEMU-KVM中PIO处理的奥秘

0
0
【系列分享】探索QEMU-KVM中PIO处理的奥秘

2017-07-10 10:13:11

阅读:12117次
点赞(0)
收藏
来源: 安全客





【系列分享】探索QEMU-KVM中PIO处理的奥秘

作者:360GearTeam





【系列分享】探索QEMU-KVM中PIO处理的奥秘

作者:Terenceli @ 360 Gear Team

投稿方式:发送邮件至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.

这里我们使用第一个例子,首先从

https://github.com/soulxu/kvmsample

把代码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:


【系列分享】探索QEMU-KVM中PIO处理的奥秘

我们可以看到如果设置了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


【系列分享】探索QEMU-KVM中PIO处理的奥秘

每一个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。

staticunsignedlong*vmx_io_bitmap_a; staticunsignedlong*vmx_io_bitmap_b; staticint__initvmx_init(void) { vmx_io_bitmap_a=(unsignedlong*)__get_free_page(GFP_KERNEL); vmx_io_bitmap_b=(unsignedlong*)__get_free_page(GFP_KERNEL); /* *AllowdirectaccesstothePCdebugport(itisoftenusedforI/O *delays,butthevmexitssimplyslowthingsdown). */ 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的访问的截获。

staticintvmx_vcpu_setup(structvcpu_vmx*vmx) { /*I/O*/ vmcs_write64(IO_BITMAP_A,__pa(vmx_io_bitmap_a)); vmcs_write64(IO_BITMAP_B,__pa(vmx_io_bitmap_b)); return0; }

二. 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中

staticint(*constkvm_vmx_exit_handlers[])(structkvm_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中执行:

root@ubuntu:/home/test#echog>/proc/sysrq-trigger

这样调试机中的gdb会断下来,给handle_io下个断点:

(gdb)bhandle_io Breakpoint1at0xffffffff81037dca:filearch/x86/kvm/vmx.c,line4816. (gdb)c 接着,我们用gdb启动target中的kvmsample,并且在main.c的84行下个断点。 test@ubuntu:~/kvmsample$gdb./kvmsample ... Readingsymbolsfrom./kvmsample...done. (gdb)bma mainmain.cmallocmalloc@plt (gdb)bmain.c:84 Breakpoint1at0x400cac:filemain.c,line84.

第84行恰好是从ioctl KVM_RUN中返回回来的时候。


【系列分享】探索QEMU-KVM中PIO处理的奥秘

好了,开始r,会发现debugger已经断下来了:

Thread434hitBreakpoint1,handle_io(vcpu=0xffff8800ac528000) atarch/x86/kvm/vmx.c:4816 4816{ (gdb)

从handle_io的代码我们可以看出,首先会从vmcs中读取exit的一些信息,包括访问这个端口是in还是out, 大小,以及端口号port等。

staticinthandle_io(structkvm_vcpu*vcpu) { unsignedlongexit_qualification; intsize,in,string; unsignedport; exit_qualification=vmcs_readl(EXIT_QUALIFICATION); string=(exit_qualification&16)!=0; in=(exit_qualification&8)!=0; ++vcpu->stat.io_exits; if(string||in) returnemulate_instruction(vcpu,0)==EMULATE_DONE; port=exit_qualification>>16; size=(exit_qualification&7)+1; skip_emulated_instruction(vcpu); returnkvm_fast_pio_out(vcpu,size,port); }

之后通过skip_emulated_instruction增加guest的rip之后调用kvm_fast_pio_out,在该函数中, 我们可以看到首先读取guest的rax,这个值放的是向端口写入的数据,这里是,0xa

intkvm_fast_pio_out(structkvm_vcpu*vcpu,intsize,unsignedshortport) { unsignedlongval=kvm_register_read(vcpu,VCPU_REGS_RAX); intret=emulator_pio_out_emulated(&vcpu->arch.emulate_ctxt, size,port,&val,1); /*donotreturntoemulatorafterreturnfromuserspace*/ vcpu->arch.pio.count=0; returnret; }

我们可以对比gdb中看看数据:

Thread434hitBreakpoint1,handle_io(vcpu=0xffff8800ac528000) atarch/x86/kvm/vmx.c:4816 4816{ (gdb)n 4821exit_qualification=vmcs_readl(EXIT_QUALIFICATION); (gdb)n 4825++vcpu->stat.io_exits; (gdb)n 4827if(string||in) (gdb)n 4832skip_emulated_instruction(vcpu); (gdb)n [NewThread3654] 4834returnkvm_fast_pio_out(vcpu,size,port); (gdb)s kvm_fast_pio_out(vcpu=0xffff8800ac528000,size=16,port=16) atarch/x86/kvm/x86.c:5086 5086{ (gdb)n [NewThread3656] 5087unsignedlongval=kvm_register_read(vcpu,VCPU_REGS_RAX); (gdb)n [NewThread3657] 5088intret=emulator_pio_out_emulated(&vcpu->arch.emulate_ctxt, (gdb)p/xval $1=0xa (gdb)

再往下,我们看到在emulator_pio_out_emulated,把值拷贝到了vcpu->arch.pio_data中,接着调用 emulator_pio_in_out。

staticintemulator_pio_out_emulated(structx86_emulate_ctxt*ctxt, intsize,unsignedshortport, constvoid*val,unsignedintcount) { structkvm_vcpu*vcpu=emul_to_vcpu(ctxt); memcpy(vcpu->arch.pio_data,val,size*count); returnemulator_pio_in_out(vcpu,size,port,(void*)val,count,false); } staticintemulator_pio_in_out(structkvm_vcpu*vcpu,intsize, unsignedshortport,void*val, unsignedintcount,boolin) { 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; return1; } 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; return0; }

在后一个函数中,我们可以看到vcpu->run->io.data_offset设置为4096了,我们可以看到之前已经把我们 向端口写的值通过memcpy拷贝到了vpuc->arch.pio_data中去了,通过调试我们可以看出其中的端倪。 vcpu->arch.pio_data就在kvm_run后面一个页的位置。这也可以从kvm_vcpu_init中看出来。

4405vcpu->run->io.size=size; (gdb)n [NewThread3667] 4406vcpu->run->io.data_offset=KVM_PIO_PAGE_OFFSET*PAGE_SIZE; (gdb)n 4407vcpu->run->io.count=count; (gdb)n 4408vcpu->run->io.port=port; (gdb)pcount $7=1 (gdb)n 4410return0; (gdb)x/2b0xffff88002a2a2000+0x1000 0xffff88002a2a3000:0x0a0x00 (gdb)pvcpu->run $9=(structkvm_run*)0xffff88002a2a2000 (gdb)pvcpu->arch.pio_data $10=(void*)0xffff88002a2a3000 (gdb)

这样,我们看到vcpu->run->io保存了一些PIO的基本信息,比如大小,端口号等,run后面的一个页 vcpu->arch.pio_data则保存了实际out出来的数据。让target继续执行,这个时候我们断回了kvmsample 程序中。

(gdb)pkvm->vcpus->kvm_run->io $2={direction=1'\001',size=2'\002',port=16,count=1, data_offset=4096} (gdb)

这里简单说一下kvm_run,这是用于vcpu和应用层的程序(典型如qemu)通信的一个结构,user space的 程序通过KVM__VCPU_MMAP_SIZE这个ioctl得到大小得到大小,然后映射到用户空间。

(gdb)x/2b0x7ffff7ff4000+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 #Atestcodeforkvmsample .globl_start .code16 _start: xorw%ax,%ax mov$0x0a,%al in$0x10,%ax out%ax,$0x10 hlt

对main.c做如下修改:


【系列分享】探索QEMU-KVM中PIO处理的奥秘

在处理KVM_EXIT_IO的时候区分了一下in/out,对in我们拷贝一个0xbeff过去。然后用在guest中用out向 端口0x10输出这个值。

执行in指令的第一次仍然是陷入kvm handle_io处理,只是这次走另一条路:

Thread486hitBreakpoint1,handle_io(vcpu=0xffff88011d428000) atarch/x86/kvm/vmx.c:4816 4816{ (gdb)n 4821exit_qualification=vmcs_readl(EXIT_QUALIFICATION); (gdb) 4825++vcpu->stat.io_exits; (gdb) 4827if(string||in) (gdb) 4828returnemulate_instruction(vcpu,0)==EMULATE_DONE; (gdb)s emulate_instruction(emulation_type=<optimizedout>,vcpu=<optimizedout>) at/home/test/linux-3.10.105/arch/x86/include/asm/kvm_host.h:811 811returnx86_emulate_instruction(vcpu,0,emulation_type,NULL,0); (gdb)s

调用x86_emulate_instruction,这之中调用的最重要的两个函数时x86_decode_insn, x86_emulate_insn。

intx86_emulate_instruction(structkvm_vcpu*vcpu, unsignedlongcr2, intemulation_type, void*insn, intinsn_len) { intr; structx86_emulate_ctxt*ctxt=&vcpu->arch.emulate_ctxt; boolwriteback=true; boolwrite_fault_to_spt=vcpu->arch.write_fault_to_shadow_pgtable; /* *Clearwrite_fault_to_shadow_pgtableheretoensureitis *neverreused. */ 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; }elseif(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; returnr; } EXPORT_SYMBOL_GPL(x86_emulate_instruction);

第一个函数,x86_decode_insn,顾名思义,就是解码当前的指令。

intx86_decode_insn(structx86_emulate_ctxt*ctxt,void*insn,intinsn_len) { /*Legacyprefixes.*/ for(;;){ switch(ctxt->b=insn_fetch(u8,ctxt)){ } /*Opcodebyte(s).*/ opcode=opcode_table[ctxt->b]; /*Two-byteopcode?*/ 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) gotodone; /* *Decodeandfetchthesecondsourceoperand:register,memory *orimmediate. */ rc=decode_operand(ctxt,&ctxt->src2,(ctxt->d>>Src2Shift)&OpMask); if(rc!=X86EMUL_CONTINUE) gotodone; /*Decodeandfetchthedestinationoperand:registerormemory.*/ rc=decode_operand(ctxt,&ctxt->dst,(ctxt->d>>DstShift)&OpMask); }

首先通过insn_fetch获取指令,从下面的调试可以看到取到的指令正好是我们的in指令的机器码:

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

之后根据指令,查表opcode_table找到对应的回调函数,将回调赋值给ctxt->execute.对于我们的in指令 来说这个回调是em_in函数。

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

接下来就是调用三次decode_operand取出对应指令的操作数了。从下面的调试结果我们看出,源操作数 的值为ctxt->src->val=16,需要写到的寄存器是RAX,即ctxt->dst->addr.reg

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

继续回到x86_emulate_instruction函数中,指令解码之后就是执行了,这是通过调用x86_emulate_insn 实现的。

intx86_emulate_insn(structx86_emulate_ctxt*ctxt) { conststructx86_emulate_ops*ops=ctxt->ops; intrc=X86EMUL_CONTINUE; intsaved_dst_type=ctxt->dst.type; if(ctxt->execute){ if(ctxt->d&Fastop){ void(*fop)(structfastop*)=(void*)ctxt->execute; rc=fastop(ctxt,fop); if(rc!=X86EMUL_CONTINUE) gotodone; gotowriteback; } rc=ctxt->execute(ctxt); if(rc!=X86EMUL_CONTINUE) gotodone; gotowriteback; } writeback: rc=writeback(ctxt); if(rc!=X86EMUL_CONTINUE) gotodone; done: if(rc==X86EMUL_PROPAGATE_FAULT) ctxt->have_exception=true; if(rc==X86EMUL_INTERCEPTED) returnEMULATION_INTERCEPTED; if(rc==X86EMUL_CONTINUE) writeback_registers(ctxt); return(rc==X86EMUL_UNHANDLEABLE)?EMULATION_FAILED:EMULATION_OK; }

最重要的当然是调用回调函数了

rc=ctxt->execute(ctxt);

从之前的解码中,我们已经知道这是em_in了,相关调用函数如下:

staticintem_in(structx86_emulate_ctxt*ctxt) { if(!pio_in_emulated(ctxt,ctxt->dst.bytes,ctxt->src.val, &ctxt->dst.val)) returnX86EMUL_IO_NEEDED; returnX86EMUL_CONTINUE; } staticintpio_in_emulated(structx86_emulate_ctxt*ctxt, unsignedintsize,unsignedshortport, void*dest) { structread_cache*rc=&ctxt->io_read; if(rc->pos==rc->end){/*refillpioreadahead*/ ... rc->pos=rc->end=0; if(!ctxt->ops->pio_in_emulated(ctxt,size,port,rc->data,n)) return0; 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; } return1; } staticintemulator_pio_in_emulated(structx86_emulate_ctxt*ctxt, intsize,unsignedshortport,void*val, unsignedintcount) { structkvm_vcpu*vcpu=emul_to_vcpu(ctxt); intret; if(vcpu->arch.pio.count) gotodata_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; return1; } return0; }

在最后一个函数中,由于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; }elseif(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。

caseKVM_EXIT_IO: printf("KVM_EXIT_IO\n"); if(kvm->vcpus->kvm_run->io.direction==KVM_EXIT_IO_OUT) printf("outport:%d,data:0x%x\n", kvm->vcpus->kvm_run->io.port, *(int*)((char*)(kvm->vcpus->kvm_run)+kvm->vcpus->kvm_run->io.data_offset) ); elseif(kvm->vcpus->kvm_run->io.direction==KVM_EXIT_IO_IN) { printf("inport:%d\n",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 是否设置,如果设置就会调用。

intkvm_arch_vcpu_ioctl_run(structkvm_vcpu*vcpu,structkvm_run*kvm_run) { intr; sigset_tsigsaved; if(unlikely(vcpu->arch.complete_userspace_io)){ int(*cui)(structkvm_vcpu*)=vcpu->arch.complete_userspace_io; vcpu->arch.complete_userspace_io=NULL; r=cui(vcpu); if(r<=0) gotoout; }else WARN_ON(vcpu->arch.pio.count||vcpu->mmio_needed); r=__vcpu_run(vcpu); returnr; }

从之前的分之知道

vcpu->arch.complete_userspace_io=complete_emulated_pio;

看看相应的代码

staticintcomplete_emulated_pio(structkvm_vcpu*vcpu) { BUG_ON(!vcpu->arch.pio.count); returncomplete_emulated_io(vcpu); } staticinlineintcomplete_emulated_io(structkvm_vcpu*vcpu) { intr; 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) return0; return1; } staticinlineintemulate_instruction(structkvm_vcpu*vcpu, intemulation_type) { returnx86_emulate_instruction(vcpu,0,emulation_type,NULL,0); }

最终也是调用了x86_emulate_instruction,值得注意的是用了参数EMULTYPE_NO_DECODE,这就不会再次 解码。而是直接执行我们之前的em_in函数。

staticintemulator_pio_in_emulated(structx86_emulate_ctxt*ctxt, intsize,unsignedshortport,void*val, unsignedintcount) { structkvm_vcpu*vcpu=emul_to_vcpu(ctxt); intret; if(vcpu->arch.pio.count) gotodata_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; return1; } return0; }

在最终的emulator_pio_in_emulated中,由于这个时候vcpu->arch.pio.count已经有值了,表示数据可用了。 最终会把数据拷贝到ctx->dst.val中。

(gdb)n em_in(ctxt=0xffff88011d429550)atarch/x86/kvm/emulate.c:3440 3440returnX86EMUL_CONTINUE; (gdb)n 3441} (gdb)pctxt->dst.val $58=48895 (gdb)p/xctxt->dst.val $59=0xbeff (gdb)n

回到x86_emulate_insn,执行完了指令回调之后,会调到writeback函数去:

if(ctxt->execute){ if(ctxt->d&Fastop){ void(*fop)(structfastop*)=(void*)ctxt->execute; rc=fastop(ctxt,fop); if(rc!=X86EMUL_CONTINUE) gotodone; gotowriteback; } writeback: rc=writeback(ctxt); if(rc!=X86EMUL_CONTINUE) gotodone;

我们之前解码得到ctxt->dst.type是一个寄存器,所以会执行write_register_operand

staticintwriteback(structx86_emulate_ctxt*ctxt) { intrc; if(ctxt->d&NoWrite) returnX86EMUL_CONTINUE; switch(ctxt->dst.type){ caseOP_REG: write_register_operand(&ctxt->dst); break; returnX86EMUL_CONTINUE; } staticvoidwrite_register_operand(structoperand*op) { /*The4-bytecase*is*correct:in64-bitmodewezero-extend.*/ switch(op->bytes){ case1: *(u8*)op->addr.reg=(u8)op->val; break; case2: *(u16*)op->addr.reg=(u16)op->val; break; case4: *op->addr.reg=(u32)op->val; break;/*64b:zero-extend*/ case8: *op->addr.reg=op->val; break; } } 最后一个函数op->addr.reg是解码过程中的目的操作数的寄存器,由之前知道是rax(&ctxt->_regs[0]),这样 就把数据(0xbeff)写到了寄存器了。但是这里是ctxt的寄存器,最后还需要写到vmcs中去,通过调用如下函数 实现 if(rc==X86EMUL_CONTINUE) writeback_registers(ctxt); staticvoidwriteback_registers(structx86_emulate_ctxt*ctxt) { unsignedreg; for_each_set_bit(reg,(ulong*)&ctxt->regs_dirty,16) ctxt->ops->write_gpr(ctxt,reg,ctxt->_regs[reg]); } staticvoidemulator_write_gpr(structx86_emulate_ctxt*ctxt,unsignedreg,ulongval) { kvm_register_write(emul_to_vcpu(ctxt),reg,val); } staticinlinevoidkvm_register_write(structkvm_vcpu*vcpu, enumkvm_regreg, unsignedlongval) { vcpu->arch.regs[reg]=val; __set_bit(reg,(unsignedlong*)&vcpu->arch.regs_dirty); __set_bit(reg,(unsignedlong*)&vcpu->arch.regs_avail); }

这样,接着进入guest状态的时候,guest得RAX就有了user space传来的数据了。下面是一些调试数据。

(gdb)n x86_emulate_insn(ctxt=0xffff88011d429550)atarch/x86/kvm/emulate.c:4828 4828ctxt->dst.type=saved_dst_type; (gdb)pctxt->dst.val $64=48895 (gdb)p&ctxt->dst.val $65=(unsignedlong*)0xffff88011d429640 (gdb)p&op->val Nosymbol"op"incurrentcontext. (gdb)n 4830if((ctxt->d&SrcMask)==SrcSI) (gdb)pctxt->dst.type $66=OP_REG (gdb)n [NewThread2976] 4833if((ctxt->d&DstMask)==DstDI) (gdb)n [NewThread2978] [NewThread2977] 4836if(ctxt->rep_prefix&&(ctxt->d&String)){ (gdb)n 4866ctxt->eip=ctxt->_eip; (gdb)n 4875writeback_registers(ctxt);

四. 参考

oenhan: KVM源代码分析5:IO虚拟化之PIO

Alex Xu: 使用KVM API实现Emulator Demo



【系列分享】探索QEMU-KVM中PIO处理的奥秘
【系列分享】探索QEMU-KVM中PIO处理的奥秘
本文由 安全客 原创发布,如需转载请注明来源及本文地址。
本文地址:http://bobao.360.cn/learning/detail/4079.html

Viewing all articles
Browse latest Browse all 12749