备份与恢复机制详解
概述
KernelPatch 的备份与恢复机制是确保内核完整性的关键。它涉及三处关键备份:
- 内核头部备份(header_backup,8 字节)
- Map area 备份(map_backup,~2KB)
- paging_init 备份(paging_init_backup,4 字节)
本文详细追踪这三处备份的完整数据流。
v2.0 更新摘要:
hook_install/hook_uninstall改用 hotpatch(stop_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 中 | ⭐⭐⭐⭐ |
关键原则:
- ✅ 先备份后覆盖:确保数据不丢失
- ✅ 尽早恢复:减少风险窗口
- ✅ 验证完整性:确保备份正确
- ✅ 内存屏障:确保多核可见性
- ✅ 缓存刷新:确保指令一致性
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
评论
- 还没有评论,来说点什么吧。