﻿# 系统调用深入细节 —— 同一线程从用户态到内核态的完整过程

> 前一篇 [syscall.md](/concepts/process/syscall.md) 讲了系统调用的基本过程、开销来源和优化思路。本篇深入更底层的问题：**同一个线程从用户态进入内核态，到底发生了什么？内核怎么管理代码段和数据段的权限？段寄存器在这过程中怎么变化？特权级切换的硬件细节是什么？** 这是理解"为什么用户态不能访问内核内存""为什么 syscall 之后栈会变"的关键。


> 本文涉及大量 x86-64 硬件寄存器（CR3/MSR/段寄存器等），不熟悉可先看 [x86-64-registers.md](/concepts/process/x86-64-registers.md)。

## 零、核心问题：同一个线程，为什么进入内核后"变了一个人"？

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<u>> #E3F2FD
  BorderColor<<u>> #1976D2
  BackgroundColor<<k>> #FFCDD2
  BorderColor<<k>> #C62828
}
rectangle "用户态（ring 3）" <<u>> as U {
  rectangle "用户栈\n(RSP 指向这里)" as US
  rectangle "用户代码段 CS=0x33" as UCS
  rectangle "用户数据段 DS/ES/SS=0x2B" as UDS
  rectangle "用户页表\n(只能看到用户空间)" as UPT
}
rectangle "内核态（ring 0）" <<k>> as K {
  rectangle "内核栈\n(RSP 指向这里)" as KS
  rectangle "内核代码段 CS=0x10" as KCS
  rectangle "内核数据段 DS/ES/SS=0x18" as KDS
  rectangle "内核页表\n(能看到所有物理内存)" as KPT
}
U -down-> K : syscall 指令
note right of U
  **同一个 task_struct**
  同一个线程 ID
  同一个 pid/tgid
  但是：
  · 栈换了（用户栈 → 内核栈）
  · 段换了（CS/SS/DS 全换）
  · 页表可能换了（KPTI）
  · 权限变了（ring 3 → ring 0）
end note
@enduml
```

> 一句话定位：**同一个线程、同一个 `task_struct`，但执行环境全换了——栈、段、页表、特权级。这就是"模式切换"的本质：不是换人，是同一个人的"身份"变了。**

## 一、段寄存器的管理：用户态和内核态怎么隔离

### 1.1 为什么需要段

在 x86-64 长模式下，分段机制被大幅简化，但**段寄存器仍然存在且至关重要**——它们决定当前代码运行在哪个特权级（ring）。

x86-64 中四个关键段寄存器：

| 寄存器 | 全称 | 作用 | 用户态值 | 内核态值 |
|--------|------|------|---------|---------|
| **CS** | Code Segment | 代码段选择子，决定当前特权级（CPL） | `0x33`（ring 3） | `0x10`（ring 0） |
| **SS** | Stack Segment | 栈段选择子 | `0x2B`（ring 3） | `0x18`（ring 0） |
| **DS** | Data Segment | 数据段选择子 | `0x2B`（ring 3） | `0x18`（ring 0） |
| **ES** | Extra Segment | 附加数据段 | 同 DS | 同 DS |

> 注意：在 x86-64 长模式下，DS/ES/SS 的基址和界限被硬件忽略（强制为 0 和 4GB 以上），但**段选择子中的 RPL（Requested Privilege Level）字段仍然有效**，用于权限检查。

### 1.2 段选择子的结构

每个段寄存器实际存的是一个 16 位的**段选择子**：

```bash
CS = 0x33 = 0b 0000 0000 0011 0011
            │              ││  ││
            │              ││  └── RPL = 11 (ring 3)
            │              │└── TI = 0 (GDT)
            │              └── Index = 6 (GDT[6])
            └── 高 13 位 = 索引
CS = 0x10 = 0b 0000 0000 0001 0000
            │              ││  ││
            │              ││  └── RPL = 00 (ring 0)
            │              │└── TI = 0 (GDT)
            │              └── Index = 2 (GDT[2])
