代码之家  ›  专栏  ›  技术社区  ›  Yves Lhuillier

全局描述符表位置

  •  3
  • Yves Lhuillier  · 技术社区  · 10 年前

    我对全局描述符表(GDT)的位置感到困惑。根据i386到更早版本的英特尔手册,GDTR寄存器包含GDT表的一个基址,该基址被伪装成一个线性地址。 按照英特尔的惯例,线性地址需要分页。

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

    因为线性地址会分页,我从英特尔手册中了解到,段描述符加载会经过当前进程的内存分页。因为Linux当然不想将GDT结构暴露给用户登陆程序,我认为它设法在用户登陆进程的地址空间中引入了一个漏洞;防止这些进程读取GDT,同时允许处理器读取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的线性地址和当前进程的线性地址指向不同的内存区域?

    1 回复  |  直到 10 年前
        1
  •  3
  •   Yves Lhuillier    10 年前

    我找到了答案,这并不简单,所以我把它发布在这里,也许它可以帮助其他人。

    首先,我需要承认 OSDev.org 帮助我理解这一点。

    尽管该代码是为i386编译的,但它在x86_64 linux系统上运行。因此,它不是在传统的32位模式下运行,而是在所谓的“兼容模式”下运行。在此模式下,允许在x86_64环境上运行传统的32位软件。

    当系统进入intel64(长)模式时,它使用64位地址空间的高端(类似于0xffff88021dd89000)将GDT置于线性地址。每当“compat”32位应用程序使用LGDT检索GDTR线性地址时,它只检索线性地址的低32位(0x1dd89000)。当处理器访问GDT时,它使用GDTR寄存器的全64位线性地址,即使在兼容模式下也是如此。