ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux 内核 x86-64 入口机制详解:entry_64.S 入口点、IDT 向量布局与 SWAPGS 处理

Linux 内核 x86-64 入口机制详解:entry_64.S 入口点、IDT 向量布局与 SWAPGS 处理 Linux 内核 x86-64 入口机制详解entry_64.S 入口点、IDT 向量布局与 SWAPGS 处理【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux本文基于 Linux 内核仓库中 Documentation/arch/x86/entry_64.rst 的官方文档展开系统讲解 x86-64 内核的全部入口类型系统调用、int 0x80、sysenter、中断、异常、系统向量并逐条对照 arch/x86/entry/entry_64.S、arch/x86/include/asm/irq_vectors.h 与 arch/x86/kernel/idt.c 中的真实实现。读完后你可以理解CPU 是如何从用户态跳入内核的、256 个 IDT 向量在 Linux 中如何分工以及为什么swapgs是内核入口代码中最脆弱、必须逐字节配对的一条指令。一、x86 架构进入内核的路径家族x86 架构有相当多不同的方式可以跳入内核代码。官方文档指出这些入口点在 arch/x86/kernel/traps.c 中注册当前代码中该文件调用 idt_setup_traps() 完成 IDT 安装具体实现则分布在三个汇编文件64 位原生代码arch/x86/entry/entry_64.S32 位代码arch/x86/entry/entry_32.S64 位内核上运行 32 位进程的兼容系统调用arch/x86/entry/entry_64_compat.S文档列出的入口类型可以在源码中一一对应文档中的入口源码位置说明system_call64 位 syscall 指令entry_SYSCALL_64entry_64.S#L87-L17064 位程序最主要的系统调用入口entry_INT80_compatint 0x80int80_emulationentry_64_compat.S#L29432 位或 64 位代码执行int 0x80均落入此处按兼容系统调用处理ia32_sysentersysenterentry_SYSENTER_compatentry_64_compat.S#L5032 位代码的 sysenter 快速系统调用interrupt中断数组irq_entries_start/spurious_entries_startidtentry.h#L522-L552所有未显式指向别处的 IDT 向量统一落入该数组最终汇合到common_interrupt()APIC 中断系统向量 0xf0-0xff 区段TLB shootdowncall_function IPI、重调度等专用中断架构定义异常如 divide_errorexc_divide_error等见 idtentry.h#L572-L595处理器硬件异常一个值得注意的细节文档提到中断入口“指向一组魔术生成的函数magically-generated functions”。在 arch/x86/include/asm/idtentry.h 中确实如此——汇编预处理阶段用一个.rept循环为每个设备中断向量生成一条固定对齐的存根stub.align IDT_ALIGN SYM_CODE_START(irq_entries_start) vectorFIRST_EXTERNAL_VECTOR .rept NR_EXTERNAL_VECTORS UNWIND_HINT_IRET_REGS 0 : ENDBR .byte 0x6a, vector ; pushq vector编码为 0x6a 避免 5 字节长 push jmp asm_common_interrupt .fill 0b IDT_ALIGN - ., 1, 0xcc vector vector1 .endr SYM_CODE_END(irq_entries_start)即每个向量存根只做一件事把向量号压栈占据pt_regs中 error_code 的位置然后jmp到统一的asm_common_interrupt由 C 函数common_interrupt()携带向量号完成分发。存根使用.byte 0x6a, vector而非直接pushq是因为编译器会把大于 0x7F 的向量展开成 5 字节指令破坏固定对齐代价是在 C 侧必须把向量号截断为 u8 再还原见DEFINE_IDTENTRY_IRQ中u32 vector (u32)(u8)error_codeidtentry.h#L206-L222。二、IDT 向量布局256 个槽位的分工文档指明“IDT 向量分配列于 arch/x86/include/asm/irq_vectors.h”。该文件头部注释给出了总体布局Vectors 0 ... 31 : 系统陷阱与异常 —— 硬编码事件 Vectors 32 ... 127 : 设备中断 Vector 128 : 遗留 int80 系统调用接口 Vectors 129 ... FIRST_SYSTEM_VECTOR-1 : 设备中断 Vectors FIRST_SYSTEM_VECTOR ... 255 : 专用系统中断64 位 x86 拥有每 CPU 一份的 IDT 表32 位则全机共享一张。关键常量均为该头文件中的宏FIRST_EXTERNAL_VECTOR 0x20外部设备中断向量从 0x20 开始IA32_SYSCALL_VECTOR 0x80int 0x80 入口向量ISA_IRQ_VECTOR(irq)0x30-0x3f 保留给 ISA 中断宏向上取整到 16 的边界0xf0-0xff 为 SMP 系统向量其中部分“稀有”向量被合并进单个向量以节省空间。系统向量区0xef-0xff 附近的具体分配如下全部可在 irq_vectors.h#L54-L100 中核实向量宏名用途0xffSPURIOUS_APIC_VECTOR伪中断APIC 无源中断0xfeERROR_APIC_VECTORAPIC 错误报告0xfdRESCHEDULE_VECTOR重调度 IPI0xfcCALL_FUNCTION_VECTOR广播 IPITLB shootdown 等0xfbCALL_FUNCTION_SINGLE_VECTOR单目标 IPI0xfaTHERMAL_APIC_VECTOR热事件0xf9THRESHOLD_APIC_VECTORMCE 阈值中断0xf8REBOOT_VECTOR重启 IPI0xf7X86_PLATFORM_IPI_VECTOR平台特定 IPI0xf6IRQ_WORK_VECTORIRQ work0xecLOCAL_TIMER_VECTOR本地 APIC 定时器0xf0-0xf2POSTED_INTR_*_VECTORKVM posted interrupt IPI这些“魔术生成”的入口存根与 IDT 的绑定在启动时由 arch/x86/kernel/idt.c 完成idt_setup_apic_and_irq_gates()idt.c#L294-L318按位图遍历每个未被占用的向量将其指向irq_entries_start IDT_ALIGN * (i - FIRST_EXTERNAL_VECTOR)或spurious_entries_start ...即精确命中上述汇编存根数组中的对应槽位随后idt_map_in_cea()将 IDT 映射进 CPU 入口区CEAKPTI 缓解措施并load_idt()装载。文档中“TLB shootdown 之类的专用中断”正是通过CALL_FUNCTION_VECTOR/CALL_FUNCTION_SINGLE_VECTOR以 IPI 形式送达各 CPU进入sysvec_call_function()等处理函数声明见 idtentry.h#L678-L688。三、64 位系统调用入口entry_SYSCALL_64 逐段解析entry_SYSCALL_64是 x86-64 最重要的入口其完整实现见 entry_64.S#L87-L170。核心流程SYM_CODE_START(entry_SYSCALL_64) UNWIND_HINT_ENTRY ENDBR # 间接分支跟踪CET入口 swapgs # 切换到内核 GS /* tss.sp2 is scratch space. */ movq %rsp, PER_CPU_VAR(cpu_tss_rw TSS_sp2) # 保存用户态 RSP SWITCH_TO_KERNEL_CR3 scratch_reg%rsp # PTI: 切换到内核页表 movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp # 切换到内核任务栈 ...后续步骤同一段代码内构造pt_regs帧依次压入ss__USER_DS、用户sp、r11flags、cs__USER_CS、rcx返回 ip到entry_SYSCALL_64_after_hwframe标签再压入raxorig_axPUSH_AND_CLEAR_REGS rax$-ENOSYS保存并清零其余通用寄存器rax 默认 -ENOSYS防止信息泄漏movslq %eax, %rsi将系统调用号符号扩展为 64 位系统调用号按 int 处理随后call do_syscall_64entry_64.S#L121快速返回路径若无任何工作被挂起%al为 0走syscall_return_via_sysret——弹寄存器、切到 trampoline 栈、SWITCH_TO_USER_CR3_STACK换回用户页表最后swapgssysretqentry_64.S#L162-L166。源码注释中特别说明了 SYSRET 的取舍当用户可能修改pt_regs-foo时一律强制 IRET因为 SYSRET 对非规范uncanonical地址处理存在 AMD 与 Intel 双方 CPU 的历史 bugXen PV 环境下也必须用 IRET。这正是文档所说“syscall 和 sysenter 指令有自己奇怪的调用约定peculiar calling conventions”的具体体现syscall 把返回地址隐式存入 RCX/R11参数寄存器顺序rdi/rsi/rdx/r10/r8/r9也不完全等于 C ABI因此入口汇编必须手工重组pt_regs并把 r10 搬到 rcx。四、int 0x80 与 sysenter32 位兼容入口对于运行在 64 位内核上的 32 位进程入口实现在 arch/x86/entry/entry_64_compat.Sentry_SYSENTER_compatentry_64_compat.S#L50处理sysenter指令注意其寄存器约定与 syscalls 表完全不同参数在 eax/ebx/ecx/edx/edi/esI因此需要专门的修正逻辑代码中的.Lsysenter_fix_flags分支处理标志寄存器int80_emulationentry_64_compat.S#L294int 0x80从 32 位或 64 位代码发出都会落到向量 0x80IA32_SYSCALL_VECTOR该存根直接jmp do_int80_emulationC 侧实现见 arch/x86/entry/syscall_32.c#L123。do_int80_emulation()开头有一个值得留意的防御性检查/* Kernel does not use INT $0x80! */ if (unlikely(!user_mode(regs))) { irqentry_enter(regs); instrumentation_begin(); panic(Unexpected external interrupt 0x80\n); }即内核态执行int 0x80属于严重错误直接 panic——这印证了文档中“entry_INT80_compat 对 32 位或 64 位代码都是兼容系统调用compat syscall”的限定它只服务用户态。五、SWAPGS内核入口中最脆弱的指令这是官方文档着墨最多的部分也是理解内核入口代码的关键。swapgs在用户 GS 与内核 GS 之间切换其危险性在于必须完美配对且只嵌套一层深度只应在“从用户态进入内核态”时执行一次并在“返回用户态”时精确地执行再一次稍有错位多一次、少一次内核立即崩溃。由此产生两类场景的处理差异正常中断/用户陷阱边界可以用“廉价检查”——直接从内核栈入口帧的 CS 字段读出原模式xorl %ebx,%ebx testl $3,CS8(%rsp) je error_kernelspace SWAPGS在正常边界栈上的 CS 是可靠的指示器看到是内核态的次级进入secondary entry说明 GS base 已经切过不能再 swap看到打断的是用户态执行则必须执行 SWAPGS。超原子super-atomic入口NMI/MCE/DEBUG 等它们可能在“正常入口刚把 CS 写入栈、但尚未执行 SWAPGS”的中间瞬间触发此时栈上的 CS 已失去指示意义。唯一安全的办法是“昂贵检查”——直接读回 SWAPGS 修改的 MSRmovl $1,%ebx movl $MSR_GS_BASE,%ecx rdmsr testl %edx,%edx js 1f /* 负值 - 在内核态 */ SWAPGS xorl %ebx,%ebx 1: retGS base 的高位为负js说明当前是内核 GS不需要再切换。文档总结需要昂贵检查的超原子入口NMI 除外NMI 单独处理必须使用 paranoid 变体的入口代码这会带来三个行为变化中断入口使用较慢的 gsbase 检查从用户态进入时切离 IST中断栈栈退回到内核态时不尝试重新调度reschedule。内核的原则是只让真正需要昂贵检查的向量使用 IST 入口和 paranoid 代码所有“普通”入口都用更快的常规变体。当前代码中的对应实现文档写作于 2011 年前后内容改编自 Ingo Molnar 的内核邮件其中“paranoid1 的 idtentry”思想在当前代码中已演化为一族显式的汇编宏全部定义在 entry_64.Sidtentry_bodyL289调用error_entry()完成用户态入口的换栈等工作把%rsppt_regs 指针放入 rdi、error_code 放入 rsi 后调用 C 处理函数idtentryL329普通异常/中断存根无 IST、无 paranoid 检查对无硬件错误码的向量手工压-1填充 ORIG_RAXidtentry_irqL376-L379设备中断专用按 L1 缓存行对齐“common_interrupt 是热路径”idtentry_mce_dbL397-L430这正是文档 paranoid 机制的现行形态——先testb $3, CS-ORIG_RAX(%rsp)判断来源用户态入口走正常的idtentry_body noist_##cfunc换栈路径内核态入口则call paranoid_entry内部做 MSR_GS_BASE 昂贵检查返回的 GS 状态保存在 EBX 供paranoid_exit使用处理完jmp paranoid_exit返回且不做抢占检查idtentry_dfL518-L541双重故障入口注释明确写着“Straight paranoid. No checks from which context this comes”即无条件昂贵检查——对应文档“#DF 等超原子入口”的要求idtentry_vcL454AMD 内存加密的 #VC 入口同样先paranoid_entry再调用vc_switch_off_ist()切离 IST 栈——正是文档所述“用户态进入时切离 IST 栈”行为变化的实例。而 NMI 之所以“单独处理”体现在 idtentry.h#L614-L624DECLARE_IDTENTRY_NMI展开为空不生成 asm 存根NMI 入口由 arch/x86/kernel/nmi.c 专门管理与 IST 机制和“executing”标记配合工作。六、调用约定差异错误码与 IST文档指出三个复杂点不同入口调用约定不同部分 IDT 项会压入错误码、部分不会使用 IST 备用栈的 IDT 项需要额外魔术才能得到正确的栈帧参考 AMD APM 卷 2 第 8 章、Intel SDM 卷 3 第 6 章。源码中的落实方式错误码idtentry宏以has_error_code参数区分两类向量。无错误码的向量在压 pt_regs 前补一个pushq $-1占位entry_64.S#L343-L345有错误码的向量如 #GP、#PF则由硬件直接压栈C 侧宏DECLARE_IDTENTRY_ERRORCODE/DEFINE_IDTENTRY_ERRORCODE为处理函数附加unsigned long error_code第二参数idtentry.h#L86-L114。idtentry_body在has_error_code 1时从ORIG_RAX(%rsp)取回错误码放入 rsientry_64.S#L307-L310。IST 栈需要 IST 的向量如 MCE、#DB、#DF在 C 侧使用DECLARE_IDTENTRY_IST/DEFINE_IDTENTRY_ISTidtentry.h#L331-L359并额外声明noist_##func用户态变体配合idtentry_mce_db宏的双路径分派。C 侧统一骨架DEFINE_IDTENTRY宏保证每个普通入口都被irqentry_enter()/irqentry_exit()包裹idtentry.h#L53-L66处理函数声明为noinstr保证入口关键段不被动态插桩破坏——这是阅读 arch/x86/kernel/traps.c 中各DEFINE_IDTENTRY(exc_*)处理函数时的基本心智模型。七、实践导读如何按图索骥阅读入口代码结合本文的脉络推荐阅读路径先看向量布局arch/x86/include/asm/irq_vectors.h 是全局地图256 个向量的分区一目了然再看存根生成arch/x86/include/asm/idtentry.h 被entry_64.S以#include asm/idtentry.h方式内联entry_64.S#L548-L556同一份宏在 C 侧生成函数声明、在汇编侧生成入口存根保证两者一致精读入口汇编arch/x86/entry/entry_64.S 中entry_SYSCALL_64快速路径、idtentry家族宏常规入口、idtentry_mce_db/idtentry_dfparanoid 路径三条路线分别对应文档描述的三类复杂度看 IDT 安装arch/x86/kernel/idt.c 的set_intr_gate()/idt_setup_traps()/idt_setup_apic_and_irq_gates()展示存根数组如何写入 IDT 表并映射到 CEA兼容层收尾arch/x86/entry/entry_64_compat.S 与 arch/x86/entry/syscall_32.c 说明 int 0x80 / sysenter 在 64 位内核上的落地方式。需要说明的前提本文所有行号引用对应当前仓库快照入口代码与具体 CPU 特性强相关PTI/KPTI、CET IBT、FRED、Xen PV 等均有 ALTERNATIVE 或条件编译分支阅读时应注意CONFIG_MITIGATION_PAGE_TABLE_ISOLATION、CONFIG_X86_FRED、CONFIG_XEN_PV等配置项对实际生成代码的影响。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表