ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux x86_64 用户态 FS/GS 段寄存器完全指南:从 TLS 到 FSGSBASE 指令与上下文切换

Linux x86_64 用户态 FS/GS 段寄存器完全指南:从 TLS 到 FSGSBASE 指令与上下文切换 Linux x86_64 用户态 FS/GS 段寄存器完全指南从 TLS 到 FSGSBASE 指令与上下文切换【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux导读本文基于 Linux 内核官方文档 Documentation/arch/x86/x86_64/fsgs.rst系统讲解 x86-64 架构下 FS/GS 段寄存器在用户态程序中的两种访问机制——arch_prctl()系统调用与 FSGSBASE 指令族并深入剖析 GCC/Clang 编译器对 FS/GS 相对寻址的支持方式以及内核在上下文切换中处理 GS 的历史演进与安全约束。读完本文你将掌握线程本地存储TLS的底层原理、如何检测并安全使用 FSGSBASE 指令、如何用__seg_fs/__seg_gs地址空间或 Clang attribute 实现段相对寻址并理解 SWAPGS、LKGS、WRGSBASE 与 MSR_KERNEL_GS_BASE 在切换流程中的职责边界。段寄存器寻址概念基础x86 架构原生支持内存分段。凡是访问内存的指令都可以使用基于段寄存器的寻址方式其记号形式为Segment-register:Byte-address实际访问时CPU 会把**段基地址base address**与 Byte-address 相加得到最终被访问的虚拟地址。这种机制的核心价值在于同一份代码可以用完全相同的 Byte-address 访问多份数据实例而实例的选取完全由段寄存器中的基地址决定——例如每个线程一份的数据副本只要各自设置不同的段基地址就能运行完全相同的通用代码而无需复杂的偏移计算。32 位模式与 64 位模式的差异32 位模式CPU 提供 6 个段CS/SS/DS/ES/FS/GS且支持段限长segment limits可用限长强制实施地址空间保护。64 位模式CS/SS/DS/ES 段被忽略基地址恒为 0从而提供完整的 64 位平坦地址空间FS 和 GS 段仍然有效且其基地址被扩展为完整的 64 位。因此FS/GS 成为 64 位模式下用户态程序唯一可以自由控制的段寄存器。FS 与 GS 的常见用途FS 段普遍用于线程本地存储TLS。FS 通常由运行时runtime或线程库管理以__thread存储类说明符声明的变量每个线程各有一份实例编译器对这类变量的访问自动生成FS:地址前缀每个线程拥有独立的 FS 基地址因此公共代码无需复杂偏移计算即可访问各自的线程实例。文档特别强调当应用程序使用了会管理每线程 FS 的运行时或线程库时不应再将 FS 挪作他用否则会与线程库的 FS 管理冲突。GS 段没有常见用途应用程序可以自由使用。GCC 与 Clang 都通过地址空间标识符address space identifiers支持基于 GS 的寻址具体语法见后文编译器对 FS/GS 相对寻址的支持一节。读写 FS/GS 基地址的两种机制读写 FS/GS 基地址共有两套机制arch_prctl()系统调用——通用、兼容性最好FSGSBASE 指令族——性能更优、更灵活但需要硬件与内核双重支持。通过 arch_prctl() 访问 FS/GS 基地址arch_prctl(2)机制在所有 64 位 CPU 和所有内核版本上均可用是最保守的选择。其相关命令码定义在内核 UAPI 头文件 arch/x86/include/uapi/asm/prctl.h 中命令码值含义ARCH_SET_GS0x1001设置 GS 基地址ARCH_SET_FS0x1002设置 FS 基地址ARCH_GET_FS0x1003读取 FS 基地址ARCH_GET_GS0x1004读取 GS 基地址读取基地址arch_prctl(ARCH_GET_FS, fsbase); arch_prctl(ARCH_GET_GS, gsbase);写入基地址arch_prctl(ARCH_SET_FS, fsbase); arch_prctl(ARCH_SET_GS, gsbase);需要特别注意的是ARCH_SET_GS可能被禁用具体取决于内核配置与安全设置详见下文GS 在上下文切换中的复杂性。从源码看arch_prctl()的实现位于 arch/x86/kernel/process_64.c 的do_arch_prctl_64()ARCH_SET_GS/ARCH_SET_FS会先检查arg2 TASK_SIZE_MAX非法地址返回-EPERM设置基地址的同时会把段选择子index清零——注释明确说明ARCH_SET_GS 始终会覆盖 index 和 base0 是最合理的 index 值也是 FSGSBASE 不可用时唯一有意义的值基地址同时保存在task-thread.fsbase/task-thread.gsbase中供旧内核的save_base_legacy()路径使用ARCH_GET_FS/ARCH_GET_GS通过put_user()把当前任务的基地址拷贝到用户缓冲区。通过 FSGSBASE 指令访问 FS/GS 基地址Intel 从Ivy Bridge第三代酷睿开始引入一组可直接在用户态读写 FS/GS 基地址的新指令AMDFamily 17HZenCPU 同样支持。共四条指令指令作用RDFSBASE %reg读取 FS 基地址寄存器RDGSBASE %reg读取 GS 基地址寄存器WRFSBASE %reg写入 FS 基地址寄存器WRGSBASE %reg写入 GS 基地址寄存器这些指令避免了arch_prctl()系统调用的开销无需陷入内核让用户态程序可以更灵活地使用 FS/GS 寻址模式。但文档也指出这并不能消除线程库/运行时与应用程序各自使用 FS 时的潜在冲突——FS 的所有权问题依旧存在。FSGSBASE 指令的启用条件硬件枚举FSGSBASE 指令由 CPUID leaf 7、EBX 的 bit 0 枚举。如果可用/proc/cpuinfo的 flags 中会显示fsgsbase。内核显式开启CR4硬件支持不会自动启用这些指令内核必须显式地在 CR4 寄存器中开启CR4.FSGSBASE。原因在于旧内核会对 GS 寄存器的值做出假设并在通过arch_prctl()设置 GS base 时强制这些假设如果允许用户态向 GS base 写入任意值就会破坏这些假设导致功能异常。在未启用 FSGSBASE 的内核上执行这些指令会触发#UDUndefined Opcode异常。内核启用该特性的具体代码位于 arch/x86/kernel/cpu/common.cif (cpu_feature_enabled(X86_FEATURE_FSGSBASE)) { cr4_set_bits(X86_CR4_FSGSBASE); elf_hwcap2 | HWCAP2_FSGSBASE; }即CPU 支持X86_FEATURE_FSGSBASE时置位 CR4 的 FSGSBASE 位并同步在 ELF AUX vector 的HWCAP2_FSGSBASE位中通告用户态。用户态检测推荐做法内核通过 ELF AUX vector 提供可靠的启用状态信息。若 AUX vector 中的HWCAP2_FSGSBASE位被置位说明内核已启用 FSGSBASE 指令应用程序可以使用。该位的正式定义位于 arch/x86/include/uapi/asm/hwcap2.hHWCAP2_FSGSBASE _BITUL(1)。文档给出的检测代码如下#include sys/auxv.h #include elf.h /* Will be eventually in asm/hwcap.h */ #ifndef HWCAP2_FSGSBASE #define HWCAP2_FSGSBASE (1 1) #endif .... unsigned val getauxval(AT_HWCAP2); if (val HWCAP2_FSGSBASE) printf(FSGSBASE enabled\n);FSGSBASE 指令的编译器内建支持GCC 4.6.4 及更新版本提供 FSGSBASE 指令的内建函数intrinsicsClang 5同样支持。内建函数作用_readfsbase_u64()读取 FS 基地址寄存器_readgsbase_u64()读取 GS 基地址寄存器_writefsbase_u64()写入 FS 基地址寄存器_writegsbase_u64()写入 GS 基地址寄存器使用这些内建函数需要满足两个条件源码中#include immintrin.h编译时添加-mfsgsbase选项。编译器对 FS/GS 相对寻址的支持GCC 的 Named Address SpacesGCC 6 及更新版本通过 Named Address Spaces 支持 FS/GS 相对寻址提供了两个 x86 地址空间标识符标识符含义__seg_fs变量相对于 FS 寻址__seg_gs变量相对于 GS 寻址当编译器支持这些地址空间时会预定义预处理符号__SEG_FS与__SEG_GS。实现回退模式fallback的代码应检查这些符号是否已定义。文档给出的用法示例#ifdef __SEG_GS long data0 0; long data1 1; long __seg_gs *ptr; /* Check whether FSGSBASE is enabled by the kernel (HWCAP2_FSGSBASE) */ .... /* Set GS base to point to data0 */ _writegsbase_u64(data0); /* Access offset 0 of GS */ ptr 0; printf(data0 %ld\n, *ptr); /* Set GS base to point to data1 */ _writegsbase_u64(data1); /* ptr still addresses offset 0! */ printf(data1 %ld\n, *ptr);这个示例精妙地演示了段寻址的本质ptr 0固定访问当前 GS 基地址处的偏移 0切换 GS 基地址后同一个指针访问到的是另一份数据——这正是同一 Byte-address、多份实例的体现。Clang 的 attribute 机制Clang不提供GCC 的地址空间标识符但在Clang 2.6 及更新版本中通过 attribute 提供等价能力attribute含义__attribute__(address_space(256))变量相对于 GS 寻址__attribute__(address_space(257))变量相对于 FS 寻址内联汇编中的 FS/GS 寻址当编译器不支持地址空间时可以直接用内联汇编实现 FS/GS 相对寻址mov %fs:offset, %reg mov %gs:offset, %reg mov %reg, %fs:offset mov %reg, %gs:offsetGS 在上下文切换中的复杂性这一部分是本文档的技术深水区涉及内核如何在每次进程/线程切换时安全地在用户态 GS 与内核 GS 之间切换。理解它需要先回顾历史。历史背景32 位时代进入内核时需要重载数据段退出时恢复。由于所有段数据都在 GDT/LDT 中只需保存段选择子即可GDT/LDT 中的基地址是 32 位宽的段选择子值由用户自行选择、实际上是任意的。64 位时代32 位机制太慢因此段被拉平mostly flat进入/退出内核不再需要重载。FS/GS 基地址扩展为 64 位并通过 MSR 访问同时引入了独立的GS_SHADOW值。SWAPGS 指令交换 GS_BASE 与 GS_SHADOW成为进入/退出内核时唯一的必要动作。64 位用户态通过prctl(ARCH_SET_GS)设置超过 32 位的基地址该系统调用的副作用是把 GS 选择子清零。FSGSBASE 指令出现后用户态终于可以不经系统调用选择任意基地址。Linux 的 ABI 承诺即使选择子值与基地址脱钩两者都会被完整保留。会修改 GS 的硬件指令全集审视硬件能力有多条 x86 指令会修改 GS1. SWAPGS交换MSR_KERNEL_GS_BASE与 GS 选择子隐藏部分中的活动 GS.base。这是进入/退出内核时唯一的常规切换动作。2. MOV segment selector, GS传统路径非 FRED用指定选择子装载 GS并从 GDT/LDT 获取描述符属性、限长与基地址把 32 位基地址写入活动 GS.base零扩展为 64 位不触碰MSR_KERNEL_GS_BASE。问题在于它写的是当前GS.base会破坏内核当前活动的每 CPU 指针在 %gs 中——这就是为什么内核在经典路径下不能直接使用 MOV GS 的原因。3. LKGS selectorFRED 路径替代 MOV GS与 MOV GS 类似加载选择子与描述符属性但重定向基地址写入不更新活动 GS.base而是把描述符基地址写入IA32_KERNEL_GS_BASE即MSR_KERNEL_GS_BASE。关键局限由于 GDT/LDT 描述符只编码 32 位基地址LKGS 只能写入零扩展的 32 位值无法正确表示完整的 64 位用户态 GS base例如 TLS 指针因此之后仍需要一次完整的 64 位 WRMSR。LKGS 保证了内核每 CPU 指针始终完好无需自定义错误处理。MOV GS 与 LKGS 是仅有的能更新 GS 描述符其他字段选择子、属性、限长的指令。4. WRGSBASE reg在 64 位模式下直接向当前活动的 GS.base 写入完整的 64 位值64 位模式下 FS.base 与 GS.base 被扩展为 64 位以覆盖整个地址空间。内核上下文中的问题当前活动的 GS.base 属于内核而非用户任务。因此在上下文切换期间使用它会破坏内核自身的 GS.base——除非用 SWAPGS 包裹仅在 IDT 模式下安全。在其余模式下基地址的高 32 位会被清零。5. WRMSR MSR_KERNEL_GS_BASE / WRMSRNS MSR_KERNEL_GS_BASE向MSR_KERNEL_GS_BASE写入完整的 64 位值。该 MSR 保存非活动的用户态GS.base——即 SWAPGS 时会被交换进活动 GS.base 的那个值。这是内核模式下上下文切换时唯一能正确设置 64 位用户态 GS.base 的指令。两个厂商以不同方式实现了写入的非串行化non-serializing语义AMD从 Zen4 开始作为默认行为通过CPUID_Fn80000021_EAXExtended Feature 2 EAX的 bit 1FsGsKernelGsBaseNonSerializing固定为 1 表明。Intel通过WRMSRNS 指令作为非串行化变体实现。命名与语义提醒在内核模式下MSR_KERNEL_GS_BASE里实际保存的是用户态的 GS.base命名容易造成混淆。可以这样理解由于 MSR 只能在 CPL0 访问这个寄存器是内核访问 GS.base 的窗口——它承载着内核需要代为保存/恢复的用户 GS 基地址。实战要点汇总兼容性优先选arch_prctl()在所有 64 位 CPU 与所有内核版本上可用代价是系统调用开销注意ARCH_SET_GS可能被内核配置/安全策略禁用。性能优先选 FSGSBASE 指令需同时满足CPU 支持/proc/cpuinfo含fsgsbase与内核已启用AUX vectorHWCAP2_FSGSBASE置位两个条件否则执行会触发 #UD。检测优先级运行时检测应使用getauxval(AT_HWCAP2) HWCAP2_FSGSBASE而非仅检查/proc/cpuinfo——后者只反映硬件能力不代表内核已启用。编译器选型GCC 6 用__seg_fs/__seg_gs配__SEG_FS/__SEG_GS宏做回退判断Clang 用address_space(256/257)attribute都不支持时退回内联汇编。FS 所有权纪律使用线程库或运行时其会管理每线程 FS的应用不要将 FS 挪作他用避免与 TLS 机制冲突。理解内核 GS 切换约束内核路径下设置用户态 GS 基地址必须写MSR_KERNEL_GS_BASE而非活动 GS.baseFRED 路径使用 LKGS 保证内核每 CPU 指针不被破坏且需后续 64 位 WRMSR 补全完整基地址。延伸阅读内核 arch_prctl 实现arch/x86/kernel/process_64.cprctl 命令码 UAPI 定义arch/x86/include/uapi/asm/prctl.hFSGSBASE 能力位定义与 AUX vector 通告arch/x86/include/uapi/asm/hwcap2.h、arch/x86/kernel/cpu/common.c本文档原文Documentation/arch/x86/x86_64/fsgs.rst【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进