paging_init 备份与恢复机制详解
2026-06-29 · 阅读 0
概述
paging_init 的备份与恢复是 KernelPatch 启动流程中的核心机制之一。它通过劫持 paging_init 函数,在页表初始化阶段介入,完成内存映射和代码重定位等关键操作。
整体流程
T1: setup() 阶段
└─ map_prepare()
├─ 备份 paging_init[0] → map_data.paging_init_backup
├─ 写入 B _paging_init → paging_init[0]
└─ 复制 _map_start 代码 → PA + map_offset
T2: 内核启动,到达 paging_init
└─ paging_init() → B _paging_init → 跳转到 _paging_init
T3: _paging_init() 执行
├─ 恢复 paging_init[0] = map_data.paging_init_backup
├─ 调用原始 paging_init()
└─ 完成内存映射和代码重定位
阶段 1:备份阶段(map_prepare)
代码位置
// backup and hook paging_init
// uint64_t paging_init_pa = paging_init_offset + kernel_pa;
add x13, x15, x19
// map_data.paging_init_backup = *(uint32_t *)(paging_init_pa);
ldr w12, [x13]
mov w3, #0x201F
movk w3, #0xD503, lsl#16
orr w1, w3, #0x100
mov w2, #0xFFFFFD1F
and w0, w12, w2
// if ((map_data.paging_init_backup & 0xFFFFFD1F) == 0xD503211F)
cmp w0, w1
b.ne .backup
// map_data.paging_init_backup = NOP
mov w12, w3
// uint32_t *p = (uint32_t *)paging_init_pa + 1;
add x11, x13, #4
.cmp_auti:
// while ((*p & 0xFFFFFD1F) != 0xD503211F) ++p;
ldr w0, [x11], #4
and w0, w0, w2
cmp w0, w1
b.ne .cmp_auti
// *p = NOP
stur w3, [x11, #-4]
.backup:
str w12, [x9, #map_paging_init_backup_offset]
dsb ish
// uint64_t replace_offset = (uint64_t)(_paging_init - _map_start) + map_offset;
adrp x11, _paging_init
add x11, x11, :lo12:_paging_init
adrp x12, _map_start
add x12, x12, :lo12:_map_start
sub x11, x11, x12
add x11, x11, x14
// *(uint32_t *)paging_init_pa = B_REL(paging_init_offset, replace_offset);
// #define B_REL(src, dst) (0x14000000u | (((dst - src) & 0x0FFFFFFFu) >> 2u))
sub x15, x11, x15
ubfx w15, w15, #2, #26
mov w12, #0x14000000
orr w15, w15, w12
str w15, [x13]
详细步骤解析
步骤 1:计算 paging_init 物理地址
// x15 = paging_init_offset(来自 setup_preset)
// x19 = kernel_pa(内核物理地址)
add x13, x15, x19 // x13 = paging_init 的物理地址
说明:
x15保存了paging_init在内核镜像中的偏移x19是内核的物理基地址x13现在指向paging_init函数第一条指令的物理地址
步骤 2:读取第一条指令
ldr w12, [x13] // w12 = paging_init[0](第一条指令)
可能的值:
0xD503245F:BTI C(ARMv8.5+)0xD503233F:PACIASP(ARMv8.3+)0xF85F8A5E:str x30, [x18], #8(SCS,运行时替换后的)0xA9BF7BFD:STP X29, X30, [SP, #-16]!(普通函数序言)- 其他指令...
步骤 3:BTI/PAC 检测(当前实现有 BUG)
mov w3, #0x201F
movk w3, #0xD503, lsl#16 // w3 = 0xD503201F (NOP)
orr w1, w3, #0x100 // w1 = 0xD503211F
mov w2, #0xFFFFFD1F // w2 = 掩码
and w0, w12, w2 // w0 = insn & 0xFFFFFD1F
cmp w0, w1 // 比较是否等于 0xD503211F
b.ne .backup // 如果不匹配,跳转到备份
问题分析:
掩码 0xFFFFFD1F 的问题:
BTI C (0xD503245F) & 0xFFFFFD1F = 0xD503245F ≠ 0xD503211F PACIASP (0xD503233F) & 0xFFFFFD1F = 0xD503231F ≠ 0xD503211F AUTIASP (0xD503231F) & 0xFFFFFD1F = 0xD503211F ✅ 匹配!实际检测的是什么:
- 这段代码实际上在检测 AUTIASP(PAC 验证指令),而不是 BTI C
- 如果第一条指令是 AUTIASP,会进入特殊处理
错误的备份逻辑:
// 如果匹配(实际上是 AUTIASP) mov w12, w3 // ❌ 备份改为 NOP,丢失原始指令
步骤 4:查找并 NOP 化 AUTIASP(错误)
add x11, x13, #4 // 从第二条指令开始
.cmp_auti:
ldr w0, [x11], #4 // 读取指令
and w0, w0, w2 // 应用掩码
cmp w0, w1 // 查找 0xD503211F (AUTIASP)
b.ne .cmp_auti // 继续查找
stur w3, [x11, #-4] // ❌ 找到后 NOP 化
问题:
- 这段代码会查找函数中的
AUTIASP指令并 NOP 化 - 这会导致 PAC 验证失败,因为签名后的返回地址无法被验证
步骤 5:备份指令
.backup:
str w12, [x9, #map_paging_init_backup_offset] // 保存到 map_data
dsb ish // 数据同步屏障
存储位置:
map_data.paging_init_backup(map_data_t结构体的第一个字段)- 偏移:
0(map_paging_init_backup_offset)
问题:
- 如果检测到 AUTIASP,
w12已经被改为 NOP - 备份的是 NOP,而不是原始指令
- 恢复时会写入 NOP,导致函数缺少必要的保护指令
步骤 6:生成跳转指令
// 计算 _paging_init 的位置
adrp x11, _paging_init
add x11, x11, :lo12:_paging_init
adrp x12, _map_start
add x12, x12, :lo12:_map_start
sub x11, x11, x12 // _paging_init - _map_start
add x11, x11, x14 // + map_offset = _paging_init 的物理地址
// 计算相对偏移
sub x15, x11, x15 // offset = _paging_init_pa - paging_init_pa
// 生成 B 指令
ubfx w15, w15, #2, #26 // 提取 26 位偏移(右移 2 位)
mov w12, #0x14000000 // B 指令的操作码
orr w15, w15, w12 // 组合:0x14000000 | (offset >> 2)
// 写入 paging_init[0]
str w15, [x13] // 覆盖原始指令
B 指令格式:
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
0 1 0 1 0 1 0 0 0 0 0 0
│ 26-bit signed offset (PC-relative) │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
示例:
paging_init_pa = 0x81234567
_paging_init_pa = 0x81ABCDEF
offset = 0x81ABCDEF - 0x81234567 = 0x098888
B 指令 = 0x14000000 | (0x098888 >> 2) = 0x14026222
备份阶段的内存状态
物理内存布局:
[PA + paging_init_offset]:
原始:BTI C / PACIASP / 其他指令
修改后:B _paging_init (0x14xxxxxx)
[PA + map_offset] (tcp_init_sock 位置):
原始:tcp_init_sock 代码
修改后:_paging_init 代码(被覆盖)
[PA + start_offset]:
map_data 结构体
├─ paging_init_backup: 0xD503245F (BTI C) 或 0xD503201F (NOP,错误)
├─ map_offset: ...
├─ paging_init_relo: paging_init_offset (虚拟地址偏移)
└─ ...
阶段 2:恢复阶段(_paging_init)
代码位置
// paging_init
uint64_t paging_init_va = data->paging_init_relo;
*(uint32_t *)(paging_init_va) = data->paging_init_backup;
flush_icache_all();
((paging_init_f)(paging_init_va))();
详细步骤解析
步骤 1:获取虚拟地址
uint64_t paging_init_va = data->paging_init_relo;
说明:
data->paging_init_relo在mem_proc()中已经被重定位:data->paging_init_relo += kernel_va; // 从偏移转换为虚拟地址- 现在
paging_init_va是paging_init的虚拟地址
步骤 2:恢复原始指令
*(uint32_t *)(paging_init_va) = data->paging_init_backup;
操作:
- 将备份的指令写回
paging_init[0] - 覆盖之前写入的
B _paging_init指令
可能的值:
- ✅
0xD503245F:BTI C(如果正确备份) - ✅
0xD503233F:PACIASP(如果正确备份) - ✅
0xF85F8A5E:str x30, [x18], #8(SCS,如果正确备份) - ❌
0xD503201F:NOP(如果备份阶段出错)
步骤 3:刷新指令缓存
flush_icache_all();
说明:
- ARM64 有独立的指令缓存(I-cache)和数据缓存(D-cache)
- 修改代码后必须刷新 I-cache,否则 CPU 可能执行旧指令
flush_icache_all()实现:asm volatile("dsb ish" : : : "memory"); asm volatile("ic ialluis"); // Invalidate All Instruction Cache asm volatile("dsb ish" : : : "memory"); asm volatile("isb" : : : "memory");
步骤 4:调用原始 paging_init
((paging_init_f)(paging_init_va))();
说明:
paging_init_f是函数指针类型:typedef void (*paging_init_f)(void);- 直接调用
paging_init函数 - 此时
paging_init[0]已经恢复,函数可以正常执行
恢复阶段的时序
T1: 进入 _paging_init
paging_init[0] = B _paging_init (0x14xxxxxx) ❌
T2: 恢复指令
paging_init[0] = data->paging_init_backup ✅
flush_icache_all()
T3: 调用原始 paging_init
paging_init() 正常执行
├─ 初始化页表
├─ 设置内存映射
└─ 返回
T4: paging_init 返回
继续 _paging_init 的后续操作
├─ 创建 kpimg 的页表映射
├─ 复制代码到新位置
└─ 调用 start()
数据结构
map_data_t 结构体
// kernel/base/setup.h:25-50
typedef struct
{
uint32_t paging_init_backup; // paging_init[0] 的备份(4 字节)
uint32_t __; // 对齐填充
int64_t map_offset; // map 区域的偏移
int64_t start_offset; // start 区域的偏移
int64_t start_size; // start 区域的大小
int64_t start_img_size; // start 镜像的大小
int64_t extra_size; // extra 区域的大小
int64_t alloc_size; // 分配的大小
uint64_t kernel_pa; // 内核物理地址
uint64_t paging_init_relo; // paging_init 虚拟地址(重定位后)
map_symbol_t map_symbol; // 内存管理函数地址
// ... 其他字段
} map_data_t;
关键字段:
paging_init_backup:存储paging_init[0]的备份paging_init_relo:paging_init的虚拟地址(在mem_proc()中重定位)
问题与修复
问题 1:BTI 指令备份错误
当前实现:
// 如果检测到 BTI C(实际检测的是 AUTIASP)
mov w12, w3 // ❌ 备份改为 NOP (0xD503201F)
正确实现:
// 直接备份,不做任何修改
ldr w12, [x13] // 读取原始指令
str w12, [x9, #map_paging_init_backup_offset] // 直接备份
问题 2:AUTIASP 被 NOP 化
当前实现:
.cmp_auti:
ldr w0, [x11], #4
and w0, w0, w2
cmp w0, w1
b.ne .cmp_auti
stur w3, [x11, #-4] // ❌ NOP 化 AUTIASP
问题:
- 如果函数使用 PAC,
AUTIASP必须存在 - NOP 化后,返回地址无法验证,会导致 PAC 失败
正确实现:
- 删除
.cmp_auti循环 - 不要修改函数内部的 PAC 指令
问题 3:SCS 指令未正确处理
问题:
- 如果系统使用 SCS(Shadow Call Stack),
paciasp会被替换为str x30, [x18], #8 - 当前代码只检测 PAC 指令,不检测 SCS 指令
修复建议:
// 检测 SCS 指令
mov w1, #0x8A5E
movk w1, #0xF85F, lsl#16 // w1 = 0xF85F8A5E (str x30, [x18], #8)
cmp w12, w1
b.eq .is_scs
// 检测 PAC 指令
mov w1, #0x233F
movk w1, #0xD503, lsl#16 // w1 = 0xD503233F (PACIASP)
cmp w12, w1
b.eq .is_pac
// 其他指令,直接备份
.backup:
str w12, [x9, #map_paging_init_backup_offset]
// ...
修复后的代码
简化版本(推荐)
// kernel/base/setup1.S:221-249(修复版)
map_prepare:
// ... 前面的代码 ...
// ============ 备份和劫持 paging_init ============
// 计算 paging_init 物理地址
add x13, x15, x19 // x13 = paging_init_offset + kernel_pa
// 读取第一条指令
ldr w12, [x13] // w12 = paging_init[0]
// ✅ 直接备份,不做任何修改
str w12, [x9, #map_paging_init_backup_offset]
dsb ish
// 计算 _paging_init 的位置
adrp x11, _paging_init
add x11, x11, :lo12:_paging_init
adrp x12, _map_start
add x12, x12, :lo12:_map_start
sub x11, x11, x12
add x11, x11, x14 // x11 = _paging_init 的物理地址
// 生成 B 指令
sub x15, x11, x15 // offset = _paging_init_pa - paging_init_pa
ubfx w15, w15, #2, #26 // 提取 26 位偏移
mov w12, #0x14000000 // B 指令操作码
orr w15, w15, w12 // 组合指令
str w15, [x13] // 写入 B _paging_init
// ... 后面的代码 ...
修改要点:
- ✅ 删除所有 BTI/PAC 检测代码
- ✅ 删除
.cmp_auti循环 - ✅ 直接备份原始指令
- ✅ 不修改函数内部的任何指令
验证机制
验证备份是否正确
// 在 _paging_init 中添加验证
void verify_paging_init_backup()
{
map_data_t *data = get_data();
uint64_t paging_init_va = data->paging_init_relo;
uint32_t actual = *(uint32_t *)paging_init_va;
uint32_t backup = data->paging_init_backup;
log_boot("KP: paging_init[0] before restore: %08x\n", actual);
log_boot("KP: paging_init_backup: %08x\n", backup);
// 恢复
*(uint32_t *)paging_init_va = backup;
flush_icache_all();
uint32_t after = *(uint32_t *)paging_init_va;
log_boot("KP: paging_init[0] after restore: %08x\n", after);
// 验证
if (after != backup) {
log_boot("❌ Restore failed: %08x != %08x\n", after, backup);
} else {
log_boot("✅ Restore verified\n");
}
}
总结
关键点
- 备份时机:
map_prepare()阶段,在内核启动之前 - 备份内容:
paging_init[0](第一条指令,4 字节) - 备份位置:
map_data.paging_init_backup - 恢复时机:
_paging_init()中,在调用原始paging_init之前 - 恢复操作:将备份写回
paging_init[0],刷新 I-cache,调用函数
注意事项
- ✅ 直接备份原始指令,不要修改
- ✅ 不要 NOP 化 PAC/SCS 指令
- ✅ 恢复后必须刷新 I-cache
- ✅ 在调用原始函数前恢复
- ✅ 考虑 SCS 指令的支持
修复建议
- 删除所有 BTI/PAC 检测代码
- 直接备份原始指令
- 不修改函数内部的任何指令
- 添加验证机制,确保备份和恢复正确
参考资料
文档版本:2.0
最后更新:2026-06-26
相关 KernelPatch 版本:0.13.2+
评论
- 还没有评论,来说点什么吧。