IDT 与 SYSCALL:差异、演化、Linux 实现与性能
全文分三部分:
- IDT 与
SYSCALL的机制差异与历史脉络 - x86-64 Linux 上从
syscall指令到内核服务的执行路径(对照 SDM 与arch/x86) - 经 IDT 的入核与
SYSCALL入核在开销与实现上的对比
硬件叙述以 Intel Software Developer’s Manual(Volume 3A 等)为准,软件以 Linux 主线 arch/x86 为准;引用标号见文末 References。
主题一:IDT 与 SYSCALL 的区别与演化
1.1 谁在决定内核入口
- 异常、硬件中断、
INT n:CPU 用 IDT(Interrupt Descriptor Table) 按 向量号 取门描述符,再按架构规则完成特权级与栈等处理;OS 负责 填表 并用LIDT之类加载 IDTR。该路径与一组 MSR 配合编程的SYSCALL入核是两套并存机制12。 SYSCALL(64 位长模式下的系统调用主路径之一):CPU 根据IA32_STAR、IA32_LSTAR、IA32_FMASK等 MSR 切到 ring 0 并跳转到IA32_LSTAR指向的 RIP,不查 IDT34。
二者都是架构规定的入口协议,但针对的事件类别不同:前者服务 异步/异常类事件 的统一交付,后者服务 用户态主动发起的系统调用 的专用快速通道。
1.2 64 位模式下的 IDT 索引
在 64-bit / IA-32e 下,门描述符为 16 字节;向量 k 对应表项在 IDT 中的字节偏移为 k × 16(与 legacy 模式下 8 字节项不同)1。
手册在 64-bit mode IDT gate 处写道5:
In 64-bit mode, the IDT index is formed by scaling the interrupt vector by 16. The first eight bytes (bytes 7:0) of a 64-bit mode interrupt gate are similar but not identical to legacy 32-bit interrupt gates. The type field (bits 11:8 in bytes 7:4) is described in Table 3-2. The Interrupt Stack Table (IST) field (bits 4:0 in bytes 7:4) is used by the stack switching mechanisms described in Section 6.14.5, “Interrupt Stack Table.” Bytes 11:8 hold the upper 32 bits of the target RIP (interrupt segment offset) in canonical form.
1.3 对照表
| 特性 | 经 IDT 的路径 | SYSCALL 路径 |
|---|---|---|
| 典型触发 | 硬件中断、CPU 异常、INT n(含历史上的 int 0x80) |
用户态执行 syscall |
| 入口定位 | CPU 按向量查 IDT 门 | CPU 读 IA32_LSTAR 等 MSR |
| 门/MSR 语义 | 类型、DPL、IST、段选择子等 由 CPU 解释 | STAR/LSTAR/FMASK 组合,由 OS 预编程 |
| 是否使用 IDT | 是 | 否(本条目不讨论 FRED 等后续扩展) |
1.4 与“系统调用号 → 内核函数”的关系
抽象上都可说成 编号映射到处理逻辑:IDT 用 中断向量,系统调用用 RAX 中的调用号。
差别在于:IDT 的查表与跳转是 CPU 事件交付的一部分;而 RAX → __x64_sys_* 属于 内核在进入 do_syscall_64 之后的纯软件分发,处理器并不解析“系统调用号”的语义。
1.4.1 三条不同的“表/入口/快车道”
将机制分为以下三层(可与 §1.3、§3.2 对照阅读):
-
IDT(及经其投递的中断/异常/
INT n)
由 CPU 规定、面向全体异步与异常事件的 通用交付协议:功能全、约束多,不以“最短一次用户主动系统调用”为唯一优化目标12。 -
系统调用分发(软件)
Linux 仍保留sys_call_table[],方便 trace 等子系统解析符号地址;64 位主路径上则由x64_sys_call()的switch (nr)落到__x64_sys_*。无论数组还是switch,都属于syscall已经进核之后 的普通控制流,不是 CPU 替代的 IDT 查表6。 -
系统调用硬件快车道(
SYSCALL+ 若干 MSR)
入口RIP与CS/SS/RFLAGS掩码由STAR/LSTAR/FMASK(及EFER.SCE) 预编程;这是在 不进 IDT 的前提下完成的ring 3 → ring 0专用序列35。__x64_sys_*分发在这一硬件入核序列完成之后,才由do_syscall_64/x64_sys_call等以 普通内核控制流执行6。
1.5 一条简化的演化脉络(x86 / Linux 相关)
- 80386 及保护模式:IDT 与
INT n成为统一的异常/中断/软中断交付入口;内核通过设置向量 n 的门,把控制流交给对应处理例程。 - 32 位 Linux:用户态系统调用长期使用
int 0x80,即 CPU 查 IDT 向量 0x80 进入内核(仍属 IDT 路径)7。 - 约 Pentium II / Pro 一代:Intel 引入
SYSENTER/SYSEXIT,配合 MSR 提供另一条 不经 IDT 门描述符的 快速进核通道(Linux 在 32 位兼容路径等场景仍会碰到与SYSENTER/SYSCALL相关的入口约定)8。 - x86-64(AMD64 / Intel 64):架构在 长模式下提供
SYSCALL/SYSRET(由IA32_EFER.SCE等控制使能,细节以 SDM 为准)。64 位 Linux 用户态通常通过 glibc 等内联syscall,内核入口落在entry_SYSCALL_6439。 - 并存:今日 64 位内核仍可能为 32 位进程 保留
int 0x80/SYSENTER/ 兼容入口(向量与实现见内核头文件与entry_64_compat等);本文明细以 64 位syscall主线为主。
主题二:x86-64 Linux 上 syscall 从 CPU 到内核的完整机制
2.1 三层结构(总览)
- CPU(SDM):用户态约定
RAX=调用号、参数寄存器后执行syscall。硬件将RIP → RCX、RFLAGS → R11,按 MSR 加载CS/SS/RIP,并令RFLAGS <- RFLAGS & ~IA32_FMASK;不保存RSP、不向栈压帧。 - 内核入口
entry_SYSCALL_64(arch/x86/entry/entry_64.S):swapgs、切换到 per-CPU 内核栈,在栈上构造struct pt_regs,再call do_syscall_64。 - 分发与返回:
do_syscall_64→x64_sys_call的switch (nr)→ 各__x64_sys_*。返回时若满足契约则SYSRET,否则IRET。
对比 IDT 路径:IDT 处理「向量 → 硬件按门交付」;syscall 处理「寄存器约定 + MSR 指定 RIP → 软件补全栈帧再交付」。
2.1.1 SYSCALL 与 MSR:多寄存器协同,而非单一 LSTAR
MSR(Model Specific Register) 指通过 RDMSR/WRMSR 访问的 按编号独立编址 的一类寄存器;体系结构里与 SYSCALL 相关的常量名 IA32_STAR、IA32_LSTAR、IA32_FMASK 等各自对应不同 MSR 地址与语义。长模式下执行 SYSCALL 时,处理器按 IA32_EFER.SCE 判定该机制是否可用,再从 STAR/LSTAR/FMASK 读出 CS/SS、目标 RIP 与 RFLAGS 掩码35。
SDM 在 STAR/LSTAR/FMASK 布局处写明5:
See Figure 5-14 for the layout of IA32_STAR, IA32_LSTAR and IA32_FMASK.
并在同一节给出 RIP 取自 IA32_LSTAR、RFLAGS 与 IA32_FMASK 的组合关系(正文 §2.3 另有逐句引文)。
Linux 在 64 位内核引导路径中与上述分工对齐:syscall_init() 写 MSR_STAR(用户/内核段选择子约定),再调用 idt_syscall_init() 写 MSR_LSTAR(entry_SYSCALL_64)与 MSR_SYSCALL_MASK(对应 IA32_FMASK)10:
void syscall_init(void)
{
/* The default user and kernel segments */
wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);
if (!cpu_feature_enabled(X86_FEATURE_FRED))
idt_syscall_init();
}
static inline void idt_syscall_init(void)
{
wrmsrq(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
/* ia32_enabled() / SYSENTER_* / MSR_CSTAR 分支:见 common.c 全文 */
wrmsrq(MSR_SYSCALL_MASK,
X86_EFLAGS_CF|X86_EFLAGS_PF|X86_EFLAGS_AF|
X86_EFLAGS_ZF|X86_EFLAGS_SF|X86_EFLAGS_TF|
X86_EFLAGS_IF|X86_EFLAGS_DF|X86_EFLAGS_OF|
X86_EFLAGS_IOPL|X86_EFLAGS_NT|X86_EFLAGS_RF|
X86_EFLAGS_AC|X86_EFLAGS_ID);
}
内核里 MSR_SYSCALL_MASK 与手册 IA32_FMASK 对应同一类编程接口;idt_syscall_init() 在 MSR_LSTAR 与兼容路径 MSRs 之间的分支仍以 arch/x86/kernel/cpu/common.c 为准,§2.5 给出与当前主线一致的更长摘录。
从机制上概括:IA32_LSTAR 只给出 ring-0 入口 RIP;IA32_STAR 给出 SYSCALL/SYSRET 使用的 CS/SS 选择子场;IA32_FMASK 规定 RFLAGS 在进入时被清除的位;IA32_EFER.SCE 使能整条 SYSCALL/SYSRET 路径35。三颗 MSR 与总开关共同构成 SDM Figure 5-14 所描述的配置平面,操作系统需一并初始化,而不是仅写 LSTAR 一项。
2.1.2 长模式专用:SYSCALL 与 SYSRET —— 三颗 MSR 如何协同工作
一、核心概念:三个 MSR 各司其职
在 x86-64 长模式下,syscall 和 sysret 指令依赖三个 MSR(模型特定寄存器)来完成用户态到内核态、再回到用户态的完整流程。可以这样理解:
| MSR 寄存器 | 作用 | 类比 |
|---|---|---|
| IA32_STAR | 告诉 CPU:进入内核时用什么段(CS/SS),返回用户时用什么段 | 门禁卡的双重配置——进去刷A区,出来刷B区 |
| IA32_LSTAR | 告诉 CPU:内核的入口函数地址在哪里 | 紧急出口的指向标——从这里进内核 |
| IA32_FMASK | 告诉 CPU:进入内核时,RFLAGS 寄存器里哪些位要强制清零 | 安检过滤器——某些标志位不能带进内核 |
重要说明:本文只讨论 IA-32e 长模式下带
REX.W的syscall/sysret指令,不涉及IA32_CSTAR和SYSENTER/SYSEXIT等其他机制。
二、流程图:一条系统调用的完整旅程
下面这个流程图展示了从用户态执行 syscall 到内核处理再到返回用户态的完整过程。每个框里都注明了“此时谁在读/写哪个 MSR”。
sequenceDiagram
participant OS as 操作系统(启动时)
participant User as 用户态程序
participant CPU as CPU硬件
participant Kernel as 内核态代码
Note over OS: 操作系统启动时,预先配置 MSR
OS->>CPU: IA32_EFER.SCE = 1 (开启 syscall 支持)
OS->>CPU: IA32_STAR = 入核/出核的 CS/SS 选择子
OS->>CPU: IA32_LSTAR = 内核入口地址
OS->>CPU: IA32_FMASK = RFLAGS 清零掩码
Note over User: 用户态准备系统调用
User->>User: RAX = 系统调用号,参数存入 RDI/RSI/RDX/R10/R8/R9
User->>User: RSP 指向用户栈
User->>CPU: 执行 syscall 指令
Note over CPU: syscall 指令的硬件自动行为
CPU->>CPU: RCX = 用户态下一条指令的 RIP
CPU->>CPU: R11 = 用户态完整 RFLAGS
CPU->>CPU: RIP = IA32_LSTAR (读 MSR)
CPU->>CPU: CS/SS = IA32_STAR 入核位域
CPU->>CPU: RFLAGS = RFLAGS & (~IA32_FMASK) (按 FMASK 清零)
Note over CPU: 特权级从 Ring 3 切换到 Ring 0
CPU->>Kernel: 跳转到 LSTAR 指向的内核入口
Note over Kernel: 内核处理系统调用
Kernel->>Kernel: swapgs (切换到内核 GS)
Kernel->>Kernel: 手动切换 RSP 到内核栈
Kernel->>Kernel: 保存完整寄存器到内核栈 (形成 pt_regs)
Kernel->>Kernel: 根据 RAX 查 sys_call_table 分发
Kernel->>Kernel: 执行具体内核函数,返回值写入 RAX
Kernel->>Kernel: 恢复寄存器,准备返回
Kernel->>CPU: 执行 sysretq 指令
Note over CPU: sysret 指令的硬件自动行为
CPU->>CPU: CS/SS = IA32_STAR 出核位域
CPU->>CPU: RIP = RCX (恢复用户态返回地址)
CPU->>CPU: RFLAGS = R11 (恢复用户态标志位)
Note over CPU: 特权级从 Ring 0 切换回 Ring 3
CPU->>User: 跳转到用户态返回地址
Note over User: 继续执行,RAX 中为系统调用返回值
三、关键要点(避免踩坑)
1. syscall 不会自动切换 RSP
- 用户栈指针(RSP)不会被
syscall指令改变。 - 内核必须在入口代码中手动切换到内核栈(通常用
swapgs+ 写rsp)。 - 这意味着:RSP 的保存和恢复是软件的责任,硬件不管。
2. sysret 的“契约”
sysret指令假设:- RCX 中存放着用户态的返回地址(由
syscall自动保存)。 - R11 中存放着用户态的 RFLAGS(由
syscall自动保存)。
- RCX 中存放着用户态的返回地址(由
- 如果内核代码不小心破坏了 RCX 或 R11,就不能再用
sysret返回,必须改用iret路径。
3. 返回值约定
- 系统调用的返回值必须放在 RAX 中。
- 这是用户态和内核态的约定,
sysret不会动 RAX。
四、与 int 0x80 + IDT 路径的对比(可选扩展)
如果你想理解为什么这套机制比 int 0x80 快,可以这样对比:
| 动作 | int 0x80(老方法) |
syscall(新方法) |
|---|---|---|
| 保存返回地址 | 压栈(内存访问) | 存 RCX(寄存器) |
| 保存 RFLAGS | 压栈(内存访问) | 存 R11(寄存器) |
| 查找入口 | 查内存中的 IDT 表 | 读 MSR 寄存器(CPU 内部) |
| 切换栈 | 硬件自动切(TSS 机制) | 软件手动切(更灵活) |
| 保存段寄存器 | 硬件自动保存 5 个 | 根本不保存(因为用不上) |
| 返回指令 | iret(重量级) |
sysret(轻量级) |
核心结论:syscall 快,不是因为它“做的事少”,而是因为它“用寄存器代替了内存”,并且“去掉了历史包袱”。
在 syscall/sysret 机制中,最核心的 MSR 寄存器是以下三个:
核心三颗 MSR
| MSR 名称 | 地址 | 作用 | 读/写时机 |
|---|---|---|---|
| IA32_STAR | 0xC0000081 |
syscall/sysret 各自的 CS、SS 怎么取由 Figure 5-14 规定的不同位域决定(syscall 用入核场、sysret(长模式)用出核场;不是「高 32 位=内核段、低 32 位=用户段」这种对半分) |
操作系统启动时写入一次 |
| IA32_LSTAR | 0xC0000082 |
存储内核入口地址:syscall 指令执行后 RIP 跳转的目标 |
操作系统启动时写入一次 |
| IA32_FMASK | 0xC0000084 |
存储RFLAGS 掩码: 进入内核时,RFLAGS 中对应位被强制清零 |
操作系统启动时写入一次 |
辅助 MSR
还有一个前提条件相关的 MSR:
| MSR 名称 | 地址 | 作用 | 说明 |
|---|---|---|---|
| IA32_EFER | 0xC0000080 |
第 0 位(SCE 位)必须为 1 | 否则 syscall 指令会触发 #UD 异常 |
一句话总结
IA32_STAR管“段”(权限),IA32_LSTAR管“地址”(去哪),IA32_FMASK管“标志位”(环境),三颗 MSR 配合IA32_EFER.SCE开关,共同决定了syscall的完整行为。
2.2 端到端序列(示意)
sequenceDiagram
participant User as 用户态进程
participant CPU as CPU硬件
participant Kernel as Linux内核
User->>User: 1)RAX 系统调用号 nr,RDI RSI RDX R10 R8 R9 为 arg0 至 arg5
User->>CPU: 2)执行 syscall
CPU->>CPU: 3)RCX 存返回点 RIP,R11 存 RFLAGS
CPU->>CPU: 4)RIP 取 IA32_LSTAR,RFLAGS 按 IA32_FMASK 清零若干位
CPU->>Kernel: 5)进入 entry_SYSCALL_64
Kernel->>Kernel: 6)swapgs,切内核栈,推 pt_regs
Kernel->>Kernel: 7)do_syscall_64,x64_sys_call 按 nr 分发
Kernel->>Kernel: 8)写回 RAX 返回值或负 errno
Kernel->>Kernel: 9)可 SYSRET 则 SYSRET,否则 IRET
CPU->>User: 10)回到用户态,自 RCX 所指指令继续
与上图步骤对应的内核代码(linux/arch/x86)
序列图 1) 由用户态约定(glibc / vDSO 等内联 syscall,见 man syscall(2)9);2)–4) 为 CPU 根据 IA32_LSTAR/IA32_FMASK/IA32_STAR 的行为,内核侧在启动时写 MSR(idt_syscall_init() 等,见 §2.5 与 10)。自 5) 起按下述代码块列举,惯例与 /Users/weli/works/bootimage-example/LINUX_X86_64_ENTRY_AND_PT_REGS.md 一致:围栏第一行为 起始行:结束行:arch/…/文件(相对 linux/ 源码树根;本文行号依 /Users/weli/works/linux)。
5)–6)entry_SYSCALL_64(arch/x86/entry/entry_64.S) — IA32_LSTAR 指向此处:swapgs、装入 cpu_current_top_of_stack、pt_regs 布局压栈、PUSH_AND_CLEAR_REGS、movq %rsp,%rdi / movslq %eax,%rsi、call do_syscall_64。
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
SYM_INNER_LABEL(entry_SYSCALL_64_safe_stack, SYM_L_GLOBAL)
ANNOTATE_NOENDBR
/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
SYM_INNER_LABEL(entry_SYSCALL_64_after_hwframe, SYM_L_GLOBAL)
pushq %rax /* pt_regs->orig_ax */
PUSH_AND_CLEAR_REGS rax=$-ENOSYS
/* IRQs are off. */
movq %rsp, %rdi
/* Sign extend the lower 32bit as syscall numbers are treated as int */
movslq %eax, %rsi
/* clobbers %rax, make sure it is after saving the syscall nr */
IBRS_ENTER
UNTRAIN_RET
CLEAR_BRANCH_HISTORY
call do_syscall_64 /* returns with IRQs disabled */
7)do_syscall_64(前半)、do_syscall_x64、x64_sys_call(arch/x86/entry/syscall_64.c) — 与上引 112–114 行入参一致;合法系统调用号下 regs->ax 在 do_syscall_x64 → x64_sys_call 链上更新。
__visible noinstr bool do_syscall_64(struct pt_regs *regs, int nr)
{
add_random_kstack_offset();
nr = syscall_enter_from_user_mode(regs, nr);
instrumentation_begin();
if (!do_syscall_x64(regs, nr) && !do_syscall_x32(regs, nr) && nr != -1) {
/* Invalid system call, but still a system call. */
regs->ax = __x64_sys_ni_syscall(regs);
}
instrumentation_end();
syscall_exit_to_user_mode(regs);
static __always_inline bool do_syscall_x64(struct pt_regs *regs, int nr)
{
/*
* Convert negative numbers to very high and thus out of range
* numbers for comparisons.
*/
unsigned int unr = nr;
if (likely(unr < NR_syscalls)) {
unr = array_index_nospec(unr, NR_syscalls);
regs->ax = x64_sys_call(regs, unr);
return true;
}
return false;
}
#define __SYSCALL(nr, sym) case nr: return __x64_##sym(regs);
long x64_sys_call(const struct pt_regs *regs, unsigned int nr)
{
switch (nr) {
#include <asm/syscalls_64.h>
default: return __x64_sys_ni_syscall(regs);
}
}
8)__x64_sys_* 原型、sys_call_table[]、生成 syscalls_64.h — 各 __x64_sys_* 实现分布在 kernel/、fs/ 等;编号表 arch/x86/entry/syscalls/syscall_64.tbl,Kbuild 生成 arch/x86/include/generated/asm/syscalls_64.h($(out) 见下)。
#define __SYSCALL(nr, sym) extern long __x64_##sym(const struct pt_regs *);
#define __SYSCALL_NORETURN(nr, sym) extern long __noreturn __x64_##sym(const struct pt_regs *);
#include <asm/syscalls_64.h>
#define __SYSCALL(nr, sym) __x64_##sym,
const sys_call_ptr_t sys_call_table[] = {
#include <asm/syscalls_64.h>
};
# SPDX-License-Identifier: GPL-2.0
out := arch/$(SRCARCH)/include/generated/asm
uapi := arch/$(SRCARCH)/include/generated/uapi/asm
syscall32 := $(src)/syscall_32.tbl
syscall64 := $(src)/syscall_64.tbl
$(out)/syscalls_64.h: abis := common,64
$(out)/syscalls_64.h: $(syscall64) $(systbl) FORCE
$(call if_changed,systbl)
9)–10)SYSRET 快路径与 IRET 慢路径 — do_syscall_64 末尾 return true 且 entry_SYSCALL_64 中 testb %al,%al 成功则 sysretq;否则 jmp / jz 汇入 swapgs_restore_regs_and_return_to_usermode 后经 iretq。
/*
* Check that the register state is valid for using SYSRET to exit
* to userspace. Otherwise use the slower but fully capable IRET
* exit path.
*/
/* XEN PV guests always use the IRET path */
if (cpu_feature_enabled(X86_FEATURE_XENPV))
return false;
/* SYSRET requires RCX == RIP and R11 == EFLAGS */
if (unlikely(regs->cx != regs->ip || regs->r11 != regs->flags))
return false;
/* CS and SS must match the values set in MSR_STAR */
if (unlikely(regs->cs != __USER_CS || regs->ss != __USER_DS))
return false;
if (unlikely(regs->ip >= TASK_SIZE_MAX))
return false;
if (unlikely(regs->flags & (X86_EFLAGS_RF | X86_EFLAGS_TF)))
return false;
/* Use SYSRET to exit to userspace */
return true;
/*
* Try to use SYSRET instead of IRET if we're returning to
* a completely clean 64-bit userspace context. If we're not,
* go to the slow exit path.
* In the Xen PV case we must use iret anyway.
*/
ALTERNATIVE "testb %al, %al; jz swapgs_restore_regs_and_return_to_usermode", \
"jmp swapgs_restore_regs_and_return_to_usermode", X86_FEATURE_XENPV
/*
* We win! This label is here just for ease of understanding
* perf profiles. Nothing jumps here.
*/
syscall_return_via_sysret:
IBRS_EXIT
POP_REGS pop_rdi=0
/*
* Now all regs are restored except RSP and RDI.
* Save old stack pointer and switch to trampoline stack.
*/
movq %rsp, %rdi
movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp
UNWIND_HINT_END_OF_STACK
pushq RSP-RDI(%rdi) /* RSP */
pushq (%rdi) /* RDI */
/*
* We are on the trampoline stack. All regs except RDI are live.
* We can do future final exit work right here.
*/
STACKLEAK_ERASE_NOCLOBBER
SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi
popq %rdi
popq %rsp
SYM_INNER_LABEL(entry_SYSRETQ_unsafe_stack, SYM_L_GLOBAL)
ANNOTATE_NOENDBR
swapgs
CLEAR_CPU_BUFFERS
sysretq
SYM_CODE_START_LOCAL(common_interrupt_return)
SYM_INNER_LABEL(swapgs_restore_regs_and_return_to_usermode, SYM_L_GLOBAL)
IBRS_EXIT
#ifdef CONFIG_XEN_PV
ALTERNATIVE "", "jmp xenpv_restore_regs_and_return_to_usermode", X86_FEATURE_XENPV
#endif
#ifdef CONFIG_MITIGATION_PAGE_TABLE_ISOLATION
ALTERNATIVE "", "jmp .Lpti_restore_regs_and_return_to_usermode", X86_FEATURE_PTI
#endif
STACKLEAK_ERASE
POP_REGS
add $8, %rsp /* orig_ax */
UNWIND_HINT_IRET_REGS
.Lswapgs_and_iret:
swapgs
CLEAR_CPU_BUFFERS
/* Assert that the IRET frame indicates user mode. */
testb $3, 8(%rsp)
jnz .Lnative_iret
ud2
.Lnative_iret:
UNWIND_HINT_IRET_REGS
/*
* Are we returning to a stack segment from the LDT? Note: in
* 64-bit mode SS:RSP on the exception stack is always valid.
*/
#ifdef CONFIG_X86_ESPFIX64
testb $4, (SS-RIP)(%rsp)
jnz native_irq_return_ldt
#endif
SYM_INNER_LABEL(native_irq_return_iret, SYM_L_GLOBAL)
ANNOTATE_NOENDBR // exc_double_fault
/*
* This may fault. Non-paranoid faults on return to userspace are
* handled by fixup_bad_iret. These include #SS, #GP, and #NP.
* Double-faults due to espfix64 are handled in exc_double_fault.
* Other faults here are fatal.
*/
iretq
从 entry_SYSCALL_64 经 ALTERNATIVE 失败分支也会落到 swapgs_restore_regs_and_return_to_usermode,最终 iretq(上引 559–580、640–659 行;完整标签关系见 11)。
本地树路径:/Users/weli/works/linux(与主线 torvalds/linux 同源时行号一致;若你本地的 fork 有差异,以 git blame / 实际文件为准。)
2.3 CPU 侧(与 Vol.3A §5.8.8 等一致)
RIP(下一条指令)→RCX;RFLAGS→R113。RIP来自IA32_LSTAR;CS/SS的选择子与IA32_STAR的位域布局按 SDM Figure 5-143。RFLAGS <- RFLAGS & ~IA32_FMASK。Linux 在arch/x86/kernel/cpu/common.c的idt_syscall_init()中向MSR_SYSCALL_MASK写入含X86_EFLAGS_IF等位,使进入内核后IF通常被清除310。SYSCALL不改变RSP;SYSRET也不恢复RSP,栈由内核显式管理34。
同一节(§5.8.8)对 SYSCALL/SYSRET 的英文原文可对照如下5:
For SYSCALL, the processor saves RFLAGS into R11 and the RIP of the next instruction into RCX; it then gets the privilege-level 0 target code segment, instruction pointer, stack segment, and flags as follows:
Target instruction pointer — Reads a 64-bit address from IA32_LSTAR. (The WRMSR instruction ensures that the value of the IA32_LSTAR MSR is canonical.)
Flags — The processor sets RFLAGS to the logical-AND of its current value with the complement of the value in the IA32_FMASK MSR.
The SYSCALL instruction does not save the stack pointer, and the SYSRET instruction does not restore it. It is likely that the OS system-call handler will change the stack pointer from the user stack to the OS stack. If so, it is the responsibility of software first to save the user stack pointer.
(手册在「gets the … as follows」之后对 Target code segment、Stack segment 等另有逐条说明,此处摘入与 LSTAR/FMASK 及 RSP 最直接相关的句子;完整列举见 1 中 §5.8.8 与 Figure 5-14。)
2.4 Linux 侧(源码锚点)
| 内容 | 文件与要点 |
|---|---|
STAR/LSTAR/SYSCALL_MASK 初始化 |
arch/x86/kernel/cpu/common.c:syscall_init()、idt_syscall_init() |
| 入口汇编 | arch/x86/entry/entry_64.S:entry_SYSCALL_64(swapgs、pt_regs、do_syscall_64、若可则 sysretq) |
C 分发与 SYSRET/IRET 判定 |
arch/x86/entry/syscall_64.c:do_syscall_64、x64_sys_call;sys_call_table[] 仍存在于镜像中,主路径分发为 switch |
2.5 内核源码摘录(与上表对应)
下列片段与主线 Linux 树一致,便于和 SDM 对照阅读10116。
arch/x86/kernel/cpu/common.c — idt_syscall_init() 中写入 MSR_LSTAR 与 MSR_SYSCALL_MASK:
static inline void idt_syscall_init(void)
{
wrmsrq(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
/* ... IA32_SYSENTER_* and ia32_enabled() branches omitted ... */
/*
* Flags to clear on syscall; clear as much as possible
* to minimize user space-kernel interference.
*/
wrmsrq(MSR_SYSCALL_MASK,
X86_EFLAGS_CF|X86_EFLAGS_PF|X86_EFLAGS_AF|
X86_EFLAGS_ZF|X86_EFLAGS_SF|X86_EFLAGS_TF|
X86_EFLAGS_IF|X86_EFLAGS_DF|X86_EFLAGS_OF|
X86_EFLAGS_IOPL|X86_EFLAGS_NT|X86_EFLAGS_RF|
X86_EFLAGS_AC|X86_EFLAGS_ID);
}
arch/x86/entry/entry_64.S — entry_SYSCALL_64 入口(硬件不压栈后,由这里构造 pt_regs 并调用 do_syscall_64):
SYM_CODE_START(entry_SYSCALL_64)
swapgs
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
/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
pushq %rax /* pt_regs->orig_ax */
PUSH_AND_CLEAR_REGS rax=$-ENOSYS
movq %rsp, %rdi
movslq %eax, %esi
call do_syscall_64 /* returns with IRQs disabled */
arch/x86/entry/syscall_64.c — sys_call_table[] 注释与 x64_sys_call() 的 switch 分发:
/*
* The sys_call_table[] is no longer used for system calls, but
* kernel/trace/trace_syscalls.c still wants to know the system
* call address.
*/
#define __SYSCALL(nr, sym) case nr: return __x64_##sym(regs);
long x64_sys_call(const struct pt_regs *regs, unsigned int nr)
{
switch (nr) {
#include <asm/syscalls_64.h>
default: return __x64_sys_ni_syscall(regs);
}
}
同文件 do_syscall_64() — 前半dispatch、末尾返回值决定 SYSRET 与 IRET(以下与中版内核树连续片段一致,仅删去空白行以便排版):
/* Returns true to return using SYSRET, or false to use IRET */
__visible noinstr bool do_syscall_64(struct pt_regs *regs, int nr)
{
add_random_kstack_offset();
nr = syscall_enter_from_user_mode(regs, nr);
instrumentation_begin();
if (!do_syscall_x64(regs, nr) && !do_syscall_x32(regs, nr) && nr != -1) {
regs->ax = __x64_sys_ni_syscall(regs);
}
instrumentation_end();
syscall_exit_to_user_mode(regs);
if (cpu_feature_enabled(X86_FEATURE_XENPV))
return false;
if (unlikely(regs->cx != regs->ip || regs->r11 != regs->flags))
return false;
if (unlikely(regs->cs != __USER_CS || regs->ss != __USER_DS))
return false;
if (unlikely(regs->ip >= TASK_SIZE_MAX))
return false;
if (unlikely(regs->flags & (X86_EFLAGS_RF | X86_EFLAGS_TF)))
return false;
return true;
}
主题三:经 IDT 的路径与 SYSCALL 路径的性能与开销
syscall 相对 int + IDT 更快,主要不是因为“少查一次内存里的表”,而是因为 int 走 IDT 门与异常/中断类交付,含 门与特权相关检查、中断帧布局,返回侧又常配合 IRET;SYSCALL/SYSRET 针对系统调用做了裁剪。内核里的 调用号分发发生在两条路径入核之后,不是整体差距的主因。
3.1 路径对比(示意)
graph TD
subgraph 快路径_syscall
A[用户态] -->|1. syscall| B[CPU]
B -->|2. 读取 LSTAR/STAR/FMASK| C[内核入口 entry_SYSCALL_64]
C -->|3. do_syscall_64 + x64_sys_call| D[__x64_sys_*]
end
subgraph 传统路径_int0x80
E[用户态] -->|1. int 0x80| F[CPU]
F -->|2. 通过 IDT 向量门进入| G[中断门入口]
G -->|3. 中断类交付与返回语义| H[内核处理]
end
graph TD
subgraph 快路径_syscall
A1[用户态] -->|1. syscall| B1[CPU]
B1 -->|2. 从 MSR 取入口| C1[内核入口]
C1 -->|3. 软件分发到具体例程| D1[__x64_sys_* 等]
end
subgraph 慢路径_int_idt
E1[用户态] -->|1. int 0x80| F1[CPU]
F1 -->|2. 硬件查 IDT| G1[IDT 门]
G1 -->|3. 特权与栈等检查 + 转入处理程序| H1[内核入口]
H1 -->|4. 同样要再做软件分发| I1[具体例程]
end
3.2 机制层对比
| 特性 | int 0x80 + IDT |
syscall + MSR |
|---|---|---|
| 核心机制 | 软件中断,走 异常/中断类交付 | 系统调用专用指令 |
| 入口 | CPU 按向量查 IDT 门 | CPU 从 MSR 取目标 RIP 等 |
| 特权与门 | DPL、门类型 等 | 不经同一套 IDT 门 |
| 硬件保存的现场 | 中断/异常帧(含段与标志等,因事件与模式而异) | 主要为 RCX/R11 的返回契约 |
| 返回 | 常见 IRET |
条件满足时 SYSRET,否则 IRET |
3.3 单次查表与整条路径
硬件对 IDT 的一次访问与 内核对 switch (nr) 的几条指令各自都很快;差别主要来自 整条入核/出核:多保存了哪些状态、是否经过 IDT 门语义、返回是 IRET 全功能还是 SYSRET 窄契约、以及 Linux 在出口是否 回退到 IRET。
3.4 入核与出核:int 0x80 与 syscall 的步骤对照
下表沿用在 IDT + IRET 与 SYSCALL + SYSRET(及 Linux 可能回退的 IRET) 之间做对照的常见写法;其中 int 路径的栈帧以 64 位长模式下向内核栈压入的字段为准(SS、RSP、RFLAGS、CS、RIP 及可能的错误码等)1,与 legacy 保护模式下部分教材中的“多段寄存器”示意图并不完全同形。
| 动作 | int 0x80(经 IDT,IRET 返回) |
syscall(SYSRET 快路径;条件不满足则 IRET) |
性能与实现上的含义 |
|---|---|---|---|
| 特权级切换 | Ring 3 → Ring 0 | Ring 3 → Ring 0 | 两者都必须发生;不是时间差的主要来源。 |
| 栈切换 | 与 TSS / IST 等绑定的 中断交付 语义下切到 内核栈 | swapgs,再由软件把 RSP 切到 per-CPU 内核栈顶11 |
int 走通用中断模型的硬件路径;syscall 由内核显式维护 RSP,与 “SYSCALL 不改 RSP” 的硬件契约一致3。 |
| 硬件自动保存 | 向栈压中断帧(长模式典型含 SS、RSP、RFLAGS、CS、RIP;另视向量压错误码)1 | 不向栈压帧;仅用 RCX/R11 等约定配合 MSR 改变 RIP/特权级/RFLAGS 掩码3 |
int 在硬件一侧完成较多现场记录;syscall 把栈上工作留到 entry_SYSCALL_64。 |
| 软件补全现场 | 入口例程继续保存其余寄存器、建 pt_regs |
PUSH_AND_CLEAR_REGS 等补齐 pt_regs11 |
进入 C 分发前,两条路径通常都要把通用寄存器镜像补全。 |
| 权限 / 门检查 | IDT 门的 DPL、类型 等与 INT n 相关的一致检查 |
不经与 int 同一条 门描述符路径 |
int 多一层 IDT 门禁 语义的固定成本。 |
| 返回时现场恢复 | IRET 从栈帧恢复 SS、RSP、RFLAGS、CS、RIP 等 |
SYSRET:RIP←RCX、RFLAGS←R11(窄);否则走 IRET6 |
IRET 通用、重;SYSRET 轻,但 Linux 在 do_syscall_64 中细查与 SYSRET 契约是否仍可满足6。 |
同一组维度在 syscall 专题里也可以压缩理解:宏观上都要完成 ring 切换与寄存器约定,微观上 SYSCALL/SYSRET 把可由专用指令“包办”的部分收紧,int/IDT/IRET 为覆盖全体中断/异常类型保留更宽的默认行为。
3.4.1 与上表对应的三个技术要点(64 位长模式)
以下三点承接 §3.4 表格,用语与 IA-32e 长模式 下的栈帧布局及当前 Linux arch/x86/entry 实现一致。
-
硬件保存的寄存器现场不同
INT n经 IDT 时走 通用中断/异常交付:在 64 位长模式下,CPU 向 当前特权级 0 栈 压入 SS、RSP、RFLAGS、CS、RIP 及视向量而定的 错误码 等,与同一条 IRET 恢复约定兼容、并由全体 IDT 向量共享这一框架1。SYSCALL不向栈压帧,仅用RCX、R11分别保留RIP、RFLAGS的返回契约信息;通用寄存器与RSP等由entry_SYSCALL_64等 软件路径 写入struct pt_regs311。 -
是否经过 IDT 与 DPL 检查
INT n根据 门描述符 做 DPL、门类型 等与 软件中断 相关的一致性检查1。SYSCALL不读取 IDT 门:CPL 0 入口RIP、段与RFLAGS掩码由IA32_LSTAR、IA32_STAR、IA32_FMASK及IA32_EFER.SCE预先约定35;合法性依赖 OS 对这些 MSR 与 GDT 项的初始化以及内核入口实现。 -
返回路径的恢复范围
IRET从栈上 中断帧 恢复 SS、RSP、RFLAGS、CS、RIP 等,语义覆盖完整1。SYSRET(长模式下REX.W)在契约成立时仅从RCX、R11恢复RIP、RFLAGS,用户态CS/SS按IA32_STAR出核位域装载4。Linux 在do_syscall_64中若判定SYSRET契约不成立或须走通用返回路径,则 改用IRET6。
3.5 数量级举例
在常见 x86-64 桌面平台上,对 getpid 类极短系统调用做周期计数,int 0x80 有时可达约 二百周期量级,syscall 多在约 数十至百余周期量级,可差数倍。结果强依赖 CPU、微架构、是否实际走 SYSRET 与测量方法;定量的结论应在目标机上用 perf 等重复测量。
3.6 小结
- IDT:通用 事件交付 机制,优先保证覆盖面与一致性,不以最短系统调用为唯一目标。
- 系统调用分发:
x64_sys_call的switch为主路径;sys_call_table[]仍服务 观测/枚举 等需求;二者都在syscall已进核之后 执行。 SYSCALL+ MSR:系统调用 专用硬件入口协议;真正缩短的是 经 MSR 的入核与在条件允许时的SYSRET返回,不是“少做一次 C 层分发”。- Linux:即便从
syscall入核,仍可能在出口选用IRET,与SYSRET契约及历史、安全问题有关6。
建议的自修顺序
- SDM:中断/异常与 IDT、
SYSCALL/SYSRET。 - Linux:
common.c(MSR)→entry_64.S→syscall_64.c。 - 对照阅读:
entry_64.S与syscall_64.c,结合文末 References。
References
-
Intel® 64 and IA-32 Architectures SDM — Combined Volumes - 官方总入口(含 Volume 3 系统编程);文中 IDT 64-bit 描述与中断/异常机制以此为准 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
OSDev Wiki — Interrupt Descriptor Table - IDT 结构与模式差异的教学索引 ↩ ↩2
-
x86 Instruction Reference — SYSCALL - 指令级语义(
RCX/R11、LSTAR、FMASK、RSP不保存) ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 -
x86 Instruction Reference — SYSRET -
SYSRET返回语义与RSP处理约束 ↩ ↩2 ↩3 -
正文所引 Intel SDM 英文原文出自 Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A: System Programming Guide, Part 1(约 §6.14 64-bit IDT gate、§5.8.8
SYSCALL/SYSRET);完整手册见 1 的官方下载入口 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 -
Linux Source — arch/x86/entry/syscall_64.c -
do_syscall_64、x64_sys_call与SYSRET/IRET判定 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 -
Linux Kernel Documentation — entry_64 - x86 多入口说明(含
entry_INT80_compat、system_call等) ↩ -
Intel x86 Instruction Set Reference — SYSENTER -
SYSENTER/SYSEXIT的历史快速调用路径 ↩ -
man7 — syscall(2) - Linux 用户态系统调用 ABI 与调用约定说明 ↩ ↩2
-
Linux Source — arch/x86/kernel/cpu/common.c -
syscall_init()/idt_syscall_init()与MSR_SYSCALL_MASK初始化 ↩ ↩2 ↩3 ↩4 -
Linux Source — arch/x86/entry/entry_64.S -
entry_SYSCALL_64路径(swapgs、pt_regs、sysretq) ↩ ↩2 ↩3 ↩4 ↩5