IDT 与 SYSCALL:差异、演化、Linux 实现与性能

全文分三部分:

  1. IDT 与 SYSCALL 的机制差异与历史脉络
  2. x86-64 Linux 上从 syscall 指令到内核服务的执行路径(对照 SDM 与 arch/x86
  3. 经 IDT 的入核与 SYSCALL 入核在开销与实现上的对比

硬件叙述以 Intel Software Developer’s Manual(Volume 3A 等)为准,软件以 Linux 主线 arch/x86 为准;引用标号见文末 References


主题一:IDT 与 SYSCALL 的区别与演化

1.1 谁在决定内核入口

二者都是架构规定的入口协议,但针对的事件类别不同:前者服务 异步/异常类事件 的统一交付,后者服务 用户态主动发起的系统调用 的专用快速通道。

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 对照阅读):

  1. IDT(及经其投递的中断/异常/INT n
    由 CPU 规定、面向全体异步与异常事件通用交付协议:功能全、约束多,不以“最短一次用户主动系统调用”为唯一优化目标12

  2. 系统调用分发(软件)
    Linux 仍保留 sys_call_table[],方便 trace 等子系统解析符号地址;64 位主路径上则由 x64_sys_call()switch (nr) 落到 __x64_sys_*。无论数组还是 switch,都属于 syscall 已经进核之后 的普通控制流,不是 CPU 替代的 IDT 查表6

  3. 系统调用硬件快车道(SYSCALL + 若干 MSR)
    入口 RIPCS/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 相关)

  1. 80386 及保护模式IDTINT n 成为统一的异常/中断/软中断交付入口;内核通过设置向量 n 的门,把控制流交给对应处理例程。
  2. 32 位 Linux:用户态系统调用长期使用 int 0x80,即 CPU 查 IDT 向量 0x80 进入内核(仍属 IDT 路径)7
  3. 约 Pentium II / Pro 一代:Intel 引入 SYSENTER/SYSEXIT,配合 MSR 提供另一条 不经 IDT 门描述符的 快速进核通道(Linux 在 32 位兼容路径等场景仍会碰到与 SYSENTER/SYSCALL 相关的入口约定)8
  4. x86-64(AMD64 / Intel 64):架构在 长模式下提供 SYSCALL/SYSRET(由 IA32_EFER.SCE 等控制使能,细节以 SDM 为准)。64 位 Linux 用户态通常通过 glibc 等内联 syscall,内核入口落在 entry_SYSCALL_6439
  5. 并存:今日 64 位内核仍可能为 32 位进程 保留 int 0x80 / SYSENTER / 兼容入口(向量与实现见内核头文件与 entry_64_compat 等);本文明细以 64 位 syscall 主线为主

主题二:x86-64 Linux 上 syscall 从 CPU 到内核的完整机制

2.1 三层结构(总览)

  1. CPU(SDM):用户态约定 RAX=调用号、参数寄存器后执行 syscall。硬件将 RIP → RCXRFLAGS → R11,按 MSR 加载 CS/SS/RIP,并令 RFLAGS <- RFLAGS & ~IA32_FMASK不保存 RSP、不向栈压帧。
  2. 内核入口 entry_SYSCALL_64arch/x86/entry/entry_64.S):swapgs、切换到 per-CPU 内核栈,在栈上构造 struct pt_regs,再 call do_syscall_64
  3. 分发与返回do_syscall_64x64_sys_callswitch (nr) → 各 __x64_sys_*。返回时若满足契约则 SYSRET,否则 IRET

对比 IDT 路径IDT 处理「向量 → 硬件按门交付」;syscall 处理「寄存器约定 + MSR 指定 RIP软件补全栈帧再交付」。

2.1.1 SYSCALL 与 MSR:多寄存器协同,而非单一 LSTAR

MSR(Model Specific Register) 指通过 RDMSR/WRMSR 访问的 按编号独立编址 的一类寄存器;体系结构里与 SYSCALL 相关的常量名 IA32_STARIA32_LSTARIA32_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_LSTARRFLAGSIA32_FMASK 的组合关系(正文 §2.3 另有逐句引文)。