```

**关键**：选择子的低 2 位（RPL）和 CS 寄存器中的 CPL 共同决定当前特权级。

### 1.3 GDT（全局描述符表）中的段描述符

CPU 根据段选择子的 Index 字段去 GDT 中查找对应的**段描述符**（8 字节）：

```bash
GDT 典型布局（Linux x86-64）：
索引  偏移     内容                           用途
─────────────────────────────────────────────────
0     0x00     NULL 描述符                    必须为空
1     0x08     内核代码段(32位)               兼容模式用
2     0x10     内核代码段(64位)  DPL=0        syscall 入口
3     0x18     内核数据段(64位)  DPL=0        内核栈/数据
4     0x20     用户代码段(32位)               兼容模式用
5     0x28     用户数据段(64位)  DPL=3        
6     0x30     用户代码段(64位)  DPL=3        用户态代码
7     0x38     （保留）
8     0x40     TSS（Task State Segment）      存储内核栈指针等
```

段描述符的格式（8 字节）：

```bash
63       55       47       39       31       23       15       7      0
├────────┼────────┼────────┼────────┼────────┼────────┼────────┼────────┤
│ Base   │ Flags  │ Limit  │ Access │ Base   │     Base[23:0]     │ Limit  │
│ 31:24  │        │ 19:16  │ Byte   │ 23:16  │                    │ 15:0   │
├────────┼────────┼────────┼────────┼────────┼────────┼────────┼────────┤
Access Byte 的关键位：
  Bit 7  P (Present)        = 1（段在内存中）
  Bit 6-5 DPL (Descriptor Privilege Level) = 00(ring0) 或 11(ring3)
  Bit 4  S (System)         = 1（代码/数据段） 0（系统段如 TSS）
  Bit 3-0 Type              = 1010(代码段-可执行可读) 或 0010(数据段-可读写)
```

**DPL 是关键的隔离机制**：CPU 在加载段寄存器时会检查 `max(CPL, RPL) <= DPL`。用户态 CPL=3，只能加载 DPL=3 的段描述符——这保证了用户态永远无法加载内核的段（DPL=0）。

### 1.4 syscall 指令如何切换段

`syscall` 指令执行时，CPU 硬件自动完成段切换：

```asm
; syscall 指令的硬件动作（简化）
; 1. 保存返回信息
RCX := RIP          ; 保存用户态返回地址
R11 := RFLAGS       ; 保存用户态标志寄存器
; 2. 加载内核入口地址
RIP := MSR_LSTAR    ; 跳到内核入口点（开机时设定）
; 3. 切换代码段为内核段（ring 0）
CS := MSR_STAR[47:32] & ~3    ; CS.Selector = 内核代码段索引
                               ; CS.Base = 0（长模式忽略）
                               ; CS.Limit = 0（长模式忽略）
                               ; CS.DPL = 0 → CPL = 0
; 4. 切换栈段
SS := (MSR_STAR[47:32] + 8) | 3  ; SS.Selector = 内核数据段索引
                                  ; RPL = 0
; 5. 清除中断标志（可选）
RFLAGS := RFLAGS & ~MSR_FMASK
```

**注意**：`syscall` 指令**只切换 CS 和 SS**，不切换 DS/ES。内核入口代码需要手动加载内核数据段到 DS/ES：

```asm
; Linux entry_SYSCALL_64 的简化代码
swapgs                  ; 切换到内核的 GS 基址（访问 per-CPU 数据）
mov rsp, PER_CPU_VAR(cpu_current_top_of_stack)  ; 切换到内核栈
pushq $__USER_DS        ; 准备用户 SS
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp0)         ; 保存用户 RSP
...
mov ax, $__KERNEL_DS    ; 加载内核数据段
mov ds, ax
mov es, ax
```

### 1.5 用户态能"看到"内核内存吗

**不能**，有两个层面的保护：

**层面一：段级别保护**

- 用户态 CS 的 DPL=3，无法加载 DPL=0 的段。
- 即使尝试 `mov ds, 0x18`（内核数据段），CPU 会检查 `max(CPL=3, RPL=0) > DPL=0`，抛出 `#GP` 异常。

**层面二：页表级别保护（更关键的隔离）**

- 内核页表条目通常设置 **U/S 位 = 0（Supervisor-only）**。
- 用户态 CPL=3 访问 U/S=0 的页面 → 页错误（page fault）。
- 这就是为什么即使用户态知道内核地址，也无法读写。

**KPTI 的强化**：

