【UEFI实战】x86平台UEFI下的内存管理

说明

【x86架构】内存管理中已经介绍了x86操作系统(Linux)下的内存管理模式。

本文主要讲UEFI是如何进行内存管理的,这里还是以x86架构为介绍对象(并通过使用QEMU启动OVMF(OvmfPkgX64.dsc)来测试)。

x86架构下内存管理使用了分段和分页,而分段是无法关闭的,因此UEFI下一定也使用了分段,至于分页有没有使用还需要进一步确定。

不过首先可以看一下下面的例子:

从上图可以知道UEFI下读写操作的是直接的物理地址。

这说明了对于UEFI来说逻辑地址和线性地址和物理地址是统一的。

下面具体分析UEFI下的分段和分页。

分段

首先是要确定分段,需要了解CS(或者DS之类的,CS表示的是代码段,我们这里先关注这个)中的值,以及全局描述符表。通过CS中的值确定段选择符,然后通过这个选择符去全局描述表中找到对应段描述符。

段描述符的结构如下:

通过lgdt可以读取全局描述符表寄存器,从这个寄存器可以找到全局描述符表。

通过UEFI提供的接口AsmReadGdtr()可以找到GDTR的值,如下所示:

可以看到全局描述符表的位置是0x7A94F18,读取里面你的内容:

接下去就需要确定CS的值,这样就能知道具体是哪个表项。

如何确定CS,这是个问题......

不过看代码就能够确认,因为上图中的值就是代码写死的(位于MdeModulePkg\Core\DxeIplPeim\Ia32\DxeLoadFunc.c):

//
// Global Descriptor Table (GDT)
//
GLOBAL_REMOVE_IF_UNREFERENCED IA32_GDT gGdtEntries[] = {
/* selector { Global Segment Descriptor                              } */
/* 0x00 */  {{0,      0,  0,  0,    0,  0,  0,  0,    0,  0, 0,  0,  0}}, //null descriptor
/* 0x08 */  {{0xffff, 0,  0,  0x2,  1,  0,  1,  0xf,  0,  0, 1,  1,  0}}, //linear data segment descriptor
/* 0x10 */  {{0xffff, 0,  0,  0xf,  1,  0,  1,  0xf,  0,  0, 1,  1,  0}}, //linear code segment descriptor
/* 0x18 */  {{0xffff, 0,  0,  0x3,  1,  0,  1,  0xf,  0,  0, 1,  1,  0}}, //system data segment descriptor
/* 0x20 */  {{0xffff, 0,  0,  0xa,  1,  0,  1,  0xf,  0,  0, 1,  1,  0}}, //system code segment descriptor
/* 0x28 */  {{0,      0,  0,  0,    0,  0,  0,  0,    0,  0, 0,  0,  0}}, //spare segment descriptor
/* 0x30 */  {{0xffff, 0,  0,  0x2,  1,  0,  1,  0xf,  0,  0, 1,  1,  0}}, //system data segment descriptor
/* 0x38 */  {{0xffff, 0,  0,  0xa,  1,  0,  1,  0xf,  0,  1, 0,  1,  0}}, //system code segment descriptor
/* 0x40 */  {{0,      0,  0,  0,    0,  0,  0,  0,    0,  0, 0,  0,  0}}, //spare segment descriptor
};

以图中第二项为例,它的值是0xCF92000000FFFF:

对应的就是:

LimitLow=0xFFFF
BaseLow = 0
BaseMid = 0
Type = 0x2
System = 1
Dpl = 0
Present = 1
LimitHigh = 0xF
Software = 0
Reserved = 0
DefaultSize = 1
Granularity = 1
BaseHigh = 0

这于代码中的第二条完全吻合:

/* 0x08 */  {{0xffff, 0,  0,  0x2,  1,  0,  1,  0xf,  0,  0, 1,  1,  0}}, //linear data segment descriptor

这个跟Linux的分段模式很像。

逻辑地址和线性地址实际上是一致的。

分页

为了确定分页情况,需要了解几个寄存器,分别是CR0、CR4和IA32_EFER。前两个寄存器通过mov就可以得到,最后一个是MSR寄存器,需要rdmsr指令来读取。

UEFI下相应的库函数来获取它们的值。

下面是CR寄存器的详细BIT说明:

对于分页,这里需要关注的是CR0.PE和CR4.PAE,即CR0的BIT0和CR4的BIT5。

实际的打印值如下:

可以看到CR0.PE=1,CR4.PAE=1。

IA32_EFER对应的MSR的地址是0xC0000080,读取这个值,结果如下:

我们关注的是IA32_EFER的BIT8和BIT10:

正好这两位都是1。

根据下表:

可以看到UEFI下也是使用了分页,且格式是IA-32e。

这与《UEFI Spec》上是对应的(这边使用的是x64的计算机,编译的是OvmfPkgX64.dsc的OVMF):

这里所说的Long mode其实就是IA-32e模式,来自Wiki的说明:

In the x86-64 computer architecturelong mode is the mode where a 64-bit operating system can access 64-bit instructions and registers. 64-bit programs are run in a sub-mode called 64-bit mode, while 32-bit programs and 16-bit protected mode programs are executed in a sub-mode called compatibility mode. Real mode or virtual 8086 mode programs cannot be natively run in long mode.