Linux 在 64 位内核引导路径中与上述分工对齐:syscall_init()MSR_STAR(用户/内核段选择子约定),再调用 idt_syscall_init()MSR_LSTARentry_SYSCALL_64)与 MSR_SYSCALL_MASK(对应 IA32_FMASK10

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 入口 RIPIA32_STAR 给出 SYSCALL/SYSRET 使用的 CS/SS 选择子场IA32_FMASK 规定 RFLAGS 在进入时被清除的位IA32_EFER.SCE 使能整条 SYSCALL/SYSRET 路径35。三颗 MSR 与总开关共同构成 SDM Figure 5-14 所描述的配置平面,操作系统需一并初始化,而不是仅写 LSTAR 一项。

2.1.2 长模式专用:SYSCALLSYSRET —— 三颗 MSR 如何协同工作

一、核心概念:三个 MSR 各司其职

在 x86-64 长模式下,syscallsysret 指令依赖三个 MSR(模型特定寄存器)来完成用户态到内核态、再回到用户态的完整流程。可以这样理解:

MSR 寄存器 作用 类比
IA32_STAR 告诉 CPU:进入内核时用什么段(CS/SS),返回用户时用什么段 门禁卡的双重配置——进去刷A区,出来刷B区
IA32_LSTAR 告诉 CPU:内核的入口函数地址在哪里 紧急出口的指向标——从这里进内核
IA32_FMASK 告诉 CPU:进入内核时,RFLAGS 寄存器里哪些位要强制清零 安检过滤器——某些标志位不能带进内核

重要说明:本文只讨论 IA-32e 长模式下带 REX.Wsyscall/sysret 指令,不涉及 IA32_CSTARSYSENTER/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

2. sysret 的“契约”

3. 返回值约定


四、与 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.510)。自 5) 起按下述代码块列举,惯例与 /Users/weli/works/bootimage-example/LINUX_X86_64_ENTRY_AND_PT_REGS.md 一致:围栏第一行为 起始行:结束行:arch/…/文件(相对 linux/ 源码树根;本文行号依 /Users/weli/works/linux)。

5)–6)entry_SYSCALL_64arch/x86/entry/entry_64.SIA32_LSTAR 指向此处:swapgs、装入 cpu_current_top_of_stackpt_regs 布局压栈、PUSH_AND_CLEAR_REGSmovq %rsp,%rdi / movslq %eax,%rsicall 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_x64x64_sys_callarch/x86/entry/syscall_64.c — 与上引 112–114 行入参一致;合法系统调用号下 regs->axdo_syscall_x64x64_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.tblKbuild 生成 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 trueentry_SYSCALL_64testb %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_64ALTERNATIVE 失败分支也会落到 swapgs_restore_regs_and_return_to_usermode,最终 iretq(上引 559–580640–659 行;完整标签关系见 11)。

本地树路径:/Users/weli/works/linux(与主线 torvalds/linux 同源时行号一致;若你本地的 fork 有差异,以 git blame / 实际文件为准。)

2.3 CPU 侧(与 Vol.3A §5.8.8 等一致)

  1. RIP(下一条指令)→ RCXRFLAGSR113
  2. RIP 来自 IA32_LSTARCS/SS 的选择子与 IA32_STAR 的位域布局按 SDM Figure 5-143
  3. RFLAGS <- RFLAGS & ~IA32_FMASK。Linux 在 arch/x86/kernel/cpu/common.cidt_syscall_init() 中向 MSR_SYSCALL_MASK 写入含 X86_EFLAGS_IF 等位,使进入内核后 IF 通常被清除310
  4. SYSCALL 不改变 RSPSYSRET 也不恢复 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 segmentStack segment 等另有逐条说明,此处摘入与 LSTAR/FMASKRSP 最直接相关的句子;完整列举见 1§5.8.8Figure 5-14。)

2.4 Linux 侧(源码锚点)

内容 文件与要点
STAR/LSTAR/SYSCALL_MASK 初始化 arch/x86/kernel/cpu/common.csyscall_init()idt_syscall_init()
入口汇编 arch/x86/entry/entry_64.Sentry_SYSCALL_64swapgspt_regsdo_syscall_64、若可则 sysretq
C 分发与 SYSRET/IRET 判定 arch/x86/entry/syscall_64.cdo_syscall_64x64_sys_callsys_call_table[] 仍存在于镜像中,主路径分发switch

2.5 内核源码摘录(与上表对应)

下列片段与主线 Linux 树一致,便于和 SDM 对照阅读10116

arch/x86/kernel/cpu/common.cidt_syscall_init() 中写入 MSR_LSTARMSR_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.Sentry_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.csys_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、末尾返回值决定 SYSRETIRET(以下与中版内核树连续片段一致,仅删去空白行以便排版):

