分析基于 Linux 内核 master 分支(v6.15-rc 阶段) 核心参考文件:
arch/x86/entry/entry_64.Sarch/x86/kernel/process_64.carch/x86/mm/pgtable.carch/x86/kernel/cpu/common.carch/x86/include/asm/processor.harch/x86/include/asm/desc_defs.harch/x86/include/asm/pgtable_64_types.harch/x86/entry/calling.harch/x86/mm/pti.c
- x86-64 体系结构总览
- 系统调用入口:entry_SYSCALL_64 完整流程
- 寄存器保存与恢复:pt_regs 结构
- 进程切换:__switch_to 与 context_switch
- 四级与五级页表结构
- KPTI:内核页表隔离机制
- CPU 特性检测与 CPUID
- MSR 寄存器操作
- GDT / IDT / TSS 结构
- PCID:进程上下文 ID 优化
- 综合:从 syscall 到返回的完整路径
- x86 启动流程:实模式到长模式
- IDT 深度分析:中断描述符表初始化
- 系统调用两种方式:SYSCALL 与 SYSENTER
- x86 页表进阶:PCID 优化与 TLB 管理
- APIC 架构:Local APIC、IO-APIC、X2APIC
- x86 硬件调试:DR0-DR7 与单步执行
- x86 虚拟化扩展:VMX 与 SVM
- CET:控制流强制技术
- 参考文件索引(完整版)
x86-64(又称 AMD64)是当今服务器和桌面系统的主流 64 位指令集架构,由 AMD 在 2000 年提出,Intel 随后以 EM64T/Intel 64 名义兼容实现。Linux 内核对 x86-64 的支持集中在 arch/x86/ 目录,代码量超过 100 万行。
通用寄存器(64位):
RAX RBX RCX RDX RSI RDI RBP RSP
R8 R9 R10 R11 R12 R13 R14 R15
特殊用途(Linux 内核约定):
RAX -- 系统调用号 / 返回值
RDI -- 第1参数
RSI -- 第2参数
RDX -- 第3参数
R10 -- 第4参数(syscall,替代 RCX)
R8 -- 第5参数
R9 -- 第6参数
RCX -- syscall 保存的返回地址(RIP)
R11 -- syscall 保存的 RFLAGS
RSP -- 栈指针
RIP -- 指令指针(不可直接操作)
段寄存器(64位长模式下大多为0基址):
CS DS ES SS FS GS
FS/GS 可通过 MSR 设置非零基址(线程本地存储)
控制寄存器:
CR0 -- 保护模式、分页开关(PG、PE 位)
CR2 -- 缺页地址(Page Fault Linear Address)
CR3 -- 页目录基地址(含 PCID)
CR4 -- 扩展特性(PAE、PSE、SMEP、SMAP、PKE、PCIDE …)
CR8 -- 任务优先级寄存器(TPR)
系统表寄存器:
GDTR -- GDT 基址 + 限长
IDTR -- IDT 基址 + 限长
LDTR -- LDT 选择子
TR -- 任务寄存器(TSS 选择子)
arch/x86/entry/calling.h:15-31 中有对 x86-64 函数调用约定的详细注释:
arguments | callee-saved | extra caller-saved | return
[callee-clobbered] | | [callee-clobbered] |
---------------------------------------------------------------------------
rdi rsi rdx rcx r8-9 | rbx rbp [*] r12-15 | r10-11 | rax, rdx
虚拟地址空间(48位,LA48):
0x0000_0000_0000_0000 +---------------------------+
| 用户空间(128 TB) |
0x0000_7FFF_FFFF_FFFF +---------------------------+
| 不可用(canonical hole) |
0xFFFF_8000_0000_0000 +---------------------------+
| 内核空间(128 TB) |
ffffc90000000000 | vmalloc 区域(32 TB) |
ffffea0000000000 | vmemmap |
ffff888000000000 | 直接映射物理内存 |
ffffffff80000000 | 内核代码/数据(.text) |
0xFFFF_FFFF_FFFF_FFFF +---------------------------+
五级页表(LA57)时用户/内核各扩展至 64 PB
x86-64 系统调用通过 SYSCALL 指令触发。该指令由 CPU 硬件执行以下操作:
- 将
RIP(返回地址)存入RCX - 将
RFLAGS存入R11,并用MSR_SYSCALL_MASK屏蔽部分标志位 - 从
MSR_STAR加载新 CS/SS(特权级变为 0) - 从
MSR_LSTAR加载新 RIP,跳入内核
注意:SYSCALL 不切换栈,进入内核时 RSP 仍指向用户栈。
源文件:arch/x86/entry/entry_64.S:87
SYM_CODE_START(entry_SYSCALL_64) ; 87
UNWIND_HINT_ENTRY
ENDBR ; IBT 间接分支追踪(安全特性)
swapgs ; 91 -- 交换 GS.base 与 MSR_KERNEL_GS_BASE
; 使内核得以访问 per-CPU 数据
; tss.sp2 用作临时暂存(scratch space)
movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2) ; 93 -- 保存用户 RSP
SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp ; 94 -- KPTI:切换至内核页表
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp ; 95 -- 切换至内核栈
; ------ 以下在内核栈上构造 struct pt_regs ------
pushq $__USER_DS ; 101 -- pt_regs->ss
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) ; 102 -- pt_regs->sp(用户RSP)
pushq %r11 ; 103 -- pt_regs->flags(用户RFLAGS)
pushq $__USER_CS ; 104 -- pt_regs->cs
pushq %rcx ; 105 -- pt_regs->ip(用户RIP,返回地址)
pushq %rax ; 107 -- pt_regs->orig_ax(系统调用号)
PUSH_AND_CLEAR_REGS rax=$-ENOSYS ; 109 -- 保存所有通用寄存器并清零以防推测执行泄漏
movq %rsp, %rdi ; 112 -- 第1参数:pt_regs 指针
movslq %eax, %rsi ; 114 -- 第2参数:系统调用号(符号扩展)
IBRS_ENTER ; 117 -- Spectre v2 缓解
UNTRAIN_RET ; 118 -- RSB 填充
CLEAR_BRANCH_HISTORY ; 119
call do_syscall_64 ; 121 -- 跳入 C 层分发函数do_syscall_64 定义于 arch/x86/entry/common.c,负责查找系统调用表并调用具体处理函数。
; do_syscall_64 返回后(IRQ 已关闭):
ALTERNATIVE "testb %al, %al; jz swapgs_restore_regs_and_return_to_usermode", \
"jmp swapgs_restore_regs_and_return_to_usermode", X86_FEATURE_XENPV ; 130-131
; 快速返回路径(SYSRET):
syscall_return_via_sysret: ; 137
IBRS_EXIT
POP_REGS pop_rdi=0 ; 139 -- 弹出保存的寄存器(除 RDI)
movq %rsp, %rdi
movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp ; 146 -- 切换至蹦床栈
pushq RSP-RDI(%rdi) ; 149 -- 保存原 RSP
pushq (%rdi) ; 150 -- 保存 RDI
SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi ; 158 -- KPTI:切换回用户页表
popq %rdi
popq %rsp
swapgs ; 164 -- 恢复用户 GS
CLEAR_CPU_BUFFERS ; 165 -- MDS 缓解:清空 CPU 缓冲区
sysretq ; 166 -- 返回用户态
SYM_CODE_END(entry_SYSCALL_64) ; 170用户态 内核态
| |
| SYSCALL 指令 |
| RIP -> RCX |
| RFLAGS -> R11 |
| CS/SS <- MSR_STAR |
| RIP <- MSR_LSTAR |
+-------------------------------> |
entry_SYSCALL_64 (entry_64.S:87)
|
swapgs (91)
保存用户RSP (93)
SWITCH_TO_KERNEL_CR3 (94) [KPTI]
切换内核栈 (95)
构造 pt_regs (101-109)
|
do_syscall_64()
|
sys_xxx() 具体系统调用
|
返回 do_syscall_64
|
+---------+----------+
| |
SYSRET 快速路径 IRET 慢速路径
(大多数情况) (信号、ptrace等)
|
POP_REGS (139)
SWITCH_TO_USER_CR3 (158) [KPTI]
swapgs (164)
sysretq (166)
<-------------------------------+
|
用户态继续执行
arch/x86/kernel/cpu/common.c:2294 的 syscall_init() 负责设置必要的 MSR:
void syscall_init(void)
{
/* MSR_STAR: 高32位为 SYSRET 用的 CS/SS,低32位留给 SYSCALL 用的 CS */
wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS); // 2297
/* 将 entry_SYSCALL_64 入口地址写入 LSTAR */
wrmsrq(MSR_LSTAR, (unsigned long)entry_SYSCALL_64); // 2260
/* 屏蔽进入内核时的 EFLAGS 位 */
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); // 2285-2290
}struct pt_regs 是内核在栈上保存用户态寄存器现场的核心数据结构,定义于 arch/x86/include/asm/ptrace.h。
内核栈(高地址在上):
+-----------------+ <- 原用户 RSP
| SS | pt_regs->ss (entry_64.S:101)
+-----------------+
| RSP(用户栈) | pt_regs->sp (entry_64.S:102)
+-----------------+
| RFLAGS | pt_regs->flags (entry_64.S:103)
+-----------------+
| CS | pt_regs->cs (entry_64.S:104)
+-----------------+
| RIP(返回地址)| pt_regs->ip (entry_64.S:105)
+-----------------+
| orig_ax(调用号)| pt_regs->orig_ax (entry_64.S:107)
+-----------------+ <- PUSH_AND_CLEAR_REGS 开始
| R15 | pt_regs->r15
| R14 | pt_regs->r14
| R13 | pt_regs->r13
| R12 | pt_regs->r12
| RBP | pt_regs->bp
| RBX | pt_regs->bx
| R11 | pt_regs->r11
| R10 | pt_regs->r10
| R9 | pt_regs->r9
| R8 | pt_regs->r8
| RAX | pt_regs->ax (初始值 -ENOSYS)
| RCX | pt_regs->cx
| RDX | pt_regs->dx
| RSI | pt_regs->si
| RDI | pt_regs->di
+-----------------+ <- RSP 指向此处(do_syscall_64 的第1参数)
arch/x86/entry/calling.h:68-129 定义了 PUSH_REGS、CLEAR_REGS、PUSH_AND_CLEAR_REGS 宏:
; PUSH_REGS(calling.h:68)
pushq %rdi ; pt_regs->di
pushq %rsi ; pt_regs->si
pushq %rdx ; pt_regs->dx
pushq %rcx ; pt_regs->cx
pushq %rax ; pt_regs->ax
pushq %r8 ; pt_regs->r8
pushq %r9 ; pt_regs->r9
pushq %r10 ; pt_regs->r10
pushq %r11 ; pt_regs->r11
pushq %rbx ; pt_regs->rbx
pushq %rbp ; pt_regs->rbp
pushq %r12 ; pt_regs->r12
pushq %r13 ; pt_regs->r13
pushq %r14 ; pt_regs->r14
pushq %r15 ; pt_regs->r15
; CLEAR_REGS(calling.h:102)-- 防止 Spectre 推测执行泄漏
xorl %esi, %esi ; 109
xorl %edx, %edx ; 110
xorl %ecx, %ecx ; 111
... (R8-R15, RBX, RBP 均清零)arch/x86/kernel/process_64.c:70 的 __show_regs() 在 oops 时打印寄存器,可以看到 pt_regs 的完整使用:
void __show_regs(struct pt_regs *regs, enum show_regs_mode mode,
const char *log_lvl)
{
// 直接从 pt_regs 读取各字段打印
printk("%sRAX: %016lx RBX: %016lx RCX: %016lx\n",
log_lvl, regs->ax, regs->bx, regs->cx); // process_64.c:85
printk("%sRDX: %016lx RSI: %016lx RDI: %016lx\n",
log_lvl, regs->dx, regs->si, regs->di); // process_64.c:87
...
// 读取 CR 控制寄存器
cr3 = __read_cr3(); // process_64.c:118
cr4 = __read_cr4(); // process_64.c:119
...
// 读取 FS/GS base MSR
rdmsrq(MSR_FS_BASE, fs); // process_64.c:112
rdmsrq(MSR_GS_BASE, gs); // process_64.c:113
rdmsrq(MSR_KERNEL_GS_BASE, shadowgs); // process_64.c:114
}进程切换(上下文切换)是操作系统最核心的操作之一,x86-64 上分为两个阶段:汇编层的栈切换(__switch_to_asm)和 C 层的状态切换(__switch_to)。
schedule()
-> context_switch() (kernel/sched/core.c)
-> switch_mm_irqs_off() -- 切换内存上下文(CR3 更新)
-> switch_to(prev, next, prev)
-> __switch_to_asm (entry_64.S:177) -- 汇编,切换栈
-> __switch_to (process_64.c:610) -- C,切换其余状态
arch/x86/entry/entry_64.S:177:
SYM_FUNC_START(__switch_to_asm)
; 保存 callee-saved 寄存器到 prev 的内核栈
pushq %rbp ; 183
pushq %rbx ; 184
pushq %r12 ; 185
pushq %r13 ; 186
pushq %r14 ; 187
pushq %r15 ; 188
; 切换栈:保存 prev->thread.sp,加载 next->thread.sp
movq %rsp, TASK_threadsp(%rdi) ; 191 -- 保存 prev 的 RSP
movq TASK_threadsp(%rsi), %rsp ; 192 -- 加载 next 的 RSP
; 更新 stack canary(若启用 STACKPROTECTOR)
movq TASK_stack_canary(%rsi), %rbx ; 195
movq %rbx, PER_CPU_VAR(__stack_chk_guard) ; 196
; 防止推测执行攻击:填充 RSB(返回栈缓冲区)
FILL_RETURN_BUFFER %r12, RSB_CLEAR_LOOPS, X86_FEATURE_RSB_CTXSW ; 206
; 恢复 next 的 callee-saved 寄存器
popq %r15 ; 209
popq %r14 ; 210
popq %r13 ; 211
popq %r12 ; 212
popq %rbx ; 213
popq %rbp ; 214
jmp __switch_to ; 216 -- 跳入 C 层(不用 call,ret 回到 next 的调用点)
SYM_FUNC_END(__switch_to_asm)切换后内核栈示意:
prev 的内核栈(切换后休眠):
+----------+
| R15 |
| R14 |
| R13 |
| R12 |
| RBX |
| RBP |
+----------+ <- prev->thread.sp(保存)
next 的内核栈(恢复执行):
+----------+
| R15 |
| R14 |
| R13 |
| R12 |
| RBX |
| RBP |
+----------+ <- next->thread.sp(从这里恢复)
| ret addr | <- 指向 next 上次调用 __switch_to_asm 的返回地址
+----------+
arch/x86/kernel/process_64.c:609:
__no_kmsan_checks
__visible __notrace_funcgraph struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p) // 610
{
struct thread_struct *prev = &prev_p->thread;
struct thread_struct *next = &next_p->thread;
switch_fpu(prev_p, cpu); // 619 -- 切换 FPU/AVX 状态(lazy)
save_fsgs(prev_p); // 626 -- 保存 FS/GS base
load_TLS(next, cpu); // 632 -- 加载 next 的 TLS 描述符到 GDT
arch_end_context_switch(next_p); // 639 -- Xen 等半虚拟化收尾
/* 切换 DS/ES 段寄存器 */
savesegment(es, prev->es); // 655
if (unlikely(next->es | prev->es))
loadsegment(es, next->es); // 657
savesegment(ds, prev->ds); // 659
if (unlikely(next->ds | prev->ds))
loadsegment(ds, next->ds); // 661
x86_fsgsbase_load(prev, next); // 663 -- 加载 FS/GS base(MSR 或 FSGSBASE 指令)
x86_pkru_load(prev, next); // 665 -- 切换 PKRU(内存保护键)
/* 更新 per-CPU 变量 */
raw_cpu_write(current_task, next_p); // 670
raw_cpu_write(cpu_current_top_of_stack,
task_top_of_stack(next_p)); // 671
update_task_stack(next_p); // 674 -- 更新 TSS.sp0(中断栈顶)
switch_to_extra(prev_p, next_p); // 676 -- 调试寄存器、I/O 权限等
...
}arch/x86/kernel/process_64.c:391,x86_fsgsbase_load() 根据 CPU 是否支持 FSGSBASE 扩展,选择不同路径:
static __always_inline void x86_fsgsbase_load(struct thread_struct *prev,
struct thread_struct *next) // 391
{
if (static_cpu_has(X86_FEATURE_FSGSBASE)) {
/* 使用 WRFSBASE/WRGSBASE 指令,避免 MSR 访问 */
wrfsbase(next->fsbase); // 402
__wrgsbase_inactive(next->gsbase); // 403
} else {
load_seg_legacy(prev->fsindex, prev->fsbase,
next->fsindex, next->fsbase, FS); // 405
load_seg_legacy(prev->gsindex, prev->gsbase,
next->gsindex, next->gsbase, GS); // 407
}
}save_fsgs()(process_64.c:276)同样优先使用 RDFSBASE/RDGSBASE 指令:
static __always_inline void save_fsgs(struct task_struct *task)
{
savesegment(fs, task->thread.fsindex); // 278
savesegment(gs, task->thread.gsindex); // 279
if (static_cpu_has(X86_FEATURE_FSGSBASE)) {
task->thread.fsbase = rdfsbase(); // 286
task->thread.gsbase = __rdgsbase_inactive(); // 287
} else {
save_base_legacy(task, task->thread.fsindex, FS); // 289
save_base_legacy(task, task->thread.gsindex, GS); // 290
}
}arch/x86/include/asm/processor.h:448:
struct thread_struct {
struct desc_struct tls_array[GDT_ENTRY_TLS_ENTRIES]; // 450 -- TLS 段描述符
unsigned long sp; // 454 -- 内核栈指针
unsigned short es, ds, fsindex, gsindex; // 458-461
unsigned long fsbase; // 465
unsigned long gsbase; // 466
struct perf_event *ptrace_bps[HBP_NUM]; // 477 -- 硬件断点
unsigned long virtual_dr6; // 479
unsigned long ptrace_dr7; // 481
unsigned long cr2; // 483 -- 缺页地址
unsigned long trap_nr; // 484
unsigned long error_code; // 485
struct io_bitmap *io_bitmap; // 491 -- IOPL
u32 pkru; // 509 -- 保护键
};x86-64 使用多级页表实现虚拟地址到物理地址的映射。传统四级(LA48,48位虚拟地址)和现代五级(LA57,57位虚拟地址,Intel Ice Lake+)。
虚拟地址(64位,有效48位):
63 48 47 39 38 30 29 21 20 12 11 0
+------------+---------+--------+--------+--------+----------+
| 符号扩展 | PGD(9) | PUD(9) | PMD(9) | PTE(9) | 页内偏移 |
+------------+---------+--------+--------+--------+----------+
| | | | |
| | | | page offset
| | | PTE表[0..511]
| | PMD表[0..511]
| PUD表[0..511]
PGD表[0..511](CR3指向)
虚拟地址(64位,有效57位):
63 57 56 48 47 39 38 30 29 21 20 12 11 0
+-------+---------+--------+--------+--------+--------+----------+
| 符号 | PGD(9) | P4D(9) | PUD(9) | PMD(9) | PTE(9) | 页内偏移 |
+-------+---------+--------+--------+--------+--------+----------+
arch/x86/include/asm/pgtable_64_types.h:
#define PGDIR_SHIFT pgdir_shift // 50 -- 四级=48, 五级=57(运行时确定)
#define PTRS_PER_PGD 512 // 51
#define P4D_SHIFT 39 // 56
#define PTRS_PER_P4D ptrs_per_p4d // 58 -- 四级=1(折叠), 五级=512
#define PUD_SHIFT 30 // 67
#define PTRS_PER_PUD 512 // 68
#define PMD_SHIFT 21 // 74 -- PMD 覆盖 2MB
#define PTRS_PER_PMD 512 // 75
#define PTRS_PER_PTE 512 // 80 -- PTE 覆盖 4KB各级覆盖范围:
PTE 覆盖 4 KB (2^12)
PMD 覆盖 2 MB (2^21)
PUD 覆盖 1 GB (2^30)
P4D 覆盖 512 GB (2^39)
PGD 覆盖 256 TB (2^48, LA48) / 128 PB (2^57, LA57)
PTE(64位):
63 52 51 12 11 10 9 8 7 6 5 4 3 2 1 0
+--------+----------+--+--+--+--+--+--+--+--+--+--+--+--+
| 保留/NX | 物理页框号 | | | | | | | | | | | |P|
+--------+----------+--+--+--+--+--+--+--+--+--+--+--+--+
NX bit PFN(40位) 忽略 G PS D A PCD PWT U W P
^ ^ ^
User/Supervisor ----' | Present
Read/Write -------'
重要标志位(arch/x86/include/asm/pgtable_types.h):
_PAGE_PRESENT(bit 0):页存在_PAGE_RW(bit 1):可写_PAGE_USER(bit 2):用户态可访问_PAGE_PWT(bit 3):Write-Through_PAGE_PCD(bit 4):Cache Disable_PAGE_ACCESSED(bit 5):已访问_PAGE_DIRTY(bit 6):已写入_PAGE_PSE(bit 7):大页(PMD/PUD 级别)_PAGE_GLOBAL(bit 8):全局页(PCID 无效时不刷 TLB)_PAGE_NX(bit 63):不可执行
arch/x86/include/asm/pgtable_64_types.h:31:
#ifdef USE_EARLY_PGTABLE_L5
static inline bool pgtable_l5_enabled(void)
{
return __pgtable_l5_enabled; // 33 -- 早期启动阶段使用变量
}
#else
#define pgtable_l5_enabled() cpu_feature_enabled(X86_FEATURE_LA57) // 36
#endifarch/x86/mm/pgtable.c:82,新进程创建时,pgd_ctor() 将内核页表项克隆到新 PGD:
static void pgd_ctor(struct mm_struct *mm, pgd_t *pgd)
{
if (!IS_ENABLED(CONFIG_X86_PAE))
clone_pgd_range(pgd + KERNEL_PGD_BOUNDARY, // 86
swapper_pg_dir + KERNEL_PGD_BOUNDARY,
KERNEL_PGD_PTRS);
pgd_set_mm(pgd, mm); // 91
pgd_list_add(pgd); // 92 -- 加入全局 pgd_list,便于内核页表变更同步
}arch/x86/mm/pgtable.c:27-56 还实现了各级页表的 TLB-aware 释放:
#if CONFIG_PGTABLE_LEVELS > 2
void ___pmd_free_tlb(struct mmu_gather *tlb, pmd_t *pmd) // 28
{
paravirt_release_pmd(__pa(pmd) >> PAGE_SHIFT);
tlb_remove_ptdesc(tlb, virt_to_ptdesc(pmd));
}
#if CONFIG_PGTABLE_LEVELS > 3
void ___pud_free_tlb(struct mmu_gather *tlb, pud_t *pud) // 42
...
#if CONFIG_PGTABLE_LEVELS > 4
void ___p4d_free_tlb(struct mmu_gather *tlb, p4d_t *p4d) // 49KPTI(Kernel Page-Table Isolation,内核页表隔离)是 2018 年针对 Meltdown(CVE-2017-5754)漏洞引入的安全机制,代码基于 TU Graz 的 KAISER 项目。
未启用 KPTI:
用户态 内核态
+-------+ +-------+
| 用户 | | 用户 |
| 代码 | | 代码 |
+-------+ +-------+
| 内核 | <- 用户态下可见(Meltdown 可读!)
| 代码 |
+-------+
共用同一份页表(CR3)
启用 KPTI 后:
用户态 CR3 内核态 CR3
+-------------+ +-------------+
| 用户代码 | | 用户代码 |
| 用户数据 | | 用户数据 |
+-------------+ +-------------+
| 最小内核映射 | | 完整内核映射 |
| (入口蹦床) | | (全部内核) |
+-------------+ +-------------+
两份独立页表,通过 PGD 高位 bit12 区分
arch/x86/entry/calling.h:157:
#define PTI_USER_PGTABLE_BIT PAGE_SHIFT // = 12
#define PTI_USER_PGTABLE_MASK (1 << PTI_USER_PGTABLE_BIT)
#define PTI_USER_PCID_BIT X86_CR3_PTI_PCID_USER_BIT
KPTI 将用户空间和内核空间的 PGD 存放在物理上连续的两个 4KB 页中(共 8KB),通过翻转 CR3 的 bit 12 在两份页表间切换:
内核 PGD 地址: 0x........X000 (bit12=0)
用户 PGD 地址: 0x........X000 | 0x1000 (bit12=1)
arch/x86/entry/calling.h:173:
.macro SWITCH_TO_KERNEL_CR3 scratch_reg:req
ALTERNATIVE "jmp .Lend_\@", "", X86_FEATURE_PTI ; 174 -- 未启用KPTI则跳过
mov %cr3, \scratch_reg
ADJUST_KERNEL_CR3 \scratch_reg ; 清除 bit12 和 PCID 用户位
mov \scratch_reg, %cr3
.Lend_\@:
.endmADJUST_KERNEL_CR3(calling.h:167):
.macro ADJUST_KERNEL_CR3 reg:req
ALTERNATIVE "", "SET_NOFLUSH_BIT \reg", X86_FEATURE_PCID
andq $(~PTI_USER_PGTABLE_AND_PCID_MASK), \reg ; 170 -- 清除用户PCID和页表位
.endmarch/x86/entry/calling.h:184,切换回用户页表时需处理 PCID 刷新:
.macro SWITCH_TO_USER_CR3 scratch_reg:req scratch_reg2:req
mov %cr3, \scratch_reg
ALTERNATIVE "jmp .Lwrcr3_\@", "", X86_FEATURE_PCID ; 187
; 检查用户 ASID 是否需要 TLB flush
movq \scratch_reg, \scratch_reg2
andq $(0x7FF), \scratch_reg ; 193 -- 取出 ASID(低11位)
bt \scratch_reg, THIS_CPU_user_pcid_flush_mask ; 194
jnc .Lnoflush_\@ ; 195
; 需要 flush:清除 flush mask 对应位,不设 NOFLUSH
btr \scratch_reg, THIS_CPU_user_pcid_flush_mask ; 198
movq \scratch_reg2, \scratch_reg
jmp .Lwrcr3_pcid_\@
.Lnoflush_\@:
movq \scratch_reg2, \scratch_reg
SET_NOFLUSH_BIT \scratch_reg ; 204 -- CR3 bit63=1,写入CR3不刷TLB
.Lwrcr3_pcid_\@:
orq $(PTI_USER_PCID_MASK), \scratch_reg ; 208 -- 设置用户 PCID 位
.Lwrcr3_\@:
orq $(PTI_USER_PGTABLE_MASK), \scratch_reg ; 212 -- 翻转到用户 PGD
mov \scratch_reg, %cr3
.endmarch/x86/mm/pti.c:80:
void __init pti_check_boottime_disable(void)
{
if (hypervisor_is_type(X86_HYPER_XEN_PV)) { // 82
pti_mode = PTI_FORCE_OFF;
return;
}
if (pti_mode == PTI_AUTO &&
!cpu_attack_vector_mitigated(CPU_MITIGATE_USER_KERNEL)) // 88
pti_mode = PTI_FORCE_OFF;
...
setup_force_cpu_cap(X86_FEATURE_PTI); // 102 -- 设置 PTI 特性标志
}PTI 的 PGD 分配是双倍大小(8KB 对齐),kernel_to_user_pgdp() 宏通过 + PAGE_SIZE / sizeof(pgd_t) 定位用户 PGD 副本。
KPTI 的性能开销来源:
1. 每次 syscall/interrupt 进入内核时写 CR3(~200 cycle,若无 PCID)
2. 用户-内核切换导致 TLB flush(若无 PCID)
3. 内存开销:每进程额外 8KB(双 PGD)
使用 PCID(Process Context ID)后:
- CR3 写入设置 NOFLUSH 位(bit63)可跳过 TLB flush
- 内核 ASID = 用户 ASID + PTI_USER_PCID_MASK
- 性能影响从约 30% 降低至约 5%
Linux 内核使用 CPUID 指令探测 CPU 特性,并将结果存储在 struct cpuinfo_x86 的 x86_capability[] 位图中。
arch/x86/include/asm/processor.h:124:
struct cpuinfo_x86 {
union {
struct {
__u8 x86_model; // CPU 型号
__u8 x86; // CPU 系列(family)
__u8 x86_vendor; // 厂商(Intel=0, AMD=2, ...)
__u8 x86_reserved;
};
__u32 x86_vfm; // vendor-family-model 组合
};
__u8 x86_stepping; // 步进号
__u8 x86_virt_bits; // 虚拟地址位数(通常48或57)
__u8 x86_phys_bits; // 物理地址位数(通常46或52)
__u32 extended_cpuid_level; // 扩展 CPUID 最大功能号
int cpuid_level; // 标准 CPUID 最大功能号(-1=无CPUID)
union {
__u32 x86_capability[NCAPINTS + NBUGINTS]; // 特性位图
unsigned long x86_capability_alignment;
};
char x86_vendor_id[16]; // 厂商字符串("GenuineIntel" 等)
char x86_model_id[64]; // 型号字符串
struct cpuinfo_topology topo; // 拓扑信息
...
} __randomize_layout; // 190 -- 开启结构体布局随机化厂商 ID 定义(processor.h:192-202):
#define X86_VENDOR_INTEL 0
#define X86_VENDOR_AMD 2
#define X86_VENDOR_HYGON 9
#define X86_VENDOR_ZHAOXIN 10
#define X86_VENDOR_NUM 12arch/x86/kernel/cpu/common.c:927:
void cpu_detect(struct cpuinfo_x86 *c)
{
/* 通过 CPUID.0 获取最大功能号和厂商字符串 */
cpuid(0x00000000,
(unsigned int *)&c->cpuid_level, // EAX: 最大功能号
(unsigned int *)&c->x86_vendor_id[0], // EBX: "Genu" / "Auth"
(unsigned int *)&c->x86_vendor_id[8], // ECX: "ntel" / "cAMD"
(unsigned int *)&c->x86_vendor_id[4]);// EDX: "ineI" / "enti"
// 930-933
c->x86 = 4; // 935 -- 默认 486 级别
/* CPUID.1 获取家族、型号、步进 */
if (c->cpuid_level >= 0x00000001) { // 937
u32 junk, tfms, cap0, misc;
cpuid(0x00000001, &tfms, &misc, &junk, &cap0); // 940
c->x86 = x86_family(tfms); // 941 -- bits[11:8]+(bits[27:20]>>20)
c->x86_model = x86_model(tfms); // 942 -- bits[7:4]+(bits[19:16]>>16)
c->x86_stepping = x86_stepping(tfms); // 943 -- bits[3:0]
if (cap0 & (1 << 19)) { // 945 -- CLFLUSH 支持
c->x86_clflush_size = ((misc >> 8) & 0xff) * 8; // 946
c->x86_cache_alignment = c->x86_clflush_size; // 947
}
}
}arch/x86/kernel/cpu/common.c:1003:
void get_cpu_cap(struct cpuinfo_x86 *c)
{
u32 eax, ebx, ecx, edx;
/* CPUID.1: 标准特性 */
if (c->cpuid_level >= 0x00000001) {
cpuid(0x00000001, &eax, &ebx, &ecx, &edx); // 1009
c->x86_capability[CPUID_1_ECX] = ecx; // 1011 -- SSE4.2, AVX, AES...
c->x86_capability[CPUID_1_EDX] = edx; // 1012 -- SSE2, PAE, APIC...
}
/* CPUID.6: 热功耗管理 */
if (c->cpuid_level >= 0x00000006)
c->x86_capability[CPUID_6_EAX] = cpuid_eax(0x00000006); // 1017
/* CPUID.7.0: 扩展特性(AVX2, SMEP, SMAP, MPX...) */
if (c->cpuid_level >= 0x00000007) {
cpuid_count(0x00000007, 0, &eax, &ebx, &ecx, &edx); // 1021
c->x86_capability[CPUID_7_0_EBX] = ebx; // 1022 -- FSGSBASE, BMI, AVX2...
c->x86_capability[CPUID_7_ECX] = ecx; // 1023 -- PKU, LA57, UMIP...
c->x86_capability[CPUID_7_EDX] = edx; // 1024 -- SERIALIZE, AMX...
}
...
}常用特性测试宏(arch/x86/include/asm/cpufeature.h):
/* 在当前 CPU 上测试(使用 per-CPU 缓存,较慢) */
#define cpu_has(c, bit) test_cpu_cap(c, bit)
/* 在 boot CPU 上测试(最常用) */
#define boot_cpu_has(bit) cpu_feature_enabled(bit)
/* 编译时已知特性(used for static_branch) */
#define static_cpu_has(bit) ...
/* 测试 bug 标志位 */
#define boot_cpu_has_bug(bit) cpu_has_bug(&boot_cpu_data, bit)典型特性 bit(arch/x86/include/asm/cpufeatures.h):
X86_FEATURE_FPU (0*32+0) -- 浮点单元
X86_FEATURE_PAE (0*32+6) -- 物理地址扩展
X86_FEATURE_APIC (0*32+9) -- APIC 支持
X86_FEATURE_SEP (0*32+11) -- SYSENTER/SYSEXIT
X86_FEATURE_MTRR (0*32+12) -- 内存类型范围寄存器
X86_FEATURE_PGE (0*32+13) -- 全局页 TLB 标志
X86_FEATURE_CLFLUSH (0*32+19) -- CLFLUSH 指令
X86_FEATURE_MMX (0*32+23) -- MMX 扩展
X86_FEATURE_FXSR (0*32+24) -- FXSAVE/FXRSTOR
X86_FEATURE_SSE (0*32+25) -- SSE
X86_FEATURE_SSE2 (0*32+26) -- SSE2
X86_FEATURE_HT (0*32+28) -- 超线程
X86_FEATURE_PCID (4*32+17) -- 进程上下文 ID
X86_FEATURE_SMEP (7*32+7) -- 管理模式执行保护
X86_FEATURE_SMAP (7*32+20) -- 管理模式访问保护
X86_FEATURE_FSGSBASE (7*32+0) -- RDFSBASE/WRFSBASE 指令
X86_FEATURE_AVX2 (7*32+5) -- AVX2
X86_FEATURE_PTI (7*32+11) -- KPTI 已启用
X86_FEATURE_LA57 (7*32+16) -- 57位线性地址(五级页表)
X86_FEATURE_UMIP (7*32+2) -- 用户模式指令保护
X86_FEATURE_PKU (7*32+3) -- 内存保护键(用户态)
X86_BUG_CPU_MELTDOWN (19*32+14) -- Meltdown 漏洞标志
X86_BUG_SPECTRE_V1 (19*32+15) -- Spectre V1
X86_BUG_SPECTRE_V2 (19*32+16) -- Spectre V2
arch/x86/kernel/cpu/common.c:371-436,CPU 初始化时根据特性位设置 CR4:
static __always_inline void setup_smep(struct cpuinfo_x86 *c)
{
if (cpu_has(c, X86_FEATURE_SMEP))
cr4_set_bits(X86_CR4_SMEP); // 374
}
static __always_inline void setup_smap(struct cpuinfo_x86 *c)
{
if (cpu_has(c, X86_FEATURE_SMAP))
cr4_set_bits(X86_CR4_SMAP); // 385
}
static __always_inline void setup_umip(struct cpuinfo_x86 *c)
{
if (!cpu_feature_enabled(X86_FEATURE_UMIP))
goto out;
if (!cpu_has(c, X86_FEATURE_UMIP))
goto out;
cr4_set_bits(X86_CR4_UMIP); // 398
pr_info_once("x86/cpu: User Mode Instruction Prevention (UMIP) activated\n");
}cr4_init()(common.c:502)还在支持 PCID 的 CPU 上启用 CR4.PCIDE:
void cr4_init(void)
{
unsigned long cr4 = __read_cr4();
if (boot_cpu_has(X86_FEATURE_PCID))
cr4 |= X86_CR4_PCIDE; // 507 -- 开启 PCID
__write_cr4(cr4);
this_cpu_write(cpu_tlbstate.cr4, cr4); // 514 -- 更新 per-CPU shadow
}MSR(Model-Specific Register,型号专用寄存器)通过 RDMSR(读)和 WRMSR(写)指令访问,需要 CPL=0。
MSR 地址 名称 用途
-----------------------------------------------------------
0xC0000080 MSR_EFER 长模式启用、NXE
0xC0000081 MSR_STAR SYSCALL/SYSRET 段选择子
0xC0000082 MSR_LSTAR 64位 SYSCALL 入口地址
0xC0000083 MSR_CSTAR 32位兼容 SYSCALL 入口
0xC0000084 MSR_SYSCALL_MASK SYSCALL 时 EFLAGS 清零掩码
0xC0000100 MSR_FS_BASE FS 段基址
0xC0000101 MSR_GS_BASE GS 段基址
0xC0000102 MSR_KERNEL_GS_BASE SWAPGS 交换目标
0x00000174 MSR_IA32_SYSENTER_CS SYSENTER CS 段
0x00000175 MSR_IA32_SYSENTER_ESP SYSENTER 内核栈
0x00000176 MSR_IA32_SYSENTER_EIP SYSENTER 入口
0x00000277 MSR_IA32_PAT 页属性表
0xC0010010 MSR_AMD64_SYSCFG AMD 系统配置
0x0000048B MSR_PPIN_CTL PPIN 控制(平台保护ID)
MSR 地址定义在 arch/x86/include/asm/msr-index.h。
内核提供安全的 MSR 读写封装:
/* 读 MSR(可能 #GP 崩溃) */
rdmsr(msr, low, high); // 读32+32位
rdmsrq(msr, value); // 读64位(现代推荐)
native_rdmsrq(msr); // 无 instrumentation
/* 写 MSR(可能 #GP 崩溃) */
wrmsr(msr, low, high);
wrmsrq(msr, value); // 写64位
/* 安全版本(返回错误码,不崩溃) */
rdmsrq_safe(msr, &value); // 捕获 #GP
wrmsrq_safe(msr, value);arch/x86/kernel/process_64.c:100-114:
// 显示寄存器时读取 FS/GS base
rdmsrq(MSR_FS_BASE, fs); // 112
rdmsrq(MSR_GS_BASE, gs); // 113
rdmsrq(MSR_KERNEL_GS_BASE, shadowgs); // 114__rdgsbase_inactive()(process_64.c:166)说明了 SWAPGS 与 FRED 的区别:
static noinstr unsigned long __rdgsbase_inactive(void)
{
lockdep_assert_irqs_disabled();
if (!cpu_feature_enabled(X86_FEATURE_FRED) &&
!cpu_feature_enabled(X86_FEATURE_XENPV)) {
native_swapgs(); // 195 -- 传统路径:先 swapgs
gsbase = rdgsbase(); // 196
native_swapgs(); // 197
} else {
rdmsrq(MSR_KERNEL_GS_BASE, gsbase); // 200 -- FRED 路径:直接读 MSR
}
return gsbase;
}arch/x86/kernel/cpu/common.c:144 的 ppin_init() 演示了 MSR 的安全访问模式:
static void ppin_init(struct cpuinfo_x86 *c)
{
if (rdmsrq_safe(info->msr_ppin_ctl, &val)) // 160 -- 安全读,不崩溃
goto clear_ppin;
if ((val & 3UL) == 1UL) // 163 -- 被锁定在禁用状态
goto clear_ppin;
if (!(val & 2UL)) {
wrmsrq_safe(info->msr_ppin_ctl, val | 2UL); // 170 -- 尝试启用
rdmsrq_safe(info->msr_ppin_ctl, &val); // 171
}
if (val & 2UL) {
c->ppin = native_rdmsrq(info->msr_ppin); // 176 -- 读取 PPIN
set_cpu_cap(c, info->feature); // 177
return;
}
clear_ppin:
setup_clear_cpu_cap(info->feature); // 182
}投机执行漏洞(Spectre/Meltdown)相关 MSR:
/* 启用 IBRS(间接分支受限推测) */
if (cpu_has(c, X86_FEATURE_IBRS))
wrmsrq(MSR_IA32_SPEC_CTRL, SPEC_CTRL_IBRS);
/* 启用 STIBP(单线程间接分支预测器阻断) */
if (cpu_has(c, X86_FEATURE_STIBP))
spec_ctrl |= SPEC_CTRL_STIBP;
/* 启用 SSBD(推测存储绕过禁用) */
if (cpu_has(c, X86_FEATURE_SSBD))
spec_ctrl |= SPEC_CTRL_SSBD;arch/x86/kernel/cpu/common.c:210,内核为每个 CPU 定义一份 GDT:
DEFINE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page) = { .gdt = {
#ifdef CONFIG_X86_64
[GDT_ENTRY_KERNEL32_CS] = GDT_ENTRY_INIT(DESC_CODE32, 0, 0xfffff), // 220
[GDT_ENTRY_KERNEL_CS] = GDT_ENTRY_INIT(DESC_CODE64, 0, 0xfffff), // 221
[GDT_ENTRY_KERNEL_DS] = GDT_ENTRY_INIT(DESC_DATA64, 0, 0xfffff), // 222
[GDT_ENTRY_DEFAULT_USER32_CS] = GDT_ENTRY_INIT(DESC_CODE32 | DESC_USER, 0, 0xfffff), // 223
[GDT_ENTRY_DEFAULT_USER_DS] = GDT_ENTRY_INIT(DESC_DATA64 | DESC_USER, 0, 0xfffff), // 224
[GDT_ENTRY_DEFAULT_USER_CS] = GDT_ENTRY_INIT(DESC_CODE64 | DESC_USER, 0, 0xfffff), // 225
#endif
}};64 位下段寄存器中实际上只有 CS 和 SS 还有语义(特权级、L位),其余均为 base=0、limit=full flat 模式。
arch/x86/include/asm/desc_defs.h:66:
struct desc_struct {
u16 limit0; // 段限制 [15:0]
u16 base0; // 段基址 [15:0]
u16 base1: 8, // 段基址 [23:16]
type: 4, // 段类型(代码/数据/系统)
s: 1, // 0=系统段,1=代码/数据段
dpl: 2, // 描述符特权级(0=内核,3=用户)
p: 1; // 存在位
u16 limit1: 4, // 段限制 [19:16]
avl: 1, // 软件可用
l: 1, // 1=64位代码段
d: 1, // 0=16位,1=32位(64位段中为0)
g: 1, // 粒度(0=字节,1=4KB页)
base2: 8; // 段基址 [31:24]
} __attribute__((packed));arch/x86/include/asm/desc_defs.h:134:
struct gate_struct {
u16 offset_low; // 处理函数地址 [15:0]
u16 segment; // 代码段选择子(通常 __KERNEL_CS)
struct idt_bits bits; // IST、类型、DPL、P 位
u16 offset_middle; // 处理函数地址 [31:16]
u32 offset_high; // 处理函数地址 [63:32]
u32 reserved;
} __attribute__((packed));IDT 类型(desc_defs.h:90):
enum {
GATE_INTERRUPT = 0xE, // 中断门(自动关中断)
GATE_TRAP = 0xF, // 陷阱门(不关中断)
GATE_CALL = 0xC, // 调用门
GATE_TASK = 0x5, // 任务门
};struct idt_bits(desc_defs.h:119):
struct idt_bits {
u16 ist: 3, // IST 索引(0=无,1-7=使用 IST 栈)
zero: 5,
type: 5, // 门类型
dpl: 2, // 特权级(3=用户可调用,0=仅内核)
p: 1; // 存在位
} __attribute__((packed));IST(中断栈表)是 x86-64 的特性,TSS 中有 7 个 IST 入口,用于 NMI、双重错误等需要可靠栈的情况。
arch/x86/include/asm/processor.h:308,64位 TSS 硬件部分:
struct x86_hw_tss {
u32 reserved1;
u64 sp0; // 从用户态进入内核时的内核栈(ring 0 RSP)
u64 sp1; // ring 1 栈(Linux 不用)
u64 sp2; // ring 2 栈,entry_SYSCALL_64 用作临时暂存
u64 reserved2;
u64 ist[7]; // IST1-IST7:各类异常专用栈
u32 reserved3;
u32 reserved4;
u16 reserved5;
u16 io_bitmap_base; // I/O 权限位图偏移
} __attribute__((packed));完整 TSS 结构(processor.h:400):
struct tss_struct {
struct x86_hw_tss x86_tss; // 硬件部分
struct x86_io_bitmap io_bitmap; // I/O 权限位图
} __aligned(PAGE_SIZE); // 页对齐,防止跨页TSS 的 sp0 字段在每次进程切换时由 update_task_stack() 更新,指向当前进程内核栈顶:
static __always_inline void native_load_sp0(unsigned long sp0)
{
this_cpu_write(cpu_tss_rw.x86_tss.sp0, sp0); // processor.h:536
}每个 CPU 的内存布局(per-CPU 区域):
+------------------+
| gdt_page | -- DEFINE_PER_CPU_PAGE_ALIGNED,PAGE_SIZE
| [GDT 条目数组] | 由 GDTR 寄存器指向
+------------------+
| cpu_tss_rw | -- DECLARE_PER_CPU_PAGE_ALIGNED,PAGE_SIZE 对齐
| x86_hw_tss | 由 TR 寄存器(任务寄存器)指向
| io_bitmap |
+------------------+
| hardirq_stack_ptr| -- 硬中断栈指针
+------------------+
| cpu_current_top | -- 当前进程内核栈顶(DECLARE_PER_CPU_CACHE_HOT)
| _of_stack |
+------------------+
IDT 是全局共享的(idt_table[]),由 IDTR 寄存器指向
PCID(Process Context ID)是 x86-64 的硬件 TLB 分区特性,由 Intel Westmere 引入(CPUID.01H:ECX[bit17]),AMD Zen 系列同样支持。
传统 CR3 切换(无 PCID):
写 CR3 => TLB 全部刷新(INVLPG 隐式发生)
性能开销大
启用 PCID 后:
CR3 低 12 位([11:0])= ASID(地址空间 ID)
CR3 bit63 = NOFLUSH 位
不同 ASID 的 TLB 条目独立存在
切换 CR3 时只要 ASID 相同就不用刷 TLB
Linux 的 ASID 分配:
内核空间 ASID = 进程 ASID + 0
用户空间 ASID = 进程 ASID + PTI_USER_PCID_MASK(KPTI 开启时)
共支持 12 位 ASID(最多 4096 个),Linux 动态复用
arch/x86/kernel/cpu/common.c:257:
static int __init x86_nopcid_setup(char *s)
{
if (!boot_cpu_has(X86_FEATURE_PCID)) // 264
return 0;
setup_clear_cpu_cap(X86_FEATURE_PCID); // 267
pr_info("nopcid: PCID feature disabled\n");
return 0;
}
early_param("nopcid", x86_nopcid_setup); // 271类似地,noinvpcid(common.c:274)禁用 INVPCID 指令(批量 TLB 失效)。
arch/x86/kernel/cpu/common.c:502:
void cr4_init(void)
{
unsigned long cr4 = __read_cr4();
if (boot_cpu_has(X86_FEATURE_PCID))
cr4 |= X86_CR4_PCIDE; // 507 -- 开启 CR4.PCIDE 位
if (static_branch_likely(&cr_pinning))
cr4 = (cr4 & ~cr4_pinned_mask) | cr4_pinned_bits;
__write_cr4(cr4);
this_cpu_write(cpu_tlbstate.cr4, cr4); // 514 -- 维护 per-CPU shadow CR4
}在 KPTI 开启且 PCID 可用的情况下:
CR3 布局:
bit 63 -- NOFLUSH(写 CR3 时不刷 TLB)
bit 12 -- 用户/内核页表选择(PTI_USER_PGTABLE_BIT)
bit 11 -- 用户 PCID 最高位(PTI_USER_PCID_BIT)
bit[11:0] -- ASID/PCID 值
内核页表 ASID: pcid_kernel = task_pcid
用户页表 ASID: pcid_user = task_pcid | PTI_USER_PCID_MASK
切换到用户态(SWITCH_TO_USER_CR3,calling.h:184):
1. 读取当前 CR3
2. 检查 THIS_CPU_user_pcid_flush_mask 是否有该 ASID 的 flush 请求
3. 若无:设置 NOFLUSH 位(CR3[63]=1),跳过 TLB flush
4. 若有:清除 flush mask,不设 NOFLUSH,写 CR3 触发 TLB flush
5. 设置 PTI_USER_PGTABLE_MASK 切换到用户 PGD
6. 写入新 CR3
INVPCID 指令(需要 X86_FEATURE_INVPCID)支持四种类型的 TLB 失效:
- Type 0:失效单个地址(指定 PCID)
- Type 1:失效整个 PCID
- Type 2:失效所有非全局 TLB(所有 PCID)
- Type 3:失效所有 TLB(含全局页)
arch/x86/include/asm/tlbflush.h 中的 invpcid_flush_one()、invpcid_flush_single_context() 等函数封装了 INVPCID 指令。
将以上所有机制综合到一个完整流程图中:
用户空间进程执行 syscall(以 read(fd, buf, count) 为例)
RAX=0(__NR_read), RDI=fd, RSI=buf, RDX=count
|
| SYSCALL 硬件操作:
| RCX <- RIP(返回地址)
| R11 <- RFLAGS
| RSP 不变(仍是用户栈)
| CS <- MSR_STAR[47:32](内核 CS)
| RIP <- MSR_LSTAR(entry_SYSCALL_64)
v
entry_SYSCALL_64(entry_64.S:87)
|
+-- swapgs(91)
| MSR_KERNEL_GS_BASE 与 GS.base 互换
| 现在 GS 指向 per-CPU 数据
|
+-- 保存用户 RSP(93)
| movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
|
+-- SWITCH_TO_KERNEL_CR3(94)[KPTI 开启时]
| CR3 bit12 清零 -> 切换至内核页表
| 若 PCID:设置 NOFLUSH 位跳过 TLB flush
|
+-- 切换至内核栈(95)
| movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp
|
+-- 构造 pt_regs(101-109)
| 压栈顺序:SS, RSP(用户), RFLAGS, CS, RIP, orig_ax
| PUSH_AND_CLEAR_REGS:压栈并清零 R15..RDI
|
+-- IBRS_ENTER + UNTRAIN_RET + CLEAR_BRANCH_HISTORY(117-119)
| Spectre v2 / RSB 缓解措施
|
+-- call do_syscall_64(121)
|
| do_syscall_64(arch/x86/entry/common.c)
| regs->ax = sys_call_table[nr](regs)
| = sys_read(fd, buf, count)
| = ksys_read(...)
| = vfs_read(...)
| 文件系统、块层 I/O...
| 返回时 pt_regs->ax = 读取字节数 / -errno
|
syscall 返回路径(entry_64.S:129)
|
+-- 检查 pt_regs 是否被修改/是否需要 IRET
| 简单情况 -> SYSRET 快速路径
| 信号、ptrace 等 -> swapgs_restore_regs_and_return_to_usermode(IRET)
|
SYSRET 快速路径(137)
|
+-- IBRS_EXIT(138)
|
+-- POP_REGS(139)
| 弹出 pt_regs 中的寄存器(RDI 除外)
|
+-- 切换至蹦床栈(146)
| movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp
|
+-- 压栈 RSP 和 RDI(149-150,在蹦床栈上)
|
+-- SWITCH_TO_USER_CR3_STACK(158)[KPTI 开启时]
| 检查 user_pcid_flush_mask
| 翻转 CR3 bit12 -> 切换回用户页表
| 根据 flush 需求决定是否 NOFLUSH
|
+-- 从蹦床栈恢复 RDI 和 RSP(160-161)
|
+-- swapgs(164)
| 恢复用户 GS base
|
+-- CLEAR_CPU_BUFFERS(165)
| MDS(Microarchitectural Data Sampling)缓解
|
+-- sysretq(166)
硬件操作:
RIP <- RCX(用户返回地址)
RFLAGS <- R11(用户 RFLAGS)
CS <- MSR_STAR[63:48](用户 CS)
SS <- MSR_STAR[63:48]+8(用户 SS)
CPL <- 3(回到用户态)
|
v
用户空间继续执行
RAX = 返回值(read 字节数 / -errno)
task_struct
|
+-- thread_struct thread
| |-- unsigned long sp (内核栈RSP,__switch_to_asm 切换)
| |-- unsigned long fsbase (FS base,x86_fsgsbase_load 切换)
| |-- unsigned long gsbase (GS base)
| |-- struct desc_struct tls_array[] (TLS 段,load_TLS 切换)
| |-- u32 pkru (保护键,x86_pkru_load 切换)
| +-- struct io_bitmap *io_bitmap (I/O 权限)
|
+-- mm_struct *mm
|-- pgd_t *pgd (进程页目录,switch_mm 切换 CR3)
+-- mm_context_t context
+-- u64 ctx_id (mm 上下文 ID,用于 PCID 分配)
per-CPU 数据(通过 GS base 访问):
+-- struct tss_struct cpu_tss_rw
| |-- x86_hw_tss.sp0 (内核栈顶,update_task_stack 更新)
| |-- x86_hw_tss.sp2 (用户RSP暂存,entry_SYSCALL_64使用)
| +-- io_bitmap (I/O 权限位图)
|
+-- unsigned long cpu_current_top_of_stack (当前进程内核栈顶)
+-- struct task_struct *current_task (当前进程 task_struct)
+-- struct cpu_tlbstate_and_padded cpu_tlbstate
|-- unsigned long cr4 (CR4 shadow)
+-- u16 user_pcid_flush_mask (需要 flush 的 ASID 位图)
特性 对应机制 防护目标
-----------------------------------------------------------------
KPTI PTI_USER_PGTABLE 双 CR3 Meltdown(内核内存读取)
IBRS/IBPB MSR_IA32_SPEC_CTRL Spectre v2(间接分支推测)
STIBP MSR_IA32_SPEC_CTRL Spectre v2(跨线程)
RETPOLINE nospec-branch.h Spectre v2(RET 推测)
RSB 填充 FILL_RETURN_BUFFER Spectre v2(RSB 下溢)
MDS 缓解 CLEAR_CPU_BUFFERS MDS/TAA(微架构缓冲区)
SMEP CR4.SMEP 内核执行用户代码
SMAP CR4.SMAP 内核访问用户数据
UMIP CR4.UMIP 用户执行特权指令(SGDT等)
CET/IBT CR4.CET, ENDBR 控制流完整性
STACKPROTECTOR __stack_chk_guard 内核栈溢出
KASLR 随机内核地址 地址泄漏利用
x86 处理器上电后从实模式(Real Mode)启动,经过保护模式(Protected Mode),最终进入 64 位长模式(Long Mode)。Linux 内核的启动流程跨越多个文件,下面逐阶段分析。
BIOS/UEFI 上电
|
v
实模式(16位,1MB 地址空间)
|
| arch/x86/boot/header.S -- 引导扇区/setup 代码
| arch/x86/boot/main.c -- 实模式 C 代码
| copy_boot_params() -- 复制引导参数
| console_init() -- 初始化早期控制台
| detect_memory() -- 探测物理内存(INT 15h E820)
| set_video() -- 设置视频模式
| go_to_protected_mode() -- 准备切换
|
v
arch/x86/boot/pm.c:go_to_protected_mode()
| realmode_switch_hook() -- 关中断、禁用 NMI
| enable_a20() -- 启用 A20 地址线
| reset_coprocessor() -- 重置 FPU
| mask_all_interrupts() -- 屏蔽 PIC
| setup_idt() -- 加载空 IDT
| setup_gdt() -- 加载启动 GDT(CS/DS/TSS)
| protected_mode_jump() -- 跳入保护模式
|
v
arch/x86/boot/pmjump.S:.Lin_pm32(32位保护模式)
| -- 设置段寄存器(flat 模式)
| -- 跳入 arch/x86/boot/compressed/head_64.S
|
v
arch/x86/boot/compressed/head_64.S
| -- 设置临时页表(identity mapping)
| -- 启用 PAE(CR4.PAE=1)
| -- 设置 CR3 指向临时页表
| -- 设置 MSR_EFER.LME=1(Long Mode Enable)
| -- 设置 CR0.PG=1(开启分页)
| -- 此时进入 IA-32e 兼容模式
| -- ljmp 切换 CS 到 64 位代码段 -> 正式进入 64 位长模式
| -- 解压内核映像(decompress_kernel)
| -- 跳入 arch/x86/kernel/head_64.S
|
v
arch/x86/kernel/head_64.S:startup_64
| -- 建立正式 64 位页表
| -- 设置内核栈
| -- 清零 BSS 段
| -- 调用 x86_64_start_kernel()
|
v
arch/x86/kernel/head64.c:x86_64_start_kernel()
| -- idt_setup_early_traps() 初始化早期 IDT
| -- copy_bootdata() 复制引导数据
| -- start_kernel() 进入通用内核初始化
实模式下物理内存布局(1MB 以内):
0x00000 +--------------------+
| 中断向量表(IVT) | 1KB,256 个 4 字节向量
0x00400 +--------------------+
| BIOS 数据区(BDA) | 256 字节
0x00500 +--------------------+
| 可用低内存 |
0x07C00 +--------------------+
| 引导扇区(512B) | BIOS 将第一扇区加载到此
0x07E00 +--------------------+
| setup 代码加载区 | arch/x86/boot/header.S
0x10000 +--------------------+
| 实模式代码运行区 |
0x90000 +--------------------+
| 引导参数区(zero page) |
0x9A000 +--------------------+
| 实模式栈 |
0xA0000 +--------------------+
| 显存/ROM 映射区 |
0xFFFFF +--------------------+
| BIOS ROM |
arch/x86/boot/pm.c:
void go_to_protected_mode(void)
{
/* 1. 调用实模式切换钩子(关中断、禁用 NMI) */
realmode_switch_hook(); // pm.c:22
/* 2. 启用 A20 地址线(允许访问超过 1MB 的内存) */
if (enable_a20()) { // pm.c:31
puts("A20 gate not responding, unable to boot...\n");
die();
}
/* 3. 重置协处理器 */
reset_coprocessor(); // pm.c:36
/* 4. 屏蔽 PIC 所有中断 */
mask_all_interrupts(); // pm.c:39
/* 5. 加载空 IDT(保护模式前不需要中断) */
setup_idt(); // pm.c:42
/*
* static const struct gdt_ptr null_idt = {0, 0};
* asm volatile("lidtl %0" : : "m" (null_idt));
*/
/* 6. 加载启动 GDT */
setup_gdt(); // pm.c:43
/*
* boot_gdt[] 包含三个条目:
* [GDT_ENTRY_BOOT_CS] = 32位代码段,base=0,limit=4GB
* [GDT_ENTRY_BOOT_DS] = 32位数据段,base=0,limit=4GB
* [GDT_ENTRY_BOOT_TSS] = TSS(仅为 Intel VT 兼容性)
*/
/* 7. 跳入保护模式 */
protected_mode_jump(boot_params.hdr.code32_start,
(u32)&boot_params + (ds() << 4)); // pm.c:46
}arch/x86/boot/pmjump.S:
SYM_FUNC_START_NOALIGN(protected_mode_jump)
movl %edx, %esi ; 保存 boot_params 指针
xorl %ebx, %ebx
movw %cs, %bx
shll $4, %ebx
addl %ebx, 2f ; 修正跳转目标的线性地址
movw $__BOOT_DS, %cx ; 数据段选择子
movw $__BOOT_TSS, %di ; TSS 选择子
movl %cr0, %edx
orb $X86_CR0_PE, %dl ; 设置 CR0.PE=1 -> 进入保护模式!
movl %edx, %cr0
; 远跳转:刷新指令流水线,加载新 CS 选择子
.byte 0x66, 0xea ; ljmpl 操作码
2: .long .Lin_pm32 ; 32位偏移
.word __BOOT_CS ; 代码段选择子
.code32
.Lin_pm32:
; 设置所有数据段寄存器为 flat 32位模式
movl %ecx, %ds
movl %ecx, %es
movl %ecx, %fs
movl %ecx, %gs
movl %ecx, %ss
ltr %di ; 加载 TSS(Intel VT 要求)
lldt %cx ; 清除 LDTR
jmpl *%eax ; 跳入 32 位内核入口
SYM_FUNC_END(protected_mode_jump)从保护模式进入长模式需要:
步骤 1:确保分页关闭(CR0.PG=0)
步骤 2:启用 PAE(CR4.PAE=1)
步骤 3:加载 PML4 页表基址到 CR3
步骤 4:设置 MSR_EFER.LME=1(Long Mode Enable)
步骤 5:设置 CR0.PG=1(开启分页)
-- 此时 CPU 从 Protected Mode 进入 IA-32e 兼容模式(32位CS选择子)
步骤 6:远跳转到 64 位代码段(CS 选择子指向 64 位段,L=1)
-- 此时正式进入 64 位长模式
arch/x86/boot/compressed/head_64.S 中的关键序列(简化):
/* 设置临时页表(恒等映射前 4GB)*/
leal pgtable(%ebx), %eax
movl %eax, %cr3
/* 启用 PAE */
movl %cr4, %eax
orl $X86_CR4_PAE, %eax
movl %eax, %cr4
/* 设置 EFER.LME */
movl $MSR_EFER, %ecx
rdmsr
btsl $_EFER_LME, %eax
wrmsr
/* 开启分页,进入兼容模式 */
movl $(X86_CR0_PG | X86_CR0_PE), %eax
movl %eax, %cr0
/* 远跳转到 64 位长模式 */
lret ; 弹出预先压栈的 CS:RIP(CS 指向 64 位代码段)MSR_EFER (0xC0000080) 各位含义:
bit 0 SCE -- SYSCALL/SYSRET 启用(必须在长模式下设置)
bit 8 LME -- Long Mode Enable(写入,由软件设置)
bit 10 LMA -- Long Mode Active(只读,CPU 确认长模式激活)
bit 11 NXE -- No-Execute Enable(启用 PTE.NX 位)
bit 12 SVME -- Secure Virtual Machine Enable(AMD SVM)
bit 13 LMSLE -- Long Mode Segment Limit Enable(AMD 特有)
bit 14 FFXSR -- Fast FXSAVE/FXRSTOR(AMD 特有)
bit 15 TCE -- Translation Cache Extension(AMD 特有)
Linux 初始化时在 arch/x86/kernel/cpu/common.c 中验证 EFER.LMA:
/* 验证长模式已激活 */
rdmsrq(MSR_EFER, efer);
if (!(efer & EFER_LMA))
panic("CPU is not in long mode");A20 地址线是 IBM PC 兼容性的历史遗留问题。8086 只有 20 根地址线(A0-A19),当访问超过 0xFFFFF 的地址时会回绕到 0。80286+ 有更多地址线,但为了兼容性 BIOS 默认关闭 A20。
arch/x86/boot/a20.c 提供多种 A20 启用方法:
int enable_a20(void)
{
int loops = A20_ENABLE_LOOPS;
/* 尝试多种方法,直到 A20 启用 */
while (loops--) {
/* 方法1: BIOS INT 15h,AX=2401 */
if (a20_test_short()) return 0;
/* 方法2: 键盘控制器(0x64/0x60)*/
if (a20_test_short()) return 0;
/* 方法3: 快速 A20 端口(0x92)*/
if (a20_test_short()) return 0;
}
return -1;
}IDT(Interrupt Descriptor Table)是 x86 保护模式的核心机制,每个向量(0-255)对应一个门描述符,指向相应的处理程序。
向量号 名称 类型 IST 说明
-----------------------------------------------------------------------
0 #DE 除法错误 故障 - 除以零、除法溢出
1 #DB 调试 故障/陷阱 IST_DB 单步、断点
2 NMI 不可屏蔽中断 中断 IST_NMI 硬件 NMI
3 #BP 断点 陷阱 - INT3 指令(DPL=3)
4 #OF 溢出 陷阱 - INTO 指令(DPL=3)
5 #BR 边界检查 故障 - BOUND 指令
6 #UD 无效操作码 故障 - 未知指令
7 #NM 设备不可用 故障 - FPU 不存在/WAIT
8 #DF 双重错误 中止 IST_DF 严重内核错误
9 -- 协处理器段溢出 故障 - 仅 386(已弃用)
10 #TS 无效 TSS 故障 - 任务切换错误
11 #NP 段不存在 故障 - 段加载失败
12 #SS 栈段错误 故障 - 栈操作错误
13 #GP 通用保护 故障 - 访问违例
14 #PF 缺页 故障 - CR2=缺页地址
15 -- 保留 - -
16 #MF x87 FPU 错误 故障 - 浮点计算错误
17 #AC 对齐检查 故障 - 非对齐内存访问
18 #MC 机器检查 中止 IST_MCE 硬件错误
19 #XF SIMD 浮点异常 故障 - SSE 错误
20 #VE 虚拟化异常 故障 - EPT 违例(VT-x)
21 #CP 控制保护异常 故障 - CET 违例(IBT/SHSTK)
22-28 -- 保留
29 #VC VMM 通信 故障 IST_VC AMD SEV-ES
30 #SX 安全异常 故障 - AMD
31 -- 保留
32-255 -- 外部中断(IRQ) 中断 - 设备中断
arch/x86/kernel/idt.c 将 IDT 初始化分为多个阶段,以应对早期内核没有 TSS(无法使用 IST)的情况:
阶段 1: idt_setup_early_traps()(head64.c 调用)
-- 安装 #DB、#BP 早期处理器
-- 64位上不安装 #PF(需要等 TSS 设置好后的 IST 版本)
阶段 2: idt_setup_early_pf()(mm 初始化前调用)
-- 安装不使用 IST 的 #PF 处理器(用于早期页表建立)
阶段 3: idt_setup_traps()(trap_init() 中调用)
-- 安装完整的 def_idts[],此时 TSS 尚未初始化
-- 使用 IST 的异常(NMI/DF/DB/MCE)暂不安装 IST 版本
阶段 4: cpu_init() 后
-- 重新安装带 IST 的 NMI、#DF、#DB、#MCE 处理器
-- TSS 已初始化,IST 栈有效
阶段 5: idt_setup_apic_and_irq_gates()(irq_init 中调用)
-- 安装 APIC 向量(本地定时器、SMP IPI 等)
-- 安装外部 IRQ 向量(32-255)
arch/x86/kernel/idt.c:196:
static __init void
idt_setup_from_table(gate_desc *idt, const struct idt_data *t, int size, bool sys)
{
gate_desc desc;
for (; size > 0; t++, size--) {
idt_init_desc(&desc, t); // 将 idt_data 转换为 gate_desc
write_idt_entry(idt, t->vector, &desc); // 写入 IDT 表
if (sys)
set_bit(t->vector, system_vectors); // 标记为系统向量
}
}idt_data 描述符(arch/x86/include/asm/idtentry.h):
struct idt_data {
unsigned int vector; // 向量号
unsigned int segment; // 代码段选择子
struct idt_bits bits; // IST、类型、DPL
const void *addr; // 处理函数入口地址
};arch/x86/kernel/idt.c:84,完整的默认 IDT 配置:
static const __initconst struct idt_data def_idts[] = {
INTG(X86_TRAP_DE, asm_exc_divide_error), // #DE 向量0
ISTG(X86_TRAP_NMI, asm_exc_nmi, IST_INDEX_NMI), // NMI 向量2,IST
INTG(X86_TRAP_BR, asm_exc_bounds), // #BR 向量5
INTG(X86_TRAP_UD, asm_exc_invalid_op), // #UD 向量6
INTG(X86_TRAP_NM, asm_exc_device_not_available), // #NM 向量7
INTG(X86_TRAP_OLD_MF, asm_exc_coproc_segment_overrun),// 向量9
INTG(X86_TRAP_TS, asm_exc_invalid_tss), // #TS 向量10
INTG(X86_TRAP_NP, asm_exc_segment_not_present), // #NP 向量11
INTG(X86_TRAP_SS, asm_exc_stack_segment), // #SS 向量12
INTG(X86_TRAP_GP, asm_exc_general_protection), // #GP 向量13
INTG(X86_TRAP_SPURIOUS, asm_exc_spurious_interrupt_bug),
INTG(X86_TRAP_MF, asm_exc_coprocessor_error), // #MF 向量16
INTG(X86_TRAP_AC, asm_exc_alignment_check), // #AC 向量17
INTG(X86_TRAP_XF, asm_exc_simd_coprocessor_error),// #XF 向量19
/* 64位:#DF 使用 IST(独立的双重故障栈) */
ISTG(X86_TRAP_DF, asm_exc_double_fault, IST_INDEX_DF),
/* #DB 也使用 IST(防止被调试器绕过) */
ISTG(X86_TRAP_DB, asm_exc_debug, IST_INDEX_DB),
#ifdef CONFIG_X86_MCE
ISTG(X86_TRAP_MC, asm_exc_machine_check, IST_INDEX_MCE), // IST
#endif
#ifdef CONFIG_X86_CET
INTG(X86_TRAP_CP, asm_exc_control_protection), // #CP 向量21
#endif
#ifdef CONFIG_AMD_MEM_ENCRYPT
ISTG(X86_TRAP_VC, asm_exc_vmm_communication, IST_INDEX_VC),
#endif
SYSG(X86_TRAP_OF, asm_exc_overflow), // #OF DPL=3,用户态可触发
};宏含义:
INTG:中断门,DPL=0,用户态不可直接触发ISTG:使用 IST 的中断门SYSG:系统门,DPL=3,用户态可通过 INT n 触发TSKG:任务门(仅 32 位,64 位中 #DF 改用 IST)
arch/x86/include/asm/traps.h:
enum {
IST_INDEX_DF = 0, // #DF 双重错误栈
IST_INDEX_NMI = 1, // NMI 栈
IST_INDEX_DB = 2, // #DB 调试栈
IST_INDEX_MCE = 3, // #MCE 机器检查栈
IST_INDEX_VC = 4, // #VC AMD SEV-ES 栈
IST_INDEX_CP = 5, // 保留(#CP 目前不用 IST)
};IST 栈大小(arch/x86/include/asm/page_64_types.h):
#define EXCEPTION_STKSZ (PAGE_SIZE) // 4KB per IST stack
#define DEBUG_STKSZ (PAGE_SIZE * 2)// 8KB for #DB(防递归)为了兼容 KPTI,IDT 和中断栈被映射到 cpu_entry_area(CEA),这是一个在用户页表中也可见的特殊映射区域:
/* arch/x86/include/asm/cpu_entry_area.h */
struct cpu_entry_area {
char gdt[PAGE_SIZE]; // GDT
struct entry_stack_page entry_stack_page; // syscall 入口栈
struct tss_struct tss; // TSS
char exception_stacks[...] // IST 栈
struct debug_store_buffers cpu_debug_buffers; // BTS/DS 缓冲区
};x86 架构上有两种主要的快速系统调用指令:SYSCALL(AMD 引入,64位标准)和 SYSENTER(Intel 引入,32位兼容)。两者在机制上有重要区别。
特性 SYSCALL/SYSRET SYSENTER/SYSEXIT
-----------------------------------------------------------------
引入者 AMD (K6),Intel EM64T Intel (Pentium II+)
适用场景 64位长模式 32位保护模式(64位兼容模式)
栈切换 否(需软件切换) 是(从 MSR 加载 ESP)
保存返回地址 RCX <- RIP 不保存(需软件保存)
段寄存器 从 MSR_STAR 加载 从 MSR_SYSENTER_CS 派生
性能 更快(约7-20 cycle) 相似
Linux 使用 64位原生 syscall 32位兼容模式(ia32)
入口 entry_SYSCALL_64 entry_SYSENTER_compat
当 32 位用户代码执行 SYSENTER 时,CPU 硬件自动执行:
1. CPL <- 0
2. CS <- MSR_IA32_SYSENTER_CS(内核代码段)
3. EIP <- MSR_IA32_SYSENTER_EIP(入口函数)
4. SS <- MSR_IA32_SYSENTER_CS + 8(内核数据段)
5. ESP <- MSR_IA32_SYSENTER_ESP(内核栈)
注意:SYSENTER 会切换栈(直接加载内核 ESP),而 SYSCALL 不切换栈。
与 SYSCALL 的另一个重要区别:SYSENTER 不保存返回地址,必须由用户态 vDSO(__kernel_vsyscall)通过 EBP 传递返回地址。
arch/x86/kernel/cpu/common.c:
void syscall_init(void)
{
/* SYSCALL:64位路径 */
wrmsrq(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
/* SYSENTER:32位兼容模式路径(仅 64 位内核需要) */
#ifdef CONFIG_IA32_EMULATION
wrmsrq(MSR_IA32_SYSENTER_CS, (u64)__KERNEL_CS);
wrmsrq(MSR_IA32_SYSENTER_SP, (u64)this_cpu_read(cpu_tss_rw.x86_tss.sp0));
wrmsrq(MSR_IA32_SYSENTER_IP, (u64)entry_SYSENTER_compat);
#endif
/* MSR_STAR:SYSCALL 的段选择子 */
wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);
wrmsrq(MSR_SYSCALL_MASK, /* EFLAGS 清零掩码 */ ...);
}arch/x86/entry/entry_64_compat.S:50:
SYM_CODE_START(entry_SYSENTER_compat)
UNWIND_HINT_ENTRY
ENDBR
/* 中断已关闭,执行 swapgs 切换 GS */
swapgs
/* 临时保存 EAX,切换到内核页表(KPTI) */
pushq %rax
SWITCH_TO_KERNEL_CR3 scratch_reg=%rax
popq %rax
/* 切换到内核栈 */
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp
/* 构建 pt_regs(32位兼容格式)*/
pushq $__USER_DS ; pt_regs->ss
pushq $0 ; pt_regs->sp(占位符,SYSENTER不保存ESP)
pushfq ; pt_regs->flags
pushq $__USER32_CS ; pt_regs->cs
pushq $0 ; pt_regs->ip(占位符,SYSENTER不保存EIP)
movl %eax, %eax ; 零扩展系统调用号
pushq %rax ; pt_regs->orig_ax
PUSH_AND_CLEAR_REGS rax=$-ENOSYS
/* 清除危险 EFLAGS 位 */
testl $X86_EFLAGS_NT|X86_EFLAGS_AC|X86_EFLAGS_TF, EFLAGS(%rsp)
jnz .Lsysenter_fix_flags
IBRS_ENTER
UNTRAIN_RET
CLEAR_BRANCH_HISTORY
movq %rsp, %rdi
call do_SYSENTER_32 ; C 层处理(common.c)
jmp sysret32_from_system_call ; 通过 SYSRET 或 IRET 返回
SYM_CODE_END(entry_SYSENTER_compat)arch/x86/entry/syscall_32.c:362:
__visible noinstr bool do_SYSENTER_32(struct pt_regs *regs)
{
/* SYSENTER 没有保存 EIP 和 ESP,从用户寄存器恢复 */
regs->ip = regs->dx; // EDX 传入用户返回地址(vDSO 约定)
regs->sp = regs->cx; // ECX 传入用户栈指针(vDSO 约定)
/* 设置 IF=1(SYSENTER 清除了 IF 位) */
regs->flags |= X86_EFLAGS_IF;
return do_syscall_32_irqs_on(regs);
}Linux vDSO 中的 __kernel_vsyscall 实现了用户态的 SYSENTER/INT80 调用包装:
/* arch/x86/entry/vdso/vdso32/syscall.S(简化) */
__kernel_vsyscall:
pushl %ecx ; 保存 ECX
pushl %edx ; 保存 EDX
pushl %ebp ; 保存 EBP(用于传递 ESP 给内核)
movl %esp, %ebp ; EBP = 用户 ESP
/* 系统调用号已在 EAX,参数在 EBX/ECX/EDX/ESI/EDI/EBP */
sysenter ; 进入内核(EDX=返回地址,ECX=用户ESP)
/* SYSEXIT 返回后这里继续执行 */
popl %ebp
popl %edx
popl %ecx
ret除了 SYSCALL 和 SYSENTER,Linux 还支持 INT 0x80(向量 128)。这是最早的系统调用方式,现在主要用于 32 位 ELF 程序在 64 位内核上运行:
IA32_SYSCALL_VECTOR = 0x80
IDT 中注册(idt.c:122):
SYSG(IA32_SYSCALL_VECTOR, asm_int80_emulation)
-- DPL=3,用户态可直接 INT 0x80 触发
asm_int80_emulation 入口与 SYSENTER 类似,最终调用 do_int80_emulation()。
本章在前面章节基础上深入分析页表切换机制,包括 PCID 的详细实现、TLB shootdown 以及大页(HugeTLB)支持。
当一个 CPU 修改了页表,其他 CPU 的 TLB 可能仍然缓存旧的映射,需要发送 IPI 使其失效:
CPU 0 CPU 1
| |
| munmap / unmap_page_range |
| tlb_gather_mmu() |
| flush_tlb_mm_range() |
| -- 若 nr_cpus > 1 |
| -- 发送 TLB flush IPI |---> 收到 IPI
| -- 等待 ACK | flush_tlb_func()
| | invlpg 或 write CR3
| on_each_cpu_cond() |<--- ACK
| mmu_gather 完成 |
arch/x86/mm/tlb.c 中的 native_flush_tlb_others():
void native_flush_tlb_others(const struct cpumask *cpumask,
const struct flush_tlb_info *info)
{
/* 通过 SMP IPI 向目标 CPU 发送 TLB flush 请求 */
on_each_cpu_mask(cpumask, flush_tlb_func, (void *)info, true);
}Linux 使用 mm->context.ctx_id 作为 ASID 分配的基础。当系统中有超过 4096 个不同 mm 时,ASID 被重用:
/* arch/x86/mm/tlb.c */
static void choose_new_asid(struct mm_struct *next, u64 next_tlb_gen,
u16 *new_asid, bool *need_flush)
{
u16 asid;
if (!static_cpu_has(X86_FEATURE_PCID)) {
*new_asid = 0;
*need_flush = true;
return;
}
/* 复用 mm 上次使用的 ASID */
asid = this_cpu_read(cpu_tlbstate.loaded_mm_asid);
if (this_cpu_read(cpu_tlbstate.loaded_mm) == next &&
next->context.ctx_id ==
this_cpu_read(cpu_tlbstate.ctxs[asid].ctx_id)) {
/* 同一 mm,检查 TLB 是否已是最新 */
if (this_cpu_read(cpu_tlbstate.ctxs[asid].tlb_gen) >=
next_tlb_gen) {
*new_asid = asid;
*need_flush = false;
return;
}
}
/* 分配新 ASID 或强制刷新 */
...
}x86 支持两种大页:
2MB 大页(PMD 级别):
-- PMD 条目的 PS 位(bit 7)= 1
-- 直接映射 2MB 物理内存
-- 减少 TLB 条目使用(1条 vs 512条 4KB PTE)
1GB 大页(PUD 级别):
-- PUD 条目的 PS 位(bit 7)= 1
-- 需要 CPU 支持(X86_FEATURE_PDPE1GB,CPUID.80000001H.EDX[26])
-- 适合大内存映射(如 NUMA 节点内存)
大页 PTE 格式(2MB,PMD 级别):
63 52 51 21 20 13 12 11 9 8 7 6 5 4 3 2 1 0
+--------+----------+-----+--+--+--+--+--+--+--+--+--+--+--+--+
| 保留/NX | 物理基址 | PAT | | | |G | PS| D | A |PCD|PWT|U |W |P |
+--------+----------+-----+--+--+--+--+--+--+--+--+--+--+--+--+
^ ^
PAT 位(bit12) PS=1 表示大页
x86 通过 MTRR 和 PAT 控制不同物理地址范围的缓存行为:
内存类型:
UC -- Uncacheable(无缓存,设备内存)
WC -- Write-Combining(写合并,视频帧缓冲)
WT -- Write-Through(写透,读操作有缓存)
WP -- Write-Protected(写保护,读操作有缓存)
WB -- Write-Back(写回,普通内存,最高性能)
UC- -- 弱 UC(可被 MTRR 覆盖为 WC)
PAT MSR (0x277) 的8个条目对应 PTE 中 PWT/PCD/PAT 三位的组合:
PAT[0] = PWT=0, PCD=0, PAT=0 -> WB(默认)
PAT[1] = PWT=1, PCD=0, PAT=0 -> WT
PAT[2] = PWT=0, PCD=1, PAT=0 -> UC-
PAT[3] = PWT=1, PCD=1, PAT=0 -> UC
PAT[4] = PWT=0, PCD=0, PAT=1 -> WB(可重配)
PAT[5] = PWT=1, PCD=0, PAT=1 -> WP
PAT[6] = PWT=0, PCD=1, PAT=1 -> UC-
PAT[7] = PWT=1, PCD=1, PAT=1 -> UC
APIC(Advanced Programmable Interrupt Controller,高级可编程中断控制器)是现代 x86 系统的中断管理核心,取代了传统的 8259 PIC。
系统总线
|
+------ Local APIC (CPU 0) ----+
| APIC ID = 0 |
| LVT (6 registers) |
| TPR/PPR |
| |
+------ Local APIC (CPU 1) ----+
| APIC ID = 1 |
... |
| |
+------ IO-APIC ---------------+
| IRQ0-IRQ23 |
| Redirection Table |
| |
+------ MSI/MSI-X ------------+
外设直接写 APIC 寄存器
Local APIC 默认映射在物理地址 0xFEE00000(MSR_IA32_APICBASE 可重配置)。arch/x86/include/asm/apicdef.h 定义所有偏移:
Local APIC 寄存器(MMIO 偏移,4 字节访问):
偏移 名称 说明
--------------------------------------------------------------
0x020 APIC_ID -- APIC ID(物理目标地址,xAPIC 8位)
0x030 APIC_LVR -- APIC 版本寄存器
0x080 APIC_TASKPRI -- 任务优先级(TPR)
0x090 APIC_ARBPRI -- 仲裁优先级(xAPIC)
0x0A0 APIC_PROCPRI -- 处理器优先级(PPR)
0x0B0 APIC_EOI -- 中断结束(写0)
0x0D0 APIC_LDR -- 逻辑目标寄存器
0x0E0 APIC_DFR -- 目标格式寄存器(flat/cluster)
0x0F0 APIC_SPIV -- 伪中断向量寄存器(APIC使能位在bit8)
0x100-0x170 APIC_ISR -- 服务中断寄存器(256位)
0x180-0x1F0 APIC_TMR -- 触发模式寄存器(256位)
0x200-0x270 APIC_IRR -- 中断请求寄存器(256位)
0x280 APIC_ESR -- 错误状态寄存器
0x2F0 APIC_LVTCMCI -- LVT CMCI
0x300 APIC_ICR -- 中断命令寄存器低32位(IPI)
0x310 APIC_ICR2 -- 中断命令寄存器高32位(xAPIC目标)
0x320 APIC_LVTT -- LVT 定时器
0x330 APIC_LVTTHMR -- LVT 热管理
0x340 APIC_LVTPC -- LVT 性能计数器
0x350 APIC_LVT0 -- LVT LINT0(连接 8259 PIC)
0x360 APIC_LVT1 -- LVT LINT1(连接 NMI)
0x370 APIC_LVTERR -- LVT 错误
0x380 APIC_TMICT -- 初始计数寄存器
0x390 APIC_TMCCT -- 当前计数寄存器(只读)
0x3E0 APIC_TDCR -- 分频配置寄存器
arch/x86/kernel/apic/apic.c:1503,Local APIC 初始化步骤:
static void setup_local_APIC(void)
{
/* 1. 软禁用 APIC(初始化期间) */
value = apic_read(APIC_SPIV);
value &= ~APIC_SPIV_APIC_ENABLED;
apic_write(APIC_SPIV, value);
/* 2. 设置逻辑目标格式(flat 或 cluster) */
if (apic->init_apic_ldr)
apic->init_apic_ldr();
/* 3. 设置任务优先级:接受所有向量 >= 32 */
value = apic_read(APIC_TASKPRI);
value &= ~APIC_TPRI_MASK;
value |= 0x10; /* 接受 vector >= 0x10 */
apic_write(APIC_TASKPRI, value);
/* 4. 清空 ISR(服务中断寄存器)中的遗留中断 */
apic_clear_isr();
/* 5. 重新启用 APIC,设置伪中断向量 */
value = apic_read(APIC_SPIV);
value |= APIC_SPIV_APIC_ENABLED;
value |= SPURIOUS_APIC_VECTOR; /* 通常 = 0xFF */
apic_write(APIC_SPIV, value);
/* 6. 初始化 LVT(本地向量表) */
/* LVT LINT0:连接 8259 PIC ExtINT 中断 */
/* LVT LINT1:连接 NMI */
/* LVT 错误:设置 ERROR_APIC_VECTOR */
/* LVT 定时器:由 lapic_timer_setup 配置 */
}Local APIC 定时器是 x86 系统上常用的高精度中断源,有三种工作模式:
模式 1 - One-Shot(单次):
TMICT 写入计数值,递减到 0 触发中断,停止
模式 2 - Periodic(周期):
TMICT 写入计数值,每次减到 0 触发中断并重装载
模式 3 - TSC Deadline(TSC 截止时间):
需要 X86_FEATURE_TSC_DEADLINE_TIMER 支持
不写 TMICT,而是写 MSR_IA32_TSC_DEADLINE
当 TSC >= 该值时触发中断
精度最高(纳秒级),消除 APIC 时钟和 TSC 之间的抖动
Linux 内核优先使用 TSC Deadline 模式(lapic_timer_setup() 中选择)。
IPI 通过 ICR(中断命令寄存器)发送,是 SMP 系统的核心机制:
/* arch/x86/kernel/apic/apic.c */
void __default_send_IPI_dest_field(unsigned int mask, int vector,
unsigned int dest)
{
/* xAPIC 方式(MMIO):先写 ICR2(目标),再写 ICR(触发) */
apic_write(APIC_ICR2, SET_XAPIC_DEST_FIELD(mask)); // 229
apic_write(APIC_ICR, dest | APIC_DEST_PHYSICAL |
APIC_DM_FIXED | vector); // 230
}ICR 字段:
ICR(低32位):
bit[7:0] -- 向量号(中断向量)
bit[10:8] -- 投递模式
000 = Fixed(固定向量)
010 = SMI
100 = NMI
101 = INIT
110 = Startup(SIPI)
bit 11 -- 目标模式(0=物理APIC ID,1=逻辑)
bit 12 -- 投递状态(只读,0=空闲)
bit 14 -- Level(0=Deassert,1=Assert)
bit 15 -- 触发模式(0=边沿,1=电平)
bit[19:18] -- 目标简写(0=无,1=自身,2=所有,3=其他所有)
ICR2(高32位,xAPIC):
bit[31:24] -- 目标 APIC ID(物理模式)
常见 IPI 类型:
/* 重调度 IPI:通知目标 CPU 运行调度器 */
RESCHEDULE_VECTOR = 0xfd
/* 调用函数 IPI:在目标 CPU 执行指定函数 */
CALL_FUNCTION_VECTOR = 0xfc
CALL_FUNCTION_SINGLE_VECTOR = 0xfb
/* TLB flush IPI */
/* 由 native_flush_tlb_others() 发送,无固定向量号 */
/* SIPI(Startup IPI):唤醒 AP */
/* 在 SMP 初始化中使用,向量号 = 起始页面号 */x2APIC 是 xAPIC 的扩展,于 2008 年引入(Intel Nehalem+ 支持):
xAPIC vs x2APIC 对比:
xAPIC(传统模式):
- 通过 MMIO 访问(0xFEE00000 映射)
- APIC ID 最大 8 位(255个CPU)
- 读写需要映射内存页
x2APIC(扩展模式):
- 通过 MSR 访问(基址 0x800 + offset/16)
- APIC ID 最大 32 位(支持数百万CPU)
- 读写更快(无需 MMIO 映射)
- 需要 IOMMU 中断重映射支持(安全隔离)
启用 x2APIC(arch/x86/kernel/apic/apic.c:1729):
static void __x2apic_enable(void)
{
u64 msr;
rdmsrq(MSR_IA32_APICBASE, msr);
if (msr & X2APIC_ENABLE)
return; // 已启用,直接返回
wrmsrq(MSR_IA32_APICBASE, msr | X2APIC_ENABLE); // 设置 bit10
printk_once(KERN_INFO "x2apic enabled\n");
}x2APIC 下 MSR 访问方式(arch/x86/include/asm/apic.h):
/* x2APIC 读寄存器:通过 MSR(基址 0x800 + offset/16)*/
static inline u32 native_apic_msr_read(u32 reg)
{
u64 msr;
rdmsrq(APIC_BASE_MSR + (reg >> 4), msr);
return (u32)msr;
}
/* x2APIC 写寄存器 */
static inline void native_apic_msr_write(u32 reg, u32 v)
{
wrmsrq(APIC_BASE_MSR + (reg >> 4), v);
}
/* x2APIC 发送 IPI:直接写 ICR MSR(单次写,无需先写 ICR2)*/
static inline void x2apic_send_IPI(u32 dest, u32 low)
{
wrmsrq(APIC_BASE_MSR + (APIC_ICR >> 4), ((__u64)dest) << 32 | low);
}IO-APIC 负责管理外部设备(PCI、ISA 总线)的中断,将它们路由到指定的 Local APIC:
IO-APIC 内部结构:
外设中断引脚(IRQ0-IRQ23)
|
+---+-------------------+
| 重定向表(RTE) |
| RTE[0]: IRQ0 配置 |
| RTE[1]: IRQ1 配置 |
| ... |
| RTE[23]: IRQ23 配置 |
+---+-------------------+
|
系统总线 -> 目标 Local APIC(根据 RTE 配置)
RTE(64位重定向表条目)字段:
bit[7:0] -- 目标中断向量(32-255)
bit[10:8] -- 投递模式(Fixed/Lowest/NMI/INIT)
bit 11 -- 目标模式(物理/逻辑)
bit 12 -- 投递状态(只读)
bit 13 -- 极性(0=高有效,1=低有效)
bit 14 -- 远程 IRR(只读,电平触发)
bit 15 -- 触发模式(0=边沿,1=电平)
bit 16 -- 中断屏蔽(0=发送,1=屏蔽)
bit[63:56] -- 目标字段(物理模式下=APIC ID)
x86 处理器内置了强大的硬件调试支持,包括 4 个断点地址寄存器(DR0-DR3)、状态寄存器(DR6)和控制寄存器(DR7),支持指令执行、内存读写、I/O 端口访问的断点。
DR0 -- 断点地址 0
DR1 -- 断点地址 1
DR2 -- 断点地址 2
DR3 -- 断点地址 3
DR4 -- 保留(别名 DR6,已废弃)
DR5 -- 保留(别名 DR7,已废弃)
DR6 -- 调试状态寄存器(Debug Status Register)
DR7 -- 调试控制寄存器(Debug Control Register)
调试寄存器访问通过汇编指令(需要 CPL=0):
mov %db0, %rax ; 读取 DR0
mov %rax, %db0 ; 写入 DR0arch/x86/include/asm/debugreg.h 提供封装:
/* 读取调试寄存器 */
#define get_debugreg(var, register) \
(var) = native_get_debugreg(register)
/* 写入调试寄存器 */
#define set_debugreg(value, register) \
native_set_debugreg(register, value)DR6 位定义(arch/x86/include/uapi/asm/debugreg.h):
63 16 15 14 13 12 11 3 2 1 0
+-------------+--+--+---+---+---+---+---+---+---+
| 保留(全1) |BT|BS|BD |RTM|BLD| | B2| B1| B0|
+-------------+--+--+---+---+---+---+---+---+---+
B0 (bit 0) DR_TRAP0 -- DR0 断点触发
B1 (bit 1) DR_TRAP1 -- DR1 断点触发
B2 (bit 2) DR_TRAP2 -- DR2 断点触发
B3 (bit 3) DR_TRAP3 -- DR3 断点触发
BLD(bit 11) DR_BUS_LOCK -- 总线锁检测(BLD 支持时非保留)
BD (bit 13) -- 调试寄存器访问触发
BS (bit 14) DR_STEP -- 单步执行触发(TF=1)
BT (bit 15) DR_SWITCH -- 任务切换触发(T 位)
RTM(bit 16) -- RTM(受限事务内存)区域内 #DB(RTM支持时非保留)
默认值(上电/复位/INIT):0xFFFF0FF0(保留位为1)
DR7 位定义:
31 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
+------------+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| LEN3 R/W3 | --| --| LEN2 R/W2 | -- | -- | LEN1 R/W1 | -- | --| LEN0 R/W0 |
| (2+2 bits) | | |(2+2 bits) | | |(2+2 bits) | | |(2+2 bits) |
+------------+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
G3 L3 G2 L2 G1 L1 G0 L0
L0-L3 (local enable) -- 本地(任务切换时自动清除)
G0-G3 (global enable) -- 全局(跨任务有效)
R/W0-3:
00 = 执行断点(指令获取)
01 = 写断点(内存写)
10 = I/O 读写断点(需要 CR4.DE=1)
11 = 读写断点(内存读或写)
LEN0-3(断点长度):
00 = 1 字节
01 = 2 字节
10 = 8 字节(仅64位,LEN=10)
11 = 4 字节
bit 10 固定为 1(保留位,架构规定)
arch/x86/include/uapi/asm/debugreg.h 中的常量:
#define DR_RW_EXECUTE (0x0) // 执行断点
#define DR_RW_WRITE (0x1) // 写断点
#define DR_RW_READ (0x3) // 读写断点
#define DR_LEN_1 (0x0) // 1 字节
#define DR_LEN_2 (0x4) // 2 字节
#define DR_LEN_4 (0xC) // 4 字节
#define DR_LEN_8 (0x8) // 8 字节
#define DR_LOCAL_ENABLE_SHIFT 0 // L0 位偏移
#define DR_GLOBAL_ENABLE_SHIFT 1 // G0 位偏移
#define DR_CONTROL_SHIFT 16 // R/W0 位偏移(从bit16开始,每4位一组)
#define DR_CONTROL_SIZE 4 // 每个断点4位控制(LEN2 + R/W2)ptrace 设置硬件断点通过 perf_event 子系统:
ptrace(PTRACE_SETHWBREAK, pid, addr, data)
|
v
arch/x86/kernel/ptrace.c
-> ptrace_set_debugreg()
-> set_debugreg(value, regnum)
-> native_set_debugreg()
-> asm("mov %0, %%db0" ...)
内核在进程切换时保存/恢复调试寄存器(arch/x86/kernel/process_64.c):
void switch_to_extra(struct task_struct *prev_p,
struct task_struct *next_p)
{
/* 若 prev 使用了调试寄存器,清除它们 */
if (test_tsk_thread_flag(prev_p, TIF_DEBUG))
hw_breakpoint_deactivate(prev_p);
/* 若 next 使用了调试寄存器,恢复它们 */
if (test_tsk_thread_flag(next_p, TIF_DEBUG))
hw_breakpoint_restore(next_p);
}EFLAGS 的 TF(Trap Flag,位 8)启用单步模式:
TF=1 效果:
每条指令执行后触发 #DB 异常(向量 1)
CPU 硬件自动清除 TF 位
进入 #DB 处理程序时 TF=0(防止递归)
Linux 单步实现:
ptrace(PTRACE_SINGLESTEP, pid, ...)
-> set_tsk_thread_flag(child, TIF_SINGLESTEP)
-> child->thread.debugreg6 |= DR_STEP
在 syscall 返回时检查 TIF_SINGLESTEP,
设置 EFLAGS.TF = 1,使用户态下一条指令触发 #DB
arch/x86/kernel/traps.c 中的 #DB 处理:
DEFINE_IDTENTRY_DEBUG(exc_debug)
{
struct pt_regs *regs = current_pt_regs();
unsigned long dr6;
/* 读取 DR6 状态 */
get_debugreg(dr6, 6);
if (dr6 & DR_STEP) {
/* 单步触发:通知 ptrace */
if (user_mode(regs)) {
/* 触发 SIGTRAP 发送给被跟踪进程 */
force_sig_fault(SIGTRAP, TRAP_TRACE,
(void __user *)regs->ip);
}
}
/* 处理硬件断点 */
if (dr6 & DR_TRAP_BITS) {
for (i = 0; i < 4; i++) {
if (dr6 & (DR_TRAP0 << i))
handle_hw_breakpoint(i, regs);
}
}
/* 清除 DR6 状态 */
set_debugreg(0, 6);
}arch/x86/include/asm/debugreg.h:
/* per-CPU DR7 shadow,避免频繁读取 MSR */
DECLARE_PER_CPU(unsigned long, cpu_dr7);hw_breakpoint_deactivate() 和 hw_breakpoint_restore() 通过操作 DR0-DR3 和 DR7 来启用/禁用硬件断点,避免不同进程的断点互相干扰。
x86 硬件虚拟化通过 VMX(Virtual Machine Extensions,Intel VT-x)和 SVM(Secure Virtual Machine,AMD-V)实现,Linux KVM 是主要使用者。
Host(宿主机) Guest(虚拟机)
| |
| VMX Root(特权) | VMX Non-Root(受限)
| Ring 0 = 内核 | Ring 0 = Guest OS 内核
| Ring 3 = 用户态 | Ring 3 = Guest 用户态
| |
|<--- VM Exit |
| -- CPU 从 Guest 退出 |
| -- KVM 处理特权操作 |
| |
|--> VM Entry |
-- CPU 进入 Guest |
VMX 引入了两条核心指令:
VMXON:进入 VMX 操作模式(Root 模式)VMXOFF:退出 VMX 操作模式VMLAUNCH:启动新的 VM(首次进入 Guest)VMRESUME:恢复 VM 执行(后续进入 Guest)VMEXIT:VM 退出(硬件触发,由 Guest 的特权操作引起)
VMCS(Virtual Machine Control Structure)是 VMX 的核心数据结构,每个 vCPU 一份:
VMCS 布局(4KB 对齐,硬件维护):
+---------------------------+
| VMCS 修订标识符 | 用于识别 VMCS 格式版本
+---------------------------+
| 中止指示器 | VMX 中止原因
+---------------------------+
| VMCS 数据区 |
| Host-State Area | Host(KVM)保存的状态
| RIP, RSP, CR3, ... |
| CS, SS, TR, GDT, IDT |
| Guest-State Area | Guest 的状态
| RIP, RSP, RFLAGS, ... |
| CR0, CR3, CR4, EFER |
| CS, SS, DS, FS, GS, TR |
| VM-Execution Controls | 控制 VM 执行行为
| Pin-based controls | 外部中断、NMI
| Proc-based controls | CPUID、HLT、RDTSC...
| Secondary controls | EPT、VPID、RDTSCP...
| VM-Exit Controls | VM-Exit 行为
| VM-Entry Controls | VM-Entry 行为
| VM-Exit Information | 退出原因、退出信息
+---------------------------+
KVM 中的 VMCS 操作(arch/x86/kvm/vmx/vmx.c):
/* 读 VMCS 字段 */
#define vmcs_readl(field) __vmcs_readl(field)
#define vmcs_read32(field) vmcs_readl(field)
#define vmcs_read64(field) vmcs_readl(field)
#define vmcs_read16(field) vmcs_readl(field)
/* 写 VMCS 字段 */
#define vmcs_writel(field, value) __vmcs_writel(field, value)
#define vmcs_write32(field, value) vmcs_writel(field, (unsigned long)(value))arch/x86/kvm/vmx/vmx.c:2987,kvm_cpu_vmxon() 函数:
static int kvm_cpu_vmxon(u64 vmxon_pointer)
{
u64 msr;
/* 1. 设置 CR4.VMXE=1(允许执行 VMXON) */
cr4_set_bits(X86_CR4_VMXE);
/* 2. 执行 VMXON 指令,传入 VMXON region 的物理地址 */
asm goto("1: vmxon %[vmxon_pointer]\n\t"
_ASM_EXTABLE(1b, %l[fault])
: : [vmxon_pointer] "m"(vmxon_pointer)
: : fault);
return 0;
fault:
/* VMXON 失败:可能是 MSR_IA32_FEAT_CTL 未正确设置 */
WARN_ONCE(1, "VMXON faulted, MSR_IA32_FEAT_CTL = 0x%llx\n",
rdmsrq_safe(MSR_IA32_FEAT_CTL, &msr) ? 0xdeadbeef : msr);
cr4_clear_bits(X86_CR4_VMXE);
return -EFAULT;
}VMX 启用的完整前提条件:
1. CPUID.1:ECX[5] = 1(VMX 支持)
2. MSR_IA32_FEAT_CTL[bit2] = 1(VMX Outside SMX 启用)
且 MSR_IA32_FEAT_CTL[bit0] = 1(锁定位)
3. CR4.VMXE = 1
4. CR0.PE = 1,CR0.NE = 1,CR0.PG = 1
5. 执行 VMXON(传入已初始化的 VMXON region)
EPT 是 VMX 的二级地址转换机制,将 Guest 物理地址(GPA)转换为 Host 物理地址(HPA):
Guest 虚拟地址(GVA)
|
Guest CR3(GPA)
|
Guest 页表遍历 ---> Guest 物理地址(GPA)
|
EPT 表遍历
|
Host 物理地址(HPA)
EPT 结构与普通页表相同(4级,512项/级):
EPTP -> PML4E -> PDPTE -> PDE -> PTE -> 物理页
EPT 违例(EPT Violation):
-- GPA 无对应 EPT 映射
-- 权限不符
-- 触发 VM Exit,KVM 处理缺页
类似于 PCID 对于用户/内核页表的优化,VPID 为不同 VM 的 TLB 条目打标签,避免 VM Entry/Exit 时全部刷 TLB:
CR3 切换(宿主机与虚拟机之间):
无 VPID:每次 VM Entry/Exit 都要 TLB flush
有 VPID:VM Entry/Exit 保留有效 TLB 条目(标有 VPID)
只有同一 VPID 的条目才相互竞争
AMD 的 SVM 虚拟化扩展与 VMX 类似,但使用 VMCB(Virtual Machine Control Block)代替 VMCS:
SVM 核心指令:
VMRUN -- 进入 Guest(类似 VMLAUNCH/VMRESUME)
VMEXIT -- 由 Guest 的特权操作触发(Guest 退出)
VMLOAD -- 从 VMCB 加载部分状态(段寄存器等)
VMSAVE -- 保存部分状态到 VMCB
STGI -- 设置全局中断标志(Guest 中断使能)
CLGI -- 清除全局中断标志
SVM 启用(arch/x86/kvm/svm/svm.c:514):
static int svm_enable_virtualization_cpu(void)
{
uint64_t efer;
/* 1. 读取 EFER */
rdmsrq(MSR_EFER, efer);
if (efer & EFER_SVME)
return -EBUSY;
/* 2. 设置 EFER.SVME=1(启用 SVM) */
wrmsrq(MSR_EFER, efer | EFER_SVME);
/* 3. 设置 MSR_VM_HSAVE_PA(Host 状态保存区物理地址)*/
wrmsrq(MSR_VM_HSAVE_PA, sd->save_area_pa);
return 0;
}VMCB 结构(arch/x86/include/asm/svm.h):
struct vmcb {
struct vmcb_control_area control; // 控制区(拦截位图、ASID等)
struct vmcb_save_area save; // 保存区(段寄存器、通用寄存器等)
};VMCB 控制区包含拦截位图(intercept bitmap),决定哪些操作触发 VMEXIT:
拦截位图结构(vmcb_control_area):
INTERCEPT_CR_READ/WRITE -- CR 寄存器访问
INTERCEPT_DR_READ/WRITE -- DR 寄存器访问
INTERCEPT_EXCEPTION -- 异常(每个向量独立控制)
INTERCEPT_INTR -- 外部中断
INTERCEPT_NMI -- NMI
INTERCEPT_CPUID -- CPUID 指令
INTERCEPT_HLT -- HLT 指令
INTERCEPT_IOIO_PROT -- I/O 端口访问
INTERCEPT_MSR_PROT -- MSR 访问
INTERCEPT_VMRUN -- VMRUN(防止嵌套虚拟化)
INTERCEPT_VMMCALL -- VMMCALL(Hypercall)
Guest 执行触发 VM-Exit
|
v
硬件保存 Guest 状态 -> VMCS/VMCB
硬件恢复 Host 状态 <- VMCS/VMCB
|
v
KVM vCPU run loop(arch/x86/kvm/x86.c)
kvm_arch_vcpu_ioctl_run()
-> vcpu_run()
-> vcpu_enter_guest()
-> vmx_vcpu_run() / svm_vcpu_run()
[VM Entry -> Guest 运行 -> VM Exit]
-> vmx_handle_exit() / svm_handle_exit()
-> 根据 exit reason 分发处理
EXIT_REASON_CPUID -> kvm_emulate_cpuid()
EXIT_REASON_HLT -> kvm_emulate_halt()
EXIT_REASON_MSR_READ -> kvm_msr_read()
EXIT_REASON_EPT_VIOL -> kvm_mmu_page_fault()
EXIT_REASON_IO_INSTR -> kvm_io_bus_read/write()
...
CET(Control-flow Enforcement Technology)是 Intel 从第 11 代 Tiger Lake 开始引入的控制流完整性(CFI)硬件机制,包含两个独立功能:影子栈(Shadow Stack)和间接分支跟踪(Indirect Branch Tracking)。
CET 由两个组件组成:
1. Shadow Stack (SHSTK) -- 防止 ROP(Return-Oriented Programming)攻击
-- 维护一个独立的只读返回地址影子栈
-- CALL 时同时将返回地址写入影子栈
-- RET 时比较普通栈和影子栈的返回地址
2. Indirect Branch Tracking (IBT) -- 防止 JOP(Jump-Oriented Programming)
-- 合法的间接跳转/调用目标必须以 ENDBR64/ENDBR32 指令开头
-- 若跳转到没有 ENDBR 的地址,触发 #CP 异常
arch/x86/include/asm/msr-index.h:546-561:
#define MSR_IA32_U_CET 0x000006a0 // 用户态 CET 控制
#define MSR_IA32_S_CET 0x000006a2 // 内核态 CET 控制
/* CET 控制 MSR 位定义 */
#define CET_SHSTK_EN BIT_ULL(0) // 影子栈启用
#define CET_WRSS_EN BIT_ULL(1) // WRSS 指令启用(用户可写影子栈)
#define CET_ENDBR_EN BIT_ULL(2) // IBT 启用(ENDBR 检查)
#define CET_LEG_IW_EN BIT_ULL(3) // 遗留 CALL/JMP 转换
#define CET_SUPPRESS BIT_ULL(10) // 抑制下一条间接转移的 ENDBR 检查
#define CET_WAIT_ENDBR BIT_ULL(11) // 等待 ENDBR 状态(只读)
#define MSR_IA32_PL0_SSP 0x000006a4 // Ring 0 影子栈指针
#define MSR_IA32_PL3_SSP 0x000006a7 // Ring 3 影子栈指针arch/x86/kernel/cpu/common.c:621:
static __always_inline void setup_cet(struct cpuinfo_x86 *c)
{
bool user_shstk, kernel_ibt;
if (!IS_ENABLED(CONFIG_X86_CET))
return;
/* 检查内核 IBT(间接分支跟踪)是否可用 */
kernel_ibt = HAS_KERNEL_IBT && cpu_feature_enabled(X86_FEATURE_IBT);
/* 检查用户影子栈是否可用 */
user_shstk = cpu_feature_enabled(X86_FEATURE_SHSTK) &&
IS_ENABLED(CONFIG_X86_USER_SHADOW_STACK);
if (!kernel_ibt && !user_shstk)
return;
/* 标记用户影子栈特性 */
if (user_shstk)
set_cpu_cap(c, X86_FEATURE_USER_SHSTK);
/* 启用内核 IBT:写入 MSR_IA32_S_CET.ENDBR_EN */
if (kernel_ibt)
wrmsrq(MSR_IA32_S_CET, CET_ENDBR_EN);
else
wrmsrq(MSR_IA32_S_CET, 0);
/* 设置 CR4.CET=1(使 CET 生效)*/
cr4_set_bits(X86_CR4_CET);
/* 进行 IBT 自检(验证 ENDBR 机制是否正常工作)*/
if (kernel_ibt && ibt_selftest()) {
pr_err("IBT selftest: Failed!\n");
wrmsrq(MSR_IA32_S_CET, 0);
setup_clear_cpu_cap(X86_FEATURE_IBT);
}
}正常函数调用(SHSTK 启用后):
CALL 指令执行时:
普通栈:push 返回地址(可被攻击者修改)
影子栈:自动 push 返回地址(只读,硬件保护)
普通栈 影子栈(只读)
+----------+ +----------+
RSP -> | 返回地址 | SSP -> | 返回地址 |
| 帧指针 | +----------+
| 局部变量 |
+----------+
RET 指令执行时:
1. 从普通栈弹出返回地址(pop RIP)
2. 从影子栈弹出返回地址
3. 比较两个返回地址
4. 若不匹配 -> 触发 #CP(控制保护异常)
ROP 攻击场景:
攻击者修改普通栈上的返回地址 -> 指向 gadget
RET 时:普通栈返回地址 != 影子栈返回地址 -> #CP
攻击被阻断!
arch/x86/kernel/shstk.c:97-110:
static unsigned long alloc_shstk(unsigned long addr, unsigned long size,
unsigned long token_offset, bool set_res_tok)
{
/* VM_SHADOW_STACK | VM_WRITE 映射(只能通过特殊指令写入)*/
int flags = MAP_ANONYMOUS | MAP_PRIVATE | MAP_ABOVE4G;
unsigned long mapped_addr, unused;
if (addr)
flags |= MAP_FIXED_NOREPLACE;
mmap_write_lock(mm);
/* 注意:PROT_READ only,VM_WRITE 是内核设置的,用户态不能用普通写 */
mapped_addr = do_mmap(NULL, addr, size, PROT_READ, flags,
VM_SHADOW_STACK | VM_WRITE, 0, &unused, NULL);
mmap_write_unlock(mm);
return mapped_addr;
}影子栈的内存保护:
- 映射为
VM_SHADOW_STACK,内核在 PTE 中设置特殊标记 - 用户态普通
mov [addr], val无法写入影子栈(产生 #GP) - 只有
WRSS(Write to Shadow Stack)指令可以写入(需要 WRSS_EN)
arch/x86/kernel/shstk.c:194,shstk_alloc_thread_stack():
unsigned long shstk_alloc_thread_stack(struct task_struct *tsk, u64 clone_flags,
unsigned long stack_size)
{
struct thread_shstk *shstk = &tsk->thread.shstk;
unsigned long addr, size;
/* 若新线程不使用影子栈,直接返回 */
if (!features_enabled(ARCH_SHSTK_SHSTK))
return 0;
/* CLONE_VFORK:子进程共享父进程影子栈 */
if (clone_flags & CLONE_VFORK) {
shstk->base = 0;
shstk->size = 0;
return 0;
}
/* !CLONE_VM(fork):子进程使用自己的影子栈副本 */
if (!(clone_flags & CLONE_VM))
return 0;
/* 线程(CLONE_VM):分配新的影子栈 */
size = adjust_shstk_size(stack_size); // 通常 = 普通栈大小
addr = alloc_shstk(0, size, 0, false);
if (IS_ERR_VALUE(addr))
return addr;
shstk->base = addr;
shstk->size = size;
return addr + size; // 返回影子栈顶(初始 SSP 值)
}IBT 要求所有间接跳转/调用的目标必须以 ENDBR64(64位)或 ENDBR32(32位)指令开头:
IBT 工作原理:
call *%rax ; 间接调用
asm volatile("nop") ; CPU 标记下一条指令必须是 ENDBR
...
target_func:
ENDBR64 ; 合法目标标记(编译器自动插入)
...
evil_gadget:
pop %rsp ; 没有 ENDBR -> 间接跳到此处会触发 #CP
ret
ENDBR64 指令的编码是 F3 0F 1E FA,在不支持 CET 的 CPU 上解码为 repne nop(空指令),向后兼容。
内核 IBT 通过编译选项 -fcf-protection=branch 启用(arch/x86/Makefile),编译器自动在所有间接跳转目标处插入 ENDBR。
arch/x86/kernel/cet.c,控制保护异常的分发:
DEFINE_IDTENTRY_ERRORCODE(exc_control_protection)
{
if (user_mode(regs)) {
/* 用户态 #CP */
if (cpu_feature_enabled(X86_FEATURE_USER_SHSTK))
do_user_cp_fault(regs, error_code);
else
do_unexpected_cp(regs, error_code);
} else {
/* 内核态 #CP(通常是 IBT 违例)*/
if (cpu_feature_enabled(X86_FEATURE_IBT))
do_kernel_cp_fault(regs, error_code);
else
do_unexpected_cp(regs, error_code);
}
}错误码(cp_error_code 枚举,cet.c):
enum cp_error_code {
CP_EC = (1 << 15) - 1, // 错误码掩码
CP_RET = 1, // NEAR RET 违例(影子栈不匹配)
CP_IRET = 2, // FAR RET/IRET 违例
CP_ENDBR = 3, // 缺少 ENDBR(IBT 违例)
CP_RSTRORSSP = 4, // RSTORSSP 指令违例
CP_SETSSBSY = 5, // SETSSBSY 指令违例
CP_ENCL = 1 << 15, // SGX Enclave 内发生的违例
};arch/x86/kernel/cet.c:50:
static void do_user_cp_fault(struct pt_regs *regs, unsigned long error_code)
{
struct task_struct *tsk;
unsigned long ssp;
/*
* 读取当前 SSP(影子栈指针):在中断关闭时从 MSR 读取,
* 避免之后需要锁定 fpregs 才能读取。
*/
rdmsrq(MSR_IA32_PL3_SSP, ssp);
cond_local_irq_enable(regs);
tsk = current;
tsk->thread.error_code = error_code;
tsk->thread.trap_nr = X86_TRAP_CP;
/* 速率限制日志输出 */
if (show_unhandled_signals && unhandled_signal(tsk, SIGSEGV) &&
__ratelimit(&cpf_rate)) {
pr_emerg("%s[%d] control protection ip:%lx sp:%lx ssp:%lx error:%lx(%s)%s",
tsk->comm, task_pid_nr(tsk),
regs->ip, regs->sp, ssp, error_code,
cp_err_string(error_code),
error_code & CP_ENCL ? " in enclave" : "");
}
/* 向用户进程发送 SIGSEGV(SEGV_CPERR 子码)*/
force_sig_fault(SIGSEGV, SEGV_CPERR, (void __user *)0);
cond_local_irq_disable(regs);
}用户程序可以通过 arch_prctl 系统调用控制自身的 CET 功能:
/* arch/x86/kernel/process_64.c */
case ARCH_SHSTK_ENABLE:
/* 启用指定的影子栈功能(SHSTK 或 WRSS) */
return shstk_prctl(task, ARCH_SHSTK_ENABLE, arg3);
case ARCH_SHSTK_DISABLE:
/* 禁用指定功能 */
return shstk_prctl(task, ARCH_SHSTK_DISABLE, arg3);
case ARCH_SHSTK_LOCK:
/* 锁定当前配置(防止被攻击者禁用 SHSTK)*/
return shstk_prctl(task, ARCH_SHSTK_LOCK, arg3);
case ARCH_SHSTK_UNLOCK:
/* 解锁(需要 CAP_SYS_ADMIN)*/
return shstk_prctl(task, ARCH_SHSTK_UNLOCK, arg3);Linux 内核从 v6.2 开始支持内核级 IBT,通过两种方式实现:
粗粒度 IBT(kIBT):
-- 内核所有函数入口自动添加 ENDBR64
-- 防止 ROP/JOP 攻击内核
-- 可通过 ibt=off 内核参数禁用
精细粒度 IBT(FineIBT):
-- 更严格的 CFI:不仅检查 ENDBR,还验证调用类型
-- 通过 __cfi_check() 函数实现
-- 类似于 LLVM CFI
-- 性能开销略高于 kIBT
| 功能 | 文件 | 关键行 |
|---|---|---|
| syscall 入口汇编 | arch/x86/entry/entry_64.S |
87-170 |
__switch_to_asm |
arch/x86/entry/entry_64.S |
177-217 |
| PUSH_REGS/POP_REGS 宏 | arch/x86/entry/calling.h |
68-149 |
| KPTI CR3 切换宏 | arch/x86/entry/calling.h |
151-291 |
__switch_to C 函数 |
arch/x86/kernel/process_64.c |
609-720 |
| FS/GS base 切换 | arch/x86/kernel/process_64.c |
237-410 |
thread_struct 定义 |
arch/x86/include/asm/processor.h |
448-517 |
x86_hw_tss 定义 |
arch/x86/include/asm/processor.h |
308-327 |
tss_struct 定义 |
arch/x86/include/asm/processor.h |
400-409 |
cpuinfo_x86 定义 |
arch/x86/include/asm/processor.h |
124-190 |
desc_struct 定义 |
arch/x86/include/asm/desc_defs.h |
66-71 |
gate_struct 定义 |
arch/x86/include/asm/desc_defs.h |
134-143 |
idt_bits 定义 |
arch/x86/include/asm/desc_defs.h |
119-125 |
| 页表级别常数 | arch/x86/include/asm/pgtable_64_types.h |
47-88 |
| 五级页表检测 | arch/x86/include/asm/pgtable_64_types.h |
31-37 |
| 内存布局常量 | arch/x86/include/asm/pgtable_64_types.h |
96-120 |
| GDT 初始化 | arch/x86/kernel/cpu/common.c |
210-252 |
| PCID 禁用参数 | arch/x86/kernel/cpu/common.c |
257-271 |
| SMEP/SMAP 设置 | arch/x86/kernel/cpu/common.c |
371-410 |
| CR4 初始化 | arch/x86/kernel/cpu/common.c |
502-515 |
| CET 初始化(setup_cet) | arch/x86/kernel/cpu/common.c |
621-660 |
cpu_detect |
arch/x86/kernel/cpu/common.c |
927-950 |
get_cpu_cap |
arch/x86/kernel/cpu/common.c |
1003-1040 |
syscall_init / MSR 设置 |
arch/x86/kernel/cpu/common.c |
2258-2308 |
| PTI 初始化 | arch/x86/mm/pti.c |
61-107 |
| PGD 用户页表更新 | arch/x86/mm/pti.c |
131-170 |
pgd_ctor |
arch/x86/mm/pgtable.c |
82-93 |
| 各级页表 TLB 释放 | arch/x86/mm/pgtable.c |
27-56 |
| 引导扇区/setup 代码 | arch/x86/boot/header.S |
全文 |
| 实模式 C 主函数 | arch/x86/boot/main.c |
139-198 |
| 保护模式转换 | arch/x86/boot/pm.c |
100-120 |
| CR0.PE 设置与远跳转 | arch/x86/boot/pmjump.S |
全文 |
| IDT 初始化阶段 1 | arch/x86/kernel/idt.c |
55-76 |
| IDT def_idts 完整表 | arch/x86/kernel/idt.c |
84-120 |
| IDT APIC 向量 | arch/x86/kernel/idt.c |
133-173 |
| idt_setup_from_table | arch/x86/kernel/idt.c |
196-207 |
| idt_setup_early_traps | arch/x86/kernel/idt.c |
225-230 |
| idt_setup_traps | arch/x86/kernel/idt.c |
235-241 |
| SYSENTER compat 入口 | arch/x86/entry/entry_64_compat.S |
50-134 |
| do_SYSENTER_32 | arch/x86/entry/syscall_32.c |
362-380 |
| Local APIC 初始化 | arch/x86/kernel/apic/apic.c |
1503-1600 |
| x2APIC 启用 | arch/x86/kernel/apic/apic.c |
1729-1740 |
| x2APIC MSR 访问 | arch/x86/include/asm/apic.h |
208-240 |
| APIC 寄存器定义 | arch/x86/include/asm/apicdef.h |
30-153 |
| 调试寄存器头文件 | arch/x86/include/asm/debugreg.h |
全文 |
| 调试寄存器 UAPI | arch/x86/include/uapi/asm/debugreg.h |
全文 |
| VMX 硬件启用 | arch/x86/kvm/vmx/vmx.c |
2987-3030 |
| SVM 硬件启用 | arch/x86/kvm/svm/svm.c |
514-545 |
| SVM VMCB 拦截位图 | arch/x86/include/asm/svm.h |
14-100 |
| VMCB 结构定义 | arch/x86/include/asm/svm.h |
536-566 |
| VMX MSR 定义 | arch/x86/include/asm/msr-index.h |
1246-1262 |
| CET MSR 定义 | arch/x86/include/asm/msr-index.h |
546-561 |
| #CP 异常处理 | arch/x86/kernel/cet.c |
全文 |
| 用户影子栈分配 | arch/x86/kernel/shstk.c |
97-240 |
| 线程影子栈分配 | arch/x86/kernel/shstk.c |
194-240 |
由 Claude Code 分析生成