Step to UEFI (188)保护模式下的 GDT

在实模式下,内存寻址是通过 “段寄存器:偏移” 来进行的。保护模式出现之后,因为内存地址长度的增加,这样的方式无法完成(不够长)。为了解决这样的使用索引来处理成为顺理成章的事情。

同时为了考虑兼容性,最终引入了Global Descriptor Table 来进行扩展。关于内存的地址信息存放在这个  GDT 中。接下来 CS/DS/ES这样的段寄存器不再存放内存的地址而是存放 GDT 中的“第x个条目”这样的信息。再引入一个 GDTR 的寄存器存放 GDT 在内存中的位置。

在\MdePkg\Include\Library\BaseLib.h中有定义读取 GDTR 的函数。

/**
  Reads the current Global Descriptor Table Register(GDTR) descriptor.

  Reads and returns the current GDTR descriptor and returns it in Gdtr. This
  function is only available on IA-32 and x64.

  If Gdtr is NULL, then ASSERT().

  @param  Gdtr  The pointer to a GDTR descriptor.

**/
VOID
EFIAPI
AsmReadGdtr (
  OUT     IA32_DESCRIPTOR           *Gdtr
  );

读取之后的 GDTR 寄存器定义如下【参考1】:

在代码中定义如下:

///
/// Byte packed structure for an IDTR, GDTR, LDTR descriptor.
///
#pragma pack (1)
typedef struct {
  UINT16  Limit;
  UINTN   Base;
} IA32_DESCRIPTOR;
#pragma pack ()

Base 是给定 GDT 在内存中的地址,Limit 实际上是给出Table 的长度。从 Figure 2-6 可以看到,32位下和 64位下Limit 长度都是 16Bit的,但是 Base 的长度可能是32或者64位的,代码使用 UINTN 来实现一个 Structure 兼容2种情况。

获得了内存地址后就可以开始进行解析。其中的每一个项目结构体如下:

///
/// Byte packed structure for a segment descriptor in a GDT/LDT.
///
typedef union {
  struct {
    UINT32  LimitLow:16;
    UINT32  BaseLow:16;
    UINT32  BaseMid:8;
    UINT32  Type:4;
    UINT32  S:1;
    UINT32  DPL:2;
    UINT32  P:1;
    UINT32  LimitHigh:4;
    UINT32  AVL:1;
    UINT32  L:1;
    UINT32  DB:1;
    UINT32  G:1;
    UINT32  BaseHigh:8;
  } Bits;
  UINT64  Uint64;
} IA32_SEGMENT_DESCRIPTOR;

首先,我们编写一个读取解析的代码:

#include <Uefi.h>
#include <Library/BaseLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/UefiLib.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Library/DebugLib.h>
#include <Protocol/PciIo.h>
EFI_STATUS
EFIAPI
GDTMain (
  IN     EFI_HANDLE                 ImageHandle,
  IN     EFI_SYSTEM_TABLE           *SystemTable
  )
{
        UINTN                    GdtEntryCount;
        IA32_SEGMENT_DESCRIPTOR  *GdtEntry;
        IA32_DESCRIPTOR          GdtrDesc;
        UINT16                   Index;
        
        AsmReadGdtr (&GdtrDesc);
       
        GdtEntryCount = (GdtrDesc.Limit + 1) / sizeof (IA32_SEGMENT_DESCRIPTOR);
        GdtEntry = (IA32_SEGMENT_DESCRIPTOR *) GdtrDesc.Base;
         Print(L"GDTR=0x%lX\n",GdtEntry);
        for (Index = 0; Index < GdtEntryCount; Index++) {
            Print(L"No.[%d] ",Index);
            Print(L"Seg. Desc 0x%lX\n",GdtEntry->Uint64);
            Print(L"Base=0x%lX\n",
                (GdtEntry->Bits.BaseHigh<<24)|
                (GdtEntry->Bits.BaseMid<<16)|
                (GdtEntry->Bits.BaseLow));                              
            Print(L"Limit=0x%X\n",
                (GdtEntry->Bits.LimitHigh<<16)|
                (GdtEntry->Bits.LimitLow));
            if (GdtEntry->Bits.S==0) {
                Print(L"Descriptor Type: system\n");
            }
            else {
                Print(L"Descriptor Type: Code or Data\n");
            }
            if (GdtEntry->Bits.L==0) {
                Print(L"Not 64-bit code segment\n");
                
            }else {
                Print(L"64-bit code segment\n");
            }            
            GdtEntry++;
        }

  return EFI_SUCCESS;
}

