溺的文档
KernelPatch · 第 9 篇 / 共 22 篇

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                // 如果不匹配,跳转到备份

问题分析

  1. 掩码 0xFFFFFD1F 的问题

    BTI C (0xD503245F) & 0xFFFFFD1F = 0xD503245F ≠ 0xD503211F
    PACIASP (0xD503233F) & 0xFFFFFD1F = 0xD503231F ≠ 0xD503211F
    AUTIASP (0xD503231F) & 0xFFFFFD1F = 0xD503211F ✅ 匹配!
  2. 实际检测的是什么

    • 这段代码实际上在检测 AUTIASP(PAC 验证指令),而不是 BTI C
    • 如果第一条指令是 AUTIASP,会进入特殊处理
  3. 错误的备份逻辑

    // 如果匹配(实际上是 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_backupmap_data_t 结构体的第一个字段)
  • 偏移:0map_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_relomem_proc() 中已经被重定位:
    data->paging_init_relo += kernel_va;  // 从偏移转换为虚拟地址
  • 现在 paging_init_vapaging_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_relopaging_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
    
    // ... 后面的代码 ...

修改要点

  1. ✅ 删除所有 BTI/PAC 检测代码
  2. ✅ 删除 .cmp_auti 循环
  3. ✅ 直接备份原始指令
  4. ✅ 不修改函数内部的任何指令

验证机制

验证备份是否正确

// 在 _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");
    }
}

总结

关键点

  1. 备份时机map_prepare() 阶段,在内核启动之前
  2. 备份内容paging_init[0](第一条指令,4 字节)
  3. 备份位置map_data.paging_init_backup
  4. 恢复时机_paging_init() 中,在调用原始 paging_init 之前
  5. 恢复操作:将备份写回 paging_init[0],刷新 I-cache,调用函数

注意事项

  1. 直接备份原始指令,不要修改
  2. 不要 NOP 化 PAC/SCS 指令
  3. 恢复后必须刷新 I-cache
  4. 在调用原始函数前恢复
  5. 考虑 SCS 指令的支持

修复建议

  1. 删除所有 BTI/PAC 检测代码
  2. 直接备份原始指令
  3. 不修改函数内部的任何指令
  4. 添加验证机制,确保备份和恢复正确

参考资料


文档版本:2.0
最后更新:2026-06-26
相关 KernelPatch 版本:0.13.2+

评论

  • 还没有评论,来说点什么吧。

无需注册或登录,填个昵称即可评论。