所以剩下的问题就是UEFI是如何设置这个分页的,各种表是怎么样的?

这个问题也还是通过看代码来解决比较方便。

下面是在UEFI启动的时候的配置(还是以OvmfPkgX64.dsc为例):

SetCr3ForPageTables64:

    ;
    ; For OVMF, build some initial page tables at 0x800000-0x806000.
    ;
    ; This range should match with PcdOvmfSecPageTablesBase and
    ; PcdOvmfSecPageTablesSize which are declared in the FDF files.
    ;
    ; At the end of PEI, the pages tables will be rebuilt into a
    ; more permanent location by DxeIpl.
    ;

    mov     ecx, 6 * 0x1000 / 4
    xor     eax, eax
clearPageTablesMemoryLoop:
    mov     dword[ecx * 4 + 0x800000 - 4], eax
    loop    clearPageTablesMemoryLoop

    ;
    ; Top level Page Directory Pointers (1 * 512GB entry)
    ;
    mov     dword[0x800000], 0x801000 + PAGE_PDP_ATTR

    ;
    ; Next level Page Directory Pointers (4 * 1GB entries => 4GB)
    ;
    mov     dword[0x801000], 0x802000 + PAGE_PDP_ATTR
    mov     dword[0x801008], 0x803000 + PAGE_PDP_ATTR
    mov     dword[0x801010], 0x804000 + PAGE_PDP_ATTR
    mov     dword[0x801018], 0x805000 + PAGE_PDP_ATTR

    ;
    ; Page Table Entries (2048 * 2MB entries => 4GB)
    ;
    mov     ecx, 0x800
pageTableEntriesLoop:
    mov     eax, ecx
    dec     eax
    shl     eax, 21
    add     eax, PAGE_2M_PDE_ATTR
    mov     [ecx * 8 + 0x802000 - 8], eax
    loop    pageTableEntriesLoop

    ;
    ; Set CR3 now that the paging structures are available
    ;
    mov     eax, 0x800000
    mov     cr3, eax

    OneTimeCallRet SetCr3ForPageTables64

这段代码来自OvmfPkg\ResetVector\Ia32\PageTables64.asm,上面配置了页表并设置了值到CR3中。

但是,从之前的图中可以看到这个并不是我们看到的CR3的值:

代码里面也说了:

    ;
    ; For OVMF, build some initial page tables at 0x800000-0x806000.
    ;
    ; This range should match with PcdOvmfSecPageTablesBase and
    ; PcdOvmfSecPageTablesSize which are declared in the FDF files.
    ;
    ; At the end of PEI, the pages tables will be rebuilt into a
    ; more permanent location by DxeIpl.
    ;

所以还会有一次设置分页的代码在PEI阶段的最后,这部分在DxeLoadCore()函数中完成:

//
// Module Globals used in the DXE to PEI hand off
// These must be module globals, so the stack can be switched
//
CONST EFI_DXE_IPL_PPI mDxeIplPpi = {
  DxeLoadCore
};

该函数的最后有如下的代码:


  //
  // Transfer control to the DXE Core
  // The hand off state is simply a pointer to the HOB list
  //
  HandOffToDxeCore (DxeCoreEntryPoint, HobList);
  //
  // If we get here, then the DXE Core returned.  This is an error
  // DxeCore should not return.
  //
  ASSERT (FALSE);
  CpuDeadLoop ();

这已经是PEI到DXE转换的最后部分了。

HandOffToDxeCore()函数按照不同的x86架构有不同的实现,这里以x64为例有如下的代码:

  PageTables = 0;
  if (FeaturePcdGet (PcdDxeIplBuildPageTables)) {
    //
    // Create page table and save PageMapLevel4 to CR3
    //
    PageTables = CreateIdentityMappingPageTables ((EFI_PHYSICAL_ADDRESS) (UINTN) BaseOfStack, STACK_SIZE);
  } else {
    //
    // Set NX for stack feature also require PcdDxeIplBuildPageTables be TRUE
    // for the DxeIpl and the DxeCore are both X64.
    //
    ASSERT (PcdGetBool (PcdSetNxForStack) == FALSE);
  }
  
  //
  // End of PEI phase signal
  //
  Status = PeiServicesInstallPpi (&gEndOfPeiSignalPpi);
  ASSERT_EFI_ERROR (Status);

  if (FeaturePcdGet (PcdDxeIplBuildPageTables)) {
    AsmWriteCr3 (PageTables);
  }

这里CreateIdentityMappingPageTables()创建了页表,并通过AsmWriteCr3 (PageTables)将起始地址写入到CR3中。

具体的函数实现可以这里不多介绍了,可以看下函数的说明:

/**
  Allocates and fills in the Page Directory and Page Table Entries to
  establish a 1:1 Virtual to Physical mapping.

  @param[in] StackBase  Stack base address.
  @param[in] StackSize  Stack size.

  @return The address of 4 level page map.

**/
UINTN
CreateIdentityMappingPageTables (
  IN EFI_PHYSICAL_ADDRESS   StackBase,
  IN UINTN                  StackSize
  )

已经明确的写了虚拟地址和物理地址是一一对应的。

以上,就是UEFI下的内存模式简介。

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值