- Meltdown 漏洞后，KPTI 让用户态和内核态使用**两套不同的页表**。
- 用户态页表中**完全没有内核地址的映射**（除了极少量的 trampoline 代码）。
- 即使 CPU 推测执行，也读不到内核数据——页表里根本没有。

## 二、同一线程从用户态到内核态的完整过程

### 2.1 完整的步骤拆解

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor     #1976D2
  ArrowColor                 #C62828
  LifeLineBorderColor        #90A4AE
}
participant "用户态应用\n(ring 3)" as U
participant "CPU 硬件\n(syscall 执行)" as HW
participant "内核入口\nentry_SYSCALL_64" as ENTRY
participant "系统调用分发\nsys_call_table" as DISP
participant "具体处理\nsys_write()" as IMPL
participant "内核出口\nsysret 路径" as EXIT
== 阶段一：用户态准备 ==
U -> U : ① 准备参数到寄存器\nrax = __NR_write (1)\nrdi = fd\nrsi = buf\nrdx = count\nr10 = (第4参数，如果有)\nr8 = (第5参数，如果有)\nr9 = (第6参数，如果有)
note right of U: 为什么第4参数用 r10 而不是 rcx？\n因为 syscall 指令会覆盖 rcx！
U -> HW : ② 执行 syscall 指令
== 阶段二：硬件自动切换 ==
HW -> HW : ③ 保存返回信息\nRCX = RIP_next（下一条指令地址）\nR11 = RFLAGS（标志寄存器）
note right of HW: 硬件自动保存\n不经过任何软件
HW -> HW : ④ 加载内核入口地址\nRIP = MSR_LSTAR
note right of HW: MSR_LSTAR 在开机时设置\n指向 entry_SYSCALL_64
HW -> HW : ⑤ 切换代码段 CS\nCS.Selector = MSR_STAR[47:32]\n→ CS = 0x10 (内核代码段, DPL=0)
note right of HW: CPL 从 3 变为 0\nCPU 现在是 ring 0
HW -> HW : ⑥ 切换栈段 SS\nSS.Selector = MSR_STAR[47:32]+8\n→ SS = 0x18 (内核数据段)
note right of HW: 但 RSP 还没切换！\n还在用用户栈指针
== 阶段三：内核入口代码 ==
HW -> ENTRY : ⑦ 跳转到 entry_SYSCALL_64
ENTRY -> ENTRY : ⑧ swapgs\n切换 GS 基址到内核 per-CPU 区域
note right of ENTRY: 用户态 GS 指向 TLS\n内核态 GS 指向 per-CPU 数据结构
ENTRY -> ENTRY : ⑨ mov rsp, PER_CPU(cpu_current_top_of_stack)\n切换到内核栈
note right of ENTRY: **栈切换完成！**\n现在 RSP 指向内核栈顶
ENTRY -> ENTRY : ⑩ 构建 pt_regs 结构\npush 用户 SS/RSP/RFLAGS/CS/RIP\npush 其他用户寄存器\n(rax, rbx, rcx, rdx, rsi, rdi, rbp, r8-r15)
note right of ENTRY: pt_regs 完整保存了\n用户态的所有寄存器现场\n位于内核栈上
ENTRY -> ENTRY : ⑪ 加载内核数据段\nmov ds, __KERNEL_DS\nmov es, __KERNEL_DS
note right of ENTRY: DS/ES 从用户数据段\n切换到内核数据段
ENTRY -> ENTRY : ⑫ 若开启 KPTI：\n切换 CR3 到内核页表
note right of ENTRY: 内核页表包含内核地址映射\n用户态页表没有内核映射\n这是 Meltdown 的缓解措施
== 阶段四：系统调用分发 ==
ENTRY -> DISP : ⑬ 查 sys_call_table\ncmp rax, __NR_syscall_max\nja 错误处理\ncall *sys_call_table(, %rax, 8)
note right of DISP: sys_call_table 是函数指针数组\nrax 是下标
== 阶段五：具体处理 ==
DISP -> IMPL : ⑭ 调用 sys_write(fd, buf, count)\nrdi, rsi, rdx 就是参数\nC 函数可以直接用
IMPL -> IMPL : ⑮ 执行具体逻辑\n· 从 current->files 拿 struct file\n· 权限检查 (file->f_mode)\n· 调用 file->f_op->write()\n· copy_from_user() 等
IMPL -> IMPL : ⑯ 返回值放入 rax
== 阶段六：返回路径 ==
IMPL -> EXIT : ⑰ 返回到 sysret 路径
EXIT -> EXIT : ⑱ 若开启 KPTI：\n切回用户页表 (CR3)
EXIT -> EXIT : ⑲ 恢复用户寄存器\n从内核栈上的 pt_regs 恢复\n除 rax 外全部恢复
EXIT -> EXIT : ⑳ 恢复用户数据段\nmov ds, __USER_DS\nmov es, __USER_DS
EXIT -> EXIT : ㉑ swapgs\n切回用户态 GS
EXIT -> HW : ㉒ 准备 sysret\nRCX = 用户 RIP（从 pt_regs 恢复）\nR11 = 用户 RFLAGS
== 阶段七：硬件返回 ==
HW -> HW : ㉓ 执行 sysretq 指令\n· CS = MSR_STAR[63:48]（用户代码段 0x33）\n· SS = MSR_STAR[63:48]+8（用户数据段 0x2B）\n· RIP = RCX\n· RFLAGS = R11
HW -> U : ㉔ 回到用户态\nRIP 指向 syscall 下一条指令\nrax = 系统调用返回值
@enduml
```

### 2.2 栈的变化：同一个线程的两套栈

```bash
同一个 task_struct
├── thread_struct.sp0  → 内核栈顶（TSS 中也存了一份）
├── thread_struct.sp   → 内核栈当前指针（上下文切换时用）
└── 用户栈              → 用户态 RSP 指向的位置
系统调用前后栈的变化：
时间点             RSP 指向             栈内容                  CS
─────────────────────────────────────────────────────────────────
syscall 前         用户栈顶              main() 的栈帧         0x33
                                       handler() 的栈帧
                                       read() 的栈帧