运行之后结果如下:

GDTR=0x8C634718
No.[0] Seg. Desc 0x0
Base=0x0
Limit=0x0
Descriptor Type: system
Not 64-bit code segment
No.[1] Seg. Desc 0xCF92000000FFFF
Base=0x0
Limit=0xFFFFF
Descriptor Type: Code or Data
Not 64-bit code segment
No.[2] Seg. Desc 0xCF9F000000FFFF
Base=0x0
Limit=0xFFFFF
Descriptor Type: Code or Data
Not 64-bit code segment
No.[3] Seg. Desc 0xCF93000000FFFF
Base=0x0
Limit=0xFFFFF
Descriptor Type: Code or Data
Not 64-bit code segment
No.[4] Seg. Desc 0xCF9A000000FFFF
Base=0x0
Limit=0xFFFFF
Descriptor Type: Code or Data
Not 64-bit code segment
No.[5] Seg. Desc 0x0
Base=0x0
Limit=0x0
Descriptor Type: system
Not 64-bit code segment
No.[6] Seg. Desc 0xCF93000000FFFF
Base=0x0
Limit=0xFFFFF
Descriptor Type: Code or Data
Not 64-bit code segment
No.[7] Seg. Desc 0xAF9B000000FFFF
Base=0x0
Limit=0xFFFFF
Descriptor Type: Code or Data
64-bit code segment
No.[8] Seg. Desc 0x0
Base=0x0
Limit=0x0
Descriptor Type: system
Not 64-bit code segment

经过研究,代码中设置 GDT Table 是在 \Edk2\UefiCpuPkg\CpuDxe\CpuGdt.c 中,有定义 GDT 如下:

//
// Global descriptor table (GDT) Template
//
STATIC GDT_ENTRIES GdtTemplate = {
  //
  // NULL_SEL
  //
  {
    0x0,            // limit 15:0
    0x0,            // base 15:0
    0x0,            // base 23:16
    0x0,            // type
    0x0,            // limit 19:16, flags
    0x0,            // base 31:24
  },
  //
  // LINEAR_SEL
  //
  {
    0x0FFFF,        // limit 15:0
    0x0,            // base 15:0
    0x0,            // base 23:16
    0x092,          // present, ring 0, data, read/write
    0x0CF,          // page-granular, 32-bit
    0x0,
  },
  //
  // LINEAR_CODE_SEL
  //
  {
    0x0FFFF,        // limit 15:0
    0x0,            // base 15:0
    0x0,            // base 23:16
    0x09F,          // present, ring 0, code, execute/read, conforming, accessed
    0x0CF,          // page-granular, 32-bit
    0x0,
  },
  //
  // SYS_DATA_SEL
  //
  {
    0x0FFFF,        // limit 15:0
    0x0,            // base 15:0
    0x0,            // base 23:16
    0x093,          // present, ring 0, data, read/write, accessed
    0x0CF,          // page-granular, 32-bit
    0x0,
  },
  //
  // SYS_CODE_SEL
  //
  {
    0x0FFFF,        // limit 15:0
    0x0,            // base 15:0
    0x0,            // base 23:16
    0x09A,          // present, ring 0, code, execute/read
    0x0CF,          // page-granular, 32-bit
    0x0,
  },
  //
  // SPARE4_SEL
  //
  {
    0x0,            // limit 15:0
    0x0,            // base 15:0
    0x0,            // base 23:16
    0x0,            // type
    0x0,            // limit 19:16, flags
    0x0,            // base 31:24
  },
  //
  // LINEAR_DATA64_SEL
  //
  {
    0x0FFFF,        // limit 15:0
    0x0,            // base 15:0
    0x0,            // base 23:16
    0x092,          // present, ring 0, data, read/write
    0x0CF,          // page-granular, 32-bit
    0x0,
  },
  //
  // LINEAR_CODE64_SEL
  //
  {
    0x0FFFF,        // limit 15:0
    0x0,            // base 15:0
    0x0,            // base 23:16
    0x09A,          // present, ring 0, code, execute/read
    0x0AF,          // page-granular, 64-bit code
    0x0,            // base (high)
  },
  //
  // SPARE5_SEL
  //
  {
    0x0,            // limit 15:0
    0x0,            // base 15:0
    0x0,            // base 23:16
    0x0,            // type
    0x0,            // limit 19:16, flags
    0x0,            // base 31:24
  },
};

