Guest Debug Emulation on KVM

1) 前言

breakpoint和watchpoint对软件开发人员来说都是非常熟悉的词了,它们是调试器的最基本的功能之一。breakpoint用来在指定的程序中设置一个断点,如果程序运行到这个点将会被暂停且会被调试程序(例如gdb)捕捉到。watchpoint用于监视程序对指定内存的更改,如果内存的更改满足捕捉条件,程序将会被暂停并且被调试程序捕捉到。

breakpoint和watchpoint又分为hardware breakpoint, hardware watchpoint和software breakpoint, software watchpoint。前者完全利用硬件提供的debug功能来实现,后者顾名思义,就是软件自己实现这些功能而不依赖于硬件。硬件实现的debug性能会很高但是有自己的局限性,例如在x86平台上,硬件只支持最多4条breakpoint + watchpoint。软件自己实现的debug功能更灵活但性能会很低,例如在x86上, software breakpoint是在断点处插入软中断(int 3指令),software watchpoint 利用single step来实现watchpoint,以至于程序在每运行一条指令后都会去check被监视的内存。

Hardware debug功能在虚拟环境中的模拟很复杂因为virtualization的所有部件都可以使用这个功能,例如guest利用它来调试自己内部的程序,同时在host上面也要用它来调试hypervisor。更为复杂的是,现在hypervisor支持guest debug的功能,即在host上支持对guest进行debug(例如在host上直接调试guest kernel)。如何虚拟hardware debug功能用来同时满足这三种情况将是本文要讨论的主要问题。

2) 硬件背景

在分析hardware debug虚拟化之前,有必要来看一下硬件对debug的支持。以下是x86 64位环境中为debug功能所提供的寄存器:
cpu-debug-arch
在参考Intel 的spec时,需要注意到的是,breakpoint和watchpoint都被统称为breakpoint,在后文中也沿用这一习惯。在图中总共有7个寄存器,其中DR4和DR5不能被软件直接使用,任何指令使用到这两个寄存器将会产生#UD(invalid-opcode exceptions)异常。

DR0~DR3这四个寄存器用来设定要被debug对象(例如断点,内存)的虚拟地址。DR7用来控制DR0~DR3这四个寄存器。它有两个重要的作用,分别如下:

  • 用来控制DR0~DR3的enable和disable。
    DR0~DR3有两种使用模式,分别为local breakpoint和global breakpoint,前者只是用于对当前的进程debug,后者用于对系统的所有进程进行debug,所以硬件进行task switch的时候,如果DR0~DR3是处于local breakpoint,内容将会被清除,而处于global的DR0~DR3的内容将会被保留。DR7有两个位LE (local breakpoint exact enable), GE (global breakpoint exact enable)用于控制全局的local/global breakpoint的使能。而L0~L3分别用来控制DR0~DR3是否处于local模式,类似的G0~G3用于控制DR0~DR3是否处于global 模式。
  • 用来控制DR0~DR3将如何使用。
    RW0~RW3用来指定DR0~DR3的debug条件。有这几种情况:00b表示只debug 指令执行(即为前面所说的breakpoint),01b表示只debug数据写的情况,11b表示debug数据读和写的情况,10b表示只debug IO的读写(需要设置CR4.DE=1)。后面的三种情况对应于上文说的watchpoint。LEN0~LEN3用来表示数据区域的大小,有这几点情况,00b表示1个字节,01b表示2个字节,10b为8个字节(在32位CPU上为非法设置),11b表示四个字节。在这里需要注意的是,如果是debug 指令执行,即RWx位 = 00b时,它的长度必须要为1字节,即00b。另外还有一个位被称为GD(general detect enable),它用于控制对这些debug 寄存器的更改,如果有指令试图更改debug register,且GD=1, 就会触发#DB (debug exception)。

如果CPU有检测到满足的debug事件则会产生#DB(debug exception)。