syscall 执行中     用户栈顶（短暂）      同上                   0x10
                                                              (硬件切了CS但还没切RSP)
entry_SYSCALL_64   内核栈顶              pt_regs(用户寄存器)    0x10
                   ↓                    sys_read() 的栈帧
                                        vfs_read() 的栈帧
                                        ...
sysret 后          用户栈顶              read() 返回后          0x33
                                       继续使用用户栈
```

**每个线程的内核栈在哪？**

- 内核栈通常在 `task_struct` 所在页面的末尾（通过 `THREAD_SIZE` 宏）。
- x86-64 默认 `THREAD_SIZE = 16KB`（两个页），`thread_info` 结构也在同一块内存的起始处。
- 可以通过 `current` 宏（读取 per-CPU 的 `GS` 段）快速找到当前 task 的内核栈。

```c
// Linux 内核中的简化示意
union thread_union {
    struct thread_info thread_info;
    unsigned long stack[THREAD_SIZE / sizeof(long)];
};
// 内核栈从 stack 数组的高地址向低地址增长
```

### 2.3 页表的切换：为什么有时要换 CR3

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<u>> #E3F2FD
  BorderColor<<u>> #1976D2
  BackgroundColor<<k>> #FFCDD2
  BorderColor<<k>> #C62828
}
rectangle "无 KPTI（老内核）" {
  rectangle "进程页表" {
    rectangle "用户空间映射\n(0x0 - 0x7FFF...)" <<u>>
    rectangle "内核空间映射\n(0xFFFF... - 结束)" <<k>>
  }
  note bottom: 用户态也能"看到"内核映射\n（但 U/S 位阻止访问）\nMeltdown 可以通过推测执行绕过
}
rectangle "有 KPTI（新内核）" {
  rectangle "用户态页表" <<u>> {
    rectangle "用户空间映射" <<u>>
    rectangle "trampoline 代码\n（极少量内核代码）" <<k>>
  }
  rectangle "内核态页表" <<k>> {
    rectangle "用户空间映射" <<u>>
    rectangle "完整内核映射" <<k>>
  }
  note bottom: 用户态页表中没有内核映射\nMeltdown 无法泄漏内核数据\n代价：每次 syscall 要切 CR3 → 刷 TLB
}
@enduml
```