为了验证前面的代码,我们可以在最后加上一个自定义的 Segment,编译后烧写到板子上再使用上面的工具查看,可以看到多出来的一项。

同一个文件中还有加载段描述的动作,在InitGlobalDescriptorTable函数中

  //
  // Update selector (segment) registers base on new GDT
  //
  SetCodeSelector ((UINT16)CPU_CODE_SEL);
  SetDataSelectors ((UINT16)CPU_DATA_SEL);

对应代码如下(Edk2\UefiCpuPkg\CpuDxe\X64\CpuAsm.asm):

;------------------------------------------------------------------------------
; VOID
; SetCodeSelector (
;   UINT16 Selector
;   );
;------------------------------------------------------------------------------
SetCodeSelector PROC PUBLIC
    sub     rsp, 0x10
    lea     rax, setCodeSelectorLongJump
    mov     [rsp], rax
    mov     [rsp+4], cx
    jmp     fword ptr [rsp]
setCodeSelectorLongJump:
    add     rsp, 0x10
    ret
SetCodeSelector ENDP

;------------------------------------------------------------------------------
; VOID
; SetDataSelectors (
;   UINT16 Selector
;   );
;------------------------------------------------------------------------------
SetDataSelectors PROC PUBLIC
    mov     ss, cx
    mov     ds, cx
    mov     es, cx
    mov     fs, cx
    mov     gs, cx
    ret
SetDataSelectors ENDP

分别加载了前面 Table 中的  LINEAR_DATA64_SEL和  LINEAR_CODE64_SEL到 DS CS 还有其他的段寄存器中。

使用前面介绍过的 DCI【参考2】进行查看:

1.直接查看gdtr (这步之前需要先 halt),可以看到里面的内容和Application 读取的是相同的,同样Limit=0x47,  (0x47+1)/8=9 ,也就是说有9项和读取结果相同。

2.查看 cs 寄存器(我不确定是否为这个命令,只是看着像),解析如下:

看到的内容比我们普通看到的会多一些,根据【参考3】和一些资料的介绍,保护模式下,CS 中装的是selector(选择子),此外还有一部分隐藏的,资料上称之为 Cached的内容。使用DBC 工具可以看到隐藏的部分确实有存放一些其他内容。csb 可能是 CS.Base的意思,csl 可能是 CS.Limit 的意思。

同样的在【参考4】也有描述:

Every segment register has a “visible” part and a “hidden” part. (The hidden part is sometimes referred to as a “descriptor cache” or a “shadow register.”) When a segment selector is loaded into the visible part of a segment register, the processor also loads the hidden part of the segment register with the base address, segment limit, and access control information from the segment descriptor pointed to by the segment selector. The information cached in the segment register (visible and hidden) allows the processor to translate addresses without taking extra bus cycles to read the base address and limit from the segment descriptor. In systems in which multiple processors
have access to the same descriptor tables, it is the responsibility of software to reload the segment registers when the descriptor tables are modified. If this is not done, an old segment descriptor cached in a segment register might be used after its memory-resident version has been modified.

这部分在BIOS中从来不会有错,因为上来之后就会被设置为0-4G 全部都可以访问,代码中也不会进行修改。但是这部分知识对于掌握X86系统结构是非常有必要的。

参考:

  1. 64-ia-32-architectures-software-developer-vol-3a-part-1-manual  P74
  2. https://www.lab-z.com/ccadbc/ INTEL CCA/DBC简介
  3. https://www.sandpile.org/x86/sreg.htm
  4. Intel® 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D and 4     P2796

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注