说明
在【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 architecture, long 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下的内存模式简介。