**PCID（Process Context ID）优化**：

- 给不同页表打上标签（PCID）。
- 切 CR3 时如果不换 PCID，可以不刷 TLB（TLB 条目带有 PCID 标签）。
- 用户态和内核态各用一个 PCID → 切换时不刷 TLB → 显著降低 KPTI 的开销。

## 三、特权级切换的硬件细节

### 3.1 MSR 寄存器：syscall 的配置中心

| MSR 寄存器 | 作用 | 内容 |
|-----------|------|------|
| `MSR_LSTAR` (0xC0000082) | syscall 入口地址 | entry_SYSCALL_64 的虚拟地址 |
| `MSR_STAR` (0xC0000081) | 段选择子配置 | [63:48]=用户 CS/SS, [47:32]=内核 CS/SS |
| `MSR_FMASK` (0xC0000084) | RFLAGS 清除掩码 | 进入内核时自动清除的标志位（如 IF=中断使能） |
| `MSR_CSTAR` (0xC0000083) | 兼容模式入口 | 32 位程序用 syscall 时的入口 |
| `MSR_SFMASK` (0xC0000085) | 兼容模式 RFLAGS 掩码 | 32 位兼容模式的标志清除 |

开机时 Linux 设置这些 MSR：

```c
// arch/x86/kernel/cpu/common.c 中的简化逻辑
void syscall_init(void) {
    wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
    wrmsrl(MSR_STAR,  (unsigned long)((__USER_CS << 48) | (__KERNEL_CS << 32)));
    wrmsrl(MSR_FMASK, X86_EFLAGS_TF | X86_EFLAGS_DF | X86_EFLAGS_IF | ...);
}
```

### 3.2 syscall/sysret 的精确硬件序列

```bash
syscall 指令的硬件微操作（不可中断的原子序列）：
1. RCX := RIP          ; 保存用户态返回地址
2. R11 := RFLAGS       ; 保存标志寄存器
3. RIP := LSTAR        ; 跳转到内核入口
4. CS.Selector := STAR[47:32]      ; 内核代码段
   CS.Base := 0
   CS.Limit := 0xFFFFFFFF
   CS.Type := 11 (Execute/Read)
   CS.DPL := 0
   CS.P := 1
   CS.L := 1 (64-bit)
   CS.D := 0
   → CPL 变为 0
5. SS.Selector := STAR[47:32] + 8  ; 内核数据段
   SS.Base := 0
   SS.Limit := 0xFFFFFFFF
   SS.Type := 3 (Read/Write)
   SS.DPL := 0
   SS.P := 1
6. RFLAGS := RFLAGS & ~FMASK      ; 清除指定标志位
sysretq 指令的硬件微操作：
1. CS.Selector := STAR[63:48]      ; 用户代码段
   CS.DPL := 3
   → CPL 变为 3
2. SS.Selector := STAR[63:48] + 8  ; 用户数据段
3. RIP := RCX                       ; 恢复用户返回地址
4. RFLAGS := R11                    ; 恢复标志寄存器
5. RFLAGS.IF := 1                   ; 重新开启中断
```

### 3.3 为什么 rcx 和 r11 被征用

普通 x86-64 函数调用约定（System V ABI）：

| 参数位置 | 寄存器 |
|---------|--------|
| 第 1 参数 | rdi |
| 第 2 参数 | rsi |
| 第 3 参数 | rdx |
| **第 4 参数** | **rcx** |
| 第 5 参数 | r8 |
| 第 6 参数 | r9 |

系统调用约定：

| 参数位置 | 寄存器 |
|---------|--------|
| 第 1 参数 | rdi |
| 第 2 参数 | rsi |
| 第 3 参数 | rdx |
| **第 4 参数** | **r10**（不是 rcx！） |
| 第 5 参数 | r8 |
| 第 6 参数 | r9 |

**因为 `syscall` 指令硬件自动写 `RCX`（保存 RIP）和 `R11`（保存 RFLAGS）**，用户态在这两个寄存器里放参数会被覆盖。所以系统调用约定把第 4 参数挪到了 `r10`，`r11` 则完全不用于传参。

## 四、内核数据段与用户数据段的本质区别

