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