/* 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 门与异常/中断类交付,含 门与特权相关检查、中断帧布局,返回侧又常配合 IRETSYSCALL/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 0x80syscall 的步骤对照

下表沿用在 IDT + IRETSYSCALL + SYSRET(及 Linux 可能回退的 IRET 之间做对照的常见写法;其中 int 路径的栈帧64 位长模式下向内核栈压入的字段为准(SS、RSP、RFLAGS、CS、RIP 及可能的错误码等)1,与 legacy 保护模式下部分教材中的“多段寄存器”示意图并不完全同形。

动作 int 0x80(经 IDT,IRET 返回) syscallSYSRET 快路径;条件不满足则 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 SYSRETRIP←RCXRFLAGS←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 实现一致。

  1. 硬件保存的寄存器现场不同
    INT n 经 IDT 时走 通用中断/异常交付:在 64 位长模式下,CPU 向 当前特权级 0 栈 压入 SS、RSP、RFLAGS、CS、RIP 及视向量而定的 错误码 等,与同一条 IRET 恢复约定兼容、并由全体 IDT 向量共享这一框架1SYSCALL 不向栈压帧,仅用 RCXR11 分别保留 RIPRFLAGS 的返回契约信息;通用寄存器与 RSPentry_SYSCALL_64软件路径 写入 struct pt_regs311

  2. 是否经过 IDT 与 DPL 检查
    INT n 根据 门描述符DPL、门类型 等与 软件中断 相关的一致性检查1SYSCALL 不读取 IDT 门CPL 0 入口 RIP段与 RFLAGS 掩码IA32_LSTARIA32_STARIA32_FMASKIA32_EFER.SCE 预先约定35合法性依赖 OS 对这些 MSR 与 GDT 项的初始化以及内核入口实现。

  3. 返回路径的恢复范围
    IRET 从栈上 中断帧 恢复 SS、RSP、RFLAGS、CS、RIP 等,语义覆盖完整1SYSRET(长模式下 REX.W)在契约成立时仅从 RCXR11 恢复 RIPRFLAGS用户态 CS/SSIA32_STAR 出核位域装载4Linuxdo_syscall_64 中若判定 SYSRET 契约不成立或须走通用返回路径,则 改用 IRET6

3.5 数量级举例

在常见 x86-64 桌面平台上,对 getpid 类极短系统调用做周期计数,int 0x80 有时可达约 二百周期量级,syscall 多在约 数十至百余周期量级,可差数倍。结果强依赖 CPU、微架构、是否实际走 SYSRET 与测量方法;定量的结论应在目标机上用 perf重复测量。

3.6 小结


建议的自修顺序

  1. SDM:中断/异常与 IDTSYSCALL/SYSRET
  2. Linux:common.c(MSR)→ entry_64.Ssyscall_64.c
  3. 对照阅读:entry_64.Ssyscall_64.c,结合文末 References。

References

  1. Intel® 64 and IA-32 Architectures SDM — Combined Volumes - 官方总入口(含 Volume 3 系统编程);文中 IDT 64-bit 描述与中断/异常机制以此为准  2 3 4 5 6 7 8 9

  2. OSDev Wiki — Interrupt Descriptor Table - IDT 结构与模式差异的教学索引  2

  3. x86 Instruction Reference — SYSCALL - 指令级语义(RCX/R11LSTARFMASKRSP 不保存)  2 3 4 5 6 7 8 9 10 11 12 13

  4. x86 Instruction Reference — SYSRET - SYSRET 返回语义与 RSP 处理约束  2 3

  5. 正文所引 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

  6. Linux Source — arch/x86/entry/syscall_64.c - do_syscall_64x64_sys_callSYSRET/IRET 判定  2 3 4 5 6 7

  7. Linux Kernel Documentation — entry_64 - x86 多入口说明(含 entry_INT80_compatsystem_call 等) 

  8. Intel x86 Instruction Set Reference — SYSENTER - SYSENTER/SYSEXIT 的历史快速调用路径 

  9. man7 — syscall(2) - Linux 用户态系统调用 ABI 与调用约定说明  2

  10. Linux Source — arch/x86/kernel/cpu/common.c - syscall_init() / idt_syscall_init()MSR_SYSCALL_MASK 初始化  2 3 4

  11. Linux Source — arch/x86/entry/entry_64.S - entry_SYSCALL_64 路径(swapgspt_regssysretq)  2 3 4 5

My Github Page: https://github.com/liweinan

Powered by Jekyll and Theme by solid

If you have any question want to ask or find bugs regarding with my blog posts, please report it here:
https://github.com/liweinan/liweinan.github.io/issues