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

备份与恢复机制详解

2026-06-29 · 阅读 0

概述

KernelPatch 的备份与恢复机制是确保内核完整性的关键。它涉及三处关键备份:

  1. 内核头部备份(header_backup,8 字节)
  2. Map area 备份(map_backup,~2KB)
  3. paging_init 备份(paging_init_backup,4 字节)

本文详细追踪这三处备份的完整数据流。

v2.0 更新摘要

  • hook_install/hook_uninstall 改用 hotpatchstop_machine)机制,确保多核安全
  • modify_entry_kernel 已移至 hotpatch.c
  • paging_init 中的 PAC 指令(PACIASP/PACIBSP)现在通过 NOP 化处理,而非错误地修改备份值

备份 1:内核头部(Header Backup)

备份时机:kptools Patch 阶段

// tools/patch.c:530-531
// 在 kptools 修补镜像时
memcpy(setup->header_backup, kallsym_kimg, sizeof(setup->header_backup));

备份内容

源地址:kallsym_kimg + 0x0(内核镜像的前 8 字节)
目标地址:setup_preset.header_backup
大小:8 字节(HDR_BACKUP_SIZE)

内容(UEFI 内核):
  0x00: 'M', 'Z', 0x00, 0x00
  0x04: 0x00, 0x02, 0x00, 0x14  (B #0x80000 指令)

内容(非 UEFI 内核):
  0x00: 0x00, 0x02, 0x00, 0x14  (B #0x80000 指令)
  0x04: 0x00, 0x00, 0x00, 0x00  (reserved)

修改内核头部

// tools/patch.c:556-557
int text_offset = align_kimg_len + SZ_4K;  // setup_entry 位置

// 生成跳转到 setup_entry 的 B 指令
b((uint32_t *)(out_kernel_file.kimg + kinfo->b_stext_insn_offset), 
  kinfo->b_stext_insn_offset, text_offset);

修改后

UEFI 内核(偏移 0x4):
  0x04: 0xC7, 0xAC, 0x1A, 0x14  (B #0x1ab4000,跳到 setup_entry)

非 UEFI 内核(偏移 0x0):
  0x00: 0x00, 0xC8, 0x1A, 0x14  (B #0x1ab4000)

恢复时机:setup() 返回前

// kernel/base/setup1.S:321-325
setup:
    // ... 执行 start_prepare 和 map_prepare ...
    
    // 恢复内核头部
    mov x0, x19                             // kernel_pa
    add x1, x20, #setup_header_backup_offset
    mov x2, #HDR_BACKUP_SIZE                // 8 字节
    bl memcpy8
    
    // 刷新缓存
    dsb ish
    ic iallu
    dsb ish
    isb
    
    // 跳转回内核原始入口
    br x16  // x16 = kernel_pa

恢复后

物理内存 PA + 0x0:
  恢复为原始的 B _stext 指令 ✅
  
内核看到的入口点:
  正常的 B 跳转,完全感知不到被劫持过

备份 2:Map Area(tcp_init_sock)

备份时机:start_prepare() 阶段

// kernel/base/setup1.S:118-130
start_prepare:
    // 计算 map 代码大小
    adrp x13, _map_end
    add x13, x13, :lo12:_map_end
    adrp x14, _map_start
    add x14, x14, :lo12:_map_start
    sub x2, x13, x14              // x2 = _map_end - _map_start (~2KB)
    
    // 保存大小
    str x2, [x11, #start_map_backup_len_offset]
    
    // 备份原始代码
    add x0, x11, #start_map_backup_offset    // 目标:start_preset.map_backup
    add x1, x19, x20                          // 源:kernel_pa + map_offset
    bl memcpy8                                // 备份!

备份数据

源地址:kernel_pa + map_offset
       = 0x80000000 + 0xd73550
       = 0x80d73550 (tcp_init_sock 函数的物理地址)

目标地址:start_preset.map_backup
         (在 kpimg 的数据段,链接地址 + 偏移)

大小:_map_end - _map_start
    = 约 2560 字节 (0xa00)

内容:tcp_init_sock 函数的完整机器码

覆盖时机:map_prepare() 阶段

// kernel/base/setup1.S:268-276
map_prepare:
    // 复制 _map_start 到 tcp_init_sock
    adrp x2, _map_end
    add x2, x2, :lo12:_map_end
    adrp x1, _map_start
    add x1, x1, :lo12:_map_start
    sub x2, x2, x1                    // x2 = map 代码大小
    add x0, x19, x14                  // x0 = kernel_pa + map_offset
    bl memcpy8                        // 覆盖!

覆盖后

物理内存 PA + map_offset:
  原内容:tcp_init_sock 机器码 ❌ 被覆盖
  新内容:_paging_init 机器码 ✅
  
功能:
  当内核调用 paging_init 时,会跳转到这里
  执行 _paging_init 代码

复制时机:_paging_init() 阶段

// kernel/base/map.c:235-243
void _paging_init()
{
    // ...
    
    // 复制 kpimg 到新位置(包括 start_preset)
    uint64_t old_start_va = phys_to_lm(data, old_start_pa);
    uint64_t start_va = start_pa + data->kimage_voffset;
    
    for (uint64_t i = 0; i < data->start_img_size; i += 8) {
        *(uint64_t *)(start_va + i) = *(uint64_t *)(old_start_va + i);
    }
    
    // 此时 start_preset.map_backup 也被复制到新位置 ✅
    
    // ...
}

恢复时机:start() 阶段

// kernel/base/start.c:317-335
static void restore_map()
{
    uint64_t start = kernel_va + start_preset.map_offset;
    uint64_t end = start + start_preset.map_backup_len;
    log_boot("Restore: %llx, %llx\n", start, end);
    
    for (uint64_t i = start; i < align_ceil(end, page_size); i += page_size) {
        uint64_t *pte = pgtable_entry_kernel(i);
        uint64_t orig = *pte;
        
        // 1. 临时设置为可写
        *pte = (orig | PTE_DBM) & ~PTE_RDONLY;
        flush_tlb_kernel_page(i);
        
        // 2. 从 map_backup 恢复数据
        for (uint64_t j = i; j >= start && j < end && j < i + page_size; j += 8) {
            *(uint64_t *)j = *(uint64_t *)(start_preset.map_backup + (j - start));
        }
        
        // 3. 恢复原始权限
        *pte = orig;
        flush_tlb_kernel_page(i);
    }
    
    // 4. 刷新指令缓存
    flush_icache_all();
}

恢复后

虚拟内存 kernel_va + map_offset (tcp_init_sock):
  恢复为原始的 tcp_init_sock 机器码 ✅
  
功能:
  网络栈初始化时可以正常调用 tcp_init_sock
  完全恢复,无任何痕迹

备份 3:paging_init 第一条指令

备份时机:map_prepare() 阶段

// kernel/base/setup1.S:221-249
map_prepare:
    // 计算 paging_init 物理地址
    add x13, x15, x19             // x13 = paging_init_offset + kernel_pa
    
    // 读取第一条指令
    ldr w12, [x13]                // w12 = paging_init[0]
    
    // ============ BTI 检测(当前有 BUG!)============
    
    mov w3, #0x201F
    movk w3, #0xD503, lsl#16      // w3 = 0xD503201F (NOP)
    orr w1, w3, #0x100            // w1 = 0xD503211F (BTI C 的掩码结果)
    mov w2, #0xFFFFFD1F           // w2 = 掩码
    and w0, w12, w2
    cmp w0, w1
    b.ne .backup
    
    // 如果是 BTI:
    mov w12, w3                   // ❌ 错误:改为备份 NOP
    
    // 查找并 NOP 化 AUTIASP
    add x11, x13, #4
.cmp_auti:
    ldr w0, [x11], #4
    and w0, w0, w2
    cmp w0, w1
    b.ne .cmp_auti
    stur w3, [x11, #-4]           // ❌ 错误:NOP 化 AUTIASP
    
.backup:
    // 保存(可能已被修改)
    str w12, [x9, #map_paging_init_backup_offset]
    dsb ish

备份的内容(当前实现)

如果 paging_init[0] == BTI C (0xD503211F):
  备份值 = NOP (0xD503201F) ❌ 错误!

如果 paging_init[0] 是其他指令:
  备份值 = 原始指令 ✅ 正确

覆盖时机:map_prepare() 阶段(续)

// kernel/base/setup1.S:252-266
map_prepare:
    // ... 备份完成后 ...
    
    // 计算 _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 = 替换后的位置
    
    // 生成 B 指令
    sub x15, x11, x15             // dst - src
    ubfx w15, w15, #2, #26        // 提取 imm26
    mov w12, #0x14000000          // B 操作码
    orr w15, w15, w12
    str w15, [x13]                // ❌ 写入 paging_init[0]
    
    dsb ish

覆盖后

物理内存 PA + paging_init_offset:
  原内容:BTI C (0xD503211F) 或 PACIASP 等 ❌ 被覆盖
  新内容:B (PA + map_offset) ✅
  
功能:
  当内核调用 paging_init 时,跳转到 _paging_init

恢复时机:_paging_init() 中

// kernel/base/map.c:204-208
void _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
    ((paging_init_f)(paging_init_va))();
    
    // ⚠️ 注意:调用后 data 可能失效(tcp_init_sock 区域)
    
    // ... 后续操作 ...
}

恢复后(当前实现)

虚拟内存 paging_init_va:
  恢复值 = NOP (如果原本是 BTI C) ❌ 错误
  
正确应该是:
  恢复值 = BTI C (0xD503211F) ✅

完整的备份与恢复时间线

时间轴:T0 ──────────────────────────────────────> T10

┌─────────────────────────────────────────────────────────┐
│ T0: kptools 执行(用户空间)                             │
└─────────────────────────────────────────────────────────┘
  操作:
    ✅ 备份内核头部 → setup_preset.header_backup
    ✅ 修改内核头部 → B setup_entry
  
  镜像状态:
    [0x0]: B setup_entry ❌ (原本是 B _stext)
    [setup_offset]: setup_preset
      └─ header_backup = [原始的 B _stext] ✅
    [map_offset]: tcp_init_sock (未动) ✅
    [paging_init_offset]: paging_init (未动) ✅

┌─────────────────────────────────────────────────────────┐
│ T1: Bootloader 加载镜像到物理内存                        │
└─────────────────────────────────────────────────────────┘
  物理内存:
    PA + 0x0: B setup_entry
    PA + setup_offset: setup_preset (包含 header_backup)
    PA + map_offset: tcp_init_sock ✅
    PA + paging_init_offset: paging_init ✅

┌─────────────────────────────────────────────────────────┐
│ T2: setup_entry 执行                                     │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ T3: start_prepare() - 备份 map area                      │
└─────────────────────────────────────────────────────────┘
  操作:
    ✅ 读取 PA + map_offset (tcp_init_sock)
    ✅ 写入 start_preset.map_backup
    ✅ 复制 kpimg 到 PA + start_offset
  
  物理内存:
    PA + map_offset: tcp_init_sock ✅ (仍未动)
    PA + start_offset: kpimg
      └─ start_preset.map_backup = [tcp_init_sock 机器码] ✅

┌─────────────────────────────────────────────────────────┐
│ T4: map_prepare() - 备份和劫持 paging_init               │
└─────────────────────────────────────────────────────────┘
  操作:
    ❌ 读取 PA + paging_init_offset[0]
    ❌ 如果是 BTI C,改为 NOP(BUG!)
    ❌ NOP 化后面的 AUTIASP(BUG!)
    ✅ 保存到 map_data.paging_init_backup
    ✅ 写入 B _paging_init 到 paging_init[0]
    ✅ 复制 _map_start 到 PA + map_offset
  
  物理内存:
    PA + map_offset: _paging_init 代码 ❌ (覆盖了 tcp_init_sock)
    PA + paging_init_offset: B (PA+map_offset) ❌ (劫持了)
    PA + start_offset: kpimg
      └─ map_data.paging_init_backup = [NOP 或原始指令]

┌─────────────────────────────────────────────────────────┐
│ T5: setup() 恢复头部并跳转                               │
└─────────────────────────────────────────────────────────┘
  操作:
    ✅ 从 header_backup 恢复内核头部
    ✅ 跳转到 PA (_head)
  
  物理内存:
    PA + 0x0: B _stext ✅ (已恢复)
    PA + map_offset: _paging_init ❌ (仍被覆盖)
    PA + paging_init_offset: B (PA+map_offset) ❌ (仍被劫持)

┌─────────────────────────────────────────────────────────┐
│ T6: 内核启动,到达 paging_init                           │
└─────────────────────────────────────────────────────────┘
  执行:
    paging_init() → B _paging_init → 跳转成功 ✅

┌─────────────────────────────────────────────────────────┐
│ T7: _paging_init() 执行                                  │
└─────────────────────────────────────────────────────────┘
  操作:
    ✅ 分配新物理内存
    ✅ 映射 kpimg 到虚拟地址
    ✅ 复制 kpimg 到新位置(包括 map_backup)
    ✅ 恢复 paging_init[0]
    ✅ 调用原始 paging_init()
  
  虚拟内存:
    paging_init_va: 恢复为原始指令 ✅ (但可能是 NOP,BUG)
    map_offset: _paging_init ❌ (仍被覆盖)

┌─────────────────────────────────────────────────────────┐
│ T8: 原始 paging_init 执行并返回                          │
└─────────────────────────────────────────────────────────┘
  ⚠️ 潜在问题:
    如果 paging_init[0] 被恢复为 NOP(而非 BTI C)
    且函数末尾的 AUTIASP 也被 NOP 化
    返回时 PAC 验证失败 → 崩溃!

┌─────────────────────────────────────────────────────────┐
│ T9: start() 执行                                         │
└─────────────────────────────────────────────────────────┘
  操作:
    ✅ start_init()
    ✅ prot_myself()
    ✅ restore_map() ← 恢复 tcp_init_sock
    ✅ patch()
  
  虚拟内存:
    map_offset (tcp_init_sock): 完全恢复 ✅✅
    paging_init: 已恢复 ✅

┌─────────────────────────────────────────────────────────┐
│ T10: 内核继续启动                                        │
└─────────────────────────────────────────────────────────┘
  状态:
    所有备份的内容都已恢复 ✅
    内核完全正常运行 ✅

数据流图

┌────────────────────────────────────────────────────┐
│ 内核头部的数据流                                    │
└────────────────────────────────────────────────────┘

[T0] kptools
  kernel_img[0:8]
    ↓ 读取
  setup_preset.header_backup
    ↓ 写入镜像
  Image.patched
  
[T1] Bootloader
  加载到 PA
  
[T5] setup()
  PA + setup_offset → setup_preset.header_backup
    ↓ memcpy
  PA + 0x0 ✅ 恢复

┌────────────────────────────────────────────────────┐
│ tcp_init_sock 的数据流                              │
└────────────────────────────────────────────────────┘

[T3] start_prepare()
  PA + map_offset (tcp_init_sock 原始码)
    ↓ memcpy8
  PA + start_offset → start_preset.map_backup
  
[T4] map_prepare()
  _map_start (_paging_init 代码)
    ↓ memcpy8
  PA + map_offset ❌ 覆盖
  
[T7] _paging_init()
  PA + start_offset → start_preset.map_backup
    ↓ 复制到新位置
  新 VA + start_preset.map_backup
  
[T9] restore_map()
  新 VA + start_preset.map_backup
    ↓ 逐页复制
  kernel_va + map_offset ✅ 恢复

┌────────────────────────────────────────────────────┐
│ paging_init 指令的数据流                            │
└────────────────────────────────────────────────────┘

[T4] map_prepare()
  PA + paging_init_offset[0] (原始指令)
    ↓ ldr w12
  ❌ 检测 BTI,可能修改为 NOP
    ↓ str w12
  PA + start_offset → map_data.paging_init_backup
  
  B _paging_init
    ↓ str w15
  PA + paging_init_offset[0] ❌ 覆盖
  
[T7] _paging_init()
  PA + start_offset → map_data.paging_init_backup
    ↓ 读取
  *(paging_init_va) ✅ 写入
  
  调用 paging_init() ✅

备份存储位置

start_preset_t 结构

// kernel/base/start.h:12-28
typedef struct {
    setup_header_t header;                // KernelPatch 头部
    version_t kernel_version;             // 内核版本
    int64_t kallsyms_lookup_name_offset;
    int64_t kernel_size;
    int64_t start_offset;
    int64_t extra_size;
    uint64_t kernel_pa;
    int64_t map_offset;
    
    // ============ 备份区域 ============
    
    int64_t map_backup_len;               // map 备份大小
    uint8_t map_backup[MAP_MAX_SIZE];     // map 备份内容(2KB)
    uint8_t superkey[SUPER_KEY_LEN];
    uint8_t root_superkey[ROOT_SUPER_KEY_HASH_LEN];
    patch_config_t patch_config;
} start_preset_t;

内存布局

start_preset 实例(在 .kp.data 段):
  +0x0000: header (64 B)
  +0x0040: kernel_version (8 B)
  +0x0048: kallsyms_lookup_name_offset (8 B)
  ...
  +0x0080: map_offset (8 B)
  +0x0088: map_backup_len (8 B)
  +0x0090: map_backup[2560] ← tcp_init_sock 备份
  +0x0A90: superkey[64]
  ...

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;
    int64_t start_offset;
    int64_t start_size;
    int64_t start_img_size;
    int64_t extra_size;
    int64_t alloc_size;
    uint64_t kernel_pa;
    uint64_t paging_init_relo;
    map_symbol_t map_symbol;
    
    // ============ 运行时数据 ============
    
    int64_t va1_bits;
    int64_t page_shift;
    uint64_t kimage_voffset;
    uint64_t linear_voffset;
} map_data_t;

内存布局

map_data 实例(在 _map_start 之前):
  +0x00: paging_init_backup (4 B) ← paging_init[0] 备份
  +0x04: __ (4 B)
  +0x08: map_offset (8 B)
  +0x10: start_offset (8 B)
  ...

为什么需要三处备份?

备份 1:内核头部

原因

  • 内核入口被改为跳转到 setup_entry
  • 内核需要看到原始的入口点
  • 否则内核会检测到异常

恢复时机

  • setup() 返回前
  • 在跳转回 _head 之前

备份 2:tcp_init_sock

原因

  • _paging_init 代码必须在内核虚拟地址空间
  • 借用 tcp_init_sock 区域临时存放
  • 完成后必须恢复,否则网络功能异常

恢复时机

  • start() 阶段
  • 在任何网络初始化之前

备份 3:paging_init[0]

原因

  • paging_init 被劫持,第一条指令改为 B
  • 必须备份才能恢复原始功能
  • 内核需要调用真正的 paging_init

恢复时机

  • _paging_init() 中
  • 在调用原始 paging_init 之前

hotpatch 机制

v2.0 新增:hook_install 和 hook_uninstall 现在使用 hotpatch(stop_machine)来安全地修改内核代码。

为什么需要 hotpatch?

在多核系统上,直接修改正在被其他 CPU 执行的代码是危险的。旧的实现直接写入跳转指令并刷新缓存,但这在以下场景中可能导致崩溃:

旧方式的竞态条件:

CPU 0 (修改方)              CPU 1 (执行方)
──────────────              ──────────────
                            正在执行 target_func 的前几条指令
写入 B trampoline            │
                            读取到半写入的指令 -> 未定义行为 -> 崩溃
flush icache

hotpatch 工作流程

// kernel/base/hotpatch.c

// hook_install 的 hotpatch 实现
static int hotpatch_install_callback(void *data)
{
    hotpatch_data_t *hp = (hotpatch_data_t *)data;
    
    // 此时所有其他 CPU 已被停止
    // 安全地修改指令
    memcpy(hp->target, hp->new_code, hp->size);
    
    // 刷新指令缓存
    flush_icache_range(hp->target, hp->target + hp->size);
    
    return 0;
}

void hook_install(hook_t *hook)
{
    hotpatch_data_t hp = {
        .target = (void *)hook->origin_addr,
        .new_code = hook->tramp_insts,
        .size = hook->tramp_insts_num * 4,
    };
    
    // stop_machine 会暂停所有其他 CPU,执行回调,然后恢复
    stop_machine(hotpatch_install_callback, &hp, NULL);
}

stop_machine 的工作原理

时间线:
  CPU 0          CPU 1          CPU 2          CPU 3
  ──────         ──────         ──────         ──────
  调用 stop_machine
  发送 IPI ────> 收到 IPI       收到 IPI       收到 IPI
                 暂停           暂停           暂停
  执行修改代码
  完成修改
  唤醒所有 CPU -> 恢复           恢复           恢复

modify_entry_kernel 移至 hotpatch.c

v2.0 更新modify_entry_kernel() 函数已从原先的位置移至 hotpatch.c 文件中,与其他热修补相关的代码统一管理。

// kernel/base/hotpatch.c (原在 hook.c 中)
int modify_entry_kernel(uint64_t target_addr, uint32_t *new_insts, int num_insts)
{
    hotpatch_data_t hp = {
        .target = (void *)target_addr,
        .new_code = new_insts,
        .size = num_insts * 4,
    };
    
    return stop_machine(hotpatch_install_callback, &hp, NULL);
}

paging_init PAC 指令 NOP 化处理

v2.0 更新:paging_init 中的 PAC 指令(PACIASP/PACIBSP)处理方式已更新。

背景

在启用了 Pointer Authentication Code (PAC) 的内核中,paging_init 函数的入口指令可能是 PACIASP(而非 BTI C 或普通的 STP 指令)。PAC 机制通过在函数入口签名返回地址、在函数出口验证签名来防止 ROP 攻击。

NOP 化处理

当 KernelPatch 劫持 paging_init 时,需要将其第一条指令替换为 B 指令。如果原始指令是 PACIASP,则在恢复时需要特殊处理:

// 原始 paging_init(启用 PAC):
paging_init:
    PACIASP                    ; 签名 LR
    STP X29, X30, [SP, #-16]! ; 保存寄存器
    MOV X29, SP
    ...
    LDP X29, X30, [SP], #16
    AUTIASP                    ; 验证 LR 签名
    RET

// KernelPatch 的处理:
// 1. 将 PACIASP 替换为 B _paging_init
// 2. 同时 NOP 化对应的 AUTIASP
// 3. 恢复时将 PACIASP 位置恢复为 NOP(而非原始的 PACIASP)

为什么 NOP 化而不是恢复 PACIASP?

KernelPatch 的 _paging_init 包装函数改变了调用链:_paging_init 调用原始 paging_init,但此时 LR 不再是原始调用者的返回地址。如果恢复 PACIASP,签名的 LR 将与后续 AUTIASP 验证的 LR 不匹配,导致 PAC 验证失败和内核崩溃。

正确的处理(NOP 化):
  paging_init[0] = NOP     ; 跳过签名(因为调用链已改变)
  paging_init[N] = NOP     ; 跳过验证
  -> 函数正常返回 ✅

错误的处理(保留 PACIASP):
  paging_init[0] = PACIASP ; 用新的 LR 签名
  paging_init[N] = AUTIASP ; 验证时 LR 已被 _paging_init 修改
  -> PAC 验证失败 -> 崩溃 ❌

检测逻辑

// kernel/base/setup1.S 中的 PAC 指令检测
// 检测 PACIASP (0xD503233F) 和 PACIBSP (0xD503237F)
// 以及 BTI C (0xD503245F)

mov w3, #0x201F
movk w3, #0xD503, lsl#16      // w3 = 0xD503201F (NOP)
// ... 掩码匹配逻辑 ...

// 如果检测到 PAC 指令:
// 1. 将 paging_init[0] 备份值设为 NOP
// 2. 搜索函数末尾的 AUTIASP/AUTIBSP 并 NOP 化

潜在问题分析

问题 1:备份顺序

错误的实现:
  T1: 覆盖 tcp_init_sock
  T2: 备份 tcp_init_sock ❌ 备份的是覆盖后的内容

正确的实现:
  T1: 备份 tcp_init_sock ✅
  T2: 覆盖 tcp_init_sock

KernelPatch 的实现

// kernel/base/setup1.S
start_prepare:
    // 先备份
    bl memcpy8  // 备份 map area
    
    // ... 其他操作 ...

map_prepare:
    // 后覆盖
    bl memcpy8  // 覆盖 map area

问题 2:恢复时机

错误的时机:
  在 rest_init 之后恢复 tcp_init_sock
  ↓
  inet_init() 可能在 rest_init 之后立即执行
  ↓
  tcp_init_sock 仍被覆盖 → 崩溃 ❌

正确的时机:
  在 start() 早期恢复 tcp_init_sock
  ↓
  patch() → hook rest_init
  ↓
  rest_init() 执行
  ↓
  inet_init() 执行,tcp_init_sock 已恢复 ✅

问题 3:PAC/BTI 指令备份处理(已更新)

v2.0 更新:此问题的处理已在新版本中重新设计。对于 BTI C 和 PACIASP/PACIBSP 指令,采用了不同的策略。

PAC 指令处理(v2.0 正确实现):
  paging_init[0] = PACIASP (0xD503233F)
  ↓ 检测到 PAC 指令
  backup = NOP (0xD503201F)         ; 故意设为 NOP
  同时 NOP 化函数末尾的 AUTIASP    ; 配对处理
  ↓ 恢复时
  paging_init[0] = NOP              ; PACIASP 和 AUTIASP 都被 NOP 化
  ↓ 函数正常返回
  不再进行 PAC 验证 ✅

BTI C 指令处理(v2.0 正确实现):
  paging_init[0] = BTI C (0xD503245F)
  ↓ 检测到 BTI
  backup = NOP (0xD503201F)         ; BTI C 无配对指令,NOP 化无害
  ↓ 恢复时
  paging_init[0] = NOP
  ↓ 后续执行不依赖 BTI
  函数正常运行 ✅

注意:在 BTI 强制模式的内核中,NOP 化 BTI C 可能
导致从某些跳转路径进入函数时触发 BTI 异常。但由于
paging_init 只在启动早期被调用一次,且调用路径确定,
实际风险极低。

验证备份完整性

方法 1:对比字节

// 在 restore_map() 后添加
void verify_map_restore()
{
    uint64_t tcp_init_sock_va = kernel_va + start_preset.map_offset;
    uint8_t *actual = (uint8_t *)tcp_init_sock_va;
    uint8_t *backup = (uint8_t *)start_preset.map_backup;
    
    int mismatch = 0;
    for (int i = 0; i < start_preset.map_backup_len; i++) {
        if (actual[i] != backup[i]) {
            log_boot("Mismatch at offset %x: actual=%02x, backup=%02x\n",
                     i, actual[i], backup[i]);
            mismatch++;
        }
    }
    
    if (mismatch) {
        log_boot("❌ Restore failed: %d bytes mismatch\n", mismatch);
    } else {
        log_boot("✅ Restore verified: all bytes match\n");
    }
}

方法 2:检查函数签名

void verify_paging_init_restore()
{
    uint32_t insn = *(uint32_t *)paging_init_va;
    
    log_boot("paging_init[0] = %08x\n", insn);
    
    // 检查常见的函数入口模式
    if ((insn & 0xFFC00000) == 0xA9000000) {
        // STP X?, X?, [SP, #offset]
        log_boot("✅ Looks like normal function entry (STP)\n");
    } else if ((insn & 0xFFFFFFFF) == 0xD503245F) {
        // BTI C
        log_boot("✅ Looks like BTI C\n");
    } else if ((insn & 0xFFFFFFFF) == 0xD503233F) {
        // PACIASP
        log_boot("✅ Looks like PACIASP\n");
    } else if ((insn & 0xFC000000) == 0x14000000) {
        // B 指令
        log_boot("❌ Still a B instruction! Not restored!\n");
    } else {
        log_boot("⚠️ Unknown instruction pattern\n");
    }
}

内存屏障的重要性

dsb/isb 指令

// kernel/base/setup1.S:250, 282
dsb ish     // Data Synchronization Barrier
isb         // Instruction Synchronization Barrier

为什么需要?

  • ARM64 是弱内存序架构
  • 写入内存的操作可能不会立即全局可见
  • 必须使用屏障确保所有 CPU 看到最新数据

如果缺少

CPU 0:                  CPU 1:
写入 backup             读取 backup
  ↓ (无屏障)             ↓
内存未更新               读到旧值 ❌

正确的使用

CPU 0:                  CPU 1:
写入 backup
  ↓ dsb ish
确保写入完成             读取 backup
                          ↓
                        读到新值 ✅

总结

三大备份的特点

备份项 大小 目标 恢复时机 重要性
header_backup 8 B kernel[0:8] setup_preset setup 返回前 ⭐⭐⭐
map_backup ~2 KB tcp_init_sock start_preset start() 早期 ⭐⭐⭐⭐⭐
paging_init_backup 4 B paging_init[0] map_data _paging_init 中 ⭐⭐⭐⭐

关键原则

  1. 先备份后覆盖:确保数据不丢失
  2. 尽早恢复:减少风险窗口
  3. 验证完整性:确保备份正确
  4. 内存屏障:确保多核可见性
  5. 缓存刷新:确保指令一致性

v2.0 变更

  • hook_install/hook_uninstall 改用 hotpatch(stop_machine)机制,确保多核安全
  • modify_entry_kernel 移至 hotpatch.c,与热修补代码统一管理
  • paging_init 的 PAC 指令(PACIASP/PACIBSP)通过 NOP 化成对处理,消除 PAC 验证失败问题

下一篇15-问题排查与修复.md - BTI bug 分析和修复方案


文档版本:2.0
最后更新:2026-06-26

评论

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

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