KTRR / CTRR 的保护边界不只由最后一次锁寄存器写入决定。启动映射覆盖了什么、只读区间如何换算、TLB 何时失效,以及其他 CPU 如何确认锁定状态,都影响那次写入的实际意义。
下面围绕 2021 年 ARM64 内核分析中的 CTRR 路径展开,并以 Apple XNU 7195.60.75 固定版本源码核对分支。所分析二进制的设备与完整构建标识缺失;源码对应关系是交叉验证,不是对二进制身份的认定。KTRR 与 CTRR 共用部分启动逻辑,但保护范围和寄存器序列仍需分别讨论。
启动映射只保留打开 MMU 所需的一页
ADRL X8, _bootstrap_instructions
AND X21, X8, #0xFFFFFFFFFFFFC000
MOV X0, X21
BL _mmu_kvtop掩码清除最低 14 位,得到 16 KiB 页边界。接下来把启动代码的虚拟地址转换为物理地址,并沿启动页表定位相应表项。mmu_kvtop 与 phystokv 处于相反的转换方向,PTE 中的物理地址应与用于访问表页的内核虚拟地址分开记录。
公开 arm_replace_identity_map 的目的很具体:把早期大范围 V=P 映射收缩为打开 MMU 所需的映射,避免可执行 block 跨越硬件保护边界。它取得 L1、L2 表页,分配新的 L3 表页,清零 L1、L2,再仅连接相关条目;这不是把所有启动页表无条件清空。
| 层级 | 取位 | 宽度 | 索引覆盖 |
|---|---|---|---|
| L1 | bits 36–38 | 3 | 8 个索引 |
| L2 | bits 25–35 | 11 | 2048 个索引 |
| L3 | bits 14–24 | 11 | 2048 个索引 |
| 页内偏移 | bits 0–13 | 14 | 16 KiB |
ADD ..., #4, LSL #12 的步长同样为 0x4000。一张 16 KiB 表容纳 2048 个 8 字节表项;顶层只使用部分索引,并不表示顶层分配只有 8 个条目的空间。样本末级属性常量 0x40000000000683 应结合翻译配置解释,而不是独立当成“永远可执行”的结论。
设备树属于权限准备,不是普通代码段
arm_vm_prot_init 除了处理 Mach-O 段,还根据设备树的锁定状态调整 segEXTRADATA。固定版本源码在 SecureDTIsLockedDown() 成立时采用 PE_state.deviceTreeHead 与 deviceTreeSize,并包含 TrustCache 对额外数据范围的影响,随后配置 RNX 映射。
这里有两个不同的问题:设备树是否已锁定,以及额外数据区域位于什么地址。后者还由 segEXTRADATA、segLOWEST 与 segLOWESTRO 的比较决定,不应把这些条件都归入 SecureDTIsLockedDown 的返回值。该函数名也不足以证明设备树与内核映像之间的具体大小关系。
因此,保护范围分析应同时保留映像段表与启动传入数据区域。只列出 __TEXT 和 __DATA 会丢掉影响最低只读边界的对象。
KTRR 与 CTRR 的终点不应混用
rorgn_stash_range 先计算并保存范围,rorgn_lockdown 随后才配置硬件。公开实现还比较 iBoot 已设置的 AMCC 区域与 XNU 计算结果,避免两侧对边界各自作出不同解释。
| 配置 | MMU 保护终点 | 与 AMCC 的关系 |
|---|---|---|
| KTRR | kvtophys(segLASTB) - segSizeLASTDATACONST - 1 |
__LAST 在 AMCC 只读区域内,但不在 MMU KTRR 区域内 |
CTRR,有 segHIGHESTRO |
kvtophys(segHIGHESTRO) - 1 |
核对 CTRR 与 AMCC 起止相同 |
CTRR,无 segHIGHESTRO |
kvtophys(segLASTB) + segSizeLAST - 1 |
同样包含 __LAST 并核对边界 |
两者起点都由 kvtophys(segLOWESTRO) 得到。这里使用的 ctrr_begin、ctrr_end 是首字节与末字节均包含的物理端点;[start, end_exclusive) 应转为 [start, end_exclusive - 1]。空区间、地址回绕与非连续转换应在计算前排除,不应把任意虚拟地址直接送入硬件。
源码特别指出,硬件端点的处理粒度与处理器支持的最小页粒度有关,并不简单等同于当前 16 KiB 页大小。因此,“最后一页起点”和“最后一个字节”也不是可互换的输入。
CTRR 配置要保留屏障与分代条件
| 用途 | EL1 编码 | EL2 集群配置编码 |
|---|---|---|
| 起点 | S3_4_C15_C2_3 |
S3_4_C15_C11_0 |
| 终点 | S3_4_C15_C2_4 |
S3_4_C15_C11_1 |
| 控制 | S3_4_C15_C2_5 |
S3_4_C15_C11_4 |
| 锁定 | S3_4_C15_C2_2 |
S3_4_C15_C11_5 |
头文件将控制值 0x12 分解为两个启用位。其他字段被列出,不代表这次写入也启用了它们。
| 位 | 字段 | 值 |
|---|---|---|
| 7 | B_UXN | 0 |
| 6 | A_UXN | 0 |
| 5 | B_PXN | 0 |
| 4 | A_PXN | 1 |
| 3 | B_MMUON_WRPROTECT | 0 |
| 2 | B_MMUOFF_WRPROTECT | 0 |
| 1 | A_MMUON_WRPROTECT | 1 |
| 0 | A_MMUOFF_WRPROTECT | 0 |
lock_mmu 先写边界,再写控制值,最后写 1 锁定。这个概括仍不足以替代实际时序:在所核对版本中,非 APPLEVORTEX 路径于边界写入后执行 ISB 与 TLB 失效;APPLEVORTEX 路径则在后面的 ISB 之后刷新。跨芯片复用寄存器序列时,省略这一分支会丢掉必要语义。
CurrentEL == 8 对应 EL2 编码,该条件成立时才写另一组集群配置寄存器。函数包含 EL2 分支,并不证明所分析设备在特定启动方式下执行过它。AMCC/IOA 锁组与 MMU 范围也属于不同配置对象,不应将一次 MSR 概括为全部保护均已完成。
调试位限制不是一次寄存器回读
BIC X2, X2, X0
ORR X2, X2, X1
AND X2, X2, #0xFFFFFFFFFFFFDFFF该变换按调用参数清位、置位,随后强制清除 MDSCR_EL1.KDE 对应的 bit 13。固定版本的 update_mdscr 在写入后测试的是通用寄存器 X2,不是再次执行 MRS MDSCR_EL1 的硬件回读。这一区别直接影响“验证写入成功”之类结论是否成立。
源码把异常 KDE 状态与清位、重试及 MDSCR.KDE was set 的 panic 路径相连,其设计还需结合断点控制寄存器约束理解。沿正常顺序执行时,先前清位会让该测试为零;附加路径的意义包括约束从中间进入的异常控制流。仅看这段代码,不等于已经复现调试机制突破只读保护。
reset 路径先确认前提,再决定是否重配
- 1
检查 lockdown 完成状态
相关 CPU reset 路径先检查
lockdown_done;尚未完成时原地等待,不继续配置。 - 2
读取保存范围
起止值为零时,DEBUG、DEVELOPMENT 或 CONFIG_DTRACE 分支跳过 CTRR 配置;其他配置在该条件下原地等待。
- 3
检查现有锁状态
已经锁定时跳过重复写入;未锁定时才写边界、控制值和锁定位。
- 4
同步并确认
执行相应屏障和 TLB 操作,再等待锁定状态。集群主核的启动顺序与锁定职责也属于这条协议。
这描述的是特定 CPU 进入路径,而不是“每次系统重启后的第一条操作”。所核对源码区分引导集群与其他集群,并依赖启动序列化。保留这些条件,才能解释为何某个 CPU 跳过配置仍然可能是正确行为。
离线核对边界与分支
下列模型只检查低 39 位地址的索引重组、端点算术、控制掩码和简化分支,不访问系统寄存器。模型中的数值是明确的测试输入,不是设备地址。
PAGE = 1 << 14
MASK64 = (1 << 64) - 1
def split_address(addr):
return ((addr >> 36) & 7, (addr >> 25) & 0x7FF,
(addr >> 14) & 0x7FF, addr & 0x3FFF)
cases = [0, PAGE - 1, PAGE, (1 << 25) - 1, 1 << 25,
(1 << 36) - 1, 1 << 36, (1 << 39) - 1]
for addr in cases:
l1, l2, l3, offset = split_address(addr)
assert (l1 << 36) | (l2 << 25) | (l3 << 14) | offset == addr
assert (addr & ~0x3FFF) + offset == addr
assert (4 << 12) == PAGE
assert MASK64 & ~0x3FFF == 0xFFFFFFFFFFFFC000
assert ((1 << 1) | (1 << 4)) == 0x12
assert MASK64 & ~(1 << 13) == 0xFFFFFFFFFFFFDFFF
assert (8 >> 2) & 3 == 2
for base in (0, PAGE, 0x100000):
for length in (1, PAGE, PAGE * 2):
end_inclusive = base + length - 1
assert end_inclusive - base + 1 == length
def reset_action(done, begin, end, locked, diagnostic):
if not done: return "spin"
if not begin or not end: return "skip" if diagnostic else "spin"
return "skip" if locked else "configure"
assert reset_action(False, 1, 2, False, True) == "spin"
assert reset_action(True, 0, 2, False, True) == "skip"
assert reset_action(True, 0, 2, False, False) == "spin"
assert reset_action(True, 1, 2, True, False) == "skip"
assert reset_action(True, 1, 2, False, False) == "configure"
print("PASS: 8 address cases; 9 endpoint cases; 5 reset branches; masks and EL encoding")结果为 8 组地址、9 组端点、5 个 reset 分支及掩码检查通过。它没有验证硬件锁定、缓存一致性或真实设备启动;这些需要相应映像、构建配置、寄存器观测和页表数据。
最终应沿“保留哪些映射 → 哪些数据影响范围 → 使用哪一种端点 → 按何种时序锁定 → 其他 CPU 如何确认”的链条审阅证据。KTRR 与 CTRR 的差异必须在链条中显式保留,而不是只在标题里并列两个缩写。
官方参考
以下均为 Apple 发布的固定版本文件,分别覆盖页表、范围、寄存器定义、调试位与 reset 路径。