DR6是一个状态寄存器,用于检测是哪一个事件触发的#DB。其中每个位表示的含义如下:

  • B0~B1用来表示是DR0~DR3中的哪一个寄存器的条件触发了#DB。
  • BD用来判断是该#DB是否是由于更改debug register产生。
  • BS表示该#DB是否由single step产生。single step在前文中提起过,简而言之,single step是指CPU每执行完一条指令后都会产生#DB。它的使能由RFLAGS寄存器的TF位所控制。
  • BT表示该#DB是否由进程切换所导致。(它的使能由TSS.T所控制)。

此外,在前文中曾提到int 3会用来实现software breakpoint。在CPU执行int 3后会产生#BP (breakpoint exception)。

这些就是CPU对debug功能的支持,同时CPU也有针对虚拟化的情况下做一些enhancement,主要有如下几点:

  • CPU对DR7的模拟。从上面的介绍中可以看到,DR7是一个非常关键的寄存器,它用于配置debug的条件。同时更改DR7也是一个消耗CPU时间较多的一个操作。所以CPU支持guest DR7的自动保存和恢复。具体来说,在进入到Guest的时候,自动将VMCS的DR7加载到DR7寄存器,在Guest退出的时候,自动将DR7的内容保存到VMCS的DR7。
  • CPU对DR寄存器访问的捕捉。因为只有DR7可以被自动加载和保存,CPU可以控制是否捕捉对DR寄存器的访问。
  • 因为CPU debug功能会用到两个异常,分别为#DB和 #BP,CPU可以控制是否要捕捉这两个异常。

3)KVM对debug的模拟

到这里,已经介绍完了所有的硬件背景,是时候来看看KVM如何对debug进行模拟了。对debug的模拟无非就是要模拟这两种情况,其一是对DR寄存器的正确模拟,使guest能够正常访问到这些寄存器且这些寄存器中的值需要全部正确。其二是必须要能够debug设置好的条件且在检测到这些条件后guest可以接收到#DB和#BP。

在上文中曾提到使用debug有三种情况,它们分别是guest中使用debug来调试其本身的程序,host上使用debug来调试hypervisor(例如QEMU)和hypervisor上直接调试guest。前二种情况属于常见的情况因此很容易理解。后一种情况是由虚拟环境引入的特例,因此需要hypervisor的支持,需要花一点时间来看看在KVM上如何对这一情况做支持。

3.1)KVM debug guest

为了支持在hypervisor上debug guest, KVM引入了一个新的API,即KVM_SET_GUEST_DEBUG, 它用于enable guest debug的一些功能以及用于设置debug寄存器。它所携带的参数如下:

struct kvm_guest_debug {
__u32 control;
__u32 pad;
struct kvm_guest_debug_arch arch;
};

control用来控制debug的相关功能,它的功能如下:
KVM_GUESTDBG_ENABLE: 用来enable guest debug
KVM_GUESTDBG_SINGLESTEP: 用来enable guest的single-step。此后guest每执行完一条执行都会被退回到hypervisor。
这两个功能在KVM的全平台上都可以使用,在x86上还有以下几个功能:
KVM_GUESTDBG_USE_SW_BP: 使用software breakpoint来debug guest。
KVM_GUESTDBG_USE_HW_BP: 使用hardware breakpoint来debug guest。
KVM_GUESTDBG_INJECT_DB: 往guest注入#DB。
KVM_GUESTDBG_INJECT_BP: 往guest注入#BP。

arch的部份用来表示平台上的debug registers,在x86上为DR0 ~ DR7。

同样,KVM遇到guest debug的事件后会退回到hypervisor,它的退出状态为KVM_EXIT_DEBUG,在exit-info中也有一些域来表示这些事件的信息。如下所示:

struct kvm_debug_exit_arch {
__u32 exception;
__u32 pad;
__u64 pc;
__u64 dr6;
__u64 dr7;
};

里面填充的信息包括导致退回到userspace的异常(#DB or #BP),引起异常的地址,以及异常发生时DR6和DR7这两个寄存器的值。

接下来,按debug使用的各种场景以及各场景下的debug registers访问和debug功能实现,由简入繁地来分析在KVM中是如何虚拟debug的功能的。

3.2)guest中使用debug

