没有隐藏的“基础和规模”,只有一个隐藏的gs.base,您可以在正常寻址模式下使用。 (以及 GS 寄存器本身的隐藏值。这是一个选择器值,如果您确实执行了 mov gs, eax 而不是仅通过 MSR 修改 GS 基数,它将充当 GDT 的索引,或通过wrgsbase。但这与完整gs:[base + index*scale]寻址模式中偏移量的索引部分无关。
您在内核的syscall 入口点处理程序中使用swapgs,然后在某些加载和存储上使用GS 段覆盖,就像线程本地存储一样,因此使用以前隐藏的gs.base 使用您在每个加载或存储指令中使用的[base + idx*scale] 寻址模式。例如类似mov [gs:0x10], rsp 来保存用户空间堆栈指针和mov rsp, [gs:0x18] 来加载内核堆栈指针。
swapgs 存在是因为syscall 不会将 RSP 更改为指向内核堆栈(并且不会将用户空间 RSP 保存在任何地方)。因此,您需要某种线程本地(或实际上是内核本地)存储,以便每个内核都可以为在该内核上运行的任务获取正确的内核堆栈指针。隐藏的 GS 基址是该隐藏指针的存储空间,也是一种在不破坏任何架构寄存器值的情况下使用它的方法。
您不能只使用常规全局变量(绝对地址),因为它只能有一个所有内核都可以读取的值。您也没有任何备用寄存器(它们都包含宝贵的用户空间状态,您需要稍后恢复),并且您没有内核堆栈来推送它们。而且您不能使用用户空间 RSP;在内核模式下使用用户空间 RSP 运行 push 会使用户空间崩溃内核,因为在运行 syscall 之前 RSP 指向无效。
最初设计 x86-64 时(早在 2000 年,比第一个芯片还早几年),this mailing list message 解释了swapgs 的预期用途。在操作系统开发人员注意到 AMD 指定它的方式存在问题之后,它是 revised a day later,但原始电子邮件包含一个仍然适用的简单示例:
使用示例
在内核入口点,操作系统可以使用 SwapGS 获取指向内核数据结构的指针,同时保存
用户的 GS 基地。退出后可以使用 SwapGS 恢复用户的 GS
基地:
SystemCallEntryPoint:
SwapGS ; set up kernel pointer, save user's GS base
mov gs:[SavedUserRSP], rsp ; save user's stack pointer
mov rsp, gs:[KernelStackPtr] ; set up kernel stack
push rax ; now that we have a stack, save user's GPRs
mov rax, gs:[CPUnumber] ; get CPU number < or whatever >
. ; perform system service
.
SwapGS ; restore user's GS, save kernel pointer
您可能还想查看 Linux 内核如何在其 syscall 入口点中使用它,最好是在 Spectre / Meltdown 缓解使一切变得复杂之前的旧内核中。例如Linux 4.12's entry_64.S 的 ENTRY(entry_SYSCALL_64) 以 swapgs 开头,非常像 AMD 的例子。
(另请参阅Why does Windows64 use a different calling convention from all other OSes on x86-64?,了解其他 Linux 内核入口点发生的情况,来自int 0x80)。
Linux 内核源代码中的一些 cmets 指出,确保 swapgs 在内核外的每条执行路径上只运行一次可能很不方便。如果有两个操作码,一个用于“交换到用户 gs”,一个用于“交换到内核 gs”,则更容易确保您不会意外交换额外的时间。该错误会使下一个内核条目出现在错误的位置。 (并且给用户空间错误的 gs,但在 GNU/Linux 中 fs 用于线程本地存储。)