唐凌 发表于 2022-12-26 07:17:47

【处理器】详解用户中断


# 前言
Intel手册更新了,出了一章叫做用户中断(User Interrupt)的东西。顾名思义,就是把中断直接发给用户模式,其理念可谓是越俎代庖,相当前卫了。
这么前卫的东西,Intel直接不给32位用了,我直接笑死。而且,兼容模式也用不了,必须是64位的程序才能用。
Linux已经推出User-Interrupt的支持了,目前主要用途其实也就是快速的通知其他用户线程罢了(据Benchmark,比Linux的`eventfd`快九倍,具体可以看[这份PPT](https://lpc.events/event/11/contributions/985/attachments/756/1417/User_Interrupts_LPC_2021.pdf))。
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,用于发送一个处理器间的中断。其格式为:
```Asm
senduipi reg64
```
只有一个寄存器操作数,恒定为64位。重置操作数大小的前缀(如`66h`)会被忽略。
这个操作数表示一个发送UIPI时UITT的索引值。注意,这不是用户中断的向量。
GCC11里添加了`_senduipi`这个内置宏来让C语言使用这条指令。
当发送的`UIPI`无效时,会抛出`#GP`异常。

### uiret指令
`uiret`即User-Interrupt Return,用于从用户中断处理中返回。其格式为:
```Asm
uiret
```
没有操作数,其用途就是返回到用户中断发生的地方恢复执行,包括了`rsp`,`rip`和`rflags`寄存器。
此外,`uiret`会强制置位`UIF`位以继续接收用户中断。
当用户态影栈机制被启用时,会从影栈里弹出发生中断位置的`rip`的值,并判断栈上的返回值。若不匹配,则产生`#CP`异常。

### clui指令
`clui`即Clear User Interrupt Flag,用于屏蔽用户中断。其格式为:
```Asm
clui
```
没有操作数,它会将`UIF`复位。

### stui指令
`stui`即Set User Interrupt Flag,用于解除对用户中断的屏蔽。其格式为:
```Asm
clui
```
没有操作数,它会将`UIF`置位。
但和`sti`性质相反的是:`sti`解除屏蔽时,要到`sti`的下一条指令完成后才会完成对中断屏蔽的解除,而`stui`指令会在该指令完成后立即解除中断。

### testui指令
`testui`即Test User Interrupt Flag,用于获取`UIF`的值。其格式为:
```Asm
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再来勘误。
总之目前来看,用户中断的应用仅限让硬件来加速事件型的线程间通信。外源性的用户中断支持还得等等。

Ayala 发表于 2022-12-27 12:58:49

其实介绍这么多,不如先来一段演示代码,然后解释每条指令其作用更能让人理解

唐凌 发表于 2022-12-27 21:35:44

Ayala 发表于 2022-12-27 12:58
其实介绍这么多,不如先来一段演示代码,然后解释每条指令其作用更能让人理解 ...

没有CPU,无法演示。

Golden Blonde 发表于 2023-1-9 22:49:16

这玩意会不会像SGX一样,过几代就消失?

唐凌 发表于 2023-1-10 02:54:00

Golden Blonde 发表于 2023-1-9 22:49
这玩意会不会像SGX一样,过几代就消失?

得有替代的玩意它才会消失。SGX没了是因为AMD的SEV比SGX设计的好于是Intel仿了个TDX出来。

yzw92 发表于 2023-1-31 08:24:57


感谢分享制作,
页: [1]
查看完整版本: 【处理器】详解用户中断