这个是一种最基本的情况,guest中的调试器使用debug功能来调试程序。在分析这种情况时,我们需要有这样的一个概念,即hypervisor随时都可能被gdb/perf等调试器所动态调试,这些调试器会给hypervisor发送IPI,然后在IPI的handler里设置debug registers。所以vcpu在enable interrupt的时候,debug registers随时都会被更改,guest需要在安全的情况下(disable interrupt)保存好自己的debug registers信息。

3.2.1) debug registers的访问

因为debug registers随时都会被调试程序所用到,所以一种最简单的实现方式是由guest完全控制debug 功能,因此在进入到guest之前,将guest的DR0~DR6加载到寄存器(DR7由VMCS控制),在guest退出后,将guest的DR0 ~DR6保存起来。这样也不需要捕捉guest对DR registers的访问。

嗯,看上去是可以work,但是却不能这么做。因为这种做法需要在每一次进入到guest以及每次退出guest的时候都需要去恢复和保存debug registers的状态,即使在guest没有使用到debug功能的时候也如此,这样做无疑会给guest vm-enter和vm-exit造成性能上的开销。

那么退而求其次,可以让KVM每次都去捕获debug registers的访问,在写的时候将其保存起来,在读的时候将保存的值返回。这样就可以完全避免在vm-enter和vm-exit时对它们的操作。嗯,这种做法较上一种有了很大的改进,实际上旧版本的KVM上就是这样处理的。但是它的问题是,guest每次访问debug registers的开销会非常大而且调试程序通常会一次访问多个debug registers (例如在设置hardware breakpoint时,就先要将它放入DR0~DR3中的一个,然后再来配置DR7)。所以这样做也不是一种最好的做法。

所以,KVM演变成了今天的做法,即guest在第一次访问DR register的时候,被KVM捕获而退出,在处理退出时,KVM为这种情况设置一个标志且去掉DR register的捕获。当vm-enter的时候,如果检测到这个标志被设置,就会将guest的DR registers恢复,这样就返回到了guest, guest在后续访问DR registers的时候就不会再次被KVM捕获了。这样一直持续到下一次vm-exit (由其它任意事件产生),这时KVM会清除这个标志, 将guest DR register保存起来,最后重新enable 访问DR registers的捕获。因为标志被清除,所以在vm-enter的时候就不需要再次恢复了。下面这个图更清楚地解释了这一个过程。
handle-dr
上面列出了几个关键的点,我们来看看在KVM的代码中是如何处理的。

处理DR-access导致的VM-exit

在arch/x86/kvm/vmx.c的handle_dr()函数中:

5621         if (vcpu->guest_debug == 0) {
5622                 u32 cpu_based_vm_exec_control;
5623
5624                 cpu_based_vm_exec_control = vmcs_read32(CPU_BASED_VM_EXEC_CONTROL);
5625                 cpu_based_vm_exec_control &= ~CPU_BASED_MOV_DR_EXITING;
5626                 vmcs_write32(CPU_BASED_VM_EXEC_CONTROL, cpu_based_vm_exec_control);
5627
5628                 /*
5629                  * No more DR vmexits; force a reload of the debug registers
5630                  * and reenter on this instruction.  The next vmexit will
5631                  * retrieve the full state of the debug registers.
5632                  */
5633                 vcpu->arch.switch_db_regs |= KVM_DEBUGREG_WONT_EXIT;
5634                 return 1;
5635         }

 

在后续的分析可以看到vcpu→guest_debug表示vcpu是否被kvm debug。在当前分析的情况下,它不满足。5624~5626设置不要捕获DR-access,并且在5633行设置了KVM_DEBUGREG_WONT_EXIT这个flag。

在VM-enter的时候,我们可以看到,在arch/x86/kvm/x86.c的vcpu_enter_guest()函数中:

6601         if (unlikely(vcpu->arch.switch_db_regs)) {
6602                 set_debugreg(0, 7);
6603                 set_debugreg(vcpu->arch.eff_db[0], 0);
6604                 set_debugreg(vcpu->arch.eff_db[1], 1);
6605                 set_debugreg(vcpu->arch.eff_db[2], 2);
6606                 set_debugreg(vcpu->arch.eff_db[3], 3);
6607                 set_debugreg(vcpu->arch.dr6, 6);
6608                 vcpu->arch.switch_db_regs &= ~KVM_DEBUGREG_RELOAD;
6609         }
6610
6611         kvm_x86_ops->run(vcpu);

 

