简介
所谓中断窗口(Interrupt Window),就是指处理器能处理中断的时机。比如rflags.if
复位时,处理器就无法接收中断。
Intel上的中断窗口
在Intel VT-x上,VMCS中的Primary Processor-based VM-Execution Control字段直接提供了拦截中断窗口(Interrupt-Window Exiting)的位(见下图)。但在AMD-V中没有直接的相关设置。
所谓"no other blocking of interrupts",有两种情况:
- 处理器不活动:它可能处于关机(Shutdown)状态或者时等待SIPI(Startup Inter-Processor Interrupt)状态。注意这种情况下处理器会屏蔽外部中断,不位于中断窗口。当处理器因hlt指令停止运行时,若
rflags.if
置位,则可以接收中断,处于中断窗口中。
- 中断阴影之外:当处理器遇到
mov ss,xx
, pop ss
或者sti
指令时,会进入中断阴影(Interrupt Shadow)之中。中断阴影会屏蔽外部中断(External Interrupt),不可屏蔽中断(Non-Maskable Interrupt),以及调试陷阱(Debug Trap)。
AMD-V上拦截中断窗口
AMD-V没有直接提供拦截中断窗口的选项,但是它提供了更高级的虚拟local APIC的功能。详见VMCB的+0x060
的偏移处(如下图所示)。
- 第0-7位表示处理器的TPR,也就是
cr8
寄存器的作用。
- 第8位表示处理器是否有未接收的虚拟中断请求(Virtual Interrupt Request,简称
V_IRQ
)。
- 第9位表示处理器的虚拟GIF位。此位用于加速嵌套虚拟化。
- 第11位表示处理器是否有未接收的虚拟不可屏蔽中断。
- 第12位表示处理器是否处于屏蔽虚拟NMI的状态。
- 第16-19位表示
V_IRQ
的优先级。当优先级不大于TPR时,中断会被屏蔽。
- 第20位表示是否无视TPR。
- 第24位用于控制物理外部中断的屏蔽行为。
- 若此位置位,物理外部中断不受虚拟机的
rflags.if
位控制。
- 若此位复位,物理外部中断受虚拟机的
rflags.if
位控制。这种情况下,若虚拟机的rflags.if
复位,且陷入死循环中时,会使得物理处理器一并卡住,除非有物理NMI使处理器脱离死循环。这种情况往往是OS设置的基于NMI的看门狗启动了。
- 第25位表示是否启用虚拟GIF机制。此位用于加速嵌套虚拟化,此位若复位则第9位的设置无效。
- 第26位表示是否启用虚拟NMI机制。注意虚拟NMI是新机制,在Zen 4起才有。老AMD处理器需要自行模拟NMI的屏蔽性。此位若复位则第11和12位的设置无效。
- 第30位表示是否启用x2AVIC机制。x2AVIC相较于AVIC意义在于将处理器的APIC号从8位扩展至32位(GPU看了都表示够多了)。
- 第31位表示是否启用AVIC机制。AVIC即Advanced Virtual Interrupt Controller,可用于加速对APIC的虚拟化。即物理设备发送的虚拟中断以及虚拟IPI。
- 第32-39位表示
V_IRQ
的中断矢量号(Interrupt Vector),也就是具体投送到IDT/IVT的哪个中断矢量上。
此外,在VMCB的+0x00C
的偏移处,第4位可以拦截V_IRQ
。
这就好办了,我们可以将V_IRQ
和虚拟中断的拦截同时置位。如此一来,当VMM接到虚拟中断的拦截时,则表明此时虚拟处理器可以接收中断。
注意TPR也会屏蔽优先级小于等于TPR的中断。因此如果需要像Intel那样拦截中断窗口(无视TPR),则需要在虚拟local APIC字段中启用无视TPR的选项。
测试
测试内容基于NoirVisor提供的NoirCvmApi接口(暂时只支持64位Host下的AMD-V)。
咱就说Guest这一侧,其入口代码如下:
void guest_entry()
{
char a[]="Hello!\n";
init_idt();
__outbytestring(stdout_port,a,sizeof(a));
_enable();
__nop(); // The sti instruction causes an interrupt-shadow.
_disable();
__halt();
}
首先初始化IDT,其中设置两个外部中断的中断矢量。然后用rep outsb
指令对外输出一个"Hello"字符串。
然后用_enable
这个MSVC的内置宏让MSVC生成sti
指令来启用中断。注意sti
会产生一个中断阴影,因此后面加了个nop
指令来确保度过中断阴影,迎来中断窗口。
在现有的NoirVisor的设计中,中断窗口的拦截仅发生在注入一个外部中断之后,目的在于让VMM维持中断队列。因此guest_entry
里生成的中断窗口不会被拦截。但是VMM会在Guest开始之前便注入一个中断,故中断会发生在该中断窗口上。
中断处理函数内容如下:
void handle_true_interrupt()
{
char msg1[]="True interrupt...\n";
char msg2[]="Over...\n";
__outbytestring(stdout_port,msg1,sizeof(msg1));
_enable();
__nop(); // At this point, we should receive an interrupt.
_disable();
__outbytestring(stdout_port,msg2,sizeof(msg2));
}
可以看到它也会触发一次中断窗口。
在VMM一侧,在遇到第一次中断窗口的拦截时就会注入一次中断。该中断的处理函数如下:
void handle_false_interrupt()
{
char msg[]="False interrupt...\n";
__outbytestring(stdout_port,msg,sizeof(msg));
}
那么,理论而言,字符串的输出顺序就是:
True interrupt...
False interrupt...
Over...
运行结果如图所示:
可以看到两次中断窗口的拦截都发生在rip=0x604D
,可以用IDA看看这里有啥。
可以看出这个位置在第一个中断处理函数中,正好位于nop
之后。这是由于sti
会生成中断阴影,因此在nop
指令发生之前不会进入中断窗口。
而当第二个中断返回时,也会返回到这个位置,再次进入中断窗口,故再次触发中断。
总结
虽然AMD-V没有直接提供拦截中断窗口的方式,但是提供了更为高级的虚拟local APIC功能。
通过同时设置拦截虚拟中断并注入虚拟中断,可以实现拦截中断窗口的效果。
而且相比Intel VT-x,还可以实现相对于TPR的中断窗口。在Intel VT-x上,则需要自行拦截对cr8
的读写来模拟TPR。
本文的完整代码详见GitHub