镜像修补布局详解
概述
KernelPatch 的布局设计是整个项目最精妙的部分之一。它需要解决一个核心问题:
如何在不破坏内核的前提下,将 kpimg 嵌入到内核镜像中,并在运行时安全地执行?
关键挑战:
- 磁盘镜像要尽可能小
- 运行时不能覆盖内核内存
- 需要临时借用内核的某个区域
- 完成后必须完全恢复
ARM64 内核镜像结构
标准头部
// tools/image.c:18-55
typedef struct {
union {
struct {
uint8_t mz[4]; // "MZ" (UEFI 启动)
uint32_t b_insn; // B 指令 -> primary_entry
} efi;
struct {
uint32_t b_insn; // B 指令(非 UEFI)
uint32_t reserved0;
} nefi;
} hdr; // 偏移 0x00-0x07
uint64_t kernel_offset; // 偏移 0x08:加载偏移
uint64_t kernel_size_le; // 偏移 0x10:内核大小(小端序)
uint64_t kernel_flag_le; // 偏移 0x18:标志位
uint64_t reserved[3]; // 偏移 0x20-0x37
char magic[4]; // 偏移 0x38:"ARM\x64"
uint64_t pe_offset; // 偏移 0x3C:PE 头偏移(UEFI)
} arm64_hdr_t; // 总共 64 字节
关键字段:
B 指令(偏移 0x0 或 0x4)
格式:0x14000000 | (offset >> 2) 示例:0x14020000 0x14000000 = B 操作码 0x020000 = 偏移/4 = 0x80000 目标 = PC + 0x80000kernel_size_le(偏移 0x10)
内核运行时大小(非镜像文件大小) 示例:0x0000000001b00000 (27MB) 注意:小端序存储kernel_flag_le(偏移 0x18)
Bit 0: 字节序 (0=小端, 1=大端) Bit 1-2: 页面大小 00 = 未指定 01 = 4KB (page_shift=12) 10 = 16KB (page_shift=14) 11 = 64KB (page_shift=16)
修补前的内核镜像
┌─────────────────────────────────────────────────────┐
│ 偏移 0x00000000: ARM64 Header (64 bytes) │
│ 0x00: [UEFI] "MZ" + B instruction │
│ 0x00: [非UEFI] B instruction │
│ 0x08: kernel_offset │
│ 0x10: kernel_size = 0x1b00000 (27MB) │
│ 0x18: kernel_flag │
│ 0x38: "ARM\x64" │
├─────────────────────────────────────────────────────┤
│ 偏移 0x00000040: _stext / primary_entry │
│ │ │
│ ├─ Kernel Code (.text) │
│ │ - 系统调用处理 │
│ │ - 异常向量表 (vectors) │
│ │ - 驱动代码 │
│ │ - ... │
│ │ │
│ ├─ Kernel Data (.data, .rodata) │
│ │ - kallsyms_addresses/offsets │
│ │ - kallsyms_names │
│ │ - kallsyms_token_table │
│ │ - 全局变量 │
│ │ - ... │
│ │ │
│ └─ BSS (.bss) │
│ - 未初始化数据 │
├─────────────────────────────────────────────────────┤
│ 偏移 ori_kimg_len (例如 0x1ab2c38 = 26.7MB) │
│ 镜像文件结束 │
└─────────────────────────────────────────────────────┘
修补后的镜像布局
┌─────────────────────────────────────────────────────┐
│ 偏移 0x00000000: ARM64 Header (64 bytes) 【已修改】 │
│ 0x00/0x04: B setup_entry (跳转到 kpimg) │
│ 0x10: kernel_size = out_all_len (更新) │
├─────────────────────────────────────────────────────┤
│ 偏移 0x00000040: 原始内核代码【未修改】 │
│ ... │
├─────────────────────────────────────────────────────┤
│ 偏移 map_offset (例如 0xd73550 = tcp_init_sock) │
│ 【运行时会被临时覆盖】 │
│ 原始 tcp_init_sock 代码 │
├─────────────────────────────────────────────────────┤
│ 偏移 ori_kimg_len (例如 0x1ab2c38) │
│ 【4KB 对齐边界】 │
├═════════════════════════════════════════════════════┤
│ 偏移 setup_offset = align_ceil(ori_kimg_len, 4K) │
│ 例如 0x1ab3000 │
│ ┌─────────────────────────────────────────────┐ │
│ │ +0x0000: setup_header (64 bytes) │ │
│ │ - magic: "KP1158" │ │
│ │ - kp_version: 0x000d02 (0.13.2) │ │
│ │ - config_flags: ANDROID | DEBUG │ │
│ │ - compile_time: "16:23:45 Nov 2 2025" │ │
│ ├─────────────────────────────────────────────┤ │
│ │ +0x0040: setup_preset (~960 bytes) │ │
│ │ - kernel_version: 5.10.149 │ │
│ │ - kimg_size: 0x1ab2c38 │ │
│ │ - kpimg_size: 0x1f2d40 │ │
│ │ - kernel_size: 0x1b00000 │ │
│ │ - setup_offset: 0x1ab3000 │ │
│ │ - start_offset: 0x1b00000 ⚠️ 注意 │ │
│ │ - map_offset: 0xd73550 (tcp_init_sock) │ │
│ │ - kallsyms_lookup_name_offset: 0x982b0 │ │
│ │ - paging_init_offset: 0x1abe5d0 │ │
│ │ - sprintf_offset: 0x28eabc │ │
│ │ - symbol_lookup_anchor_offset: 0x982c0 │ │
│ │ - map_symbol: {...} │ │
│ │ - header_backup: [原始头部 8 字节] │ │
│ │ - superkey: "mykey123" │ │
│ │ - patch_config: {...} │ │
│ ├─────────────────────────────────────────────┤ │
│ │ +0x1000: setup_entry (汇编代码) │ │
│ │ - 设置栈 │ │
│ │ - 调用 setup() │ │
│ ├─────────────────────────────────────────────┤ │
│ │ _map_start / _map_end (~2.5KB) │ │
│ │ - _paging_init() 代码 │ │
│ │ - map_data 数据 │ │
│ │ - 会被复制到 map_offset │ │
│ └─────────────────────────────────────────────┘ │
├═════════════════════════════════════════════════════┤
│ 偏移 align_kimg_len + kpimg_len │
│ 例如 0x1ab3000 + 0x1f2d40 = 0x1ca5d40 │
│ ┌─────────────────────────────────────────────┐ │
│ │ _kp_start: kpimg 主体 │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ .kp.text (代码段,64KB 对齐) │ │ │
│ │ │ - start() 函数 │ │ │
│ │ │ - hook 实现 │ │ │
│ │ │ - patch 逻辑 │ │ │
│ │ │ - transit0/4/8/12 (trampoline) │ │ │
│ │ │ - 所有 KernelPatch 核心代码 │ │ │
│ │ ├─────────────────────────────────────┤ │ │
│ │ │ .kp.data (数据段,64KB 对齐) │ │ │
│ │ │ - start_preset_t 实例 │ │ │
│ │ │ - 全局变量 │ │ │
│ │ │ - 符号表 │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ │ _kp_end │ │
│ └─────────────────────────────────────────────┘ │
├═════════════════════════════════════════════════════┤
│ Extra Items (可选,例如 KPM 模块) │
│ ┌─────────────────────────────────────────────┐ │
│ │ Extra Item 0: │ │
│ │ - magic: "kpe" │ │
│ │ - type: EXTRA_TYPE_KPM │ │
│ │ - name: "my-module" │ │
│ │ - event: "pre-kernel-init" │ │
│ │ - args: "param=value" │ │
│ │ - content: [KPM ELF 二进制] │ │
│ ├─────────────────────────────────────────────┤ │
│ │ Extra Item 1: ... │ │
│ ├─────────────────────────────────────────────┤ │
│ │ 空 Item (结束标志) │ │
│ └─────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────┤
│ 偏移 out_all_len: 镜像文件结束 │
└─────────────────────────────────────────────────────┘
关键偏移量详解
1. setup_offset - kpimg 在镜像中的位置
// tools/patch.c:456-457
int ori_kimg_len = pimg.ori_kimg_len; // 原始内核长度
int align_kimg_len = align_ceil(ori_kimg_len, SZ_4K); // 4KB 对齐
setup->setup_offset = align_kimg_len;
计算示例:
ori_kimg_len = 0x1ab2c38 (26,796,088 字节 = 25.56 MB)
align_kimg_len = align_ceil(0x1ab2c38, 0x1000)
= 0x1ab3000 (26,796,032 字节)
setup_offset = 0x1ab3000
为什么 4KB 对齐?
- 页面大小的最小公倍数
- 方便后续的页表映射
- 符合 ELF 规范
2. start_offset - kpimg 运行时的物理地址
// tools/patch.c:460-466
int align_kernel_size = align_ceil(kinfo->kernel_size, SZ_4K);
int start_offset = align_kernel_size;
if (out_all_len > start_offset) {
// 检测到重叠,需要调整
start_offset = align_ceil(out_all_len, SZ_4K);
tools_logi("patch overlap, move start from 0x%x to 0x%x\n",
align_kernel_size, start_offset);
}
setup->start_offset = start_offset;
计算逻辑:
情况 1:无重叠(理想情况)
kernel_size = 0x1b00000 (27 MB)
ori_kimg_len = 0x1ab2c38 (25.56 MB)
kpimg_len = 0x1f2d40 (1.97 MB)
extra_size = 0x0
out_all_len = 0x1ab3000 + 0x1f2d40 + 0x0 = 0x1ca5d40 (28.65 MB)
因为 0x1ca5d40 > 0x1b00000,发生重叠!
start_offset = align_ceil(0x1ca5d40, 4K) = 0x1ca6000 (28.65 MB)
情况 2:无重叠
kernel_size = 0x3000000 (48 MB)
ori_kimg_len = 0x1ab2c38 (25.56 MB)
kpimg_len = 0x1f2d40 (1.97 MB)
out_all_len = 0x1ca5d40 (28.65 MB)
因为 0x1ca5d40 < 0x3000000,无重叠
start_offset = align_ceil(0x3000000, 4K) = 0x3000000 (48 MB)
为什么需要这个检查?
【磁盘布局】 【内存布局】
物理地址 PA: 物理地址 PA:
+0x0000000 +0x0000000
┌────────────┐ ┌────────────┐
│ Header │ │ Header │
├────────────┤ ├────────────┤
│ │ │ │
│ Kernel │ │ Kernel │
│ Code & │ │ Code & │
│ Data │ │ Data │
│ │ │ │
├────────────┤ 0x1ab3000 ├────────────┤ kernel_size
│ kpimg │ │ │ 0x1b00000
│ (setup) │ │ (unused) │
├────────────┤ 0x1ca5d40 │ │
│ (end) │ ├────────────┤ start_offset
└────────────┘ │ kpimg │ 0x1ca6000
│ 运行时位置 │
├────────────┤
│ │
└────────────┘
如果 kpimg 放在 0x1ab3000,
运行时内核会扩展到 0x1b00000,
就会覆盖 kpimg!
所以需要调整到 0x1ca6000。
3. map_offset - 临时代码注入位置
// tools/symbol.c:59-65
void select_map_area(kallsym_t *kallsym, char *image_buf,
int32_t *map_start, int32_t *max_size)
{
int32_t addr = get_symbol_offset_exit(kallsym, image_buf, "tcp_init_sock");
*map_start = align_ceil(addr, 16); // 16 字节对齐
*max_size = 0x800; // 最大 2KB
}
setup->map_offset = map_start; // 例如 0xd73550
为什么选择 tcp_init_sock?
// net/ipv4/tcp.c
void tcp_init_sock(struct sock *sk)
{
// TCP socket 初始化
// ...
}
// 调用路径:
inet_create() -> tcp_prot.init() -> tcp_init_sock()
// inet_create 在何时被调用?
kernel_init()
-> do_basic_setup()
-> do_initcalls()
-> inet_init() // subsys_initcall
-> 注册 tcp_prot
时序分析:
内核启动流程:
_head
-> start_kernel()
-> rest_init() ← 我们在这里 hook
-> kernel_init() ← 我们在这里 hook
-> do_basic_setup()
-> do_initcalls()
-> inet_init() ← tcp_init_sock 才会被调用
此时已经恢复完成!
结论:在 rest_init 之前,tcp_init_sock 不会被调用,可以安全覆盖。
map 区域的生命周期:
T1: setup_entry 阶段
├─ 备份 tcp_init_sock 到 start_preset.map_backup
└─ 覆盖 tcp_init_sock(复制 _map_start)
T2: paging_init 阶段
├─ 执行 _paging_init (位于 tcp_init_sock 区域)
└─ 跳转到 start()
T3: start() 阶段
├─ prot_myself()
├─ restore_map() ← 恢复 tcp_init_sock
└─ patch()
T4: 后续启动
└─ inet_init() 可以安全调用 tcp_init_sock
内存布局对比
磁盘布局(Image.patched 文件)
物理位置 内容 大小
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
0x00000000 内核头部 64 B
0x00000040 内核代码 ~26 MB
0x00d73550 tcp_init_sock ~2 KB
...
0x01ab2c38 (原始内核结束)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
0x01ab3000 setup_preset ~4 KB
0x01ab4000 kpimg (setup_entry) ~2 MB
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
0x01ca5d40 Extra Items 可变
0x01ca5d40 (文件结束)
运行时物理内存布局
物理地址 PA 内容 状态
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PA + 0x00000000 内核头部 ✅ 已恢复
PA + 0x00000040 内核代码 ✅ 正常
PA + 0x00d73550 tcp_init_sock ✅ 已恢复
PA + 0x01b00000 (内核运行时结束)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PA + 0x1ca6000 kpimg 代码/数据 ✅ 新分配的内存
PA + 0x1ca6000 ├─ .text (RX)
PA + 0x1cc0000 ├─ .data (RW)
PA + 0x1cd0000 ├─ hook 区 (RWX, 1MB)
PA + 0x1dd0000 ├─ rw_mem (RW, 2MB)
PA + 0x1fd0000 └─ rox_mem (RX, 4MB)
PA + 0x23d0000 Extra Items
运行时虚拟内存布局
虚拟地址 内容
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
0xffffffc008000000 内核起始 (kernel_va)
+0x00000000 内核头部
+0x00000040 _stext
+0x00080800 vectors (2KB 对齐)
+0x00982b0 kallsyms_lookup_name
+0x00d73550 tcp_init_sock (已恢复)
+0x01abe5d0 paging_init
+0x01b00000 (内核结束)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
0xffffffc00????000 kpimg 虚拟地址 (kpimg_va)
(动态分配) 由 memblock_phys_alloc 分配
映射到上面的 PA + 0x1ca6000
入口点劫持机制
修改 B 指令
// tools/patch.c:556-557
int text_offset = align_kimg_len + SZ_4K; // setup_entry 位置
b((uint32_t *)(out_kernel_file.kimg + kinfo->b_stext_insn_offset),
kinfo->b_stext_insn_offset, text_offset);
b() 函数实现:
// tools/common.c:18-25
int b(uint32_t *buf, uint64_t from, uint64_t to)
{
if (can_b_imm(from, to)) {
// B 指令编码:
// 31-26: 000101 (B 操作码)
// 25-0: signed offset >> 2
buf[0] = 0x14000000u | (((to - from) & 0x0FFFFFFFu) >> 2u);
return 4;
}
return 0; // 距离太远,无法用 B 指令
}
int can_b_imm(uint64_t from, uint64_t to)
{
// B 指令范围:±128MB
uint32_t imm26 = 1 << 25 << 2; // 128MB
return (to >= from && to - from <= imm26) ||
(from >= to && from - to <= imm26);
}
实际效果:
UEFI 内核(偏移 0x4):
修补前:
0x0000: "MZ"
0x0004: 0x14020000 B #0x80000 ; 跳到 _stext (0x80040)
修补后:
0x0000: "MZ"
0x0004: 0x141AC7C0 B #0x1ab4000 ; 跳到 setup_entry
计算:
offset = 0x1ab4000 - 0x4 = 0x1ab3ffc
offset >> 2 = 0x6acfff
指令 = 0x14000000 | 0x6acfff = 0x146acfff (实际值可能略有不同)
非 UEFI 内核(偏移 0x0):
修补前:
0x0000: 0x14020000 B #0x80000 ; 跳到 _stext (0x80000)
修补后:
0x0000: 0x141AC800 B #0x1ab4000 ; 跳到 setup_entry
重叠检测算法
为什么会重叠?
【场景】:内核镜像很大,kpimg 也很大
磁盘镜像:
0x0000000 ────┐
│ 原始内核 (25 MB)
0x1900000 ────┤
│ (对齐填充)
0x1900000 ────┤
(setup_off) │ kpimg (2 MB)
0x1b00000 ────┤
│ Extra (1 MB)
0x1c00000 ────┘
运行时内存:
PA+0x0000000 ────┐
│ 内核会扩展到 kernel_size (27 MB)
PA+0x1b00000 ────┘
如果 kpimg 在 0x1900000,运行时会被覆盖!
调整策略
// tools/patch.c:460-466
// 默认:紧接内核运行时结束位置
int start_offset = align_kernel_size; // 0x1b00000
// 检测重叠
if (out_all_len > start_offset) {
// 磁盘镜像的总长度 > 内核运行时大小
// 说明 kpimg 会侵入内核运行时空间
// 解决:将 start_offset 移到镜像末尾之后
start_offset = align_ceil(out_all_len, SZ_4K);
}
调整后:
磁盘镜像:
0x0000000 ────┐
│ 原始内核
0x1900000 ────┤
│ kpimg (磁盘位置)
0x1b00000 ────┤
│ Extra
0x1c00000 ────┘
运行时内存:
PA+0x0000000 ────┐
│ 内核 (27 MB)
PA+0x1b00000 ────┤
│ (空闲区)
PA+0x1c00000 ────┤
│ kpimg (运行时位置)
PA+0x1e00000 ────┘
磁盘上的 kpimg 会被复制到 PA+0x1c00000
setup_entry 的定位
链接脚本定义
// kernel/kpimg.lds:1-30
OUTPUT_ARCH(aarch64)
_link_base = 64k - 3 * 4k; // 0xD000
SECTIONS {
. = _link_base;
_link_base = .;
.setup.data : {
_setup_start = .;
base/setup.o(.setup.header) // 0xD000: setup_header (64B)
. = _setup_start + 64;
base/setup.o(.setup.preset) // 0xD040: setup_preset (~960B)
base/setup.o(.setup.data) // 0xD400: stack
. = _setup_start + 4K; // 总共 4KB
}
.setup.text : {
base/setup1.o(.entry.text) // 0xE000: setup_entry
base/setup1.o(.text)
_setup_end = .;
}
.setup.map : {
_map_start = .; // 0xE400: map 代码
base/map.o(.map.data)
base/map.o(.map.text)
_map_end = .;
}
. = ALIGN(64K);
_kp_start = .; // 0x10000: kpimg 主体
.kp.text : { ... }
.kp.data : { ... }
_kp_end = .;
}
布局:
链接地址 _link_base = 0xD000
0xD000 ────┐
│ setup_header (64B)
0xD040 ────┤
│ setup_preset (~960B)
0xD400 ────┤
│ stack (2KB)
0xE000 ────┤
│ setup_entry (汇编代码)
0xE400 ────┤
│ _map_start (_paging_init 代码)
0xE900 ────┤
│ _map_end
━━━━━━━━━━━━ (64KB 对齐)
0x10000 ───┤
│ _kp_start (主 kpimg 代码)
│ - start.o(.start.text)
│ - hook.o(.text)
│ - ...
在磁盘镜像中的位置:
setup_offset + 0x0000 = setup_header
setup_offset + 0x0040 = setup_preset
setup_offset + 0x1000 = setup_entry ← 入口点
setup_offset + 0x1400 = _map_start
setup_offset + 0x10000 = _kp_start
计算 text_offset:
int text_offset = align_kimg_len + SZ_4K;
// = setup_offset + 4K
// = 0x1ab3000 + 0x1000
// = 0x1ab4000 ← setup_entry 的位置
运行时重定位
setup1.S 中的地址计算
// kernel/base/setup1.S:287-352
setup:
// 1. 获取当前位置
adrp x9, _link_base
add x9, x9, :lo12:_link_base
// x9 = 当前的 _link_base(运行时地址)
// 2. 计算 kernel_pa
adrp x10, setup_preset
add x10, x10, :lo12:setup_preset
// x10 = setup_preset 的运行时地址
ldr x11, [x10, #setup_setup_offset_offset]
// x11 = setup_offset (例如 0x1ab3000)
sub x12, x9, x11
// x12 = 当前地址 - setup_offset = kernel_pa
mov x19, x12
// x19 = kernel_pa ← 内核物理地址!
原理:
_link_base 链接地址 = 0xD000
setup_offset = 0x1ab3000
运行时:
_link_base 被加载到 PA + 0x1ab3000 + 0xD000
= PA + 0x1ab4000
kernel_pa = (PA + 0x1ab4000) - 0x1ab3000 - 0xD000
= PA
实际案例:布局计算
真实设备示例(Pixel 6)
输入:
Image 文件大小: 26,796,088 字节 (0x1ab2c38)
kpimg 大小: 2,043,200 字节 (0x1f2d40)
Extra 大小: 0
内核信息(从头部读取):
kernel_size: 0x1b00000 (27 MB)
page_shift: 12 (4KB 页面)
kallsyms 分析:
kallsyms_lookup_name: 0x00982b0
paging_init: 0x01abe5d0
tcp_init_sock: 0xd73550
计算布局:
ori_kimg_len = 0x1ab2c38
align_kimg_len = align_ceil(0x1ab2c38, 0x1000) = 0x1ab3000
setup_offset = 0x1ab3000
out_img_len = 0x1ab3000 + 0x1f2d40 = 0x1ca5d40
out_all_len = 0x1ca5d40 + 0 = 0x1ca5d40
align_kernel_size = align_ceil(0x1b00000, 0x1000) = 0x1b00000
检测重叠:
out_all_len (0x1ca5d40) > align_kernel_size (0x1b00000) ✅
调整 start_offset:
start_offset = align_ceil(0x1ca5d40, 0x1000) = 0x1ca6000
最终布局:
┌─────────────────────────────────────────────┐
│ 磁盘位置 │ 大小 │ 运行时位置 │
├─────────────────────────────────────────────┤
│ 0x0000000 │ 25.56MB │ PA+0x0000000 │ 内核
│ 0x1ab3000 │ 1.97MB │ PA+0x1ca6000 │ kpimg
│ 0x1ca5d40 │ 0 │ PA+0x1ea6000 │ extra
│ 0x1ca5d40 (结束) │ │ │
└─────────────────────────────────────────────┘
日志输出:
patch overlap, move start from 0x1b00000 to 0x1ca6000
layout kimg: 0x0,0x1ab2c38, kpimg: 0x1ab3000,0x1f2d40,
extra: 0x1ca5d40,0x0, end: 0x1ca5d40, start: 0x1ca6000
map_symbol 结构更新
v2.0 更新:
MAP_SYMBOL_NUM已从 5 更新为 7。新增了memblock_phys_alloc_type和memblock_virt_alloc_type两个字段,用于区分不同内核版本使用的 memblock 分配接口变体。
// kernel/base/setup.h
#define MAP_SYMBOL_NUM 7
typedef struct {
int64_t memstart_addr_relo; // memstart_addr 的重定位偏移
int64_t memblock_reserve_relo; // memblock_reserve 函数偏移
int64_t memblock_phys_alloc_relo; // memblock_phys_alloc 函数偏移
int64_t memblock_mark_nomap_relo; // memblock_mark_nomap 函数偏移
int64_t memblock_free_relo; // memblock_free 函数偏移
int64_t memblock_phys_alloc_type; // [v2.0 新增] 分配函数类型标识
int64_t memblock_virt_alloc_type; // [v2.0 新增] 虚拟分配函数类型标识
} map_symbol_t;
为什么需要区分 alloc type?
- 不同内核版本的 memblock 分配 API 签名不同
- 某些版本使用
memblock_phys_alloc_try_nid,另一些使用memblock_alloc_range_nid type字段让_paging_init能在运行时选择正确的调用方式
setup_preset 新增字段
v2.0 更新:setup_preset 结构新增了两个偏移量字段。
sprintf_offset
sprintf_offset 记录内核 sprintf 函数在镜像中的偏移。KernelPatch 在早期启动阶段需要格式化字符串输出(如启动日志),此时 printk 尚未初始化,因此直接调用 sprintf 进行字符串格式化。
symbol_lookup_anchor_offset
symbol_lookup_anchor_offset 记录符号查询锚点的偏移。该锚点用于在运行时执行本地 kallsyms_lookup,即使在 kallsyms_lookup_name 不可用的场景下(例如某些精简内核配置),仍然能通过锚点定位符号。
boot.img 直接修补
v2.0 新增:KernelPatch 现在支持直接修补 boot.img 文件,而不仅仅是裸 Image 文件。
boot.img 布局
┌──────────────────────────────────────────────┐
│ Boot Header (v0/v1/v2/v3/v4) │
│ - magic: "ANDROID!" │
│ - kernel_size │
│ - kernel_addr │
│ - ramdisk_size │
│ - ... │
├──────────────────────────────────────────────┤
│ Kernel (可能是 gzip/lz4 压缩的 Image) │
├──────────────────────────────────────────────┤
│ Ramdisk │
├──────────────────────────────────────────────┤
│ Second Stage (可选) │
├──────────────────────────────────────────────┤
│ DTB (可选) │
└──────────────────────────────────────────────┘
修补流程:
- 解析 boot header,提取内核偏移和大小
- 解压内核(如果是压缩格式)
- 对解压后的 Image 执行标准修补流程
- 重新压缩内核(保持原有压缩格式)
- 更新 boot header 中的 kernel_size
- 重新组装 boot.img
注意事项:
- 修补后的内核会变大(增加 kpimg 大小),需确保 boot 分区空间充足
- 支持所有 Android Boot Image 版本(v0 到 v4)
UNCOMPRESSED_IMG头格式也被正确处理
特殊情况处理
1. 已 patch 的镜像
// tools/patch.c:216-256
preset_t *old_preset = get_preset(kimg, kimg_len);
pimg->preset = old_preset;
if (!old_preset) {
tools_logi("new kernel image ...\n");
pimg->ori_kimg_len = pimg->kimg_len;
return 0;
}
tools_logi("patched kernel image ...\n");
// 从 preset 中恢复原始大小
int32_t saved_kimg_len = old_preset->setup.kimg_size;
if (is_be() ^ kinfo->is_be) saved_kimg_len = i32swp(saved_kimg_len);
int align_kimg_len = (char *)old_preset - kimg;
pimg->ori_kimg_len = saved_kimg_len;
// 恢复原始头部
memcpy((char *)kimg, old_preset->setup.header_backup,
sizeof(old_preset->setup.header_backup));
// 提取已嵌入的 Extra Items
int extra_offset = align_kimg_len + old_preset->setup.kpimg_size;
// ...
更新 patch:
- 保留原始内核大小
- 替换 kpimg
- 可以添加/删除/修改 Extra Items
2. UNCOMPRESSED_IMG 头
某些设备(如 Google Pixel)的内核镜像有特殊头部:
// tools/patch.c:27-36
void read_kernel_file(const char *path, kernel_file_t *kernel_file)
{
read_file(path, &kernel_file->kfile, &kernel_file->kfile_len);
kernel_file->is_uncompressed_img =
kernel_file->kfile_len >= 20 &&
!strncmp("UNCOMPRESSED_IMG", kernel_file->kfile, 16);
int img_offset = 0;
if (kernel_file->is_uncompressed_img) img_offset = 20;
kernel_file->kimg = kernel_file->kfile + img_offset;
kernel_file->kimg_len = kernel_file->kfile_len - img_offset;
}
格式:
偏移 0x00: "UNCOMPRESSED_IMG" (16 字节)
偏移 0x10: uint32_t image_size (小端序)
偏移 0x14: ARM64 Header ...
更新镜像大小:
// tools/patch.c:38-47
void update_kernel_file_img_len(kernel_file_t *kernel_file, int kimg_len,
bool is_different_endian)
{
kernel_file->kimg_len = kimg_len;
if (kernel_file->is_uncompressed_img) {
*(uint32_t *)(kernel_file->kfile + 16) =
(uint32_t)(is_different_endian ? i32swp(kimg_len) : kimg_len);
kernel_file->kfile_len = kimg_len + 20;
} else {
kernel_file->kfile_len = kimg_len;
}
}
内存分配流程
setup_entry 阶段
// kernel/base/setup1.S:40-158 (start_prepare)
start_prepare:
// x19 = kernel_pa
// 1. 计算 start_img_size
ldr x22, [x10, #setup_kpimg_size_offset] // kpimg_size
adrp x23, _kp_start
add x23, x23, :lo12:_kp_start
adrp x24, _link_base
add x24, x24, :lo12:_link_base
sub x23, x23, x24 // _kp_start - _link_base
sub x22, x22, x23 // start_img_size
// 2. 复制 kpimg 到 start_offset
add x0, x19, x21 // kernel_pa + start_offset
adrp x1, _kp_start
add x1, x1, :lo12:_kp_start // 当前 _kp_start 位置
ldr x2, [x10, #setup_extra_size_offset]
add x2, x2, x22 // start_img_size + extra_size
bl rmemcpy32 // 复制
计算 start_img_size:
setup_preset.kpimg_size = 0x1f2d40 (整个 kpimg 文件大小)
_kp_start - _link_base = 0x10000 - 0xD000 = 0x3000 (setup 段大小)
start_img_size = 0x1f2d40 - 0x3000 = 0x1efd40
为什么?
因为 setup 段 (0xD000-0x10000) 只在启动时使用一次
复制到 start_offset 时不需要包含 setup 段
只复制 _kp_start 之后的内容
_paging_init 阶段
// kernel/base/map.c:176-252
void _paging_init()
{
map_data_t *data = mem_proc();
// 1. 计算大小
uint64_t old_start_pa = data->start_offset + data->kernel_pa;
uint64_t reserve_size = data->start_img_size + data->extra_size;
uint64_t all_size = data->start_size + align_extra_size + data->alloc_size;
// all_size 包括:
// - _kp_start 到 _kp_end (代码+数据)
// - extra items
// - hook 区域 (1MB)
// - rw_mem (2MB)
// - rox_mem (4MB)
// 2. 保留旧位置(防止被其他代码占用)
memblock_reserve(old_start_pa, reserve_size);
// 3. 分配新内存
uint64_t start_pa = memblock_phys_alloc_try_nid(all_size, page_size, 0);
// 4. 标记为 nomap(不映射到 linear region)
if (data->map_symbol.memblock_mark_nomap_relo)
memblock_mark_nomap(start_pa, all_size);
// 5. 创建页表映射
uint64_t start_va = start_pa + data->kimage_voffset;
for (uint64_t off = 0; off < all_size; off += page_size) {
uint64_t entry = get_or_create_pte(data, start_va + off,
start_pa + off, attr_indx);
*(uint64_t *)entry = ...; // 设置 RWX 权限
}
flush_tlb_all();
// 6. 复制内容到新位置
uint64_t old_start_va = phys_to_lm(data, old_start_pa);
for (uint64_t i = 0; i < data->start_img_size; i += 8) {
*(uint64_t *)(start_va + i) = *(uint64_t *)(old_start_va + i);
}
// 7. 复制 extra items
for (uint64_t i = 0; i < data->extra_size; i += 8) {
*(uint64_t *)(start_va + data->start_size + i) =
*(uint64_t *)(old_start_va + data->start_img_size + i);
}
flush_icache_all();
// 8. 释放旧位置
memblock_free(old_start_pa, reserve_size);
// 9. 跳转到 start()
((start_f)start_va)(data->kimage_voffset, data->linear_voffset);
}
完整的地址映射表
| 组件 | 磁盘偏移 | 物理地址 | 虚拟地址 | 权限 | 生命周期 |
|---|---|---|---|---|---|
| 内核头部 | 0x0 |
PA + 0x0 |
kernel_va + 0x0 |
R-X | 永久 |
| 内核代码 | 0x40 ~ ori_kimg_len |
PA + 0x40 |
kernel_va + 0x40 |
R-X | 永久 |
| tcp_init_sock | map_offset |
PA + map_offset |
kernel_va + map_offset |
R-X | 临时覆盖,后恢复 |
| setup_preset | setup_offset |
PA + setup_offset |
- | - | 仅启动时 |
| setup_entry | setup_offset + 0x1000 |
PA + setup_offset + 0x1000 |
- | - | 仅启动时 |
| _map_start | setup_offset + 0x1400 |
PA + setup_offset + 0x1400 |
- | - | 复制到 map_offset |
| _kp_start (旧) | setup_offset + 0x10000 |
PA + start_offset |
start_va |
RWX | 复制到新位置 |
| _kp_start (新) | - | 新 PA |
新 VA |
分段设置 | 永久 |
| Extra Items | setup_offset + kpimg_len |
新 PA + start_size |
新 VA + start_size |
RW- | 永久 |
优化与权衡
优化 1:setup 段不复制
kpimg 磁盘大小: 2043 KB
setup 段大小: 12 KB
_kp_start 大小: 2031 KB
复制时只复制 _kp_start,节省 12 KB
优化 2:Extra Items 紧随其后
不需要额外的对齐
直接附加到 kpimg 后面
启动时一起复制
优化 3:临时借用 tcp_init_sock
不需要额外的磁盘空间
利用内核自身的"空闲区域"
启动完成后完全恢复
安全性分析
潜在风险
tcp_init_sock 被提前调用
- 如果某些内核配置提前初始化网络
- 在 restore_map() 之前调用 socket()
- 会跳转到错误的地址 → 崩溃
start_offset 计算错误
- 如果 kernel_size 不准确
- 可能运行时覆盖 kpimg
- 导致代码损坏
重定位问题
- 如果 kernel_va 推断错误
- 符号地址全部错误
- hook 会失败
缓解措施
多重验证
- vectors 对齐检查
- pid_vnr 指令模式验证
- linux_banner 地址匹配
保守估计
- start_offset 总是选择更大的值
- map_max_size 限制为 2KB
早期恢复
- 在 start() 阶段尽早执行 restore_map()
- 在任何可能调用网络的代码之前
总结
布局设计的精妙之处:
- ✅ 磁盘高效:kpimg 紧接原始内核,无浪费空间
- ✅ 运行时安全:通过 start_offset 避免内存覆盖
- ✅ 临时借用:利用 tcp_init_sock 区域,完成后恢复
- ✅ 灵活扩展:支持 Extra Items 嵌入额外资源
- ✅ 版本兼容:自动适配不同页面大小和内核版本
关键变量总结:
| 变量 | 含义 | 典型值 | 用途 |
|---|---|---|---|
setup_offset |
kpimg 在磁盘镜像中的位置 | 0x1ab3000 | setup_preset 的位置 |
start_offset |
kpimg 运行时的物理偏移 | 0x1ca6000 | 避免覆盖内核 |
map_offset |
临时代码注入位置 | 0xd73550 | tcp_init_sock |
kernel_size |
内核运行时大小 | 0x1b00000 | 内核会占用的内存 |
ori_kimg_len |
原始内核镜像大小 | 0x1ab2c38 | 实际文件大小 |
下一篇:05-ARM64重定位机制.md - 深入解析指令重定位的实现
文档版本:2.0
最后更新:2026-06-26
评论
- 还没有评论,来说点什么吧。