29.8. 在用户空间应用程序中使用 FS 和 GS 段

x86 架构支持分段。访问内存的指令可以使用基于段寄存器的寻址模式。以下表示法用于寻址段内的字节

段寄存器:字节地址

将段基地址添加到字节地址以计算访问的最终虚拟地址。这允许使用相同的字节地址访问数据的多个实例,即相同的代码。特定实例的选择完全基于段寄存器中的基地址。

在 32 位模式下,CPU 提供 6 个段,这些段也支持段限制。可以使用这些限制来强制执行地址空间保护。

在 64 位模式下,CS/SS/DS/ES 段将被忽略,并且基地址始终为 0,以提供完整的 64 位地址空间。FS 和 GS 段在 64 位模式下仍然可用。

29.8.1. 常见的 FS 和 GS 用法

FS 段通常用于寻址线程局部存储(TLS)。FS 通常由运行时代码或线程库管理。使用“__thread”存储类说明符声明的变量是按线程实例化的,并且编译器为对这些变量的访问发出 FS: 地址前缀。每个线程都有自己的 FS 基地址,因此可以使用通用代码而无需复杂的地址偏移计算来访问每个线程的实例。当应用程序使用管理每个线程 FS 的运行时或线程库时,不应将 FS 用于其他目的。

GS 段没有常用用途,应用程序可以自由使用。GCC 和 Clang 通过地址空间标识符支持基于 GS 的寻址。

29.8.2. 读取和写入 FS/GS 基地址

存在两种机制来读取和写入 FS/GS 基地址

  • arch_prctl() 系统调用

  • FSGSBASE 指令系列

29.8.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.8.4. 使用 FSGSBASE 指令访问 FS/GS 基地址

英特尔在 Ivy Bridge CPU 中引入了一组新的指令,可以直接从用户空间访问 FS 和 GS 基址寄存器。AMD Family 17H CPU 也支持这些指令。以下指令可用

RDFSBASE %reg

读取 FS 基址寄存器

RDGSBASE %reg

读取 GS 基址寄存器

WRFSBASE %reg

写入 FS 基址寄存器

WRGSBASE %reg

写入 GS 基址寄存器

这些指令避免了 arch_prctl() 系统调用的开销,并允许在用户空间应用程序中更灵活地使用 FS/GS 寻址模式。但这并不能阻止使用 FS 的线程库和运行时与想要将其用于自身目的的应用程序之间的冲突。

29.8.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.8.4.2. FSGSBASE 指令编译器支持

GCC 4.6.4 及更高版本为 FSGSBASE 指令提供了内在函数。Clang 5 也支持它们。

_readfsbase_u64()

读取 FS 基址寄存器

_readfsbase_u64()

读取 GS 基址寄存器

_writefsbase_u64()

写入 FS 基址寄存器

_writegsbase_u64()

写入 GS 基址寄存器

要使用这些内在函数,必须在源代码中包含 <immintrin.h>,并且必须添加编译器选项 -mfsgsbase。

29.8.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.8.6. 使用内联汇编的基于 FS/GS 的寻址

如果编译器不支持地址空间,则可以使用内联汇编进行基于 FS/GS 的寻址模式

mov %fs:offset, %reg
mov %gs:offset, %reg

mov %reg, %fs:offset
mov %reg, %gs:offset