29.7. 在用户空间应用程序中使用 FS 和 GS 段¶
x86 架构支持分段。访问内存的指令可以使用基于段寄存器的寻址模式。以下符号用于寻址段中的一个字节
段寄存器:字节地址
段基地址被添加到字节地址以计算要访问的结果虚拟地址。这允许使用相同的字节地址访问数据的多个实例,即相同的代码。特定实例的选择纯粹基于段寄存器中的基地址。
在 32 位模式下,CPU 提供 6 个段,这些段也支持段限制。这些限制可用于强制执行地址空间保护。
在 64 位模式下,CS/SS/DS/ES 段被忽略,并且基地址始终为 0 以提供完整的 64 位地址空间。 FS 和 GS 段在 64 位模式下仍然有效。
29.7.1. 常见的 FS 和 GS 用法¶
FS 段通常用于寻址线程本地存储 (TLS)。 FS 通常由运行时代码或线程库管理。使用 '__thread' 存储类说明符声明的变量是按线程实例化的,并且编译器为对这些变量的访问发出 FS: 地址前缀。每个线程都有其自己的 FS 基地址,因此可以使用通用代码而无需复杂的地址偏移计算来访问每个线程的实例。当应用程序使用管理每个线程 FS 的运行时或线程库时,不应将 FS 用于其他目的。
GS 段没有常用用途,可以由应用程序自由使用。 GCC 和 Clang 通过地址空间标识符支持基于 GS 的寻址。
29.7.2. 读取和写入 FS/GS 基地址¶
存在两种机制来读取和写入 FS/GS 基地址
arch_prctl() 系统调用
FSGSBASE 指令集
29.7.3. 使用 arch_prctl() 访问 FS/GS 基地址¶
基于 arch_prctl(2) 的机制在所有 64 位 CPU 和所有内核版本上都可用。
读取基地址
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 prctl 可能会根据内核配置和安全设置禁用。
29.7.4. 使用 FSGSBASE 指令访问 FS/GS 基地址¶
通过 Ivy Bridge CPU 世代,Intel 引入了一组新的指令,可以直接从用户空间访问 FS 和 GS 基址寄存器。 AMD Family 17H CPU 也支持这些指令。以下指令可用
RDFSBASE %reg
读取 FS 基址寄存器
RDGSBASE %reg
读取 GS 基址寄存器
WRFSBASE %reg
写入 FS 基址寄存器
WRGSBASE %reg
写入 GS 基址寄存器
这些指令避免了 arch_prctl() 系统调用的开销,并允许在用户空间应用程序中更灵活地使用 FS/GS 寻址模式。这并不能防止利用 FS 的线程库和运行时与想要将其用于自己目的的应用程序之间发生冲突。
29.7.4.1. FSGSBASE 指令启用¶
这些指令在 CPUID 叶 7 中枚举,EBX 的位 0。如果可用,/proc/cpuinfo 会在 CPU 的标志条目中显示“fsgsbase”。
指令的可用性不会自动启用它们。内核必须在 CR4 中显式启用它们。原因是旧内核对 GS 寄存器中的值做出假设,并在通过 arch_prctl() 设置 GS 基址时强制执行这些假设。允许用户空间将任意值写入 GS 基址会违反这些假设并导致故障。
在未启用 FSGSBASE 的内核上,执行 FSGSBASE 指令将导致 #UD 异常。
内核在 ELF AUX 向量中提供有关启用状态的可靠信息。如果在 AUX 向量中设置了 HWCAP2_FSGSBASE 位,则内核已启用 FSGSBASE 指令,并且应用程序可以使用它们。以下代码示例显示了此检测的工作原理
#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");
29.7.4.2. FSGSBASE 指令编译器支持¶
GCC 版本 4.6.4 及更高版本为 FSGSBASE 指令提供内在函数。 Clang 5 也支持它们。
_readfsbase_u64()
读取 FS 基址寄存器
_readgsbase_u64()
读取 GS 基址寄存器
_writefsbase_u64()
写入 FS 基址寄存器
_writegsbase_u64()
写入 GS 基址寄存器
要使用这些内在函数,必须在源代码中包含 <immintrin.h> 并添加编译器选项 -mfsgsbase。
29.7.5. 编译器对基于 FS/GS 的寻址的支持¶
GCC 版本 6 及更高版本通过命名地址空间提供对基于 FS/GS 的寻址的支持。 GCC 为 x86 实现以下地址空间标识符
__seg_fs
变量是相对于 FS 寻址的
__seg_gs
变量是相对于 GS 寻址的
当支持这些地址空间时,会定义预处理器符号 __SEG_FS 和 __SEG_GS。实现回退模式的代码应检查是否定义了这些符号。用法示例
#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);
Clang 不提供 GCC 地址空间标识符,但它通过基于属性的机制在 Clang 2.6 和更新版本中提供地址空间
__attribute__((address_space(256))
变量是相对于 GS 寻址的
__attribute__((address_space(257))
变量是相对于 FS 寻址的
29.7.6. 使用内联汇编的基于 FS/GS 的寻址¶
如果编译器不支持地址空间,则可以使用内联汇编实现基于 FS/GS 的寻址模式
mov %fs:offset, %reg
mov %gs:offset, %reg
mov %reg, %fs:offset
mov %reg, %gs:offset