Skip to content

Latest commit

 

History

History
3330 lines (2679 loc) · 110 KB

File metadata and controls

3330 lines (2679 loc) · 110 KB

Linux 内核 x86/x86-64 体系结构深度解析

分析基于 Linux 内核 master 分支(v6.15-rc 阶段) 核心参考文件:

  • arch/x86/entry/entry_64.S
  • arch/x86/kernel/process_64.c
  • arch/x86/mm/pgtable.c
  • arch/x86/kernel/cpu/common.c
  • arch/x86/include/asm/processor.h
  • arch/x86/include/asm/desc_defs.h
  • arch/x86/include/asm/pgtable_64_types.h
  • arch/x86/entry/calling.h
  • arch/x86/mm/pti.c

目录

  1. x86-64 体系结构总览
  2. 系统调用入口:entry_SYSCALL_64 完整流程
  3. 寄存器保存与恢复:pt_regs 结构
  4. 进程切换:__switch_to 与 context_switch
  5. 四级与五级页表结构
  6. KPTI:内核页表隔离机制
  7. CPU 特性检测与 CPUID
  8. MSR 寄存器操作
  9. GDT / IDT / TSS 结构
  10. PCID:进程上下文 ID 优化
  11. 综合:从 syscall 到返回的完整路径
  12. x86 启动流程:实模式到长模式
  13. IDT 深度分析:中断描述符表初始化
  14. 系统调用两种方式:SYSCALL 与 SYSENTER
  15. x86 页表进阶:PCID 优化与 TLB 管理
  16. APIC 架构:Local APIC、IO-APIC、X2APIC
  17. x86 硬件调试:DR0-DR7 与单步执行
  18. x86 虚拟化扩展:VMX 与 SVM
  19. CET:控制流强制技术
  20. 参考文件索引(完整版)

1. x86-64 体系结构总览

x86-64(又称 AMD64)是当今服务器和桌面系统的主流 64 位指令集架构,由 AMD 在 2000 年提出,Intel 随后以 EM64T/Intel 64 名义兼容实现。Linux 内核对 x86-64 的支持集中在 arch/x86/ 目录,代码量超过 100 万行。

1.1 寄存器概览

通用寄存器(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

1.2 内存布局(四级页表,LA48)

虚拟地址空间(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

2. 系统调用入口:entry_SYSCALL_64 完整流程

x86-64 系统调用通过 SYSCALL 指令触发。该指令由 CPU 硬件执行以下操作:

  1. RIP(返回地址)存入 RCX
  2. RFLAGS 存入 R11,并用 MSR_SYSCALL_MASK 屏蔽部分标志位
  3. MSR_STAR 加载新 CS/SS(特权级变为 0)
  4. MSR_LSTAR 加载新 RIP,跳入内核

注意:SYSCALL 不切换栈,进入内核时 RSP 仍指向用户栈。

2.1 entry_SYSCALL_64 汇编注解

源文件: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,负责查找系统调用表并调用具体处理函数。

2.2 系统调用返回路径

; 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

2.3 完整流程图

用户态                              内核态
  |                                   |
  | 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)
  <-------------------------------+
  |
用户态继续执行

2.4 MSR 初始化(syscall_init)

arch/x86/kernel/cpu/common.c:2294syscall_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
}

3. 寄存器保存与恢复:pt_regs 结构

struct pt_regs 是内核在栈上保存用户态寄存器现场的核心数据结构,定义于 arch/x86/include/asm/ptrace.h

3.1 pt_regs 布局

内核栈(高地址在上):

  +-----------------+  <- 原用户 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参数)

3.2 PUSH_AND_CLEAR_REGS 宏

arch/x86/entry/calling.h:68-129 定义了 PUSH_REGSCLEAR_REGSPUSH_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 均清零)

3.3 show_regs 实现

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
}

4. 进程切换:__switch_to 与 context_switch

进程切换(上下文切换)是操作系统最核心的操作之一,x86-64 上分为两个阶段:汇编层的栈切换(__switch_to_asm)和 C 层的状态切换(__switch_to)。

4.1 调用链

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,切换其余状态

4.2 __switch_to_asm:汇编栈切换

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 的返回地址
  +----------+

4.3 __switch_to:C 层状态切换

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 权限等
    ...
}