因为在退出的时候在 switch_db_regs设置了 KVM_DEBUGREG_WONT_EXIT标志,所以6601行的条件可以满足。虽然DR7是由guest自己维护的,但在6602行还是将它清零了,这是因为在6603~6607行要将guest的debug registers恢复过来,这样就会debug 到错误的地址。当guest再一次退出的时候,在同一个函数中可以看到:

6619         if (unlikely(vcpu->arch.switch_db_regs & KVM_DEBUGREG_WONT_EXIT)) {
6620                 int i;
6621
6622                 WARN_ON(vcpu->guest_debug & KVM_GUESTDBG_USE_HW_BP);
6623                 kvm_x86_ops->sync_dirty_debug_regs(vcpu);
6624                 for (i = 0; i < KVM_NR_DB_REGS; i++)
6625                         vcpu->arch.eff_db[i] = vcpu->arch.db[i];
6626         }

Intel平台的 sync_dirty_debug_regs() callback是位于arch/x86/kvm/vmx.c 的vmx_sync_dirty_debug_regs()函数,代码如下:

 5661 static void vmx_sync_dirty_debug_regs(struct kvm_vcpu *vcpu)
5662 {
5663         u32 cpu_based_vm_exec_control;
5664
5665         get_debugreg(vcpu->arch.db[0], 0);
5666         get_debugreg(vcpu->arch.db[1], 1);
5667         get_debugreg(vcpu->arch.db[2], 2);
5668         get_debugreg(vcpu->arch.db[3], 3);
5669         get_debugreg(vcpu->arch.dr6, 6);
5670         vcpu->arch.dr7 = vmcs_readl(GUEST_DR7);
5671
5672         vcpu->arch.switch_db_regs &= ~KVM_DEBUGREG_WONT_EXIT;
5673
5674         cpu_based_vm_exec_control = vmcs_read32(CPU_BASED_VM_EXEC_CONTROL);
5675         cpu_based_vm_exec_control |= CPU_BASED_MOV_DR_EXITING;
5676         vmcs_write32(CPU_BASED_VM_EXEC_CONTROL, cpu_based_vm_exec_control);
5677 }

5665~ 5670行将guest的debug registers保存起来,并在5672清除了标志,最后5674~ 5676行设置捕捉DR-access。

好了,到这里已经看完了debug registers访问的模拟,再下来如何实现debug的功能。

3.2.2)debug功能的实现

如果guest没有enable debug register (在DR7中没有设置相关的enable位),只需要满足模拟access debug registers就可以了,否则,就必须要把guest的debug寄存器信息加载到硬件。所以,在设置DR7的时候有这样的处理, 在arch/x86/kvm/x86.c中:

 839 static void kvm_update_dr7(struct kvm_vcpu *vcpu)
840 {
841         unsigned long dr7;
842
843         if (vcpu->guest_debug & KVM_GUESTDBG_USE_HW_BP)
844                 dr7 = vcpu->arch.guest_debug_dr7;
845         else
846                 dr7 = vcpu->arch.dr7;
847         kvm_x86_ops->set_dr7(vcpu, dr7);
848         vcpu->arch.switch_db_regs &= ~KVM_DEBUGREG_BP_ENABLED;
849         if (dr7 & DR7_BP_EN_MASK)
850                 vcpu->arch.switch_db_regs |= KVM_DEBUGREG_BP_ENABLED;
851 }

在850行可以看到,如果debug register被enable, 就会往switch_db_regs中设置 KVM_DEBUGREG_BP_ENABLED标志。

我们回过头看看前面列出的代码,在cpu_enter_guest()的6601行,如果switch_db_regs不为零,则会恢复guest的DR registers。就这样,如果guest有enable debug。在每次进入到guest的时候都会去将环境恢复过来。

