IDT、SYSCALL 与 FRED:三条内核入口,同一份 pt_regs?
最近在看 x86-64 怎么进内核,经常听到四句:硬件中断要查 IDT;syscall 直接用 MSR;进内核后都要构建 pt_regs;FRED 把上面这些都简化了。对照 SDM、FRED 规范和 Linux 源码以后,发现它们在默认路径上只对一半,不能直接套到 FRED 上。在这里把路径捋一下。
一、那四句话怎么说才靠谱
可以这样限定(后面各节展开源码和手册):
-
硬件中断 / CPU 异常:默认走 IDT;FRED 启用后走 FRED event delivery,不再查 IDT。
「查 IDT」指 CPU 按向量读一项门描述符(SDM Vol 3A 约 §6.14)1,不是内核扫整张表。Linux 启用 FRED 时会idt_invalidate(),注释写 Any further IDT use is a bug(fred.c)。之后按 type × vector 两级软件分发2,见fred_entry_from_user()。 -
64 位
syscall:默认经STAR/LSTAR/FMASK入核、不进 IDT;FRED 启用后同一条指令改由 FRED 投递,再落到do_syscall_64。
非 FRED 时入口 RIP 来自IA32_LSTAR13。Linux 在idt_syscall_init()把MSR_LSTAR写成entry_SYSCALL_64。规范 §3 / §7.4:CR4.FRED=1时SYSCALL/SYSENTER改用 FRED event delivery,返回走ERETU4。Linux 在fred_other()里识别FRED_SYSCALL后调do_syscall_64。int $0x80仍属中断类路径,别和 64 位syscall混谈。 -
C 层几乎都拿
pt_regs *,但帧怎么来的不同。
IDT 靠硬件 IRET 帧再软件补全;传统 SYSCALL 几乎全靠entry_SYSCALL_64手工 push;FRED 靠硬件 FRED 帧 +FRED_ENTER。结果同为struct pt_regs *,过程不能互换叙述。见ptrace.h。 -
FRED 统一的是事件投递与返回,不是删掉
pt_regs、MSR 或软件分发表。
规范 §3:原会走 IDT 的事件改走 FRED;SYSCALL/SYSENTER亦并入;返回用ERETS/ERETU4。Linux 仍保留扩展后的pt_regs、更多 FRED MSR(cpu_init_fred_exceptions())、以及sysvec_table。fred=on|off(on 为默认)见5。
压成一句:
非 FRED:中断/异常查 IDT,64 位
syscall走 MSR,两边都落到(不同构建方式的)pt_regs。** FRED:**统一投递与返回——IDT 作废、SYSCALL也进 FRED;pt_regs仍在,MSR 与软件分发仍在。
对照表:
| # | 常见说法 | 传统路径(IDT + SYSCALL) | FRED 路径 |
|---|---|---|---|
| 1 | 硬件中断要查 IDT | CPU 按向量查 IDT 投递 | idt_invalidate() 后改 FRED event delivery,软件按 type/vector 分发 |
| 2 | syscall 直接用 MSR | 64 位 syscall 用 LSTAR/STAR/FMASK,不进 IDT |
变为 FRED 事件(EVENT_TYPE_OTHER)→ do_syscall_64() |
| 3 | 进内核后都要构建 pt_regs |
C 层几乎都拿 struct pt_regs * |
仍有;帧含 fred_cs/fred_ss/edata 扩展 |
| 4 | FRED 简化了上面所有 | — | 统一投递与返回;不是删掉 IDT、MSR、pt_regs |
flowchart TB
subgraph Traditional["传统路径"]
IRQ1["外部 IRQ / 异常"] --> IDT["IDT 门描述符"]
SYS1["用户 syscall"] --> MSR["LSTAR / STAR / FMASK"]
IDT --> PT1["软件补全 pt_regs"]
MSR --> PT1
end
subgraph FredPath["FRED 路径"]
EVT["任意事件含 SYSCALL"] --> FRED["FRED event delivery"]
FRED --> DISP["type × vector 软件分发"]
DISP --> PT2["FRED 帧 + FRED_ENTER"]
end
二、默认路径:中断查 IDT,syscall 走 MSR
2.1 硬件中断 / CPU 异常 → IDT
不开 FRED 时,外部中断或 CPU 异常大致是:
- 中断控制器(APIC 等)给出向量号;
- CPU 用 IDTR → IDT[vector] 读门描述符;
- 硬件压 IRET 帧(SS、RSP、RFLAGS、CS、RIP;异常可能还有 error code);
- 若门描述符 IST ≠ 0,切换到 TSS 中对应 IST 栈。
「查 IDT」是 CPU 按向量索引一项门描述符,不是内核遍历整张表。SDM Vol 3A 大约 §6.141。
Linux 里外部 IRQ:
APIC/MSI → 向量 N
└─ IDT[N] → irq_entries_start + offset(idtentry.h)
└─ push 向量号(占 orig_ax 槽)→ asm_common_interrupt
└─ error_entry → PUSH_AND_CLEAR_REGS
└─ common_interrupt(regs, vector)(irq.c)
参见:idtentry.h · entry_64.S · irq.c
外部 IRQ 与 #PF/#DE 等共用 IDT 机制,但 stub 宏不同(idtentry_irq vs idtentry)。用户态进来的,常经 error_entry + sync_regs 落到线程内核栈上的标准 pt_regs 槽位。
2.2 64 位 SYSCALL → MSR(不进 IDT)
SYSCALL 不查 IDT。硬件把 RIP 换成 MSR_LSTAR、用 RCX/R11 保住返回契约、按 MSR_STAR 推导 CS/SS,并用 IA32_FMASK 掩 RFLAGS——不保存 RSP,也不压完整 IRET 帧13。
Linux 在每 CPU 初始化时把 MSR_LSTAR 指到 entry_SYSCALL_64:
// arch/x86/kernel/cpu/common.c:2268-2270
static inline void idt_syscall_init(void)
{
wrmsrq(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
入口汇编自己切栈、手工搭 pt_regs:
// arch/x86/entry/entry_64.S:87-121
SYM_CODE_START(entry_SYSCALL_64)
UNWIND_HINT_ENTRY
ENDBR
swapgs
/* tss.sp2 is scratch space. */
movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp
...
PUSH_AND_CLEAR_REGS rax=$-ENOSYS
...
call do_syscall_64 /* returns with IRQs disabled */
调用链:
用户 syscall 指令
└─ MSR_LSTAR → entry_SYSCALL_64(entry_64.S:87)
└─ swapgs;用户 RSP → TSS.sp2;RSP ← cpu_current_top_of_stack
└─ 手工 push 用户态部分 + PUSH_AND_CLEAR_REGS
└─ do_syscall_64(regs, nr)(syscall_64.c)
可以看到,MSR 只负责跳到入口;完整 pt_regs 由 entry_SYSCALL_64 软件搭建。这里的 TSS.sp2 只是 scratch,用来暂存用户 RSP——不是 IDT 那套「硬件按 TSS.RSP0 / IST 自动切栈」(见 §4.7)。
其他系统调用入口仍走 IDT:int $0x80(向量 0x80)、ia32 sysenter 等。
2.3 传统路径对照
| 维度 | IDT(IRQ / 异常 / int 0x80) | 64 位 SYSCALL |
|---|---|---|
| 硬件查表 | IDT[vector] | MSR_LSTAR 等 |
| 硬件压栈 | IRET 帧(± error code) | 仅 RCX/R11 等,无完整 IRET 帧 |
| 软件压栈 | error_entry + PUSH_AND_CLEAR_REGS |
entry_SYSCALL_64 手工 push + PUSH_AND_CLEAR_REGS |
| 典型 C 入口 | common_interrupt / exc_* |
do_syscall_64 |
| 返用户 | 多为 iretq |
sysretq 快路径或 iretq 慢路径 |
两边进 C 之前,常见路径都会走 PUSH_AND_CLEAR_REGS(calling.h):IDT 多经 error_entry,SYSCALL 在 entry_SYSCALL_64 里直接调;走 IST 的 #DF/NMI 等也常经 paranoid_entry,里面同样调这个宏。宏主要是补通用寄存器并清掉;前面往往还有硬件压栈或手工 push,不是单靠它从零搭出整帧。
关于 TSS.RSP0:不是「凡中断/int 都读一遍 sp0」。只有 用户态→ring 0 且门上 IST=0 时硬件才用它切栈;IST≠0 用 TSS.IST[];已在 ring 0 通常不切 RSP0。Linux 上 RSP0 还多半是 per-CPU entry trampoline,不是该 process 的线程内核栈(§4.7)。
sequenceDiagram
participant U as 用户态
participant CPU as CPU
participant K as 内核入口
Note over U,K: IDT 路径
U->>CPU: 异常或外部 IRQ
CPU->>CPU: 查 IDT 门, 压 IRET 帧
CPU->>K: 跳门描述符入口
K->>K: error_entry / PUSH_AND_CLEAR_REGS
Note over U,K: SYSCALL 路径
U->>CPU: syscall
CPU->>CPU: 读 LSTAR 等 MSR, 不压栈帧
CPU->>K: entry_SYSCALL_64
K->>K: 切栈并手工构造 pt_regs
三、pt_regs:结果相同,构建方式不同
三条路径的 C ABI 目标一致:handler 收到 struct pt_regs *。但谁压什么不同:
| 路径 | 硬件贡献 | 软件贡献 |
|---|---|---|
| IDT IRQ/异常 | SS/RSP/RFLAGS/CS/RIP(+ error code) | PUSH_AND_CLEAR_REGS;用户态常 sync_regs 到线程栈 |
| SYSCALL | 无 IRET 帧;RCX/R11 等由硬件改写 | 全程手工 push 用户态部分 + PUSH_AND_CLEAR_REGS |
| FRED | FRED 栈帧(自 orig_ax 起,含 type/vector/edata) |
FRED_ENTER → PUSH_AND_CLEAR_REGS |
容易踩坑的说法:
- 「syscall 像中断一样由硬件压 IRET 帧」→ SYSCALL 不压完整帧。
- 「进了内核就有完整
pt_regs」→ 是入口汇编 +calling.h宏搭出来的,不是 C 结构体被硬件 memcpy。 - 「IRQ 向量在某个专用寄存器」→ stub 用
push imm8占位orig_ax,C 层再解释为向量号。 - 「FRED 不用
pt_regs」→ 仍用;只是布局扩展了。 - 「IST /
#DF不用pt_regs」→ 仍要建。IST 只决定压到哪条栈;#DF虽常已在内核,仍经入口补pt_regs再进exc_double_fault等。 - 「
TSS.RSP0= 该 userspace process 的内核栈」→ Linux 上通常是 entry 栈,线程栈靠后续软件迁过去。
一句话用途:pt_regs 在内核栈上保存一次进核时的寄存器现场,供中断/异常/系统调用处理,并在返回时恢复。 硬件不认识这个 C 结构体;给 handler 用的完整视图由内核整理,IDT/FRED 可有硬件贡献字节。
ptrace.h 注释写明:FRED 栈帧从 orig_ax 开始,且始终带 error code;cs/ss 槽在 FRED 下可解读为 fred_cs / fred_ss:
// arch/x86/include/asm/ptrace.h:127-163
/*
* orig_ax is used on entry for:
* - the syscall number (syscall, sysenter, int80)
* - error_code stored by the CPU on traps and exceptions
* - the interrupt number for device interrupts
*
* A FRED stack frame starts here:
* 1) It _always_ includes an error code;
* ...
*/
unsigned long orig_ax;
...
union {
u16 cs;
u64 csx;
struct fred_cs fred_cs;
};
...
union {
u16 ss;
u64 ssx;
struct fred_ss fred_ss;
};
后面 fred_entry_from_user(struct pt_regs *regs)、do_syscall_64(regs, …) 照旧吃这个指针。
四、FRED:统一了什么,没删掉什么
FRED(Flexible Return and Event Delivery)用事件投递 + ERETS/ERETU 替代传统 IDT + IRET42。规范目标包括:用更低延迟的转移替换 IDT 投递与 IRET;投递/返回时一次建全 supervisor/user 上下文。
启用后,凡原先会走 IDT 的事件改走 FRED event delivery;SYSCALL / SYSENTER 的既有操作也被替换为 FRED 投递。返回侧用 ERETU(回 ring 3)与 ERETS(仍在 ring 0)4。
统一的是进核/回用户的硬件机制,不是「syscall 也去查 IDT」。 IDT 会被作废。
4.1 Linux 如何启用;开关在 CR4.FRED
两项不要混成「都是编译期参数」:
| 类型 | 作用 | |
|---|---|---|
CONFIG_X86_FRED |
编译期 Kconfig(arch/x86/Kconfig) |
内核是否编进 FRED 代码;depends on X86_64 |
fred=on\|off |
引导 cmdline5 | 开机是否启用;on 为默认。解析在 cpu_parse_early_param():仅 fred=off 会 setup_clear_cpu_cap(X86_FEATURE_FRED);on/默认无单独分支 |
真正打开硬件机制的是 CR4.FRED。启用顺序:写 FRED MSR → 置 CR4.FRED → idt_invalidate()(作废 IDT,再走 IDT 视为 bug)。
BSP 上常见调用链:setup_arch() → cpu_init_replace_early_idt() → cpu_init_fred_exceptions();AP 在 cpu_init() 里再配一遍。MSR = Model Specific Register(型号相关寄存器),按编号用 RDMSR/WRMSR 访问。wrmsrq()(asm/msr.h)是内核「写 64 位到某 MSR」的封装,底层就是 WRMSR。
// arch/x86/kernel/fred.c:28-69
void cpu_init_fred_exceptions(void)
{
...
wrmsrq(MSR_IA32_FRED_CONFIG,
FRED_CONFIG_REDZONE |
FRED_CONFIG_INT_STKLVL(0) |
FRED_CONFIG_ENTRYPOINT(asm_fred_entrypoint_user));
...
cr4_set_bits(X86_CR4_FRED);
/* Any further IDT use is a bug */
idt_invalidate();
/* Use int $0x80 for 32-bit system calls in FRED mode */
setup_clear_cpu_cap(X86_FEATURE_SYSFAST32);
setup_clear_cpu_cap(X86_FEATURE_SYSCALL32);
}
idt_invalidate() 把 IDTR 载成空表:
// arch/x86/kernel/idt.c:337-344
void idt_invalidate(void)
{
static const struct desc_ptr idt = { .address = 0, .size = 0 };
load_idt(&idt);
}
4.2 syscall / int 仍可用;换的是底层,不是禁指令
启用 FRED 以后,用户态照样可以执行 syscall / sysenter,以及 int n / int3 等——指令还能发,变的是 CPU 进核路径4:
| 指令 | FRED 打开后 |
|---|---|
syscall / sysenter |
仍可执行 → FRED event delivery(不再走旧 LSTAR/SYSENTER 快路径) |
int n / int3 等 |
仍可执行 → FRED event delivery(不再查 IDT;门上 DPL 检查改由软件按 type/vector 做) |
SYSRET / SYSEXIT |
执行会 #UD;内核出口必须改用 ERETU/ERETS |
返回指令全称:ERETU = Event Return to User(回 ring 3);ERETS = Event Return to Supervisor(仍在 ring 0)。用户态一般不发 SYSRET;换出口的是内核。
用户态调用约定(64 位)基本不变:仍发 syscall,调用号/RAX、参数寄存器约定照旧,最后仍常进 do_syscall_64。变的是 CPU/内核进核路径与返回指令。32 位:Linux 会清掉快路径 capability,多走 int $0x80。
4.3 FRED 寄存器:自有 MSR,并复用少量旧 MSR
规范:CR4.FRED = 0 才走 existing SYSCALL;= 1 则 FRED event delivery4。入口跳转不再靠 LSTAR,但 FRED 不是和传统 syscall「完全另一套、零重叠」——规范 §4.4 写明仍用若干已有 MSR4。
对 syscall 投递而言:
| MSR | FRED 启用后 |
|---|---|
IA32_LSTAR |
不参与入口跳转(入口在 FRED_CONFIG) |
IA32_FMASK |
不参与旧那套 RFLAGS 掩码路径 |
IA32_STAR |
仍用:投递 / ERETU 时装 CS、SS 时从 STAR 推导(§4.4)4 |
IA32_FRED_CONFIG 等 |
使用 |
FRED 配套状态(规范 §4.2–4.4;PDF 约第 13–15 页)4:
| 状态 | 作用 |
|---|---|
CPUID (EAX=7,ECX=1):EAX bit 17 / 18 |
枚举 FRED / LKGS(LKGS 可独立于 FRED) |
CR4.FRED(CR4 bit 32) |
总开关 |
IA32_FRED_CONFIG(0x1D4) |
入口页、CSL、red zone、ring0 可屏蔽中断栈级别等 |
IA32_FRED_RSP0~RSP3(0x1CC–0x1CF) |
四个 stack level 的栈指针(数字是栈级别,不是 ring) |
IA32_FRED_STKLVLS(0x1D0) |
向量 0–31(及 NMI)在 ring 0 至少升到哪一级栈 |
IA32_FRED_SSP1~SSP3(0x1D1–0x1D3)+ IA32_PL0_SSP(作 SSP0) |
shadow stack(开 CET 时) |
IA32_STAR |
见上,装 CS/SS |
IA32_KERNEL_GS_BASE |
ring3 投递 / ERETU 时交换 GS(替代软件 SWAPGS) |
与传统 syscall MSR 不是同一套:主用 FRED_* + CR4.FRED;重叠主要是 STAR(CS/SS) 和 GS 相关基址。内核启动时仍可能写过 LSTAR,但 FRED 已开的 CPU 上 syscall 进核不靠它跳。
CPU 定入口:ring 3 事件 RIP = FRED_CONFIG & ~0xFFF;ring 0 事件 = 该页基址 +256。Linux 把页对齐入口放在 asm_fred_entrypoint_user,+256 是 asm_fred_entrypoint_kernel。
4.4 事件分发:统一门口,处理在 C 里
跳进 asm_fred_entrypoint_user 之前,硬件已做完 FRED event delivery(切栈、压 FRED 帧、装 CS/SS/RIP、必要时换 GS)。入口只是软件侧的统一门口:
// arch/x86/entry/entry_64_fred.S:18-45
.macro FRED_ENTER
PUSH_AND_CLEAR_REGS
movq %rsp, %rdi /* %rdi -> pt_regs */
.endm
.macro FRED_EXIT
POP_REGS
.endm
SYM_CODE_START_NOALIGN(asm_fred_entrypoint_user)
FRED_ENTER
call fred_entry_from_user
FRED_EXIT
1: ERETU
FRED_ENTER:在硬件帧上补通用寄存器 →pt_regs *fred_entry_from_user()(entry_fred.c):按 type/vector 真正分发和处理FRED_EXIT:只做软件侧POP_REGS(弹出通用寄存器);真正出核靠后面的返回指令ERETU:硬件按 FRED 帧回 ring 3
这几行汇编是用户态事件的统一门口 + 出口,不是把 int/syscall/异常业务全做完——业务在 fred_entry_from_user 及其调用的 handler 里。内核里再发生的事件走 asm_fred_entrypoint_kernel(同页 +256),出口是 ERETS。
fred_entry_from_user() 按 regs->fred_ss.type 做第一级分发:
// arch/x86/entry/entry_fred.c:238-267
__visible noinstr void fred_entry_from_user(struct pt_regs *regs)
{
unsigned long error_code = regs->orig_ax;
regs->orig_ax = -1;
switch (regs->fred_ss.type) {
case EVENT_TYPE_EXTINT:
return fred_extint(regs);
case EVENT_TYPE_NMI:
...
case EVENT_TYPE_HWEXC:
return fred_hwexc(regs, error_code);
case EVENT_TYPE_SWINT:
return fred_intx(regs);
...
case EVENT_TYPE_OTHER:
return fred_other(regs);
default: break;
}
return fred_bad_type(regs, error_code);
}
| 事件类型 | 含义 | Linux 处理 |
|---|---|---|
EVENT_TYPE_EXTINT |
外部中断 | fred_extint → sysvec_table[] / common_interrupt |
EVENT_TYPE_HWEXC |
硬件异常 | fred_hwexc → exc_* |
EVENT_TYPE_NMI |
NMI | fred_exc_nmi |
EVENT_TYPE_OTHER |
含 SYSCALL | fred_other → do_syscall_64 |
| 软中断 / INT n 等 | fred_intx / fred_swexc 等 |
64 位 syscall 在 FRED 下的落点:
// arch/x86/entry/entry_fred.c:78-85
static __always_inline void fred_other(struct pt_regs *regs)
{
if (likely(regs->fred_ss.vector == FRED_SYSCALL && regs->fred_ss.l)) {
regs->orig_ax = regs->ax;
regs->ax = -ENOSYS;
do_syscall_64(regs, regs->orig_ax);
return;
}
...
}
规范写得很直白:当 CR4.FRED = 1 时,用 FRED event delivery 替换既有 SYSCALL 操作;返回应使用 ERETU 而非 SYSRET(FRED 下 SYSRET 为 #UD)4。
因此 FRED 下:
- 执行
syscall,或者来一个硬件中断——门口都是 FRED - 外部 IRQ:不再由 CPU 查 IDT,但仍按向量软件 dispatch(
sysvec_table/common_interrupt) - 64 位 SYSCALL:不再
LSTAR → entry_SYSCALL_64,改 FRED →fred_other→do_syscall_64 - IST:不再 IDT.IST + TSS.ist[],改
FRED_RSP0–3+FRED_STKLVLS
flowchart TD
A["事件发生"] --> B["FRED event delivery"]
B --> C["asm_fred_entrypoint_user"]
C --> D["fred_entry_from_user"]
D --> E{"fred_ss.type"}
E -->|EXTINT| F["fred_extint"]
E -->|HWEXC| G["fred_hwexc"]
E -->|OTHER| H["fred_other"]
E -->|NMI 等| I["对应 handler"]
H -->|FRED_SYSCALL| J["do_syscall_64"]
F --> K["sysvec_table 或 common_interrupt"]
内核文档把这套分发概括为两级:先按事件类型,再按向量2。
4.5 FRED 并没有「简化掉」的东西
| 机制 | FRED 后 |
|---|---|
pt_regs |
仍在;布局扩展(fred_cs/fred_ss,另有 fred_info.edata) |
| MSR | 更多(FRED_CONFIG、FRED_RSP*、FRED_STKLVLS) |
| 栈级别 / 独立栈 | IST 模型 → FRED stack level 模型 |
| 返回 | ERETS / ERETU 替代 IRET / SYSRET |
| SWAPGS | FRED 启用时非法(#UD);改 LKGS / 硬件 GS 交换42 |
价值在统一投递与返回语义(更低延迟、原子上下文、edata 替代 CR2/DR6 瞬态等),不是删除所有内核数据结构。
4.6 硬件统一了什么;软件仍要做什么
投递与返回这一层是硬件替代;内核仍要做 type × vector 软件分发。
| 原先(分轨) | FRED 启用后(硬件) |
|---|---|
IDT event delivery(中断/异常/INT n 等查门) |
FRED event delivery:不读 IDT,按 FRED_CONFIG 等 MSR 建上下文与栈帧 |
SYSCALL / SYSENTER 各自的 MSR 快路径 |
同一条 FRED event delivery(规范 §3 / §7.4)4 |
IRET / SYSRET / SYSEXIT 返回 |
ERETU(回 ring 3)/ ERETS(仍在 ring 0);FRED 下 SYSRET/SYSEXIT/SWAPGS 等为 #UD |
| IST(IDT 门 IST + TSS.ist[]) | stack level 0–3 + FRED_RSP0–3 / FRED_STKLVLS |
入核后常靠 SWAPGS 切 GS |
投递 / ERETU 时硬件交换 GS 基址;属性用 LKGS |
内核侧仍须软件完成:
- 两级分发:硬件只送到
asm_fred_entrypoint_*;再按fred_ss.type→fred_ss.vector pt_regs/ 通用寄存器补全:硬件给 FRED 帧;FRED_ENTER等再补齐- IDT 表本身:
idt_invalidate()后表不再参与投递——不是换一张 IDT,而是硬件换机制
一句话:FRED 用硬件统一「怎么进 ring 0 / 怎么回」(含原 IDT 事件 + SYSCALL/SYSENTER);「进核后调哪个 handler」仍是内核软件表。
不是任务切换。 FRED 管的是事件进核/返回(ring 切换),不是 schedule / switch_to 那种进程、线程上下文切换。调度路径照旧。
也可以说:进核之后相对统一(pt_regs → handler → 返回约定);复杂度主要在切换过程(IDT/RSP0/IST vs MSR vs FRED 投递与栈级别)。FRED 收的是后一层。
4.7 TSS.RSP0 / IST:只属于 IDT 路径;#DF 仍建 pt_regs
TSS.RSP0 / IST 是 IDT(中断/异常/INT n)投递时的硬件切栈机制,传统 64 位 syscall 不走:
| IDT 路径 | 64 位 SYSCALL |
|
|---|---|---|
| 是否查 IDT | 是 | 否 |
| 硬件切栈 | 用户→ring 0 且门 IST=0 → TSS.RSP0;IST≠0 → TSS.IST[] |
不按门做 RSP0/IST;硬件不保存、不切换 RSP |
| Linux 做法 | 常经 entry trampoline / sync_regs 等到线程栈 |
entry_SYSCALL_64:用户 RSP 可暂存 TSS.sp2(scratch),再接到 cpu_current_top_of_stack |
SYSCALL 入口里出现的 TSS 字段(如 sp2)只是内核拿 TSS 当 scratch,不是 IDT 那套「硬件按 RSP0/IST 自动切栈」。FRED 启用后,切栈改由 FRED stack level + FRED_RSP*,与旧 IST/RSP0 模型并列替代,而非「给 SYSCALL 补上 RSP0」。
IST 地址存在 TSS.IST[]:门上 IST 字段选槽,硬件从那里取栈顶。#DF 等即使用 IST、且常已在内核,仍会在应急栈上建 pt_regs(常见经 paranoid_entry → PUSH_AND_CLEAR_REGS),不是「内核里 fault 就跳过 pt_regs」。
五、FRED 省了哪些步骤;快在哪里
FRED 不是把 syscall / IDT 路径整段删掉,而是把「进核 / 回用户」换成另一套硬件原语。省下的是旧机制里若干段,不是 do_syscall_64 / common_interrupt 之后的内核业务。
5.1 相对 IDT:少做 / 换掉的
| 旧步骤 | FRED 怎么处理 |
|---|---|
| 读 IDTR、按向量取 IDT 门(内存查表) | 不查 IDT;按已配好的 FRED_CONFIG 等进入口 |
| 门上的类型 / DPL 等硬件检查 | 投递侧不做这套;INT n 等需 软件按 type/vector 自己拦4 |
TSS.RSP0 / IST 切栈 |
改用 stack level + FRED_RSP* |
| 压 IRET 帧(SS/RSP/RFLAGS/CS/RIP ± error code) | 压 FRED 帧(含 type/vector/edata 等,且始终带 error code 槽) |
入核后常要 SWAPGS |
投递 / ERETU 时 硬件交换 GS 基址 |
用 IRET 返回(语义重,且与无条件放开 NMI 等纠缠) |
用 ERETU / ERETS |
#PF 读 CR2、调试相关读 DR6 的瞬时窗口 |
附加信息进帧上的 edata2 |
5.2 相对传统 SYSCALL:少做 / 换掉的
| 旧步骤 | FRED 怎么处理 |
|---|---|
另一套 LSTAR/STAR/FMASK 快路径(与 IDT 并行) |
SYSCALL/SYSENTER 并入 FRED event delivery |
硬件 几乎不压栈,入口里 整段手工搭 pt_regs |
硬件先建 FRED 帧;软件主要补通用寄存器(FRED_ENTER) |
入/出核 swapgs + 自己切栈 |
GS / 栈级别多由 FRED 硬件处理 |
快路径 SYSRET,不行再退 IRET |
统一用 ERETU(FRED 下 SYSRET 为 #UD) |
do_syscall_64 本身还在,只是前面从 entry_SYSCALL_64 换成 fred_other → 同一条 C 分发。
5.3 并没有「省掉」的
pt_regs仍在(布局扩展)- 软件两级分发(type → vector)——原先 IDT「向量 → 门里的 RIP」有一部分改由软件做
- IRQ / 异常 / syscall 进各自 handler 仍在
PUSH_AND_CLEAR_REGS仍在(FRED_ENTER里也会调)- 配置更重:更多 FRED MSR
5.4 延迟:对标谁、别指望什么
规范与内核文档写明的目标是:用更低延迟的转移替换 IDT event delivery 和 IRET42。
| 对比对象 | 含义 |
|---|---|
| 相对 IDT + IRET | 预期赢家:少 IDT 门访存、用更轻的返回原语替代 IRET、少 SWAPGS/ESPFIX/CR2/DR6/NMI 边角软件。不查 IDT 是确定的;「因此一定更快」是设计意图,不是保证的定量结论。具体周期数依赖微架构与场景。 |
相对传统 SYSCALL+SYSRET |
不是主打「再砍一刀 syscall」。旧 syscall 本就不走 IDT;FRED 把 SYSCALL 收进统一投递,换来一致性和更完整硬件帧。纯 64 位 syscall 热路径上,旧 SYSRET 往往仍很强。 |
flowchart LR
subgraph Win["FRED 延迟目标主要对标"]
IDT["IDT 查门"] --> IRET["IRET 返回"]
end
subgraph Unify["统一进同一模型"]
SYS["SYSCALL / SYSENTER"]
IRQ["IRQ / 异常"]
end
IDT -.->|替换| FRED["FRED delivery + ERETU/ERETS"]
IRET -.->|替换| FRED
SYS --> FRED
IRQ --> FRED
一句话:快在「替代 IDT 查门 + 替代笨重 IRET(及连带 SWAPGS/IST/边角软件)」这类通用事件进/出;syscall 并入同一硬件投递,主要换一致性和完整性,延迟要和旧 SYSRET 路径分开评价。
六、三路总览
| 类型 | 传统:怎么进内核 | 传统:pt_regs | FRED:怎么进内核 | FRED:pt_regs |
|---|---|---|---|---|
| 外部 IRQ | IDT[vector] → irq stub | HW IRET 帧 + SW 补全 | FRED EXTINT → fred_extint |
HW FRED 帧 + FRED_ENTER |
| CPU 异常 #PF 等 | IDT → asm_exc_* |
同上 | FRED HWEXC → fred_hwexc |
同上;fault 地址用 fred_event_data(regs) |
| 64 位 syscall | MSR_LSTAR → entry_SYSCALL_64 |
全软件 push | FRED OTHER → fred_other → do_syscall_64 |
FRED 帧 + FRED_ENTER |
| int $0x80 | IDT 向量 0x80 | IDT 路径 | FRED 软件中断分支 | 依 FRED 实现 |
| #DF / NMI | IDT + IST | IST 栈 | FRED stack level 3/2 等 | FRED_RSP* |
七、容易踩的坑
- 「硬件中断 = 软件扫 IDT 表」 → CPU 按向量索引查一项门描述符1。
- 「syscall 完全靠 MSR,与中断无关」 → 传统 64 位对;
int 0x80仍走 IDT;FRED 下 syscall 是 FRED 事件4。 - 「两种方式进内核后 pt_regs 构建相同」 → 结果都是
pt_regs *,过程不同(HW IRET vs 全软件 push vs FRED 帧)。 - 「FRED 让 IDT、MSR、pt_regs 都消失」 / 「FRED 不用 pt_regs」 → 仅统一投递;
pt_regs仍在;启用后会idt_invalidate()(§4.1)。 - 「TSS.RSP0 / IST 也管 SYSCALL」 / 「RSP0 = 该进程内核栈」 → 否;传统
syscall不用 RSP0/IST;Linux 上 RSP0 常是 entry 栈(§4.7)。 - 「IST /
#DF不用 pt_regs」 → 仍建;IST 只换栈(§3、§4.7)。 - 「FRED 打开后就不能发
syscall/int」 → 指令仍可用;出口改ERETU,SYSRET/SYSEXIT为#UD(§4.2)。 - 「FRED 下完全不用旧 MSR」 →
LSTAR/FMASK不参与跳转/旧掩码;STAR仍用于 CS/SS;另有整套 FRED MSR(§4.3)。 - 「FRED 统一了任务切换」 → 否;统一的是事件进核/返回,不是
schedule(§4.6)。 - 「FRED 一定比旧路径更快」 → 相对 IDT+IRET 是设计目标;相对
SYSRET须分开评价(§5.4)。 - 「
CONFIG_X86_FRED和fred=都是编译选项」 → 前者编译期,后者引导参数(§4.1)。 - 「生产环境已在用 FRED」 → 需 CPU 支持 + 配置;可用
fred=off关闭5。
后续如果单独跟一遍 FRED 栈级别和 KVM 路径,再写一篇。
References
-
Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A — System Programming Guide, Part 1(约 §5.8.8
SYSCALL/SYSRET、§6.14 Interrupt 64-Bit Mode)。官方入口:https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html ↩ ↩2 ↩3 ↩4 ↩5 -
Linux 内核文档,
Documentation/arch/x86/x86_64/fred.rst— two-level dispatch;完整上下文;LKGS / SWAPGS ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
x86 Instruction Reference — SYSCALL —
RCX/R11、LSTAR、FMASK、不保存RSP等指令级语义 ↩ ↩2 -
Intel,Flexible Return and Event Delivery (FRED) Specification,346446-004US Rev 4.0(May 2023)。§3 Overview;§4.2–4.4(约第 13–15 页:
CR4.FRED、新 MSR、复用STAR/KERNEL_GS_BASE);§5 投递入口 RIP;§7.4:CR4.FRED=1时SYSCALL改 FRED,返回用ERETU(SYSRET/SYSEXIT为#UD) ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 -
Linux,
Documentation/admin-guide/kernel-parameters.txt—fred=on|off;on 为默认。解析:cpu_parse_early_param()↩ ↩2 ↩3
系列:Linux 内核
- IDT 与 SYSCALL:差异、演化、Linux 实现与性能
- Linux内核调度的时钟心跳:定时器中断、抢占与实时性的权衡
- 当Linux内核不再「迁就」PostgreSQL:一次抢占模型变更引发的性能风暴
- x86架构下的中断与异常处理:从IDT到FRED的演进之路
- IDT、SYSCALL 与 FRED:三条内核入口,同一份 pt_regs?