4.4 FS/GS base 切换(线程本地存储)

arch/x86/kernel/process_64.c:391x86_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
    }
}

4.5 thread_struct:per-task 架构状态

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 -- 保护键
};

5. 四级与五级页表结构

x86-64 使用多级页表实现虚拟地址到物理地址的映射。传统四级(LA48,48位虚拟地址)和现代五级(LA57,57位虚拟地址,Intel Ice Lake+)。

5.1 四级页表(LA48)地址分解

虚拟地址(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指向)

5.2 五级页表(LA57)地址分解

虚拟地址(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) | 页内偏移 |
 +-------+---------+--------+--------+--------+--------+----------+

5.3 各级页表参数

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)

5.4 页表项(PTE)格式

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):不可执行

5.5 五级页表运行时检测

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
#endif

5.6 pgd_ctor:新进程页表初始化

arch/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)  // 49

6. KPTI:内核页表隔离机制

KPTI(Kernel Page-Table Isolation,内核页表隔离)是 2018 年针对 Meltdown(CVE-2017-5754)漏洞引入的安全机制,代码基于 TU Graz 的 KAISER 项目。

6.1 KPTI 基本原理

未启用 KPTI:
  用户态     内核态
  +-------+  +-------+
  | 用户  |  | 用户  |
  | 代码  |  | 代码  |
  +-------+  +-------+
             | 内核  |  <- 用户态下可见(Meltdown 可读!)
             | 代码  |
             +-------+
           共用同一份页表(CR3)

启用 KPTI 后:
  用户态 CR3               内核态 CR3
  +-------------+         +-------------+
  | 用户代码    |         | 用户代码    |
  | 用户数据    |         | 用户数据    |
  +-------------+         +-------------+
  | 最小内核映射 |         | 完整内核映射 |
  | (入口蹦床)  |         | (全部内核)  |
  +-------------+         +-------------+
  两份独立页表,通过 PGD 高位 bit12 区分

6.2 PTI_USER_PGTABLE_BIT

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)

6.3 SWITCH_TO_KERNEL_CR3 宏

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_\@:
.endm

ADJUST_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和页表位
.endm

6.4 SWITCH_TO_USER_CR3

arch/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
.endm

6.5 PTI 初始化

arch/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 副本。

6.6 KPTI 性能影响

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%

7. CPU 特性检测与 CPUID

Linux 内核使用 CPUID 指令探测 CPU 特性,并将结果存储在 struct cpuinfo_x86x86_capability[] 位图中。

7.1 cpuinfo_x86 结构

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     12

7.2 cpu_detect():基础 CPUID

arch/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
        }
    }
}

7.3 get_cpu_cap():批量读取特性位

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...
    }
    ...
}

7.4 特性测试宏

常用特性测试宏(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

7.5 CR4 特性位配置

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
}

8. MSR 寄存器操作

MSR(Model-Specific Register,型号专用寄存器)通过 RDMSR(读)和 WRMSR(写)指令访问,需要 CPL=0。

8.1 重要 MSR 一览

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

8.2 MSR 读写 API

内核提供安全的 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);

8.3 FS/GS base MSR 操作

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;
}

8.4 PPIN(平台保护 ID 编号)

arch/x86/kernel/cpu/common.c:144ppin_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
}

8.5 MSR_SPEC_CTRL(推测执行控制)

投机执行漏洞(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;

9. GDT / IDT / TSS 结构

9.1 GDT(全局描述符表)

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 模式。

9.2 desc_struct:8 字节段描述符

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));

9.3 gate_struct:IDT 门描述符(16字节)

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、双重错误等需要可靠栈的情况。

9.4 TSS(任务状态段)

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
}

9.5 GDT/IDT/TSS 地址空间关系

每个 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 寄存器指向

10. PCID:进程上下文 ID 优化

PCID(Process Context ID)是 x86-64 的硬件 TLB 分区特性,由 Intel Westmere 引入(CPUID.01H:ECX[bit17]),AMD Zen 系列同样支持。

10.1 PCID 原理

传统 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 动态复用

10.2 nopcid / noinvpcid 命令行参数

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 失效)。

10.3 cr4_init 中的 PCID 启用

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
}

10.4 PCID 与 KPTI 的协同

在 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

10.5 TLB flush 与 INVPCID

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 指令。


