全局描述符 Table 位置

Global Descriptor Table location

我对全局描述符 Table (GDT) 的位置感到困惑。根据从 i386 到更早版本的 Intel 手册,GDTR 寄存器包含 GDT table 的基地址,它被伪装成线性地址。 按照 Intel 惯例,线性地址需要进行分页。

尽管如此,我想知道考虑了哪个地址 space。 Ring 3(用户域)程序完全允许修改某些段选择器(例如 ES)。此修改应触发处理器从 GDT 中的相应条目加载段描述符,其基地址是使用 GDTR 寄存器给出的线性地址计算的。

因为线性地址是分页的,我从Intel手册上了解到,段描述符的加载是通过当前进程的内存分页进行的。因为 Linux 当然不想将 GDT 结构暴露给用户态程序,我认为它以某种方式设法在用户态进程的地址 space 中引入了一个漏洞;防止这些进程读取 GDT,同时允许处理器读取它以重新加载段。

我使用以下代码进行了检查,结果表明我对 GDTR 的基本线性地址完全错误。

int
main()
{
  struct
  {
    uint16_t  pad;
    uint16_t  size;
    uintptr_t base;
  } gdt_info;

  __asm__ volatile ("sgdt %0" : "=m" (gdt_info.size) );

  void* try_mmgdt = (void*)( gdt_info.base & ~0xfff );
  void* chk_mmgdt = mmap(try_mmgdt, 0x4000, PROT_EXEC | PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

  std::cout << "gdt size: \t" << std::dec << gdt_info.size << std::endl;
  std::cout << "gdt base: \t" << std::hex << gdt_info.base << std::endl;
  std::cout << "mmgdt try:\t" << std::hex << uintptr_t(try_mmgdt) << std::endl;
  std::cout << "mmgdt chk:\t" << std::hex << uintptr_t(chk_mmgdt) << std::endl;

  return 0;
}

我机器上的程序输出(i386 编译)是:

gdt size:       127
gdt base:       1dd89000
mmgdt try:      1dd89000
mmgdt chk:      1dd89000

GDT条目的线性地址和mmap块的线性地址完全重叠。尽管如此,mmap块显然与GDT无关。

所以我最后的问题是:哪种Intel/linux机制使得GDTR的线性地址和当前进程的线性地址指向不同的内存区域?

我找到了答案,但它并不简单,所以我将其张贴在这里,也许它可以帮助其他人。

首先,我需要感谢 OSDev.org 帮助我理解了这一点。

虽然代码是为 i386 编译的,但它 运行 在 x86_64 linux 系统上运行。因此,它不是传统 32 位模式中的 运行ning,而是所谓的 "compat mode"。在此模式下,允许旧版 32 位软件在 x86_64 环境中 运行。

当系统进入 intel64(长)模式时,它使用 64 位地址的高端 space 将 GDT 放置在线性地址(类似于 0xffff88021dd89000)。每当 "compat" 32 位应用程序使用 LGDT 检索 GDTR 线性地址时,它只会检索线性地址 (0x1dd89000) 的低 32 位。当处理器访问GDT时,它使用GDTR寄存器的完整64位线性地址,即使在compat-mode.