在这里有一个问题,只有第一次在访问debug register的时候才会有VM-exit,如果update DR7是在随后的指令中才执行的,那么KVM就捕获不到DR7的更新也就失去了更新 KVM_DEBUGREG_BP_ENABLED的时机。这是当前KVM的bug, 修正的patch在这里:
https://lkml.org/lkml/2016/2/26/420
修正的方法很简单,就是在没有捕获DR-access的情况下,vm-exit时都去check DR7是否有enable debug registers。

同时,debug功能需要模拟#DB和#BP,让guest自己去处理这两个异常就好了。

3.3)guest和hyperviosr同时使用debug

现在来看第二种情况,如果hypervisor和guest同时使用了debug功能。在这种情况下,guest和hypervisor是相互独立的,因此只需要在运行在非guest模式时,把debug恢复到hypervisor的状态就好了。所以,如果hypervisor有enable debug, 那么每次在vm-exit后就把debug register的值恢复过来。

在arch/x86/kvm/x86.c的vcpu_enter_guest()函数中,有这样的处理:

6628         /*
6629          * If the guest has used debug registers, at least dr7
6630          * will be disabled while returning to the host.
6631          * If we don’t have active breakpoints in the host, we don’t
6632          * care about the messed up debug address registers. But if
6633          * we have some of them active, restore the old state.
6634          */
6635         if (hw_breakpoint_active())
6636                 hw_breakpoint_restore();

当前进程的debug registers信息保存在它运行的CPU的per-cpu变量里,因此KVM不需要去保存这些信息,直接从per-cpu变量中恢复即可。
在6635行check当前进程是否被debug,如果有,那么在6636行将其恢复过来。在每次vm-enter的时候,都会用VMCS中的DR7更新成guest的DR7,这就是在vm-enter和vm-exit的代码中没有显式地处理guest DR7的原因。

同样在这种情况下,让guest自己去handle #DB和#BP就好了。

3.4) KVM debugs guest

这种情况有一点复杂,这时KVM使用debug功能去对guest进行调试,也就是说guest在运行时,加载到硬件debug registers的值是由KVM来控制的。这时,不应该去应用 guest对debug registers的设置,但是需要满足guest对debug registers的访问。

3.4.1)debug registers的访问

因为guest不能更改硬件上的寄存器,所有guest对debug registers的更改都需要被截获并且存放到另一个区域。为了处理这种情况,KVM有两个保存debug registers的地方,一个用来存放guest对debug registers的设置,姑且称这个地方为guest-debug-storage, 另一个地方用来存放KVM设置的debug信息,称之为km-debug-storage。guest对debug registers的读取和更改都发生在guest-debug-storage中,而hypervisor配置下来的debug寄存器信息都会存放在km-debug-storage并且它最终会被加载到硬件寄存器里。

来看看KVM的代码是如何处理的。首先,在guest 访问debug registers的时候,都会被捕捉,相应处理的代码位于arch/x86/kvm/vmx.c的handle_dr()中:

 5621         if (vcpu->guest_debug == 0) {
……
5635         }
5636
5637         reg = DEBUG_REG_ACCESS_REG(exit_qualification);
5638         if (exit_qualification & TYPE_MOV_FROM_DR) {
5639                 unsigned long val;
5640
5641                 if (kvm_get_dr(vcpu, dr, &val))
5642                         return 1;
5643                 kvm_register_write(vcpu, reg, val);
5644         } else
5645                 if (kvm_set_dr(vcpu, dr, kvm_register_readl(vcpu, reg)))
5646                         return 1;
5647
5648         skip_emulated_instruction(vcpu);
5649         return 1;

5621行的分支我们在前面分析第一种情况的时候已经看过了。因为现在是kvm debugs guest的情况,因此该分支不满足,会执行5637行的操作。5638行对应的是读DR register的操作,另一个分支是写DR的操作。可以看到guest读写DR register是能过kvm_get_dr()和kvm_set_dr()来完成的。
kvm_get_dr()的代码如下:

 901 int kvm_get_dr(struct kvm_vcpu *vcpu, int dr, unsigned long *val)