11. 综合:从 syscall 到返回的完整路径

将以上所有机制综合到一个完整流程图中:

用户空间进程执行 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)

11.1 关键数据结构关系

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 位图)

11.2 安全特性总结

特性              对应机制                    防护目标
-----------------------------------------------------------------
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            随机内核地址                 地址泄漏利用

12. x86 启动流程:实模式到长模式

x86 处理器上电后从实模式(Real Mode)启动,经过保护模式(Protected Mode),最终进入 64 位长模式(Long Mode)。Linux 内核的启动流程跨越多个文件,下面逐阶段分析。

12.1 启动流程总览

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()            进入通用内核初始化

12.2 实模式内存布局

实模式下物理内存布局(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            |

12.3 go_to_protected_mode() 详解

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
}

12.4 pmjump.S:CR0.PE 设置与保护模式跳转

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)

12.5 EFER 与长模式启用

从保护模式进入长模式需要:

步骤 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 位代码段)

12.6 EFER MSR 位定义

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");

12.7 A20 地址线

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;
}

13. IDT 深度分析:中断描述符表初始化

IDT(Interrupt Descriptor Table)是 x86 保护模式的核心机制,每个向量(0-255)对应一个门描述符,指向相应的处理程序。

13.1 x86 异常向量表

向量号   名称                    类型   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)     中断   -    设备中断

13.2 IDT 初始化的分阶段过程

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)

13.3 idt_setup_from_table 实现

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;      // 处理函数入口地址
};

13.4 def_idts 完整清单

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)

13.5 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(防递归)

13.6 IDT 的 CPU 入口区(cpu_entry_area)

为了兼容 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 缓冲区
};

14. 系统调用两种方式:SYSCALL 与 SYSENTER

x86 架构上有两种主要的快速系统调用指令:SYSCALL(AMD 引入,64位标准)和 SYSENTER(Intel 引入,32位兼容)。两者在机制上有重要区别。

14.1 指令对比

特性          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

14.2 SYSENTER 硬件操作

当 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 传递返回地址。

14.3 MSR 初始化:syscall_init() 中的 SYSENTER 设置

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 清零掩码 */ ...);
}

14.4 entry_SYSENTER_compat:64位内核处理32位SYSENTER

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)

14.5 do_SYSENTER_32:修复 SYSENTER 的缺失字段

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);
}

14.6 vDSO 中的 __kernel_vsyscall

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

14.7 INT 0x80:最古老的系统调用方式

除了 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()


15. x86 页表进阶:PCID 优化与 TLB 管理

本章在前面章节基础上深入分析页表切换机制,包括 PCID 的详细实现、TLB shootdown 以及大页(HugeTLB)支持。

15.1 TLB Shootdown 机制

当一个 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);
}

15.2 mTLB(mini-TLB)与 PCID 分配

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 或强制刷新 */
    ...
}

15.3 大页(HugePage)支持

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 表示大页

15.4 内存类型(PAT/MTRR)

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

16. APIC 架构:Local APIC、IO-APIC、X2APIC

APIC(Advanced Programmable Interrupt Controller,高级可编程中断控制器)是现代 x86 系统的中断管理核心,取代了传统的 8259 PIC。

16.1 APIC 架构总览

系统总线
    |
    +------ 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 寄存器

16.2 Local 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       -- 分频配置寄存器

16.3 setup_local_APIC() 初始化流程

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 配置 */
}

16.4 APIC 定时器

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() 中选择)。

16.5 IPI(处理器间中断)

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 初始化中使用,向量号 = 起始页面号 */

16.6 X2APIC 模式

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);
}

16.7 IO-APIC

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)

17. x86 硬件调试:DR0-DR7 与单步执行

x86 处理器内置了强大的硬件调试支持,包括 4 个断点地址寄存器(DR0-DR3)、状态寄存器(DR6)和控制寄存器(DR7),支持指令执行、内存读写、I/O 端口访问的断点。

17.1 调试寄存器概览

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    ; 写入 DR0

arch/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)

17.2 DR6:调试状态寄存器

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)

17.3 DR7:调试控制寄存器

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)

17.4 设置硬件断点的内核路径

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);
}

17.5 单步执行(EFLAGS.TF)

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);
}

