内存地图与指针 — 向 UEFI 问清这块地
内存地图与指针 — 向 UEFI 问清这块地
这一章讲什么: UEFI 走后,内核要自己管内存;管内存的第一步是知道「哪块能用」。 本章先换一个正经的开发工具包(EDK II)重写 Hello World——它从此有了名字 MikanLoader, 使命是把将来的内核搬进内存;然后调用 UEFI 的 GetMemoryMap() 拿到内存地图,存成文件。 为了读懂这些代码,作者插播了两节指针课——这是全书反复要用到的武器。 这是全书第一个「为操作系统本身做的功能」,之前的程序都只是演示。
1. 先看现象:一个文件里的内存账本
主线例子:让 MikanLoader 在测试机(或 QEMU)上跑一圈,它会在 U 盘里留下一个 memmap 文件,长这样:
index, Type, PhysicalStart, NumberOfPages, Attribute
1, EfiLoaderCode , 0x00000000, 0x1 , 0xF
2, EfiConvensionalMemory, 0x00001000, 0x9F, 0xF
3, EfiConvensionalMemory, 0x00100000, 0x700, 0xF
…
图说:每一行是一块连续的内存区域——从哪个地址开始(PhysicalStart)、
是什么用途(Type)、有多大(NumberOfPages,以 4KiB 页为单位)。
这个文件就是「内存地图」,CSV 格式,逗号分隔。这张表是干嘛的?告诉未来的内核:这块地里哪块是空地(EfiConvensionalMemory), 哪块住着 UEFI 自己的代码、CPU 的配置信息、ACPI 表——碰都不能碰。1 书里警告得很直接:覆盖了含 CPU 配置信息的区域,「可能会导致 CPU 故障」2。
2. 顶层全景:两个任务串成一条线
任务一(2.2 节):换上 EDK II 工具包,重写 Hello World
hello.c(第1章,什么都自己写)
→ MikanLoaderPkg(四个文件,大部分苦力交给 EDK II 库)
任务二(2.5–2.7 节):向 UEFI 要内存地图
gBS->GetMemoryMap() 填一张表 → 打开 \memmap 文件 → 逐行 Write() → 存进 U 盘
暗线(2.8–2.9 节):指针两节课
指针 = 变量的编号 + 类型 → 箭头运算符 → 指针的指针(多返回值的手法)
图说:本章结束时,引导加载器已经能「侦察地形并把情报带回来」——
这正是第 3 章把它升级成真正搬运工的前置条件。3. 核心原理
3.1 EDK II:不用从零造轮子的 UEFI 开发包
EDK II 最初是 Intel 对 UEFI 的实现,后来开源,现在是开发 UEFI 应用(以及 UEFI BIOS 本身)的标准工具包3。
第 1 章的 hello.c 里那些又臭又长的代码,在 EDK II 里变成两行 #include——
EFI_STATUS、EFI_SYSTEM_TABLE、Print() 这些非标准 C 的东西都是库里现成的4。
它的世界由「包(Pkg)」组成,本章涉及的:
| 文件/包 | 角色 |
|---|---|
edksetup.sh | 环境变量设置脚本(环境变量 = 进程级的配置项,如 PATH),编译前必须先 source(读入)它 |
Conf/target.txt | 设置编译什么(填 MikanLoaderPkg/MikanLoaderPkg.dsc)、用什么工具链 |
| MdePkg | 基础库,其他所有包都建在它的组件上 |
| OvmfPkg | OVMF——开源的 UEFI BIOS 实现,第 01 章 QEMU 用的固件就是它 |
写 UEFI 应用必须配齐三个描述文件:包声明(.dec)、包描述(.dsc)、组件定义(.inf)。
琐碎得像抄模板,附录 A 有全文,正文只讲一个关键点:.inf 里的 ENTRY_POINT 是入口点
——普通 C 程序的入口固定叫 main(),EDK II 里你可以随便起名字5。
新程序起名 MikanLoader:mikan 是作者的名字(MikanOS 的由来), Loader 点明它的使命——把操作系统从 U 盘加载进主存储器。此刻它还只是个 Hello World, 但名字先把志向立住了6。
3.2 主存储器与地址:一条没有空隙的字节长队
硬件上,主存储器是多块内存芯片拼的;软件上看,它就是一排首尾相接、没有空隙的字节, 每个字节有一个从 0 开始的序列号——这个序列号叫地址,CPU 按地址逐个读写字节7。
记住两件事:
- 地址的单位是字节;32 位 CPU 的地址范围大约从 0 到 4 亿(4 GiB 量级);
- UEFI 内存地图里,地址用字节表示、大小却用「页」表示(一页 = 4 KiB),两个单位混用,书里也抱怨「有点儿混乱」8。
3.3 内存地图:一张一维的城市地图
内存映射(memory map)是「主内存的哪个部分用于何种用途」的地图。 作者拿它类比美国式地址:全城只有一条路,门牌号沿道路连续编排——找到路,顺着号走就到9。
表的结构(EFI_MEMORY_DESCRIPTOR,每行 5 个字段)10:
| 字段 | 含义 |
|---|---|
| Type | 这块地的用途(数字编号) |
| PhysicalStart | 起始物理地址(字节) |
| VirtualStart | 虚拟地址(本章不用,第 8、19 章才登场) |
| NumberOfPages | 大小,以 4 KiB 页计 |
| Attribute | 这块地的属性标志 |
Type 值里最重要的一个:7 = EfiConvensionalMemory = 空闲区域——内核将来就从这里划地11。
两个工程细节,后文会回来讨债:
- 地图里可能有孔洞:上一行的 PhysicalStart + NumberOfPages×4KiB 不一定等于下一行的起始地址,遍历代码不能假设连续12;
- 地图是会变的:每次获取有个 MapKey 标识版本,程序动了内存(哪怕调了 UEFI 函数),地图就可能刷新——这个 MapKey 到第 3 章调用 ExitBootServices() 时是必需品13。
3.4 向 UEFI 要地图:GetMemoryMap()
UEFI 的功能分两类:引导服务(操作系统启动前用,内核接管后作废)和运行时服务(启动前后都能用)。
内存管理在引导服务里,EDK II 用全局变量 gBS 代表它14。
调用 gBS->GetMemoryMap() 的姿势很能代表 UEFI 风格——一次调用,五个参数,其中三个是输出:
| 参数 | 输入还是输出 | 干什么 |
|---|---|---|
| MemoryMapSize | 进出(IN OUT) | 进去时说「我准备的缓冲区有多大」,出来时变成「地图实际需要多大」 |
| MemoryMap | 出(OUT) | 缓冲区指针,地图填进去 |
| MapKey | 出 | 地图的版本号 |
| DescriptorSize | 出 | 每行描述符占多少字节 |
| DescriptorVersion | 出 | 版本号(本书不用) |
缓冲区不够时函数返回 EFI_BUFFER_TOO_SMALL——这是 UEFI 的典型套路:先问要多大,再问一次要数据15。
还有个反直觉的细节:遍历地图时,行间距要用函数告诉你的 DescriptorSize,而不是 sizeof(EFI_MEMORY_DESCRIPTOR)——
因为某些 UEFI 实现会塞私有扩展,行宽可能不一样16。
3.5 把地图存盘:文件操作与指针的第一次实战
存盘三步:OpenRootDir() 打开 U 盘根目录 → root_dir->Open() 以写模式打开(不存在就创建)\memmap 文件 →
SaveMemoryMap() 循环遍历地图,把每行格式化成字符串(一串字符),用 file->Write() 写进去17。
格式化用的是 EDK II 的 AsciiSPrint()(类似标准 C 的 sprintf,把数字按格式变成字符串),
类型编号到类型名(EfiLoaderCode 这种)靠程序里自己写的一张对照函数 GetMemoryTypeUnicode()18。
这里出现全书第一个「看不懂也不用怕」的标记:指针的指针(&root_dir、打开文件时传 EFI_FILE_PROTOCOL**)。
作者安慰你说 2.8 节马上讲19——它确实讲了,而且讲得很好,见下两节。
3.6 指针课(1):指针是「变量的编号 + 类型」
C++ 里指针(pointer)的意思就是「指向变量的东西」。三行入门:
int i = 42;
int* p = &i; // & 取地址:p 里存着 i 的「编号」
int r = *p; // * 间接运算符:顺着编号找到 i,读出 42
*p = 1; // 也可以顺着编号改 i 的值关键在实现层面:C++ 标准没规定这个「编号」具体是什么,但指针和整数可以互相转换, 转换回指针必须能区分不同变量。实际上(Clang 也是如此)这个编号就是内存地址—— 变量在内存里按字节编了号,指针存的就是那个号20。
uintptr_t 是「装得下任何指针的整数类型」;指针→整数是剥掉类型信息,整数→指针是贴回类型信息21。
这一来一回,就是 2.6 节遍历地图时那句「把整数 iter 转成 EFI_MEMORY_DESCRIPTOR*」的真相22。
3.7 指针课(2):箭头运算符与指针的指针
结构体的成员按声明顺序排在内存里,成员距结构体开头的字节数叫偏移量(offset):
比如 buffer_size 偏移 0 字节,buffer 偏移 8 字节(64 位环境里指针占 8 字节)23。
指向结构体的指针,读写成员用箭头 ->:
pm->map_size = 0;
(*pm).map_size = 0; // 完全等价,只是更绕这两种写法处理方式完全相同,-> 只是「语法糖」——意义不变,写得省力24。
指针的指针(如 EFI_FILE_PROTOCOL**)看着吓人,其实一句话就能拆穿:
指针变量也是变量、也住在内存里、也有地址;给指针变量的地址再包一层指针,就是指针的指针。
UEFI 里到处用它,是因为 UEFI 函数的返回值 EFI_STATUS 被用来报「成没成功」,
想再返回一个指针(比如打开的文件)只能走参数——把「指针变量的地址」传给函数,函数改写那个指针变量。
这就是「一个函数返回多个值」在 C 里的标准手法25。
4. 作者的判断与证据
判断:用 EDK II 重写得值。 hello.c 里大半内容是手工模仿 UEFI 接口;换 EDK II 后同样的程序 「一眼就能看出更简洁」——库替你写的部分,正是最容易写错的部分26。
判断:内核接管前必须先摸清内存。 这不是理论洁癖,是硬约束:用错内存会踩坏 CPU 配置、UEFI 数据, 「如果要使用内存,就必须找到空闲空间」27。所以全书把「获取内存地图」排在引导加载器的第一个正式功能。
证据类型: 本章证据是「跑出来的文件」——memmap 文件真实存在于你的 U 盘里, 可以 cat 出来、可以拿电子表格软件画成图28。这种「让机器交一份实物作业」的验证方式,全书一以贯之。
5. 边界与局限
- GetMemoryMap() 是引导服务,内核接管后它就没了。 地图必须在「还信 UEFI 的时候」拿够、存好、传下去——第 3 章会看到这一步的正式仪式(ExitBootServices)。
- 缓冲区大小的经验值(8 KiB 不够、16 KiB 凑合)只对作者的环境成立;你自己的机器地图更大,就得开更大缓冲区29。
- 指针两节讲得清楚,但只是「够用的最小集」:数组指针算术、const 指针、悬垂指针这些一概没讲,遇到再查 C 教材。
6. 可带走的
本章主线例子,一行写完: MikanLoader 跑起来 → gBS->GetMemoryMap() 填表 → 打开 \memmap → 逐行写 CSV → U 盘里多一张内存地图。
- 内存地图回答「哪块能用」:Type=7(EfiConvensionalMemory)是空地;地图可能有孔洞、会变(MapKey 记版本);
- UEFI 的调用风格:缓冲区不够就返回 TOO_SMALL,问两次;行间距用函数给的 DescriptorSize,别用 sizeof;
- EDK II 三件套(dec/dsc/inf)是写 UEFI 应用的门票,ENTRY_POINT 自由命名;
- 指针 = 变量编号(内存地址) + 类型;
&取编号,*顺编号找人; - 指向结构体的指针用
->访问成员,它只是(*pm).的语法糖; - 指针的指针 = 多返回值手法:返回值被状态码占了,额外的结果靠「指针变量的地址」带回来;
- EDK II 里
gBS是引导服务的门,gRT是运行时服务的门(本书用不到后者)。
7. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| EDK II 是什么、包结构 | 2.1 EDK II简介 | text/19-ch02-01-2-1-edk-ii.txt:4(搜「最初是Intel」) · text/19-ch02-01-2-1-edk-ii.txt:25(搜「MdePkg」) |
| MikanLoader 命名与三件套 | 2.2 EDK II的Hello World程序(osbook_day02 | text/20-ch02-02-2-2-edk-ii-hello-world-osbook-day02.txt:4(搜「MikanLoader」) · text/20-ch02-02-2-2-edk-ii-hello-world-osbook-day02.txt:16(搜「必须创建的文件」) |
| 入口点 ENTRY_POINT | 2.2 EDK II的Hello World程序(osbook_day02 | text/20-ch02-02-2-2-edk-ii-hello-world-osbook-day02.txt:22(搜「ENTRY_POINT」) |
| 主存储器 = 一排字节,地址 | 2.3 主存储器 | text/21-ch02-03-2-3.txt:18(搜「一条直线上」) · text/22-ch02-04-2-4.txt:5(搜「序列号被称为地址」) |
| 内存地图与 Type 表 | 2.4 内存映射 | text/22-ch02-04-2-4.txt:16(搜「内存映射是一种显示」) · text/22-ch02-04-2-4.txt:47(搜「Type值」) |
| 页与单位混用、孔洞 | 2.4 内存映射 | text/22-ch02-04-2-4.txt:55(搜「NumberOfPages」) · text/22-ch02-04-2-4.txt:58(搜「孔洞」) |
| 引导服务/运行时服务、gBS | 2.5 获取内存映射(osbook_day02b) | text/23-ch02-05-2-5-osbook-day02b.txt:12(搜「引导服务」) |
| GetMemoryMap 五参数 | 2.5 获取内存映射(osbook_day02b) | text/23-ch02-05-2-5-osbook-day02b.txt:33(搜「EFI_BUFFER_TOO_SMALL」) · text/23-ch02-05-2-5-osbook-day02b.txt:39(搜「MapKey」) · text/23-ch02-05-2-5-osbook-day02b.txt:42(搜「DescriptorSize」) |
| EFI_MEMORY_DESCRIPTOR 五字段 | 2.5 获取内存映射(osbook_day02b) | text/24-ch02-06-2-6.txt:1(搜「结构体的定义」) |
| 存盘三步与指针的指针预告 | 2.6 将内存映射保存到文件中 | text/24-ch02-06-2-6.txt:34(搜「OpenRootDir」) · text/24-ch02-06-2-6.txt:42(搜「16KiB」) |
| 迭代器与类型转换 | 2.6 将内存映射保存到文件中 | text/24-ch02-06-2-6.txt:57(搜「类型转换」) |
| 检查 memmap 文件 | 2.7 检查内存映射 | text/25-ch02-07-2-7.txt:27(搜「memmap」) |
| 指针入门三行 | 2.8 指针简介(1):地址和指针 | text/26-ch02-08-2-8-1.txt:4(搜「指针的意思是」) · text/26-ch02-08-2-8-1.txt:8(搜「int i = 42」) |
| 指针 = 编号 + 类型、uintptr_t | 2.8 指针简介(1):地址和指针 | text/26-ch02-08-2-8-1.txt:31(搜「唯一编号」) · text/26-ch02-08-2-8-1.txt:36(搜「uintptr_t」) |
| 地址由处理器决定、优化会重排 | 2.9 指针和箭头运算符 | text/27-ch02-09-2-9.txt:4(搜「由处理器决定」) · text/27-ch02-09-2-9.txt:14(搜「启用优化」) |
| 偏移量、箭头运算符、语法糖 | 2.9 指针和箭头运算符 | text/27-ch02-09-2-9.txt:20(搜「偏移量」) · text/27-ch02-09-2-9.txt:49(搜「pm->map_size = 0」) · text/27-ch02-09-2-9.txt:53(搜「语法糖」) |
| 指针的指针与多返回值 | 2.9 指针和箭头运算符 | text/27-ch02-09-2-9.txt:78(搜「经常使用的一种技术」) · text/27-ch02-09-2-9.txt:91(搜「重写memmap」) |
Footnotes
-
出处:「2.4 内存映射」(
text/22-ch02-04-2-4.txt:16,搜「内存映射是一种显示」;:47,搜「Type值」)。Type 各项含义表见同节(text/22-ch02-04-2-4.txt:46)。 ↩ -
出处:「2.4 内存映射」(
text/22-ch02-04-2-4.txt:61,搜「覆盖包含CPU配置信息」)。 ↩ -
出处:「2.1 EDK II简介」(
text/19-ch02-01-2-1-edk-ii.txt:4,搜「最初是Intel」)。 ↩ -
出处:「2.2 EDK II的Hello World程序」(
text/20-ch02-02-2-2-edk-ii-hello-world-osbook-day02.txt:36,搜「include」;:39,搜「EFI_STATUS」)。 ↩ -
出处:「2.2 EDK II的Hello World程序」(
text/20-ch02-02-2-2-edk-ii-hello-world-osbook-day02.txt:22,搜「ENTRY_POINT」)。 ↩ -
出处:「2.2 EDK II的Hello World程序」(
text/20-ch02-02-2-2-edk-ii-hello-world-osbook-day02.txt:4,搜「MikanLoader」)。 ↩ -
出处:「2.3 主存储器」(
text/21-ch02-03-2-3.txt:18,搜「一条直线上」;text/22-ch02-04-2-4.txt:5,搜「序列号被称为地址」)。 ↩ -
出处:「2.4 内存映射」(
text/22-ch02-04-2-4.txt:55,搜「NumberOfPages表示内存区域的大小」;:57,搜「有点儿混乱」)。4 KiB 一页的约定见同节脚注。 ↩ -
出处:「2.4 内存映射」(
text/22-ch02-04-2-4.txt:16,搜「内存映射是一种显示」;:38,搜「美式地址」)。 ↩ -
出处:「2.5 获取内存映射」(
text/24-ch02-06-2-6.txt:1,搜「结构体的定义」),表 2.4 列出 Type/PhysicalStart/VirtualStart/NumberOfPages/Attribute 五个字段。 ↩ -
出处:「2.4 内存映射」(
text/22-ch02-04-2-4.txt:52,搜「空闲区域」)。 ↩ -
出处:「2.4 内存映射」(
text/22-ch02-04-2-4.txt:58,搜「孔洞」)。 ↩ -
出处:「2.5 获取内存映射」(
text/23-ch02-05-2-5-osbook-day02b.txt:39,搜「MapKey」;:41,搜「ExitBootServices」)。 ↩ -
出处:「2.5 获取内存映射」(
text/23-ch02-05-2-5-osbook-day02b.txt:12,搜「引导服务」)。 ↩ -
出处:「2.5 获取内存映射」(
text/23-ch02-05-2-5-osbook-day02b.txt:33,搜「EFI_BUFFER_TOO_SMALL」;:34,搜「MemoryMapSize」)。 ↩ -
出处:「2.5 获取内存映射」(
text/23-ch02-05-2-5-osbook-day02b.txt:44,搜「某些UEFI实现可能有扩展结构」)。 ↩ -
出处:「2.6 将内存映射保存到文件中」(
text/24-ch02-06-2-6.txt:34,搜「OpenRootDir」;:43,搜「CSV」)。 ↩ -
出处:「2.6 将内存映射保存到文件中」(
text/24-ch02-06-2-6.txt:68,搜「AsciiSPrint」)。 ↩ -
出处:「2.6 将内存映射保存到文件中」(
text/24-ch02-06-2-6.txt:38,搜「指针的指针」)。原文:「在使用OpenRootDir()和root_dir->Open()中,使用了『指针的指针』技术」。 ↩ -
出处:「2.8 指针简介(1)」(
text/26-ch02-08-2-8-1.txt:31,搜「唯一编号」))与「2.9 指针和箭头运算符」(text/27-ch02-09-2-9.txt:5,搜「内存地址」))。原文:「指针包含分配给每个变量的唯一编号。可以说,指针是变量的特定编号和类型信息的组合」;「许多编译器,包括本书中使用的Clang,都会使用变量所在的内存地址」。 ↩ -
出处:「2.8 指针简介(1)」(
text/26-ch02-08-2-8-1.txt:36,搜「uintptr_t」;:37,搜「删除指针中的类型信息」)。 ↩ -
出处:「2.6 将内存映射保存到文件中」(
text/24-ch02-06-2-6.txt:57,搜「类型转换」;:60,搜「指针很难理解」——原文安慰「即使不理解也不用担心(请阅读2.8节)」)。 ↩ -
出处:「2.9 指针和箭头运算符」(
text/27-ch02-09-2-9.txt:20,搜「偏移量」)。 ↩ -
出处:「2.9 指针和箭头运算符」(
text/27-ch02-09-2-9.txt:49,搜「pm->map_size = 0」;:53,搜「语法糖」)。 ↩ -
出处:「2.9 指针和箭头运算符」专栏2.2(
text/27-ch02-09-2-9.txt:83,搜「EFI_FILE_PROTOCOL**」;:91,搜「别无选择」;:97,搜「多个返回值」)。 ↩ -
出处:「2.2 EDK II的Hello World程序」(
text/20-ch02-02-2-2-edk-ii-hello-world-osbook-day02.txt:30,搜「更简洁」)。 ↩ -
出处:「2.4 内存映射」(
text/22-ch02-04-2-4.txt:61,搜「正确理解主内存」;:64,搜「获取内存映射」)。 ↩ -
出处:「2.7 检查内存映射」(
text/25-ch02-07-2-7.txt:32,搜「cat命令」);:32,搜「电子表格软件」)。 ↩ -
出处:「2.6 将内存映射保存到文件中」(
text/24-ch02-06-2-6.txt:41,搜「8KiB」)。 ↩