虽然在 x86-64 长模式下，DS/ES/SS 的基址和界限被硬件忽略（都等于 0 和最大值），但它们的**权限属性**仍然有效：

```bash
用户数据段 (0x2B)：
  DPL = 3  → 用户态可以加载
  Type = 2 (Read/Write)
内核数据段 (0x18)：
  DPL = 0  → 只有 ring 0 可以加载
  Type = 2 (Read/Write)
```

**实际保护主要靠页表 U/S 位**，但段权限提供额外的纵深防御：

- 用户态代码（CS.DPL=3）试图加载内核数据段（DPL=0）→ `#GP(0)` 异常。
- 即使有某种方法绕过了页表保护，段级别仍然阻止访问。

## 五、观测手段

### 5.1 查看 MSR 寄存器

```bash
# 需要 root 权限
sudo rdmsr 0xC0000082  # MSR_LSTAR，syscall 入口地址
sudo rdmsr 0xC0000081  # MSR_STAR，段选择子配置
sudo rdmsr 0xC0000084  # MSR_FMASK，标志清除掩码
```

### 5.2 查看 GDT

```bash
# 通过 debugfs 查看
sudo cat /sys/kernel/debug/x86/gdt | head -20
```

### 5.3 确认 KPTI 是否开启

```bash
dmesg | grep -i "page.*table.*isolation\|PTI\|KPTI"
# 或
cat /proc/cpuinfo | grep -o 'pcid' | head -1  # PCID 支持
```

### 5.4 用 gdb 观察段寄存器

```gdb
(gdb) info registers cs ds es ss
cs             0x33     51
ds             0x0      0
es             0x0      0
ss             0x2b     43
```

### 5.5 测量 syscall 开销

```bash
# 使用 lmbench 的 lat_syscall
lat_syscall null       # 空系统调用的延迟
lat_syscall read       # read 的延迟
lat_syscall write      # write 的延迟
# 对比 KPTI 开启和关闭的差异
# 启动参数添加 pti=off 关闭 KPTI
```

## 六、一句话总结

> **同一个线程从用户态进入内核态，thread ID 不变，task_struct 不变，但执行环境彻底换了：CS 从 0x33（ring 3）变为 0x10（ring 0）、SS/DS/ES 从用户数据段切为内核数据段、RSP 从用户栈切到内核栈（task_struct 所在页的末尾）、GS 从 TLS 切到 per-CPU 区域、若开 KPTI 还要切 CR3 换页表。整个过程由 `syscall` 指令硬件触发，CPU 原子完成 CS/SS 切换和 RIP/RFLAGS 保存，然后跳转到 MSR_LSTAR 指向的 entry_SYSCALL_64；内核入口代码完成栈切换、寄存器保存（pt_regs）、DS/ES 切换、可能的页表切换。返回时 `sysret` 做反向操作。段选择子中的 DPL 和页表中的 U/S 位构成双重保护，确保用户态无法访问内核内存。`rcx` 和 `r11` 被 syscall 硬件征用，所以系统调用约定的第 4 参数必须用 `r10` 而非 `rcx`。**

## 七、与仓库其他文档的关系

- **系统调用基础**：[syscall.md](/concepts/process/syscall.md) — 系统调用是什么、开销来源、优化方法、vDSO
- **段管理深入**：[segment-management.md](/concepts/process/segment-management.md) — 本文 §一 的独立深化版，完整拆解 GDT 描述符、CPL/RPL/DPL 权限检查、段保护与页表保护的双层机制
- **read+write 完整过程**：[../io/read-write-process.md](/concepts/io/read-write-process.md) — 以 read/write 为例的完整链路，含模式切换/栈变化/特权级硬件细节
- **上下文切换**：[context-switch.md](/concepts/process/context-switch.md) — 模式切换 vs 上下文切换的区别（本篇关注前者）
- **中断处理**：[interrupts.md](/concepts/process/interrupts.md) — 另一种进入内核态的方式（被动），与 syscall（主动）对比
- **内存布局**：[../elf/memory-layout.md](/concepts/elf/memory-layout.md) — 用户态和内核态的地址空间划分
- **KPTI/熔断**：[../cache/branch-prediction.md](/concepts/microarch/branch-prediction.md) — Meltdown 漏洞与 KPTI 页表隔离的来龙去脉