17.6 内核调试寄存器的 per-CPU 保存

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 来启用/禁用硬件断点,避免不同进程的断点互相干扰。


18. x86 虚拟化扩展:VMX 与 SVM

x86 硬件虚拟化通过 VMX(Virtual Machine Extensions,Intel VT-x)和 SVM(Secure Virtual Machine,AMD-V)实现,Linux KVM 是主要使用者。

18.1 虚拟化基本概念

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     |

18.2 VMX(Intel VT-x)

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))

18.3 VMX 硬件启用流程

arch/x86/kvm/vmx/vmx.c:2987kvm_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)

18.4 EPT(Extended Page Tables,扩展页表)

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 处理缺页

18.5 VPID(虚拟处理器 ID)

类似于 PCID 对于用户/内核页表的优化,VPID 为不同 VM 的 TLB 条目打标签,避免 VM Entry/Exit 时全部刷 TLB:

CR3 切换(宿主机与虚拟机之间):
  无 VPID:每次 VM Entry/Exit 都要 TLB flush
  有 VPID:VM Entry/Exit 保留有效 TLB 条目(标有 VPID)
         只有同一 VPID 的条目才相互竞争

18.6 SVM(AMD-V)

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)

18.7 VMX/SVM VM-Exit 处理架构

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()
                      ...

19. CET:控制流强制技术

CET(Control-flow Enforcement Technology)是 Intel 从第 11 代 Tiger Lake 开始引入的控制流完整性(CFI)硬件机制,包含两个独立功能:影子栈(Shadow Stack)和间接分支跟踪(Indirect Branch Tracking)。

19.1 CET 整体架构

CET 由两个组件组成:

1. Shadow Stack (SHSTK) -- 防止 ROP(Return-Oriented Programming)攻击
   -- 维护一个独立的只读返回地址影子栈
   -- CALL 时同时将返回地址写入影子栈
   -- RET 时比较普通栈和影子栈的返回地址

2. Indirect Branch Tracking (IBT) -- 防止 JOP(Jump-Oriented Programming)
   -- 合法的间接跳转/调用目标必须以 ENDBR64/ENDBR32 指令开头
   -- 若跳转到没有 ENDBR 的地址,触发 #CP 异常

19.2 MSR 寄存器

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 影子栈指针

19.3 setup_cet():内核 CET 初始化

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);
    }
}

19.4 影子栈工作原理

正常函数调用(SHSTK 启用后):

CALL 指令执行时:
  普通栈:push 返回地址(可被攻击者修改)
  影子栈:自动 push 返回地址(只读,硬件保护)

              普通栈                影子栈(只读)
             +----------+          +----------+
  RSP ->     | 返回地址  |  SSP ->  | 返回地址  |
             | 帧指针    |          +----------+
             | 局部变量  |
             +----------+

RET 指令执行时:
  1. 从普通栈弹出返回地址(pop RIP)
  2. 从影子栈弹出返回地址
  3. 比较两个返回地址
  4. 若不匹配 -> 触发 #CP(控制保护异常)

ROP 攻击场景:
  攻击者修改普通栈上的返回地址 -> 指向 gadget
  RET 时:普通栈返回地址 != 影子栈返回地址 -> #CP
  攻击被阻断!

19.5 影子栈内存映射

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)

19.6 线程影子栈分配

arch/x86/kernel/shstk.c:194shstk_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 值)
}

19.7 IBT(间接分支跟踪)

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

19.8 #CP 异常处理

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 内发生的违例
};

19.9 用户态 #CP 处理

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);
}

19.10 CET 相关的 ARCH_PRCTL

用户程序可以通过 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);

19.11 内核 IBT 与 FineIBT

Linux 内核从 v6.2 开始支持内核级 IBT,通过两种方式实现:

粗粒度 IBT(kIBT):
  -- 内核所有函数入口自动添加 ENDBR64
  -- 防止 ROP/JOP 攻击内核
  -- 可通过 ibt=off 内核参数禁用

精细粒度 IBT(FineIBT):
  -- 更严格的 CFI:不仅检查 ENDBR,还验证调用类型
  -- 通过 __cfi_check() 函数实现
  -- 类似于 LLVM CFI
  -- 性能开销略高于 kIBT

20. 参考文件索引(完整版)

功能 文件 关键行
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 分析生成