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

镜像修补布局详解

2026-06-29 · 阅读 0

概述

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 字节

关键字段

  1. B 指令(偏移 0x0 或 0x4)

    格式:0x14000000 | (offset >> 2)
    
    示例:0x14020000
      0x14000000 = B 操作码
      0x020000 = 偏移/4 = 0x80000
      目标 = PC + 0x80000
  2. kernel_size_le(偏移 0x10)

    内核运行时大小(非镜像文件大小)
    
    示例:0x0000000001b00000 (27MB)
    注意:小端序存储
  3. 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_typememblock_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 (可选)                                   │
└──────────────────────────────────────────────┘

修补流程

  1. 解析 boot header,提取内核偏移和大小
  2. 解压内核(如果是压缩格式)
  3. 对解压后的 Image 执行标准修补流程
  4. 重新压缩内核(保持原有压缩格式)
  5. 更新 boot header 中的 kernel_size
  6. 重新组装 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

不需要额外的磁盘空间
利用内核自身的"空闲区域"
启动完成后完全恢复

安全性分析

潜在风险

  1. tcp_init_sock 被提前调用

    • 如果某些内核配置提前初始化网络
    • 在 restore_map() 之前调用 socket()
    • 会跳转到错误的地址 → 崩溃
  2. start_offset 计算错误

    • 如果 kernel_size 不准确
    • 可能运行时覆盖 kpimg
    • 导致代码损坏
  3. 重定位问题

    • 如果 kernel_va 推断错误
    • 符号地址全部错误
    • hook 会失败

缓解措施

  1. 多重验证

    • vectors 对齐检查
    • pid_vnr 指令模式验证
    • linux_banner 地址匹配
  2. 保守估计

    • start_offset 总是选择更大的值
    • map_max_size 限制为 2KB
  3. 早期恢复

    • 在 start() 阶段尽早执行 restore_map()
    • 在任何可能调用网络的代码之前

总结

布局设计的精妙之处

  1. 磁盘高效:kpimg 紧接原始内核,无浪费空间
  2. 运行时安全:通过 start_offset 避免内存覆盖
  3. 临时借用:利用 tcp_init_sock 区域,完成后恢复
  4. 灵活扩展:支持 Extra Items 嵌入额外资源
  5. 版本兼容:自动适配不同页面大小和内核版本

关键变量总结

变量 含义 典型值 用途
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

评论

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

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