前言
Intel手册更新了,出了一章叫做用户中断(User Interrupt)的东西。顾名思义,就是把中断直接发给用户模式,其理念可谓是越俎代庖,相当前卫了。
这么前卫的东西,Intel直接不给32位用了,我直接笑死。而且,兼容模式也用不了,必须是64位的程序才能用。
Linux已经推出User-Interrupt的支持了,目前主要用途其实也就是快速的通知其他用户线程罢了(据Benchmark,比Linux的eventfd快九倍,具体可以看这份PPT)。
AMD暂时没有对User-Interrupt的支持,但早晚会有。
架构
Intel在支持用户中断的处理器上,新增了数个MSR寄存器,并定义了多个指令来支持用户中断。
在支持用户中断的处理器上,中断被分为:普通中断(Ordinary Interrupts)和用户中断(User Interrupt)。普通中断就是指普通的,发往内核经IDT接收的中断。
处理器的cr4寄存器的第25位被定义为UINTR位,置位则表示操作系统启用用户中断。
中断接收
用户中断不使用IDT来接,而是由且仅由MSR来指定地址。
发生用户中断时,处理器不更改cs段选择子,仅修改rsp和rip,因此CPL不会发生变化。
在支持用户中断的处理器上,多出了一个标识位,叫做UIF,即User-Interrupt Flag,表示处理器当前是否能接收用户中断。置位表示能接,复位表示不能接。
用户中断不使用IDT接收,而是一个专门定义的MSR寄存器。
除UIF置位外,接收用户中断还要满足以下条件:
- 处理器在中断阴影(Interrupt-Shadow,即
mov ss,xx, pop ss, sti等造成的一条指令下临时中断屏蔽)之外。
- 处理器位于用户模式,即
CPL=3。
- 处理器位于长模式,即
CS.L置位。也就是说即便处理是64位的系统里,32位程序也用不了。
- 处理器位于飞地(Enclave,即SGX保护区)之外。倒也正常,SGX基本上就要被Intel废了。
用户中断一般发生在一条指令完成的时候。但如果被打断的指令带有rep(z/nz)前缀,则中断会打断迭代,rip停留在这条带rep的指令之前,而rcx寄存器的值表示未完成的迭代次数,rsi,rdi寄存器则表示下一次迭代的线性地址。这样可以使得从用户中断返回时,继续完成迭代。
用户中断可以唤醒因tpause指令和umwait指令造成的处理器休眠状态。注意这两条指令均可以在用户态里使处理器休眠。
而hlt指令造成的休眠发生在内核态,故用户中断无法唤醒hlt状态下的处理器。
同理,由INIT信号造成的Wait-for-SIPI睡眠状态的处理器也无法被唤醒。
当用户中断发生时,UIF会被强制复位以防止用户中断重入。
当用户态影栈机制被启用时,用户中断会向影栈压入发生中断位置的rip的值以防止ROP攻击。
指令
Intel定义了五个新指令:senduipi, uiret, clui, stui, testui。
senduipi指令
senduipi即Send User Inter-Processor Interrupt,用于发送一个处理器间的中断。其格式为:
senduipi reg64
只有一个寄存器操作数,恒定为64位。重置操作数大小的前缀(如66h)会被忽略。
这个操作数表示一个发送UIPI时UITT的索引值。注意,这不是用户中断的向量。
GCC11里添加了_senduipi这个内置宏来让C语言使用这条指令。
当发送的UIPI无效时,会抛出#GP异常。
uiret指令
uiret即User-Interrupt Return,用于从用户中断处理中返回。其格式为:
uiret
没有操作数,其用途就是返回到用户中断发生的地方恢复执行,包括了rsp,rip和rflags寄存器。
此外,uiret会强制置位UIF位以继续接收用户中断。
当用户态影栈机制被启用时,会从影栈里弹出发生中断位置的rip的值,并判断栈上的返回值。若不匹配,则产生#CP异常。
clui指令
clui即Clear User Interrupt Flag,用于屏蔽用户中断。其格式为:
clui
没有操作数,它会将UIF复位。
stui指令
stui即Set User Interrupt Flag,用于解除对用户中断的屏蔽。其格式为:
clui
没有操作数,它会将UIF置位。
但和sti性质相反的是:sti解除屏蔽时,要到sti的下一条指令完成后才会完成对中断屏蔽的解除,而stui指令会在该指令完成后立即解除中断。
testui指令
testui即Test User Interrupt Flag,用于获取UIF的值。其格式为:
testui
没有操作数,它会把UIF的值写入RFLAGS.CF位中。因此需要结合setc,cmovc,jc或pushf+pop-reg64指令使用。
此外它还会将rflags的zf,af,of,pf,sf位复位。因此如果在testui后仍需要使用这些位,请先用pushf指令保存之。
MSR寄存器
Intel定义了六个新MSR寄存器:IA32_UINTR_RR (MSR-Index=0x985), IA32_UINTR_HANDLER (MSR-Index=0x986), IA32_UINTR_STACKADJUST (MSR-Index=0x987), IA32_UINTR_MISC (MSR-Index=0x988), IA32_UINTR_PD (MSR-Index=0x989) 和 IA32_UINTR_TT (MSR-Index=0x98A)
UIRR寄存器
User-Interrupt Request Register,即IA32_UINTR_RR (MSR-Index=0x985)这个MSR寄存器,是一个用来向当前处理器发送用户中断的MSR。
该寄存器是一个位图,置位的项表示要发出去的用户中断的向量号。比方说写入0x11这个值就表示发出去的用户中断的向量号是1和4。
处理器在准备发布用户中断时,就会向该MSR写入值。
UIHANDLER寄存器
User-Interrupt Handler,即IA32_UINTR_HANDLER (MSR-Index=0x986)这个MSR寄存器,表示在接收用户中断时,rip寄存器的值。
值得注意的是,如果写进去了一个超出处理器支持范围的合法线性地址,那就会报#GP异常。
UISTACKADJUST寄存器
User-Interrupt Stack Adjustment寄存器,即IA32_UINTR_STACKADJUST (MSR-Index=0x987)这个MSR寄存器,表示在接收用户中断时,处理器如何修改rsp寄存器的值。
| 位 |
含义 |
| 0 |
该位置位时,从该MSR里加载rsp寄存器的值 |
| 1-2 |
保留不用 |
| 3-63 |
用户中断处理函数的rsp的高61位;低3位复位以在8字节上对齐 |
虽然表格里说“低3位复位以在8字节上对齐”,但实则处理器会将低4位复位以在16字节上对齐。
UINV与UITTSZ寄存器
User-Interrupt Notification Vector和User-Interrupt Target Table Size寄存器以IA32_UINTR_MISC (MSR-Index=0x988)寄存器表达。这一点和普通中断的性质一样,栈地址会放在16字节对齐的位置上。
| 位 |
含义 |
| 0-31 |
该32位表达UITTSZ寄存器的值 |
| 32-39 |
该8位表达UINV的值 |
| 40-63 |
保留不用 |
UITTSZ表示UITT的最大索引值,也就是senduipi指令的操作数允许的最大值。
UINV表示用户中断的通知向量。
UITT寄存器
User-Interrupt Target Table寄存器,即IA32_UINTR_TT (MSR-Index=0x98A)这个MSR寄存器,表示UITT这张表的基址。
每个UITT表项占128位,结构如下:
| 位 |
含义 |
| 0 |
Valid位,表示是否有效。无效时,senduipi指令会抛出#GP异常 |
| 1-7 |
保留不用 |
| 8-15 |
用户中断向量 |
| 16-63 |
保留不用 |
| 64-127 |
UPID基址,但由于UPID需要按64字节对齐,故64-69位必须复位 |
UPID寄存器
User Posted-Interrupt Descriptor,即IA32_UINTR_PD (MSR-Index=0x989)这个MSR寄存器,用于描述如何发送和接收中断。
但UPID仅占16个字节,实在是不明白为什么要按64字节对齐。
| 位 |
含义 |
| 0 |
ON位,即Outstanding Notification位。若该位置位,则表示有一个或多个未完成的中断通知 |
| 1 |
SN位,即Suppress Notification位。若该位置位,则表示中断通知应当被按下不表 |
| 2-15 |
保留不用 |
| 16-23 |
NV位,即Notification Vector。该位表示通知目标处理器时使用的普通中断向量 |
| 24-31 |
保留不用 |
| 32-63 |
表示接收通知的逻辑处理器的x2APIC号。若处理器未启用x2APIC,则以40-47位表示APIC号 |
| 40-47 |
表示接收通知的逻辑处理器的APIC号。若处理器启用了x2APIC,则以32-63位表示x2APIC号 |
| 64-127 |
PIR位图,即Posted-Interrupt Request,置位则表示该用户中断的向量号存在中断请求 |
不论UITT还是UPID,原则上OS应当把它们放置在内核内存里。
但由于熔断漏洞(即CVE-2017-5715)会打破页表的用户内存和内核内存的隔离能力,OS的熔断漏洞补丁会为一个进程建立两套页表,第一套页表完整映射了内核内存和进程的用户内存,而第二套页表仅映射了进程的用户内存和一些用户与内核交界处(如系统调用函数,中断处理函数等开头)的内存。
用户态代码运行时,则使用第二套页表;进入内核时,则使用第一套页表。确保用户态的代码运行时,内核内存不被映射,这样就修复了熔断漏洞。
很明显,UITT和UPID应当在第二套页表里也有所映射。
扩展状态
最初的时候,扩展状态只有x87 FPU,因此用f(n)save和f(n)rstor指令来保存恢复FPU的状态即可。
后来出了SSE,于是新增了fxsave和fxrstor指令引用一个512字节的内存区域实现保存与恢复。
再后来则出现了AVX,512个字节塞不下AVX拓展出来的ymm寄存器,因此新增了xsave和xrstor指令来把所有ymm寄存器的高128位塞到后面去。
同时也意识到以后还会增强SIMD,不能再为此新增指令了,故xsave和xrstor指令带了一个隐含的操作数:edx:eax。这是一个64位的值,表示一个掩码。
当edx:eax & xcr0的某一位被置位时,则xsave和xrstor会保存恢复该扩展状态。比如当这个逻辑与运算的结果是7的时候,就保存恢复FPU(第0位置位),SSE(第1位置位)和AVX(第2位置位)的状态。
效果不错,以至于出了AVX-512的时候,zmm寄存器也能用xsave和xrstor指令来保存恢复。
但后来Intel出了Processor Trace,AMD出了Light-Weight Profiling,以及Control-Flow Enforcement,还有现在的User Interrupt这些新增了内核MSR的东西。
而xsave和xrstor在用户态就能用,允许这两条指令来保存恢复的话显然会越权。
但如果用rdmsr和wrmsr来一个一个保存恢复就效率太差了,故新增了xsaves和xrstors指令来保存恢复这些MSR。这两条指令仅在内核态可用。
为了xsaves和xrstors,处理器新增了Extended Supervisor State Mask,即IA32_XSS (MSR-Index=0xDA0)这个寄存器。
而xsaves和xrstors的逻辑变成了当edx:eax & (xcr0 | xss)的某一位置位时,保存恢复其状态。
其中的第14位就用于表示xsaves和xrstros指令会保存恢复用户中断的状态。这个状态包括新增的六个MSR的值,但不包括UIF位的值。
总而言之,操作系统可以借用xsaves和xrstors这两条指令来负责进线程的上下文切换。
用户中断的发送与接收的逻辑
先说UIPI吧。
执行senduipi时,处理器读取该指令指定的寄存器操作数,以该寄存器的值作为索引,访问UITT这张表中某一项。
如果这个索引超过了限制(即IA32_UINTR_MISC这个MSR中的UITTSZ),或者指定的UITT表项中的Valid位被复位,就会抛出#GP异常表示无法发送。
如果任意一个保留位(包括表项指定的UPID)被置位,也会抛出#GP异常。
senduipi会在UPID里的PIR域里,根据UITT表项的用户中断向量进行置位。(由此可以看出,多个UITT表项可以共用一个UPID)
若UPID的SN位和ON位均被复位,则将UPID的ON位置位,并且确认要发送通知。
注意以上的内存操作全部以内核模式权限进行的原子操作,即便senduipi这条指令是在用户态里完成的。
若确认了要发送通知,则以UPID的32-63位作为目标处理器的x2APIC号,并以UPID的NV位作为中断向量,发送普通IPI。
接下来是接收。当另一个处理器的Local APIC接收到一个外部中断时,会判断中断向量与接收者处理器上的IA32_UINTR_MISC的UINV是否相等。
若不等,则表示这就是纯粹一个外部中断,或者其他用途的IPI,处理器会走IDT来接收中断。
若相等,则表示这是个用户中断,此时接收者的Local APIC会自动发出EOI(即End-of-Interrupt)信号,表示普通中断已被处理。然后处理器走用户中断的专有途径来处理用户中断。
接收者识别出用户中断后,会接着对自己的UPID里的ON位进行一个复位操作,然后读取并清零UPID里的PIR位,并将读出来的PIR位以逻辑或写入UIRR寄存器中。
当前处理器里未处理的用户中断会反映在UIRR里。当处理器具备接收用户中断的条件时,若UIRR不为0,则产生用户中断。
硬件虚拟化
由于目前只有Intel提出了用户中断,故硬件虚拟化的相关支持目前也就只有Intel VT-x才有。
新增的VMCS字段
Intel在VMCS里新增了一个Guest UINV字段,该字段是一个16位Guest状态字段(注意UINV本身是8位的,但VMCS的最小粒度是16位)。
在VM-Entry控制字段里新增了Load UINV字段,表示VM-Entry时,是否根据VMCS里新增的Guest UINV字段加载UINV状态。
在VM-Exit控制字段里新增了Clear UINV字段,表示VM-Exit时,是否直接清掉UINV状态。
当Load UINV字段置位时,处理器在VM-Entry会检查Guest UINV字段的高八位是否为零
外部中断
若VMCS指定拦截外部中断,则当Local APIC接收到外部中断时,不识别其是否为用户中断,直接当成外部中断进行拦截并触发VM-Exit。
MSR位图
在MSR位图中拦截用户中断的MSR操作不会影响处理器本身访问这些MSR。就好比拦截LSTAR这个MSR的读取不影响处理器syscall指令的执行。
虚拟中断
当VMCS中启用了虚拟中断处理的时候,不由Local APIC处理接收到的中断,而是由Intel VT-x定义的虚拟中断处理方式来负责识别处理用户中断。其具体过程大同小异。
值得一提的是,事件注入的行为也会发生变化。当虚拟机管理器向开启了用户中断的vCPU注入一个外部中断时,处理器会根据向量号识别该中断是否为用户中断,并进行相关处理。
EPT
当用户中断的行为自身触发了EPT相关VM-Exit(如EPT-Violation)时,Exit Qualification字段的第16位会被置位,表示这是异步于指令执行的VM-Exit。
OS设计
本章简单谈一谈设计OS时,如何支持用户中断。
OS可以选定一个空闲的普通中断向量,该中断向量会被处理器视为用户中断处理。
OS可以给出一个系统调用的接口,用于创建删除“用户中断控制器”对象。
该对象有一个UPID,一个线程在接收中断:即一个中断处理函数和一个中断处理栈。
OS可以提供一个接口让线程注册一个中断处理函数(即IA32_UINTR_HANDLER),并注明是否使用隔离的栈,用多大的隔离栈(即IA32_UINTR_STACKADJUST)。
每个控制器只可注册一个线程的中断处理,除非逻辑上senduipi的接收者可以不是唯一的。
OS可以为每个线程创建一个UITT表,并提供注册发送器的系统调用。线程根据控制器注册了发送器后,系统会在UITT表上分配一个空闲的UITT项,填入控制器的UPID,并将该索引返回给线程。
线程就可以用senduipi指令来给别的线程发UIPI了,操作数里填注册发送器时系统返回的值即可。
在每个线程的扩展状态(即xsaves和xrstors管理的状态)里:
IA32_UINTR_RR, IA32_UINTR_HANDLER, IA32_UINTR_STACKADJUST, IA32_UINTR_TT是独立的,每个线程都各管各的。
IA32_UINTR_PD取决于线程接收用户中断的控制器。
IA32_UINTR_MISC由系统设定,一般所有线程均使用同一个中断向量号,否则管理普通中断的向量号会极为混乱。最大UITT的索引号可以视情况给予弹性设置。
在OS调度线程的时候,可以检测哪些非活动线程的UIRR不是零,然后优先调度到CPU上来处理用户中断。
最后别忘了用testui保存线程的UIF位并且用stui或clui恢复之。
目前还没有外部设备给处理器发送用户中断的文档。猜测如下:
- 配置中断控制器(注意是I/O APIC之类的硬件中断控制器)时,需要把中断向量配置为
UINV。
- 中断控制器需要能让IRQ选择
UPID,否则接收对象会存在多义性。
总结
其实我并没有搞到支持用户中断的CPU,本文描述的一切都是根据阅读手册来的。因此不可避免地会出现错误,以后搞到一块支持用户中断的CPU再来勘误。
总之目前来看,用户中断的应用仅限让硬件来加速事件型的线程间通信。外源性的用户中断支持还得等等。