902 {
903         switch (dr) {
904         case 0 … 3:
905                 *val = vcpu->arch.db[dr];
906                 break;
907         case 4:
908                 /* fall through */
909         case 6:
910                 if (vcpu->guest_debug & KVM_GUESTDBG_USE_HW_BP)
911                         *val = vcpu->arch.dr6;
912                 else
913                         *val = kvm_x86_ops->get_dr6(vcpu);
914                 break;
915         case 5:
916                 /* fall through */
917         default: /* 7 */
918                 *val = vcpu->arch.dr7;
919                 break;
920         }
921         return 0;
922 }

看到这里就明白了,guest-debug-storage就是vcpu→arch.db[0,1,2,3],vcpu→arch.dr6和vcpu→arch.dr7。可以通过kvm_set_dr()再次验证一下,guest对debug的更改也会被反映到这些区域当中。

需要注意到的是在这种情况下handle_dr()并没有disable对DR-access的捕获,因此每一次guest访问debug registers都会产生vm-exit。

接下来看一下从hypervisor下来的debug guest信息是怎么样处理的。KVM_SET_GUEST_DEBUG这个API最终会被kvm_arch_vcpu_ioctl_set_guest_debug()所处理。代码如下:

7181         if (vcpu->guest_debug & KVM_GUESTDBG_USE_HW_BP) {
7182                 for (i = 0; i < KVM_NR_DB_REGS; ++i)
7183                         vcpu->arch.eff_db[i] = dbg->arch.debugreg[i];
7184                 vcpu->arch.guest_debug_dr7 = dbg->arch.debugreg[7];
7185         } else {
7186                 for (i = 0; i < KVM_NR_DB_REGS; i++)
7187                         vcpu->arch.eff_db[i] = vcpu->arch.db[i];
7188         }
7189         kvm_update_dr7(vcpu);

7181行用来处理hypervisor用hardware breakpoint来debug guest的情况。可以看到,从hypervisor下来的debug配置信息都会存放到 vcpu->arch.eff_db[0,1,2,3]和 vcpu->arch.guest_debug_dr7中。可能有人会有疑问,为什么没有对DR6的设置呢?这是因为DR6只是个状态寄存器,不会影响到debug功能。
至此可以看到,km-debug-storage是 vcpu->arch.eff_db[]和 vcpu->arch.guest_debug_dr7。

所以,guest对debug registers的操作与kvm debugs guest的操作互相不影响。接下来看看如何去实现debug功能的。

3.4.2)debug功能的现实

这要从上面所列代码(kvm_arch_vcpu_ioctl_set_guest_debug()函数)的7189行说起。即在KVM_SET_GUEST_DEBUG处理的最后,会调用kvm_update_dr7()来更新DR7。事实上这个函数在前面已经列出来过。在这里再回头来看看:

 839 static void kvm_update_dr7(struct kvm_vcpu *vcpu)
840 {
841         unsigned long dr7;
842
843         if (vcpu->guest_debug & KVM_GUESTDBG_USE_HW_BP)
844                 dr7 = vcpu->arch.guest_debug_dr7;
845         else
846                 dr7 = vcpu->arch.dr7;
847         kvm_x86_ops->set_dr7(vcpu, dr7);
848         vcpu->arch.switch_db_regs &= ~KVM_DEBUGREG_BP_ENABLED;
849         if (dr7 & DR7_BP_EN_MASK)
850                 vcpu->arch.switch_db_regs |= KVM_DEBUGREG_BP_ENABLED;
851 }

在843行可以看到,用来更新到DR7寄存器的值是来自于vcpu→arch.guest_debug_dr7, 也就是位于km-debug-storage中的值。并行在849~850可以看到,如果kvm有enable debug guest, 会将 switch_db_regs上的 KVM_DEBUGREG_BP_ENABLED位设置。根据前面分析可得知,在每次vm-enter时都会去reload guest debug register。再把相关的代码拿出来看看。

在vcpu_enter_guest()函数中:

6601         if (unlikely(vcpu->arch.switch_db_regs)) {
6602                 set_debugreg(0, 7);
6603                 set_debugreg(vcpu->arch.eff_db[0], 0);
6604                 set_debugreg(vcpu->arch.eff_db[1], 1);
6605                 set_debugreg(vcpu->arch.eff_db[2], 2);
6606                 set_debugreg(vcpu->arch.eff_db[3], 3);
6607                 set_debugreg(vcpu->arch.dr6, 6);
6608                 vcpu->arch.switch_db_regs &= ~KVM_DEBUGREG_RELOAD;
6609         }

所以,加载到硬件寄存器的都是从km-debug-storage中来的。注意DR7在上面列出的 kvm_update_dr7()函数中已经更新到了VMCS的dr7里面(通过kvm_x86_ops->set_dr7())。

对于#DB的处理就比较简单了,在这种情况下,只需要将#DB返回给hypervisor即可。

3.5)KVM_GUESTDBG_USE_SW_BP的实现

从前面的分析可以看到,KVM_SET_GUEST_DEBUG还可以控制KVM_GUESTDBG_USE_SW_BP。它的实现就比较简单了,只需要设置捕捉#BP(breakpoint exception), 并且在捕捉到#BP后将它返回给hypervisor就可以了。

3.6)KVM_GUESTDBG_SINGLESTEP的实现

同理,KVM_SET_GUEST_DEBUG也可以enable KVM_GUESTDBG_SINGLESTEP,它的实现也很简单,只需要在guest的RFLAGS寄存器一直设置TF位就可以了。就样进入guest后,guest每执行一条指令都会产生#BP,KVM再把#BP转发到hypervisor。

这种情况下,有一个side-effect就是,guest自己设置的TF位会被丢失,那就是说,如果guest自己设置了TF位后,再去读RFLAGS寄存器后会发现TF被莫名丢失了。

4)小结

本文分析了guest debug功能在KVM上的实现,因为debug registers的使用场景非常的复杂,所以其中用较多的篇幅来分析guest的debug registers以及它的functionality是如何在KVM上面实现的。这部份代码比较晦涩难懂,希望本文可以起一个提纲挈领的作用。

3 thoughts on “Guest Debug Emulation on KVM

  • by eric This is post author

    作为一个爱思考有节操的合格的社会主义接班人,在写这篇文章的时候,一边给自己注了一个备注:如果guest没有被debug,那么#DB不应该被捕获才对,那为何当前KVM一直都去捕获呢?

    正想欢快地修正掉这个问题以让它沿着共产主义的正确方向前进,查了以前的 changelog,发现了这原是为了一个修正资本主义入侵的严重错误:
    $ git show cbdb967af3d54993f5814f1cee0ed311a055377d
    commit cbdb967af3d54993f5814f1cee0ed311a055377d
    Author: Paolo Bonzini Date: Tue Nov 10 09:14:39 2015 +0100

    KVM: svm: unconditionally intercept #DB

    This is needed to avoid the possibility that the guest triggers
    an infinite stream of #DB exceptions (CVE-2015-8104).

    VMX is not affected: because it does not save DR6 in the VMCS,
    it already intercepts #DB unconditionally.

    这是一个可以由guest产生的拒绝服务攻击,具体来说就是如果CPU在 delivery 一个 exception 的时候如果再次产生相同的 exception,那么这个CPU就会一直做这样的死循环,也没有办法响应中断。在#DB的情况就是,如果设置了 debug 中断堆栈的内存,那么CPU在 delivery #DB 的时候就会再次产生 #DB。类似的异常还有#AC。

    这种情况在 native 的情况下并不是一个问题,由编程错误crash掉一台机器很合理,而在虚拟化情况就成了拒绝服务攻击,后果相当严重。

    修正的方法很简单,就是无条地去捕获这种类型的异常,让其退出guest从而有机会去响应中断,所以它可以为其它的guest继续提供服务。

    这里有一个链接可参考:http://www.openwall.com/lists/oss-security/2015/11/10/1

    摸了摸胸前的红领巾,原来无产阶级的成果是这么来之不易。不管怎么样,可以将它放一个段落了。

发表评论

电子邮件地址不会被公开。

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> 

*