UEFI开发实战:如何用GetMemoryMap函数精准获取内存布局(附完整代码解析)
最近在调试一个UEFI驱动时,遇到了一个棘手的内存访问问题。系统在特定阶段会随机崩溃,排查了半天,最终发现是驱动尝试访问了一块已被标记为保留的内存区域。这让我重新审视了UEFI环境下内存管理的重要性。对于固件和驱动开发者而言,系统内存的实时布局图,就像是城市的地下水管和电路图,不搞清楚就动工,随时可能“挖断电缆”。而GetMemoryMap函数,正是UEFI为我们提供的这张至关重要的“内存地图”的读取工具。它不是简单的信息罗列,其背后涉及引导服务、运行时服务的状态转换,以及操作系统加载前的关键资源规划。今天,我们就抛开理论,直接从代码入手,拆解如何安全、高效地调用GetMemoryMap,并深度解析获取到的每一个字节所代表的意义,让你在UEFI开发中,对内存了如指掌。
1. 理解UEFI内存地图:不只是地址列表
在传统BIOS时代,获取内存信息通常依赖于中断调用,信息相对简单,主要是可用的物理内存范围。而UEFI引入的GetMemoryMap服务,则提供了一个动态的、类型化的内存描述符数组。这不仅仅是地址的起止,更是对每一块内存区域“身份”和“用途”的精确标注。
为什么内存地图是动态的? 因为在UEFI启动过程中,内存的使用情况是不断变化的。例如:
- UEFI自身会占用一部分内存作为代码和数据区。
- 加载的每个UEFI应用程序或驱动都会申请内存。
- 某些硬件(如显卡)会通过“预留”方式声明其帧缓冲区所需的内存。
- 在调用
ExitBootServices()之前,内存地图可能会因为驱动的加载和卸载而改变。
因此,GetMemoryMap返回的是一个快照,反映了调用那一瞬间整个系统的内存视图。这个视图的核心数据结构是EFI_MEMORY_DESCRIPTOR。理解这个结构体是解读内存地图的关键。
#define EFI_MEMORY_DESCRIPTOR_VERSION 1
typedef struct {
UINT32 Type; // 内存类型
EFI_PHYSICAL_ADDRESS PhysicalStart; // 物理起始地址
EFI_VIRTUAL_ADDRESS VirtualStart; // 虚拟起始地址
UINT64 NumberOfPages; // 页数
UINT64 Attribute; // 内存属性(如缓存策略)
} EFI_MEMORY_DESCRIPTOR;
我们来重点看看Type字段,它定义了内存区域的用途,是开发者最需要关心的部分。UEFI规范定义了几十种内存类型,但最核心的几种如下表所示:
| 内存类型 (Type) | 值 (示例) | 含义与典型用途 |
|---|---|---|
EfiReservedMemoryType | 0 | 保留内存。硬件专用或系统保留,绝对不可被分配或使用。如ACPI NVS区域、芯片组保留区域。 |
EfiLoaderCode | 1 | 加载器代码。操作系统加载器(如GRUB)的代码段所在内存。 |
EfiLoaderData | 2 | 加载器数据。操作系统加载器的数据段、堆栈所在内存。 |
EfiBootServicesCode | 3 | 引导服务代码。UEFI固件自身及驱动提供的引导服务例程代码。 |
EfiBootServicesData | 4 | 引导服务数据。UEFI固件及驱动的全局变量、堆内存等数据区。 |
EfiRuntimeServicesCode | 5 | 运行时服务代码。操作系统运行后仍需调用的UEFI服务(如GetTime、ResetSystem)代码。 |
EfiRuntimeServicesData | 6 | 运行时服务数据。运行时服务所需的持久化数据区。 |
EfiConventionalMemory | 7 | 可用内存。操作系统可以自由支配的物理内存。这是OS内核和应用程序的主要舞台。 |
EfiUnusableMemory | 8 | 不可用内存。已损坏或有硬件缺陷的内存,必须避免使用。 |
EfiACPIReclaimMemory | 9 | ACPI可回收内存。ACPI表暂存区,OS读取ACPI表后,可回收此区域作为可用内存。 |
EfiACPIMemoryNVS | 10 | ACPI NVS内存。供ACPI固件组件使用,操作系统应保留。 |
EfiMemoryMappedIO | 11 | 内存映射I/O。映射到内存空间的设备寄存器,访问即操作硬件。 |
注意:
EfiBootServicesCode和EfiBootServicesData这两类内存在操作系统调用ExitBootServices()之后,会被回收并转变为EfiConventionalMemory。这意味着你的驱动如果在这之后还试图访问之前申请的引导服务数据,会导致未定义行为(通常是系统崩溃)。这是UEFI内存管理的一个关键分水岭。
2. GetMemoryMap调用实战:两步法与错误处理
直接调用GetMemoryMap并期望一次成功是不现实的。由于我们无法预知当前系统中有多少个内存描述符,UEFI设计了一个经典的“两步调用”模式。这个模式在UEFI编程中非常常见,用于处理输出缓冲区大小未知的情况。
2.1 核心参数解析
首先,我们看看函数原型:
EFI_STATUS EFIAPI gBS->GetMemoryMap (
IN OUT UINTN *MemoryMapSize, // 输入为缓冲区大小,输出为实际所需大小/实际大小
IN OUT EFI_MEMORY_DESCRIPTOR *MemoryMap, // 存放描述符数组的缓冲区
OUT UINTN *MapKey, // 内存地图的“版本号”或“钥匙”
OUT UINTN *DescriptorSize, // 单个描述符的实际大小(可能带填充)
OUT UINT32 *DescriptorVersion // 描述符结构版本
);
MemoryMapSize: 这是一个输入输出参数。首次调用时,我们将其指向的变量设为0(或一个较小的估计值),函数会返回EFI_BUFFER_TOO_SMALL,并在这个变量中填充存储完整内存地图所需的字节数。第二次调用时,我们分配足够大的缓冲区,并将缓冲区大小通过此参数传入,函数成功后会在此变量中返回实际写入的字节数。MemoryMap: 指向我们分配的缓冲区。首次试探性调用时传入NULL。MapKey: 一个唯一标识符。每次内存地图发生变化(如分配/释放内存),这个键值都会改变。在调用ExitBootServices()时,必须提供调用GetMemoryMap得到的最新MapKey,以证明调用者拥有当前内存布局的最新视图,这是防止竞争条件的安全机制。DescriptorSize: 这是一个极易被忽略但至关重要的参数。UEFI规范允许不同厂商或版本的固件在EFI_MEMORY_DESCRIPTOR结构体尾部添加私有字段。因此,我们不能直接用sizeof(EFI_MEMORY_DESCRIPTOR)来遍历数组。必须使用GetMemoryMap返回的DescriptorSize作为步进值来遍历内存描述符数组。DescriptorVersion: 结构体版本,目前通常是1。
2.2 稳健的调用流程与代码实现
基于以上理解,一个健壮的GetMemoryMap调用流程如下,我将其封装成了一个函数:
#include <Uefi.h>
#include <Library/UefiBootServicesTableLib.h> // 提供gBS
#include <Library/DebugLib.h> // 提供DEBUG宏
#include <Library/MemoryAllocationLib.h> // 提供AllocatePool/FreePool
/**
* 获取当前系统的UEFI内存地图。
*
* @param[out] MemoryMapSize 实际获取到的内存地图数据大小(字节)。
* @param[out] MemoryMap 指向分配的内存地图缓冲区的指针,调用者使用后需负责释放。
* @param[out] MapKey 获取到的内存地图键值。
* @param[out] DescriptorSize 单个内存描述符的大小(字节)。
* @param[out] DescriptorVersion 描述符结构版本。
*
* @retval EFI_SUCCESS 操作成功。
* @retval Others 操作失败。
*/
EFI_STATUS
GetCurrentMemoryMap (
OUT UINTN *MemoryMapSize,
OUT EFI_MEMORY_DESCRIPTOR **MemoryMap,
OUT UINTN *MapKey,
OUT UINTN *DescriptorSize,
OUT UINT32 *DescriptorVersion
)
{
EFI_STATUS Status;
UINTN LocalMemoryMapSize;
UINTN LocalMapKey;
UINTN LocalDescriptorSize;
UINT32 LocalDescriptorVersion;
EFI_MEMORY_DESCRIPTOR *LocalMemoryMap;
// 1. 初始化输出参数为安全值
if (MemoryMapSize != NULL) {
*MemoryMapSize = 0;
}
if (MemoryMap != NULL) {
*MemoryMap = NULL;
}
// 2. 第一次调用:获取所需缓冲区大小
LocalMemoryMapSize = 0;
Status = gBS->GetMemoryMap (
&LocalMemoryMapSize,
NULL, // 第一次传入NULL
&LocalMapKey,
&LocalDescriptorSize,
&LocalDescriptorVersion
);
// 我们期望的返回状态是 EFI_BUFFER_TOO_SMALL
if (Status != EFI_BUFFER_TOO_SMALL) {
DEBUG ((DEBUG_ERROR, "[ERROR] First GetMemoryMap call failed: %r\n", Status));
return Status; // 可能是其他错误,如参数无效
}
// 3. 循环分配和尝试,处理可能的竞争条件
// 在分配和调用间隙,可能有其他事件(如定时器)分配内存,导致地图变化。
do {
// 分配缓冲区,额外增加一个描述符的大小作为安全余量是常见做法
LocalMemoryMap = AllocatePool (LocalMemoryMapSize + (2 * LocalDescriptorSize));
if (LocalMemoryMap == NULL) {
DEBUG ((DEBUG_ERROR, "[ERROR] Failed to allocate pool for memory map.\n"));
return EFI_OUT_OF_RESOURCES;
}
// 第二次(或第N次)调用:尝试获取内存地图
Status = gBS->GetMemoryMap (
&LocalMemoryMapSize,
LocalMemoryMap,
&LocalMapKey,
&LocalDescriptorSize,
&LocalDescriptorVersion
);
if (EFI_ERROR (Status)) {
// 如果失败(例如因为地图在分配后发生了变化),释放缓冲区并重试
FreePool (LocalMemoryMap);
// 如果是缓冲区仍然太小,循环会继续
if (Status != EFI_BUFFER_TOO_SMALL) {
DEBUG ((DEBUG_WARN, "[WARN] GetMemoryMap failed (not buffer too small): %r\n", Status));
// 对于其他错误,可以选择返回或重试有限次数,这里直接返回
return Status;
}
}
} while (Status == EFI_BUFFER_TOO_SMALL); // 只要报告缓冲区不足,就重试
// 4. 到这里说明Status == EFI_SUCCESS
// 将局部变量赋值给输出参数
if (MemoryMapSize != NULL) {
*MemoryMapSize = LocalMemoryMapSize;
}
if (MemoryMap != NULL) {
*MemoryMap = LocalMemoryMap;
} else {
// 如果调用者不想要缓冲区,我们需要释放它
FreePool (LocalMemoryMap);
}
if (MapKey != NULL) {
*MapKey = LocalMapKey;
}
if (DescriptorSize != NULL) {
*DescriptorSize = LocalDescriptorSize;
}
if (DescriptorVersion != NULL) {
*DescriptorVersion = LocalDescriptorVersion;
}
DEBUG ((DEBUG_INFO, "[INFO] Got Memory Map. Size=0x%08lx, DescSize=0x%08lx, MapKey=0x%08lx\n",
LocalMemoryMapSize, LocalDescriptorSize, LocalMapKey));
return EFI_SUCCESS;
}
提示:上面的代码中,我在分配缓冲区时增加了
2 * LocalDescriptorSize的安全余量。这是一个实践经验。因为在多处理器(MP)系统或某些特定固件实现中,在GetMemoryMap调用执行期间,可能会发生内存分配事件(例如处理器间中断),导致实际所需大小比第一次查询时略大。增加一点余量可以避免因极端情况导致的无限循环或失败。
3. 解析与遍历内存描述符数组
成功获取到内存地图缓冲区后,下一步就是遍历它,提取我们需要的信息。关键在于使用正确的步长——DescriptorSize。
/**
* 遍历并打印内存地图信息。
*
* @param[in] MemoryMap 内存地图缓冲区。
* @param[in] MemoryMapSize 内存地图数据大小(字节)。
* @param[in] DescriptorSize 单个内存描述符的大小(字节)。
*/
VOID
DumpMemoryMap (
IN EFI_MEMORY_DESCRIPTOR *MemoryMap,
IN UINTN MemoryMapSize,
IN UINTN DescriptorSize
)
{
EFI_MEMORY_DESCRIPTOR *Entry;
UINTN NumEntries;
UINTN Index;
UINT64 TotalPages = 0;
UINT64 ConventionalPages = 0;
if (MemoryMap == NULL || MemoryMapSize == 0 || DescriptorSize == 0) {
DEBUG ((DEBUG_ERROR, "[ERROR] Invalid parameters for DumpMemoryMap.\n"));
return;
}
// 计算描述符的个数
NumEntries = MemoryMapSize / DescriptorSize;
DEBUG ((DEBUG_INFO, "\n=== UEFI Memory Map Dump ===\n"));
DEBUG ((DEBUG_INFO, "Number of memory descriptors: %lu\n", NumEntries));
// 遍历所有描述符
Entry = MemoryMap;
for (Index = 0; Index < NumEntries; Index++) {
// 计算该描述符的物理结束地址(注意:结束地址是 exclusive 的)
UINT64 PhysicalEnd = Entry->PhysicalStart + (Entry->NumberOfPages * EFI_PAGE_SIZE);
DEBUG ((DEBUG_INFO,
"[%03lu] Type: %02u (%a) Phys: [0x%016lx - 0x%016lx) Pages: %6lu Attr: 0x%016lx\n",
Index,
Entry->Type,
GetMemoryTypeString (Entry->Type), // 需要一个将类型转换为字符串的函数
Entry->PhysicalStart,
PhysicalEnd,
Entry->NumberOfPages,
Entry->Attribute
));
// 统计总内存和可用内存
TotalPages += Entry->NumberOfPages;
if (Entry->Type == EfiConventionalMemory) {
ConventionalPages += Entry->NumberOfPages;
}
// 关键步骤:使用DescriptorSize移动到下一个描述符
// 不能使用 Entry++ 或 sizeof(EFI_MEMORY_DESCRIPTOR)!
Entry = (EFI_MEMORY_DESCRIPTOR *)((UINT8 *)Entry + DescriptorSize);
}
DEBUG ((DEBUG_INFO, "\n=== Summary ===\n"));
DEBUG ((DEBUG_INFO, "Total Physical Memory: %lu MB\n",
(TotalPages * EFI_PAGE_SIZE) / (1024 * 1024)));
DEBUG ((DEBUG_INFO, "Available Memory (Conv): %lu MB\n",
(ConventionalPages * EFI_PAGE_SIZE) / (1024 * 1024)));
}
上面代码中的GetMemoryTypeString函数是一个简单的辅助函数,用于将数字类型转换为可读的字符串,例如:
CONST CHAR8*
GetMemoryTypeString (
IN EFI_MEMORY_TYPE Type
)
{
switch (Type) {
case EfiReservedMemoryType: return "Reserved";
case EfiLoaderCode: return "LoaderCode";
case EfiLoaderData: return "LoaderData";
case EfiBootServicesCode: return "BootCode";
case EfiBootServicesData: return "BootData";
case EfiRuntimeServicesCode: return "RtCode";
case EfiRuntimeServicesData: return "RtData";
case EfiConventionalMemory: return "Conventional";
case EfiUnusableMemory: return "Unusable";
case EfiACPIReclaimMemory: return "ACPIReclaim";
case EfiACPIMemoryNVS: return "ACPINvs";
case EfiMemoryMappedIO: return "MMIO";
case EfiMemoryMappedIOPortSpace: return "MMIOPort";
case EfiPalCode: return "PAL";
default: return "Unknown";
}
}
4. 实战整合:在UEFI应用中的完整用例
现在,我们将上述所有步骤整合到一个简单的UEFI应用程序中。这个应用会获取内存地图,分析并打印摘要,然后根据地图信息,安全地分配一块可用内存。
/**
UEFI应用程序入口点。
**/
EFI_STATUS
EFIAPI
UefiMain (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS Status;
UINTN MemoryMapSize;
EFI_MEMORY_DESCRIPTOR *MemoryMap;
UINTN MapKey;
UINTN DescriptorSize;
UINT32 DescriptorVersion;
VOID *AllocatedBuffer = NULL;
UINTN BufferPages = 16; // 尝试分配16页(64KB)
// 1. 获取当前内存地图
Status = GetCurrentMemoryMap (
&MemoryMapSize,
&MemoryMap,
&MapKey,
&DescriptorSize,
&DescriptorVersion
);
if (EFI_ERROR (Status)) {
DEBUG ((DEBUG_ERROR, "[APP] Failed to get memory map: %r\n", Status));
return Status;
}
// 2. 解析并打印内存地图
DumpMemoryMap (MemoryMap, MemoryMapSize, DescriptorSize);
// 3. 利用内存地图信息进行安全的内存分配
// 思路:遍历地图,找到第一块足够大的EfiConventionalMemory区域。
Status = FindAndAllocateFromConventional (
MemoryMap,
MemoryMapSize,
DescriptorSize,
BufferPages,
&AllocatedBuffer
);
if (EFI_ERROR (Status)) {
DEBUG ((DEBUG_WARN, "[APP] Could not allocate %lu pages from conventional memory: %r\n",
BufferPages, Status));
} else {
DEBUG ((DEBUG_INFO, "[APP] Successfully allocated buffer at 0x%p\n", AllocatedBuffer));
// ... 使用分配的缓冲区 ...
// 使用完毕后需要释放: gBS->FreePool(AllocatedBuffer);
}
// 4. 释放内存地图缓冲区(由GetCurrentMemoryMap分配)
if (MemoryMap != NULL) {
FreePool (MemoryMap);
}
// 5. 演示MapKey的变化:尝试分配一点内存,再次获取地图,比较Key是否不同。
DEBUG ((DEBUG_INFO, "\n[APP] Demonstrating MapKey change...\n"));
UINTN NewMapKey;
VOID *TempBuffer;
Status = gBS->AllocatePool (EfiBootServicesData, 1024, &TempBuffer);
if (!EFI_ERROR (Status)) {
// 再次快速获取地图,只关心MapKey
MemoryMapSize = 0;
Status = gBS->GetMemoryMap (&MemoryMapSize, NULL, &NewMapKey,
&DescriptorSize, &DescriptorVersion);
if (Status == EFI_BUFFER_TOO_SMALL) {
DEBUG ((DEBUG_INFO, "[APP] Original MapKey: 0x%08lx, New MapKey after allocation: 0x%08lx\n",
MapKey, NewMapKey));
if (NewMapKey != MapKey) {
DEBUG ((DEBUG_INFO, "[APP] MapKey changed! Memory layout was altered.\n"));
}
}
gBS->FreePool (TempBuffer);
}
return EFI_SUCCESS;
}
其中,FindAndAllocateFromConventional是一个辅助函数,展示了如何基于内存地图做出智能分配决策:
EFI_STATUS
FindAndAllocateFromConventional (
IN EFI_MEMORY_DESCRIPTOR *MemoryMap,
IN UINTN MemoryMapSize,
IN UINTN DescriptorSize,
IN UINTN RequiredPages,
OUT VOID **Buffer
)
{
EFI_MEMORY_DESCRIPTOR *Entry;
UINTN NumEntries;
UINTN Index;
EFI_PHYSICAL_ADDRESS StartAddress;
NumEntries = MemoryMapSize / DescriptorSize;
Entry = MemoryMap;
for (Index = 0; Index < NumEntries; Index++) {
// 只关注可用内存
if (Entry->Type == EfiConventionalMemory) {
// 检查这块区域是否足够大
if (Entry->NumberOfPages >= RequiredPages) {
// 尝试从这块区域的起始地址开始分配
StartAddress = Entry->PhysicalStart;
// 使用AllocatePages按页分配,可以指定地址(如果允许)
// 这里使用AllocateAnyPages让固件选择地址,但我们已经知道有足够空间。
// 更精确的做法是使用AllocatePages with EfiAllocateAddress,但需要复杂对齐处理。
// 此处仅作演示,使用简单的AllocatePool。
// 实际项目中,对于大块、对齐要求高的内存,AllocatePages是更好的选择。
DEBUG ((DEBUG_INFO, "[ALLOC] Found conventional region at 0x%lx, size %lu pages.\n",
Entry->PhysicalStart, Entry->NumberOfPages));
// 实际分配略过,这里假设调用gBS->AllocatePages成功
// Status = gBS->AllocatePages(AllocateAnyPages, EfiBootServicesData, RequiredPages, &StartAddress);
// 简单起见,我们直接使用AllocatePool
return gBS->AllocatePool (EfiBootServicesData, RequiredPages * EFI_PAGE_SIZE, Buffer);
}
}
Entry = NEXT_MEMORY_DESCRIPTOR (Entry, DescriptorSize); // 使用宏或手动计算下一个
}
return EFI_NOT_FOUND;
}
// 一个常用的遍历宏定义
#define NEXT_MEMORY_DESCRIPTOR(Ptr, Size) ((EFI_MEMORY_DESCRIPTOR *)((UINT8 *)(Ptr) + (Size)))
调试这个应用,你会在串口或UEFI Shell中看到类似下面的输出,这为你呈现了系统启动阶段内存的全景视图:
=== UEFI Memory Map Dump ===
Number of memory descriptors: 42
[000] Type: 03 (BootCode) Phys: [0x0000000000000000 - 0x0000000000001000) Pages: 1 Attr: 0x0000000000000000
[001] Type: 07 (Conventional) Phys: [0x0000000000001000 - 0x000000000009f000) Pages: 158 Attr: 0x0000000000000000
[002] Type: 04 (BootData) Phys: [0x000000000009f000 - 0x00000000000a0000) Pages: 1 Attr: 0x0000000000000000
[003] Type: 07 (Conventional) Phys: [0x0000000000100000 - 0x0000000007e00000) Pages: 12672 Attr: 0x0000000000000000
... // 更多条目
[041] Type: 0A (ACPINvs) Phys: [0x00000000fed20000 - 0x00000000fed21000) Pages: 1 Attr: 0x0000000000000000
=== Summary ===
Total Physical Memory: 8174 MB
Available Memory (Conv): 8062 MB
5. 高级话题:内存地图在OS加载器中的关键作用
对于UEFI应用程序开发者,尤其是编写操作系统加载器(如GRUB2、systemd-boot或自定义引导程序)的开发者,GetMemoryMap的角色从“信息查看”升级为“资源交接”。
核心任务:调用ExitBootServices()。这是UEFI启动过程中的一个仪式性事件,标志着固件将系统的完全控制权移交给操作系统。在此调用之后,UEFI的引导服务(包括AllocatePool、CreateEvent、gBS表中的大部分函数)将不再可用。而调用ExitBootServices有一个硬性要求:
EFI_STATUS EFIAPI gBS->ExitBootServices (
IN EFI_HANDLE ImageHandle,
IN UINTN MapKey // 必须传入最新GetMemoryMap得到的MapKey
);
为什么需要MapKey? 这是一个同步机制。设想以下场景:
- 加载器调用
GetMemoryMap,获得地图A和Key_A。 - 在准备调用
ExitBootServices的瞬间,一个UEFI定时器中断触发,某个驱动分配了内存,地图变为B。 - 如果加载器拿着旧的Key_A去调用
ExitBootServices,固件会拒绝(返回EFI_INVALID_PARAMETER),因为Key不匹配,表明加载器拥有的地图不是最新的。 - 加载器必须重新调用
GetMemoryMap,获取新的地图B和Key_B,然后用Key_B再次尝试ExitBootServices。
因此,一个健壮的OS加载器代码中,ExitBootServices的调用通常包裹在一个循环里:
EFI_STATUS Status;
UINTN MapKey;
// ... 获取内存地图 ...
do {
// 尝试退出引导服务
Status = gBS->ExitBootServices (ImageHandle, MapKey);
if (EFI_ERROR (Status)) {
// 如果失败原因是Key无效(通常是EFI_INVALID_PARAMETER)
// 则重新获取内存地图和新的Key
if (Status == EFI_INVALID_PARAMETER) {
// 释放旧的地图缓冲区
if (MemoryMap != NULL) {
FreePool (MemoryMap);
MemoryMap = NULL;
}
// 重新获取
Status = GetCurrentMemoryMap (&MemoryMapSize, &MemoryMap, &MapKey,
&DescriptorSize, &DescriptorVersion);
if (EFI_ERROR (Status)) {
// 处理错误...
break;
}
// 现在有了新的MapKey,循环会再次尝试ExitBootServices
} else {
// 其他错误,退出循环
break;
}
}
} while (EFI_ERROR (Status));
if (!EFI_ERROR (Status)) {
DEBUG ((DEBUG_INFO, "[Loader] Successfully exited boot services.\n"));
// 此时,gBS, gRT(运行时服务)部分函数失效,系统进入OS接管阶段
// 必须使用内存地图中的信息来设置OS的内核页表等结构。
}
内存地图的最终用途:在成功退出引导服务后,加载器需要将内存地图(通常是转换后的格式,如E820 map或UEFI内存描述符数组指针)以及其大小、描述符大小等信息,作为启动参数的一部分传递给操作系统内核。内核将依据这份权威的“财产清单”来初始化自己的物理内存管理器,知道哪些内存可用,哪些是MMIO,哪些必须保留。
有一次我在移植一个轻量级内核时,忽略了将ACPI NVS区域标记为保留,结果系统在进入ACPI模式时挂掉了。排查了很久才发现,内核把那片内存当普通内存用了,覆盖了固件的重要数据。所以,精确传递和解析这份地图,是系统稳定性的基石。GetMemoryMap的调用,远不止是获取一些地址信息那么简单,它贯穿了从固件到操作系统的权力交接全过程,是UEFI开发中最需要严谨对待的操作之一。
6158


&spm=1001.2101.3001.11974&articleId=152499627&d=1&t=3&u=6dd048ee2f23491eb8a21122f8a64c9b)

被折叠的 条评论
为什么被折叠?



