x86-64 关键寄存器 —— Linux 性能/底层机制的前置知识
核心问题:看 context-switch 时遇到
CR3、CR0.TS、XMM/YMM是什么?看 syscall-details 时MSR_LSTAR、CS=0x33、RFLAGS又是什么?看 interrupts 时IDT、IF标志、APIC又是怎么工作的?
本文是一份体系化寄存器速查手册——按功能分组列出 x86-64 长模式下 Linux 性能分析和底层调试最关键的寄存器,每个都说明"它是什么、在哪个场景出现、对性能意味着什么"。它是阅读 syscall-details、context-switch、segment-management、interrupts 等底层文档的前置知识。
零、总览:x86-64 寄存器分类全景
一、通用寄存器(GPR —— General Purpose Registers)
16 个 64 位通用寄存器,是 CPU 做运算、传参、存地址的"工作台"。
1.1 寄存器表
| 寄存器 | 低 32 位 | 低 16 位 | 低 8 位 | 调用约定角色 | 在 Linux 底层中的关键场景 |
|---|---|---|---|---|---|
| RAX | EAX | AX | AL | 返回值 / 累加器 | syscall 调用号(mov $N, %rax)、函数返回值 |
| RBX | EBX | BX | BL | callee-saved | switch_to() 中压栈保存、内核栈上的固定"锚点" |
| RCX | ECX | CX | CL | 第 4 参数(用户态) | 被 syscall 硬件征用——存返回 RIP,这就是为什么 syscall 第 4 参数用 r10 而非 rcx |
| RDX | EDX | DX | DL | 第 3 参数 | syscall 的第 3 个参数、IO 端口操作(in/out) |
| RSP | ESP | SP | SPL | 栈指针 | 上下文切换的核心——switch_to() 一换 RSP 执行流就变了;syscall 时从用户栈切到内核栈 |
| RBP | EBP | BP | BPL | callee-saved / 帧指针 | 栈回溯(frame pointer chain)、GDB 中 bt 命令追溯调用链 |
| RSI | ESI | SI | SIL | 第 2 参数 | syscall 的第 2 个参数、字符串操作源地址 |
| RDI | EDI | DI | DIL | 第 1 参数 | syscall 的第 1 个参数(如 fd、buf 指针) |
| R8 | R8D | R8W | R8B | 第 5 参数 | syscall 的第 5 个参数 |
| R9 | R9D | R9W | R9B | 第 6 参数 | syscall 的第 6 个参数 |
| R10 | R10D | R10W | R10B | 临时(caller-saved) | syscall 的第 4 参数——因为 rcx 被征用 |
| R11 | R11D | R11W | R11B | 临时(caller-saved) | 被 syscall 硬件征用——存 RFLAGS,sysret 时恢复 |
| R12~R15 | R12D~R15D | R12W~R15W | R12B~R15B | callee-saved | switch_to() 中压栈/弹栈保存恢复 |
1.2 调用约定:为什么有些寄存器由调用者保存、有些由被调用者保存?
System V AMD64 ABI 的寄存器分工:
caller-saved(调用者保存):
rax rcx rdx rsi rdi r8 r9 r10 r11
→ 如果你调一个函数后还需要这些值,调用前自己 push
→ 被调函数可以随意覆盖它们,不用恢复
callee-saved(被调用者保存):
rbx rbp r12 r13 r14 r15
→ 被调函数如果用这些寄存器,必须先 push,返回前 pop 回去
→ 调用者可以放心——这些寄存器的值不会被破坏
栈指针(特殊):
rsp → 必须 16 字节对齐(call 指令前),被调函数通过 sub/add 管理自己的栈帧1.3 在上下文切换中的作用
; context_switch() → switch_to(prev, next) 简化版
; 这不是真实内核代码,是逻辑等价版本
switch_to:
; 1. 保存 callee-saved 寄存器到 prev 的内核栈上
pushq %rbp
pushq %rbx
pushq %r12
pushq %r13
pushq %r14
pushq %r15
; 2. ★ 关键:保存 prev 的 RSP 到 prev->thread.sp
movq %rsp, %rax ; rax = prev 当前的 RSP
movq %rax, prev(%rip) ; prev->thread.sp = rax
; 3. ★ 关键:把 RSP 切换到 next 的内核栈
movq next(%rip), %rsp ; rsp = next->thread.sp
; ↑ 从这行开始,执行流已经是 next 了!
; push/pop/call/ret 都操作 next 的内核栈
; 4. 恢复 next 的 callee-saved 寄存器
popq %r15
popq %r14
popq %r13
popq %r12
popq %rbx
popq %rbp
ret ; 返回到 next 的调用者为什么只保存/恢复 callee-saved 寄存器?
caller-saved 寄存器(rax/rcx/rdx/rsi/rdi/r8-r11)在调用
switch_to()时已经被调用者(即context_switch())保存过了——这是 C 调用约定的自然结果。switch_to()本身只需要负责 callee-saved 的那几个。详见 context-switch §5.4。
1.4 RCX 和 R11 被 syscall 征用的完整故事
这是理解"为什么系统调用第 4 个参数用 r10 而不是 rcx"的关键:
syscall 指令的硬件微操作(一条指令做了三件事):
1. 保存返回地址
RCX ← RIP (下一条用户态指令的地址)
2. 保存标志寄存器
R11 ← RFLAGS (当前的标志位状态)
3. 跳转到内核入口
RIP ← MSR_LSTAR (内核系统调用入口地址,如 entry_SYSCALL_64)
所以:
- rcx 的值被破坏了 → 不能用它传第 4 个参数
- r11 的值被破坏了 → 不能用它传参数
- 因此约定:syscall 第 4 参数用 r10(而非普通函数约定的 rcx)
sysret 指令反向操作:
RIP ← RCX (恢复用户态返回地址)
RFLAGS ← R11 (恢复标志)详见 syscall-details §二、§四。
二、RIP 和 RFLAGS
2.1 RIP —— 指令指针
| 属性 | 说明 |
|---|---|
| 全称 | Instruction Pointer(64 位模式下) |
| 32 位名 | EIP |
| 可读否 | 不可直接 mov 读取,但 lea (%rip), %rax 可间接获取 |
| 可写否 | 不可直接 mov,只能通过跳转指令(jmp/call/ret/syscall/sysret)间接修改 |
在性能分析中的关键使用场景:
场景 1: crash 分析
→ GDB 中 info registers 看 rip
→ rip 指向崩溃的那条指令地址
→ 通过 /proc/pid/maps 判断落在哪个段(代码段/库/堆/栈)
场景 2: perf 采样
→ perf record 采样时记录的就是 RIP
→ perf report 把 RIP 映射回函数名 → 火焰图
场景 3: syscall/sysret
→ syscall: RCX ← RIP(保存返回地址)
→ sysret: RIP ← RCX(恢复返回地址)
场景 4: 中断/异常
→ CPU 硬件自动把 RIP 压入内核栈
→ iret 时从栈上弹出恢复2.2 RFLAGS —— 标志寄存器
64 位的标志寄存器,每一位代表一个状态或控制标志。关键位:
| 位 | 名称 | 含义 | 在 Linux 底层的关键场景 |
|---|---|---|---|
| 0 | CF | 进位标志 | 无符号运算溢出 |
| 6 | ZF | 零标志 | 结果为 0 |
| 7 | SF | 符号标志 | 结果为负 |
| 9 | IF | 中断使能标志 | cli 清除(关中断)、sti 设置(开中断)。关中断 = 禁止本 CPU 响应可屏蔽中断,是 spin_lock 等临界区保护的硬件基础 |
| 10 | DF | 方向标志 | 字符串操作方向(cld/std) |
| 11 | OF | 溢出标志 | 有符号运算溢出 |
| 12-13 | IOPL | IO 特权级 | 控制哪些 ring 可执行 IO 指令(in/out) |
| 14 | NT | 嵌套任务 | 与任务切换相关(Linux 几乎不用硬件任务切换) |
| 16 | RF | 恢复标志 | 调试相关,屏蔽指令断点 |
| 17 | VM | 虚拟 8086 模式 | 兼容 16 位实模式程序 |
| 18 | AC | 对齐检查 | 启用后可检测未对齐的内存访问 |
| 21 | ID | CPUID 可用 | 标识 CPU 支持 CPUID 指令 |
在 Linux 中的两个关键点:
- syscall 时 RFLAGS 的处理:
syscall: R11 ← RFLAGS (硬件保存)
RFLAGS ← RFLAGS & ~MSR_FMASK (清除 FMASK 中指定的位,通常包括 IF)
sysret: RFLAGS ← R11 (硬件恢复)MSR_FMASK 在 Linux 中默认清除 IF——进入内核时自动关中断(但不是完全关,NMI 仍可到达)。
中断上下文中 IF 的意义:
- 关中断区域(
local_irq_disable())→cli清除 IF → 可屏蔽中断不响应 - 中断 handler 中默认 IF=0(关),下半部(softirq)中 IF=1(开)
- 中断处理中 IF 的状态决定了能否嵌套
- 关中断区域(
三、段寄存器 —— 特权级的门禁
本文只做速查。完整的段选择子结构、GDT 描述符逐位拆解、权限检查公式见 segment-management。
3.1 六个段寄存器
| 寄存器 | 全称 | 用途 | x86-64 Linux 典型值(用户态) | x86-64 Linux 典型值(内核态) |
|---|---|---|---|---|
| CS | Code Segment | 代码段选择子 + CPL | 0x33(ring 3, 用户代码段) | 0x10(ring 0, 内核代码段) |
| DS | Data Segment | 默认数据段 | 0x2B(用户数据段) | 0x18(内核数据段) |
| SS | Stack Segment | 栈段 | 0x2B(同数据段) | 0x18(同数据段) |
| ES | Extra Segment | 附加数据段 | 0x2B | 0x18 |
| FS | F Segment | 通用,常用作 TLS 基址 | 指向线程局部存储(TLS) | 切换为内核态的 per-CPU 数据 |
| GS | G Segment | 通用,常用作 per-CPU 基址 | 可能指向 TLS | 切换为内核态的 per-CPU 数据(或保持) |
3.2 CS 的段选择子结构
CS = 0x33 的位分解(16 位段选择子):
bit 15 3 2 1 0
┌──────────┬──┬─────┐
│ Index │TI│ RPL │
└──────────┴──┴─────┘
13 bits 1 2 bits
0x33 = 0b 0000000 0 11 0011
Index = 6 TI=0(查GDT) RPL=3(用户态)
0x10 = 0b 0000000 0 00 0010
Index = 2 TI=0(查GDT) RPL=0(内核态)关键理解:RPL(Requested Privilege Level)嵌在段选择子低 2 位。syscall 指令硬件将 CS 从 0x33(RPL=3)改为 0x10(RPL=0)——CPL 就这样跟着 CS 变了。
3.3 FS 和 GS:per-CPU 和 TLS 的秘密
FS 和 GS 是 x86-64 长模式下唯一仍然使用"基址"的段寄存器:
用户态:
FS 基址 → 线程局部存储(TLS)的起始地址
→ 访问方式:mov %fs:0xNN, %rax 读取 TLS 中偏移 0xNN 的值
→ C/C++ 中的 thread_local 变量、errno 都通过 FS 访问
内核态(通过 swapgs 指令切换):
GS 基址 → 指向 per-CPU 数据结构(struct per_cpu_data)
→ 访问方式:mov %gs:0xNN, %rax 读取本 CPU 的内核数据
→ 如 current(当前 task_struct)通常通过 GS 获取
swapgs 指令:
→ 交换 MSR_KERNEL_GS_BASE 和 MSR_GS_BASE
→ syscall 入口执行 swapgs,出口执行 swapgs
→ 实现用户 GS(TLS)↔ 内核 GS(per-CPU)的原子切换3.4 GDTR 和 IDTR
| 寄存器 | 全称 | 指向什么 | 在 Linux 中的特点 |
|---|---|---|---|
| GDTR | Global Descriptor Table Register | GDT(全局描述符表) | 每个 CPU 有自己的 GDT(per-CPU),lgdt 加载 |
| IDTR | Interrupt Descriptor Table Register | IDT(中断描述符表) | 全局一张表,lidt 加载;将中断向量号映射到 handler 入口 |
| TR | Task Register | 当前 TSS(任务状态段) | ltr 加载;TSS 存内核栈指针(RSP0),中断/特权级切换时用 |
四、控制寄存器 —— CPU 模式的开关
五个控制寄存器(CR0/CR2/CR3/CR4)+ 一个扩展特性寄存器(EFER),是决定 CPU 行为模式的关键。
4.1 速查表
| 寄存器 | 关键位 | 在 Linux 底层的关键场景 |
|---|---|---|
| CR0 | PG (bit 31) | 分页使能——打开后才能用虚拟地址。开机在实模式→保护模式切换时设置 |
| PE (bit 0) | 保护模式使能——脱离实模式 | |
| TS (bit 3) | Task Switched——FPU 惰性切换的核心。switch_to() 后设置 TS,新 task 首次用浮点触发 #NM 异常,异常 handler 中才真正保存/恢复 FPU 状态 | |
| WP (bit 16) | Write Protect——置 1 后,即使 ring 0 也不能写只读页。Linux 依赖此位实现 CoW(写时复制) | |
| MP/EM/NE (bit 1/2/5) | FPU 相关控制 | |
| CR2 | 全部 64 位 | 缺页异常时,CPU 自动把引发缺页的线性地址写入 CR2。do_page_fault() handler 从 CR2 读出这个地址做判断 |
| CR3 | PML4 表物理地址 (bits 12-51) | 页表根指针——存当前进程的 PML4 表物理基址。切进程时 mov cr3, next->pgd。KPTI 下每次 syscall 进出要切两次 CR3 |
| PCID (bits 0-11, 需 CR4.PCIDE=1) | Process-Context Identifier——给 TLB 条目打标签,换 CR3 时不刷 TLB | |
| CR4 | PAE (bit 5) | 物理地址扩展——64 位模式的先决条件 |
| PGE (bit 7) | 全局页使能——标记为 Global 的页不随 CR3 切换刷 TLB | |
| OSFXSR (bit 9) | 支持 SSE 指令(FXSAVE/FXRSTOR) | |
| OSXMMEXCPT (bit 10) | 支持 SSE 非掩码异常 | |
| FSGSBASE (bit 16) | 允许用户态直接读写 FS/GS 基址(rdfsbase/wrfsbase) | |
| PCIDE (bit 17) | 启用 PCID(与 CR3 的 PCID 字段配合) | |
| SMEP (bit 20) | Supervisor Mode Execution Prevention——禁止 ring 0 执行用户态页面的代码 | |
| SMAP (bit 21) | Supervisor Mode Access Prevention——禁止 ring 0 访问用户态页面(除非设置 RFLAGS.AC) | |
| EFER | SCE (bit 0) | System Call Extension——启用 syscall/sysret 指令。不开此位,syscall = #UD 异常 |
| (MSR 0xC0000080) | LME (bit 8) | Long Mode Enable——启用 64 位长模式 |
| LMA (bit 10) | Long Mode Active——只读,表示 CPU 当前在长模式 | |
| NXE (bit 11) | No-Execute Enable——启用页表中的 NX 位,支持 DEP/W^X |
4.2 CR3 与 KPTI —— 为什么每次 syscall 要切 CR3?
普通情况(无 KPTI):
用户态和内核态共用一套页表
→ 内核空间映射在所有进程的页表中(0xFFFF8000... 区域)
→ syscall 不换 CR3
KPTI(Kernel Page Table Isolation,Meltdown 漏洞缓解):
每个进程有两套页表:
- 用户页表:只有用户空间映射 + 极小的内核跳板
- 内核页表:只有内核空间映射
→ syscall 进入:CR3 切到内核页表
→ syscall 返回:CR3 切回用户页表
→ 每次切 CR3 导致 TLB 刷新(PCID 可部分缓解)
开销量化:
- 无 KPTI + 无 PCID:~0 cycles(不切)
- 有 KPTI + 无 PCID:~300-500 cycles(刷 TLB)
- 有 KPTI + 有 PCID:~100 cycles(保留打了标签的 TLB 条目)详见 syscall-details §五 和 tlb。
4.3 CR0.TS —— FPU 惰性切换的核心
惰性 FPU 切换的工作流程:
步骤 1: switch_to(A→B) 时
① 不立即保存 A 的 XMM/YMM/ZMM 寄存器(它们可能很大,最多 2KB)
② 只设置 CR0.TS = 1
→ 开销极小
步骤 2: B 首次使用浮点指令(如 movaps %xmm0, ...)
① CPU 检查 CR0.TS = 1
② 触发 #NM(Device Not Available)异常
③ 异常 handler:
- 如果 A 用过 FPU,先保存 A 的 FPU/SIMD 状态到 A->thread.fpu
- 如果 B 之前用过 FPU,恢复 B 的 FPU/SIMD 状态
- 清除 CR0.TS = 0
- 返回 B 继续执行
结果:
- B 不用浮点 → 零开销
- B 用浮点 → 只在这时付出保存/恢复开销详见 context-switch §5.5。
五、MSR 寄存器 —— 模型专用寄存器
MSR(Model-Specific Register)不是一种"类型",而是一种访问方式——通过 rdmsr/wrmsr 指令以索引方式读写。每个 MSR 有一个 32 位地址。
5.1 与系统调用相关的 MSR(Linux 最关键的 MSR)
| MSR | 地址 | 作用 | 初始化内容 |
|---|---|---|---|
| MSR_STAR | 0xC0000081 | 设置 syscall/sysret 的 CS/SS 段选择子 | 高 32 位:syscall 目标 CS/SS;低 32 位:sysret 目标 CS/SS |
| MSR_LSTAR | 0xC0000082 | syscall 入口地址(64 位 RIP) | entry_SYSCALL_64 |
| MSR_CSTAR | 0xC0000083 | 兼容模式 syscall 入口地址(32 位进程在 64 位内核上) | entry_SYSCALL_compat |
| MSR_SFMASK | 0xC0000084 | syscall 时清除 RFLAGS 的哪些位 | 通常清除 IF(关中断)、TF(调试陷阱)、DF 等 |
MSR_STAR 的位域分解(理解 CS/SS 切换的关键):
MSR_STAR 是一个 64 位寄存器:
位 [63:48] — sysret 到用户态后的 CS 基址
→ CS = (SYSCALL_CS + 16) | 3 → 即 0x33(ring 3 用户代码段)
位 [47:32] — syscall 进入内核态后的 CS 基址
→ CS = (SYSCALL_CS + 0) → 即 0x10(ring 0 内核代码段)
位 [31:0] — 保留(不用)
Linux 初始化:
wrmsr(MSR_STAR, 0, (GDT_ENTRY_KERNEL32_CS << 16) | GDT_ENTRY_KERNEL_CS)
→ 高 32 位 = 0x001B_0008(实际上 SYSCALL_CS=8,所以:
syscall CS = 8+0 = 8 → 加上 DPL 等 → 0x10
sysret CS = 8+16 = 24 → 加上 RPL=3 → 0x335.2 与 FS/GS 基址相关的 MSR
| MSR | 地址 | 作用 |
|---|---|---|
| MSR_FS_BASE | 0xC0000100 | FS 段基址——用户态指向 TLS |
| MSR_GS_BASE | 0xC0000101 | GS 段基址——内核态指向 per-CPU 数据 |
| MSR_KERNEL_GS_BASE | 0xC0000102 | swapgs 的交换目标——存内核 per-CPU 基址(或用户 GS 基址) |
swapgs 指令的原子交换:
tmp ← MSR_GS_BASE
MSR_GS_BASE ← MSR_KERNEL_GS_BASE
MSR_KERNEL_GS_BASE ← tmp
用户态:GS_BASE=TLS基址, KERNEL_GS_BASE=per-CPU基址
→ 用 GS 访问 TLS
↓ swapgs
内核态:GS_BASE=per-CPU基址, KERNEL_GS_BASE=TLS基址
→ 用 GS 访问 current、preempt_count 等 per-CPU 变量5.3 其他关键 MSR
| MSR | 地址 | 作用 |
|---|---|---|
| IA32_EFER | 0xC0000080 | 即 EFER,见 §四控制寄存器 |
| IA32_APIC_BASE | 0x1B | Local APIC 基址——xAPIC 模式下存 MMIO 地址 |
| IA32_TSC | 0x10 | 时间戳计数器——rdtsc 指令读取,高精度计时 |
| IA32_PAT | 0x277 | Page Attribute Table——控制内存类型(WC/WB/UC/WP/WT) |
| IA32_MTRR 系列 | 0x200 起 | Memory Type Range Registers——物理地址范围的内存类型控制 |
| IA32_MISC_ENABLE | 0x1A0 | 杂项使能——Fast String、EIST、Turbo 等 |
5.4 如何观测 MSR
# 读取 MSR(需要 root 权限 + msr 内核模块)
sudo modprobe msr
sudo rdmsr -a 0xC0000082 # 所有 CPU 的 LSTAR
sudo rdmsr -p 0 0xC0000082 # CPU 0 的 LSTAR
sudo rdmsr 0xC0000080 # 当前 CPU 的 EFER
# 观测 syscall 入口地址
sudo rdmsr 0xC0000082
# 输出: ffffffffabcd1234 → 这就是 entry_SYSCALL_64 的虚拟地址
# 确认 KPTI 是否开启(看 PTI 启动参数)
dmesg | grep -i "pti\|kaiser\|meltdown"
cat /sys/devices/system/cpu/vulnerabilities/meltdown六、SIMD / 浮点寄存器 —— 被上下文切换遗忘的代价
6.1 寄存器组一览
| 寄存器组 | 数量 × 宽度 | 总大小 | 指令集 | 上下文切换时是否保存 |
|---|---|---|---|---|
| x87 FPU | 8 × 80 bits | 80 bytes | x87 (FADD/FMUL) | 是(惰性) |
| MMX | 8 × 64 bits | 64 bytes | MMX(复用 x87 ST 寄存器) | 是(复用 x87 保存) |
| XMM | 16 × 128 bits | 256 bytes | SSE/SSE2/SSE3/SSE4 | 是(惰性) |
| YMM | 16 × 256 bits | 512 bytes | AVX/AVX2 | 是(惰性,如果不用则为零开销) |
| ZMM | 32 × 512 bits | 2048 bytes | AVX-512 | 是(惰性,最大开销) |
| MXCSR | 1 × 32 bits | 4 bytes | SSE 状态/控制寄存器 | 是 |
关键事实:虽然惰性切换让"不用浮点就零开销",但一旦新 task 使用了浮点/SIMD,#NM 异常 handler 中就要执行完整的 xsave/xrstor,最多搬动 2KB+ 的数据。对于 AVX-512 频繁切换的混合负载,这是一个不可忽视的隐性开销。
6.2 XCR0 —— 控制哪些状态组件参与 xsave/xrstor
XCR0(通过 xgetbv/xsetbv 指令访问)控制 xsave/xrstor 指令保存/恢复哪些状态组件:
| 位 | 名称 | 控制的组件 |
|---|---|---|
| 0 | X87 | x87 FPU 状态 |
| 1 | SSE | XMM 寄存器 + MXCSR |
| 2 | AVX | YMM 高 128 位 |
| 5 | OPMASK | AVX-512 掩码寄存器 (k0-k7) |
| 6 | ZMM_Hi256 | ZMM 高 256 位 |
| 7 | Hi16_ZMM | ZMM16-ZMM31 |
内核在 fpu__init_system_xstate() 中设置 XCR0,启用它支持的所有状态组件。 如果用户态代码使用了内核未启用的组件(如 AVX-512 而内核未在 XCR0 中置位),将触发 #UD 异常。
七、APIC 寄存器 —— 中断的硬件调度器
完整的中断投递机制和亲和性实践见 interrupts 和 irq-affinity。
7.1 Local APIC 关键寄存器
Local APIC 通过 MMIO 地址空间访问(基址从 IA32_APIC_BASE MSR 读取):
| 寄存器(偏移) | 作用 | 关键场景 |
|---|---|---|
APIC ID (0x020) | 本 CPU 的唯一标识 | MSI/MSI-X 目标地址中指定的目标 APIC ID |
EOI (0x0B0) | End of Interrupt | 中断 handler 结束时写入,通知 APIC"我处理完了,可以投递下一个" |
TPR (0x080) | Task Priority Register | 屏蔽低优先级中断 |
LVT Timer (0x320) | 本地定时器 | 调度器时钟中断的硬件来源 |
LVT LINT0/1 (0x350/0x360) | 本地中断引脚 | 连接 NMI、SMI 等 |
LVT Error (0x370) | 错误中断 | APIC 内部错误 |
LVT PMU (0x340) | 性能计数器中断 | perf 采样依赖的 PMU 溢出中断 |
ICR (0x300/0x310) | Interrupt Command Register | 发送 IPI——SMP 调度中的 reschedule IPI、TLB flush IPI 都通过 ICR |
7.2 IO-APIC 重定向表
IO-APIC 有 24 个重定向表条目(RTE),每个 64 位,控制一个 IRQ 线的投递策略:
RTE 64 位结构(简化):
位 [63:56] — 目标 APIC ID(物理模式)或目标 CPU 集合(逻辑模式)
位 [16] — 中断屏蔽位(1=屏蔽)
位 [15] — 触发模式(0=边沿, 1=电平)
位 [13] — 极性(0=高有效, 1=低有效)
位 [11] — 目标模式(0=物理, 1=逻辑)
位 [10:8] — 投递模式(000=固定, 111=ExtINT, ...)
位 [7:0] — 中断向量号(0-255)
/proc/irq/N/smp_affinity 本质上就是修改 RTE 的目标字段八、调试寄存器(DR0-DR7)和 TSC
8.1 DR0-DR7 —— 硬件断点
| 寄存器 | 作用 |
|---|---|
| DR0-DR3 | 线性断点地址(最多 4 个硬件断点) |
| DR6 | 调试状态——哪个断点命中了 |
| DR7 | 调试控制——断点类型(执行/读/写/IO)、长度、使能 |
GDB 的 watch 命令使用这些寄存器——所以最多只能设 4 个硬件 watchpoint。更多就降级为软件单步(极慢)。
8.2 TSC —— 时间戳计数器
TSC(Time Stamp Counter):
→ 从 CPU 上电开始以固定频率递增的 64 位计数器
→ 通过 rdtsc 指令读取(返回 EDX:EAX,64 位模式用 RAX:RDX)
→ 恒速 TSC(invariant TSC):频率不随 CPU 频率变化而变化
Linux 中的使用:
→ ktime_get() 底层
→ perf 的时间基准
→ ftrace 的时间戳
→ 用户态通过 __rdtsc() 获取高精度时间
注意事项:
→ 不同 CPU 核的 TSC 可能不同步(老 CPU)
→ 新 CPU(Nehalem+)有 invariant TSC,跨核同步
→ 读取开销约 20-30 cycles(比 gettimeofday 快一个数量级)九、寄存器使用场景速查表
当你阅读某个主题的文档时,可能遇到以下寄存器:
| 主题 | 涉及的关键寄存器 |
|---|---|
| 系统调用 | RAX(调用号), RDI/RSI/RDX/R10/R8/R9(参数), RCX(被征用存 RIP), R11(被征用存 RFLAGS), MSR_LSTAR/MSR_STAR/MSR_FMASK, CS/SS, CR3(KPTI) |
| 上下文切换 | RSP(SP 切换), RBX/RBP/R12-R15(callee-saved), CR3(换页表), CR0.TS(FPU 惰性), XMM/YMM/ZMM(FPU 状态) |
| 中断处理 | RFLAGS.IF(关中断), IDT(IDTR 指向), APIC ID/EOI/ICR, CS/SS(硬件压栈) |
| 段管理 | CS/DS/SS/ES/FS/GS(段选择子), GDTR, TR(TSS), MSR_FS_BASE/MSR_GS_BASE/MSR_KERNEL_GS_BASE |
| 页表/TLB | CR3(页表根), CR4.PGE/PCIDE/SMEP/SMAP, EFER.NXE, CR2(缺页地址) |
| FPU/SIMD | XMM/YMM/ZMM, MXCSR, XCR0, CR0.TS/MP/EM, CR4.OSFXSR/OSXMMEXCPT |
| 中断亲和性 | IO-APIC RTE, Local APIC ID/ICR, MSI-X Message Address |
| 原子操作 | RFLAGS(lock 前缀影响), 通用寄存器(CAS 的 cmpxchg 操作数) |
| 缺页异常 | CR2(缺页地址), CR3(页表根) |
| perf/火焰图 | RIP(采样点), TSC(时间戳) |
| core dump 分析 | RIP(崩溃指令), RSP/RBP(栈回溯), 通用寄存器(现场) |
十、交叉引用
- 系统调用详细过程:syscall-details —— RCX/R11 被征用、段寄存器切换、KPTI CR3 切换的完整时序
- 上下文切换:context-switch —— RSP 切换魔法、callee-saved 寄存器保存、FPU 惰性切换
- 段管理:segment-management —— 段选择子结构、GDT 描述符逐位拆解、CPL/RPL/DPL 权限检查
- 中断处理:interrupts —— IDT 映射、RFLAGS.IF 控制、硬件压栈
- 中断亲和性:irq-affinity —— APIC 寄存器与中断投递目标
- TLB:tlb —— CR3 切换与 TLB 刷新、PCID 优化
- IO 全链路:read-write-process —— read/write 中模式切换的寄存器变化
十一、总结
x86-64 寄存器 = CPU 的"工作台" + "控制面板"
工作台(通用寄存器 × 16):
→ 存数据、传参数、指向内存
→ syscall 时 rax=调用号,rdi/rsi/rdx/r10/r8/r9=参数
→ 上下文切换时 rsp 一换,执行流就变了
控制面板(系统寄存器):
→ CR3 = 页表根指针(切进程必换,KPTI 下 syscall 也换)
→ CR0.TS = FPU 惰性切换的开关
→ EFER.SCE = syscall 指令的总开关
→ RFLAGS.IF = 中断的总开关
→ MSR_LSTAR = syscall 入口地址
→ GDTR/IDTR = 段表/中断表的指针
这些寄存器构成了 CPU 硬件和 Linux 内核之间的"接口协议"——
理解它们,才能真正看懂 syscall、上下文切换、中断处理的每一行代码。总结
x86-64 寄存器可分“工作台”和“控制面板”两类:16 个通用寄存器负责运算、传参、存址,System V ABI 区分 caller/callee-saved——syscall 时 rax 放调用号、rdi/rsi/rdx/r10/r8/r9 放参数(第 4 参数用 r10 是因为 rcx/r11 被 syscall 指令硬件征用存返回 RIP 和 RFLAGS),rsp 一换执行流就变,rbp 供栈回溯;控制面板里 CR3 是页表根(切进程必换、KPTI 下 syscall 也换)、CR0.TS 控制 FPU 惰性切换、EFER.SCE 是 syscall 总开关、RFLAGS.IF 是中断总开关、MSR_LSTAR 存 syscall 入口、GDTR/IDTR 指向段表与中断表,另有 APIC/IO-APIC 管中断投递(perf 采样就靠 LVT PMU 溢出中断)、DR0–7 提供最多 4 个硬件断点、invariant TSC 作 perf/时间基准;这些寄存器构成 CPU 与内核之间的“接口协议”,是看懂 syscall、上下文切换、中断处理每行代码的前置知识。