本节摘要:内存映射并非一张静态图表,而是 CPU、芯片组、固件、设备四方在通电后持续数毫秒的空间主权谈判结果。本节讲解 UEFI 内存类型体系里最要紧的五类各归谁管,拆开实模式→保护模式→长模式的切换仪式,并说明运行时服务与内核同处 Ring 0 却各守各的"宪法"这一隔离设计。
读完后请自测四点:能否用"动态宪章"的视角还原内存映射的生成过程;能否说出五类内存(启动服务区、运行时服务区、常规内存、ACPI NVS、不可用内存)各自的语义与限制;能否复述三级模式切换的步骤链及其不可逆性;能否讲清 ExitBootServices 失败与内存映射冲突之间的因果。
教科书爱把内存映射画成一张静止的表:低 640KB 是常规内存,0xA0000 往上是显存,0xC0000 起是各 ROM,0xF0000 起是系统 BIOS。可这张表只是立宪前的草稿。真实情况是:CPU、芯片组、固件、设备控制器四方从通电一刻起,展开一场持续几毫秒的主权谈判与划界仪式。
谈判的核心矛盾在于:物理地址空间是一根有限的直线,而逻辑需求却分层异构、彼此打架。CPU 要放代码的区间,GPU 要帧缓冲的直通车道,NVMe 控制器要 DMA 用的大块连续内存,TPM 要隔离的受保护页,UEFI 运行时服务还得在 OS 接管之后仍可被调用。这些诉求无法按"谁先到谁得"裁决,只能交给一套分阶段、分权限、可验证的地址治理机制。
机制的地基是内存类型划分。它不是"RAM 与 ROM"的二元切分,而是按访问语义、缓存策略、一致性模型与安全域归属做的多维分类。UEFI 规范列了十二种内存类型,实践中最有分量的五类构成理解现代固件内存治理的棱镜:
启动服务代码/数据区。 UEFI 启动期的"经济特区",固件装载的驱动、协议实现、Shell 程序都在这里跑。要害在于:退出启动服务之后,操作系统有权把整块区域收归可用物理内存重新分配。这不是简单的释放,而是一次语义主权移交——固件主动交出对这段地址的解释权,把它降格为裸物理页。
运行时服务代码/数据区。 固件留给操作系统的"驻外使馆"。系统即便被 OS 全面接管,仍要经它读写变量、查询时间。这块内存背上两个苛刻条件:物理上不得被 OS 分页换出(必须钉在物理内存里);还得支持缓存一致性的 DMA 访问(S3 唤醒时固件要直接读写这些区域)。Intel 平台要求其映射符合 MTRR 的写合并或不可缓存类型,AMD 平台对应页属性表的相应设置。这份映射,是硬件、固件、OS 三方在内存描述符结构体里签下的跨生命周期契约。
常规内存区。 操作系统眼里的"无主荒地"。启动服务结束前,固件交给 OS 一份按 4KB 页粒度描述的可用内存清单。注意,这份清单绝不是物理内存的全貌——被占用、被保留、被设备 MMIO 占据、被 ACPI 表驻留、被 SMM 代码锁定的区间都已被固件剔除。它是一份经过政治过滤的资源白名单。
ACPI 非易失存储区。 ACPI 规范压给固件的"宪法附件"。系统进 S3 挂起时,电源管理器切断大部分供电,但这一区的内容必须原样保住,供唤醒后恢复上下文。固件必须保证它退出启动服务后仍被 OS 视作保留,且不能被任何 DMA 波及——否则一次网卡中断就可能抹掉唤醒向量。它的位置由 ACPI 固定硬件描述表的字段间接指明,串起一条从 ACPI 表到固件保留区再到 OS 保留区的跨栈信任链。
不可用内存区。 这不是报错,而是防御。部分 PCIe 配置空间、某些 SoC 内部寄存器窗口、有硬件缺陷的内存段,会被固件主动标记为不可用:不参与分配、不暴露给 OS。这是固件作为硬件抽象层行使的最终否决权——一次主动的空间封禁。
| 内存类型 | 主权归属 | OS 接管后命运 | 关键约束 |
|---|---|---|---|
| 启动服务代码/数据 | 固件(临时) | 整体回收为可用页 | 退出启动服务即失效 |
| 运行时服务代码/数据 | 固件(永久使馆) | 必须保留映射 | 不可换出,S3 供电保持 |
| 常规内存 | 待分配白名单 | 纳入 OS 内存管理 | 已过滤保留与占用区 |
| ACPI NVS | 平台恢复区 | 识别为保留 | 不可被 DMA 覆盖 |
| 不可用内存 | 固件封禁 | 不可见 | 防御性剔除缺陷段 |
五类合在一起,织成一张持续演化的地址空间拓扑:POST 早期粗划,DXE 阶段各驱动依设备枚举结果细填,启动设备选择阶段由加载器裁边,退出启动服务那一刻完成法律意义上的主权交割。它不画在纸上,而是刻进 MTRR、页属性表、运行时服务数据结构与 ACPI 表的字节流——是硬件意志、固件策略与软件契约三方角力后凝固下来的时空结构。
内存映射决定"在哪跑",模式切换决定"以什么身份、什么权限、按什么规矩跑"。这不是一条 jmp 指令的事,而是牵动 CPU 架构、特权等级、地址转换与安全边界的系统级改朝换代。x86/x64 上,这场更替横跨三条线:保护模式与长模式的切换、特权环的语义重载、分页的逐级启用。UEFI 正是这场更替的总设计师兼首席公证。
第一次让渡:实模式到保护模式。 通电瞬间 x86 处于 16 位实模式:1MB 空间、没有保护、没有特权分级。固件的第一段代码只能在这里跑,而它的头等大事竟是亲手关掉自己的摇篮——置起控制寄存器的保护允许位,把 CPU 推进 32 位保护模式。切换看似一步,实则凶险:保护模式一生效,CPU 立刻不再解释 16 位段描述符,转而索要一张有效的 GDT;表没备好,通用保护异常,系统锁死。所以固件安全阶段的汇编代码必须在跳转前,用加载描述符表的指令精确装入一张预置的、含 32 位代码与数据描述符的表。这张表就是新政权的第一部宪法草案——写明代码段的基址、限长与特权级。
更巧的是固件对一项硬件特性的利用:保护模式下代码段寄存器的低两位(RPL)仍可改动,从而临时调低当前特权级。固件因此能在最高环里安全模拟低特权环境,给后续装载、未必可信的 Option ROM 提供沙箱。这种"特权级弹性",是固件超越纯硬件抽象、展露软件治理手腕的漂亮一笔。
第二次升维:保护模式到长模式。 驱动执行环境就绪后,系统要进 64 位长模式解锁 4GB 以上的寻址。四步缺一不可:打开物理地址扩展(为 36 位物理地址铺路);往扩展功能寄存器写入长模式开关;启用分页(此刻四级页表必须已经建好,否则页错误异常);用 64 位代码段描述符做一次远跳转,强制切换。四步是一条不可逆的单行道——长模式与分页同时生效后,CPU 拒绝再执行 16 位或 32 位兼容代码。固件在其中扮演"架构翻译官":把遗留 Option ROM 的 16 位实模式代码,经由 SMM 或 CSM 做二进制翻译与沙箱封装,让旧生态在新政权下获得特赦式生存。对历史包袱的包容治理,比一刀切的淘汰更能看出系统工程的成熟度。
⚠️ 常见坑:长模式切换翻车的三大经典原因——页表没有按四级结构对齐、PAE 没有先于长模式开启、远跳用的选择子指向了 32 位段。排查先看异常向量号:页错误多半是页表问题,通用保护异常多半是描述符问题。
Ring 0 的两副面孔。 长模式下,固件运行时服务与内核同坐最高特权环,执行上下文与内存视图却完全两样。运行时服务住在受限的、静态映射的空间里:代码段可执行,数据段严格只读写、禁止执行;页表项管理位设成监管级,意味着内核即便换掉页表,也绕不过固件设定的访问权限——这是硬件级的宪法隔离。内核则住在一套完全自建自管的、动态的、支持地址随机化与管理模式访问防护的页表体系里,能摸到全部常规内存,但对运行时服务数据的访问必须经由预注册函数指针、由硬件强制跳到固件保留的已签名入口。这不是普通函数调用,而是一次受控的、硬件背书的跨主权域调用。这层设计让运行时服务成为 OS 之下、硬件之上的可信执行子系统:TPM 要在 S3 唤醒时核对固件完整性、Secure Boot 要在运行期动态换平台密钥、ACPI 要在休眠期间维持计时精度——全靠这个在 Ring 0 里保持独立宪法的"影子政府"。
至此可以点出本节最内核的命题:内存映射与执行环境合起来,是整个计算系统的时空基座。它规定信息在物理空间(地址)里的合法落点,也规定指令在时间序列(执行流)里的合法身份。没有这个基座,Secure Boot 只是白纸,SMM 只是一段无法调度的代码,ACPI 只是一堆解不开的二进制,OS 的虚拟内存更是无源之水。
基座的稳固来自三层嵌套的信任。硬件层:控制寄存器、MTRR、页属性表给出不可篡改的执行策略与内存类型语义,是信任的物理锚。固件层:UEFI 规范定死内存描述符的格式、退出启动服务的原子语义、运行时服务重映射的严格流程,把硬件能力翻译成可编程、可验证、可审计的软件契约。协议层:各协议接口不只是函数表,而是在特定内存映射与执行模式下、被授权触碰特定硬件资源的数字凭证——一个驱动只有在被调度器装进启动服务代码区并成功安装协议之后,才算领到与显卡、键盘、磁盘对话的"外交护照"。
💡 关键直觉:分析启动失败,表象在软件、病灶常在契约缝隙。日志里报退出启动服务失败,往往不是代码 bug,而是内存映射的深层冲突:也许某块 ACPI NVS 被 OS 误当常规内存释放重用;也许某 PCIe 设备的地址窗口与运行时服务数据区撞车;也可能虚拟地址重映射违反了 MTRR 对运行时代码的缓存类型要求。排障次序应是:先取固件内存图,再对照内核的映射转换逻辑,最后查设备地址窗口分配。
从 BIOS 钉死的 1MB,到 UEFI 的弹性空间;从实模式的混沌初开,到长模式的秩序井然;从 Option ROM 的野蛮生长,到驱动模型的协议治理——这一切演化的底层动力,都是对同一个问题的不懈求解:在有限的物理世界里,造出可预测、可验证的确定性秩序。
下一节看固件在这套时空基座上开了哪些服务,Boot 与 Runtime 两个域又是怎么分权的。
上一节的地址表可以现场复现。Linux 把 EFI 启动时的完整内存映射留在 efi 内存表里,内核日志一行一条,直接对照:
$ sudo dmesg | grep efi.*mem | head -12 efi: mem000: type=7 phys=0x0000000000000000-0x000000000009FFFF ...Conventional efi: mem001: type=3 phys=0x0000000000100000-0x000000007A6BFFFF ...BootServicesCode efi: mem002: type=4 phys=0x000000007A6C0000-0x000000007A7EFFFF ...BootServicesData efi: mem003: type=7 phys=0x000000007A7F0000-0x000000009FBFFFFF ...Conventional efi: mem004: type=5 phys=0x0000000100000000-0x000000026FFFFFFF ...RuntimeCode/ACPI efi: mem005: type=10 phys=0x0000000270000000-0x000000026F5FFFFF ...MMIO efi: mem032: type=6 phys=0x00000000FEE00000-0x00000000FEE00FFF ...APIC efi: mem040: type=7 phys=0x0000001000000000-0x00000011DFFFFFFF ...Conventional (PCIEXBAR above) efi: 43 memory map entries
type=7(Conventional)的空闲区就是引导加载器分配内存的来源;type=5 的 Runtime 区小而关键——它们在 ExitBootServices 之后仍被页表保留,第 3 章讲运行时服务时还会回到这几个区间。
执行环境差异也能用一段最小代码量出来。同一个"读 RTC 时间"的需求,两种环境下写出的是两份截然不同的代码:
/* BIOS/实模式:INT 1AH,AH=02H 读 CMOS RTC,结果放 BCD 寄存器 */ /* NASM,运行于 0x7C00 的引导扇区 */ mov ah, 0x02 int 0x1A ; CF=0 成功;CH=时 CL=分 DH=秒(BCD 码) jc .fail ; 把 BCD 拆成二进制还得手工 al>>4*10 + al&0xF /* UEFI:ConOut->OutputString 直接拿时间没有 BCD,用 gRT->GetTime */ EFI_TIME t; gRT->GetTime(&t, NULL); /* 返回已是二进制,附带世纪与时区字段 */ AsciiPrint("now %04d-%02d-%02d\n", t.Year, t.Month, t.Day);
一边要自己拆 BCD、处理世纪翻转,一边拿到的是带时区的结构体——"执行环境即服务"不是口号,是两段并排的代码。