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

启动劫持流程详解

2026-06-29 · 阅读 1

概述

本文详细解读 KernelPatch 如何在内核启动的最早期阶段接管执行流程。这是整个 patch 机制能够工作的基础。

核心文件kernel/base/setup1.S(381 行汇编代码)

主要阶段

  1. setup_entry:内核入口被劫持后的第一站
  2. start_prepare:准备 kpimg 启动所需的数据
  3. map_prepare:劫持 paging_init,准备内存映射
  4. 恢复并跳转:恢复内核头部,跳回原始入口

启动流程概览

Bootloader
  │
  ├─ 加载 Image.patched 到物理内存 PA
  ├─ 跳转到 PA + 0x0 (或 PA + 0x4,UEFI)
  │
  ▼
setup_entry (kernel/base/setup1.S:369)
  │
  ├─ 设置临时栈
  ├─ 调用 setup()
  │     │
  │     ├─ start_prepare()
  │     │     ├─ 复制配置数据到 start_preset
  │     │     │   (含 sprintf_offset, symbol_lookup_anchor_offset)
  │     │     ├─ 备份 tcp_init_sock 到 map_backup
  │     │     └─ 复制 kpimg 到 PA + start_offset
  │     │
  │     ├─ map_prepare()
  │     │     ├─ 备份 paging_init 第一条指令
  │     │     ├─ 写入 B _paging_init 指令
  │     │     └─ 复制 _map_start 到 tcp_init_sock 位置
  │     │
  │     ├─ 恢复内核头部(从 header_backup)
  │     └─ 返回
  │
  ├─ 恢复原始栈指针
  └─ 跳转到 PA (内核原始入口 _head)
  
_head (内核原始入口)
  │
  └─ 内核继续正常启动...

setup_entry - 入口点

代码分析

// kernel/base/setup1.S:366-380
.section .entry.text, "ax"
.global setup_entry
.type entry, %function
setup_entry:
    // x0 = physical address to the FDT blob (设备树)
    // Bootloader 传递的参数保存在 x0..x3
    
    // 1. 保存原始栈指针
    mov x9, sp
    
    // 2. 设置临时栈
    adrp x11, stack
    add x11, x11, :lo12:stack
    add x11, x11, STACK_SIZE        // stack + 2KB
    mov sp, x11
    
    // 3. 保存原始 sp 到新栈
    stp x9, x10, [sp, -16]!         // 将 x9 (原始 sp) 压栈
    
    // 4. 跳转到 setup
    b setup

为什么需要临时栈?

  • Bootloader 设置的栈可能很小或不稳定
  • Setup 代码会调用函数(memcpy 等),需要可靠的栈
  • 使用 kpimg 自带的栈(在 .setup.data 段)

栈的定义

// kernel/base/setup.c:25-29
struct {
    uint8_t fp[STACK_SIZE];    // 2KB 栈空间
    uint8_t sp[0];             // sp 指向栈顶
} stack __section(.setup.data) __aligned(16);

setup() - 主设置函数

寄存器保存

// kernel/base/setup1.S:299-313
setup:
    // 保存所有寄存器(Bootloader 的参数和状态)
    stp x29, x30, [sp, -16]!
    stp x0, x1, [sp, -16]!      // x0 = FDT 地址
    stp x2, x3, [sp, -16]!
    stp x4, x5, [sp, -16]!
    stp x6, x7, [sp, -16]!
    stp x8, x18, [sp, -16]!
    stp x19, x20, [sp, -16]!
    stp x21, x22, [sp, -16]!
    stp x23, x24, [sp, -16]!
    stp x25, x26, [sp, -16]!
    stp x27, x28, [sp, -16]!

为什么保存所有寄存器?

  • 内核需要 Bootloader 传递的参数(FDT 地址等)
  • Setup 代码会修改寄存器
  • 跳回 _head 前必须恢复原始状态

计算 kernel_pa

// kernel/base/setup1.S:314-326
// 1. 获取当前位置
adrp x9, _link_base
add x9, x9, :lo12:_link_base
// x9 = _link_base 的运行时地址

// 2. 读取 setup_offset
adrp x10, setup_preset
add x10, x10, :lo12:setup_preset
// x10 = setup_preset 的运行时地址

ldr x11, [x10, #setup_setup_offset_offset]
// x11 = setup_preset.setup_offset (kpimg 在镜像中的偏移)

// 3. 计算 kernel_pa
sub x12, x9, x11
mov x19, x12
// x19 = kernel_pa = _link_base 运行时地址 - setup_offset

计算原理

_link_base 链接地址 = 0xD000
setup_offset = 0x1ab3000

Bootloader 将镜像加载到物理地址 PA:
  _link_base 实际位置 = PA + 0x1ab3000 + 0xD000
                      = PA + 0x1ab4000

因此:
  kernel_pa = (PA + 0x1ab4000) - (0x1ab3000 + 0xD000)
            = PA

这是一个巧妙的自举计算


start_prepare() - 准备启动数据

复制配置到 start_preset

// kernel/base/setup1.S:40-162
start_prepare:
    stp x29, x30, [sp, -16]!
    stp x19, x20, [sp, -16]!
    stp x21, x22, [sp, -16]!
    stp x23, x24, [sp, -16]!
    
    mov x19, x0  // x19 = kernel_pa
    
    // 获取数据结构地址
    adrp x9, map_data
    add x9, x9, :lo12:map_data
    // x9 = map_data 地址
    
    adrp x10, setup_preset
    add x10, x10, :lo12:setup_preset
    // x10 = setup_preset 地址
    
    adrp x11, start_preset
    add x11, x11, :lo12:start_preset
    // x11 = start_preset 地址
    
    adrp x12, header
    add x12, x12, :lo12:header
    // x12 = setup_header 地址
    
    
    // ============ 复制配置数据 ============
    
    // start_preset.kernel_version = setup_preset.kernel_version
    ldr w13, [x10, #setup_kernel_version_offset]
    str w13, [x11, #start_kernel_version_offset]
    
    // start_preset.kallsyms_lookup_name_offset = setup_preset.kallsyms_lookup_name_offset
    ldr x13, [x10, #setup_kallsyms_lookup_name_offset_offset]
    str x13, [x11, #start_kallsyms_lookup_name_offset_offset]
    
    // start_preset.kernel_size = setup_preset.kernel_size
    ldr x13, [x10, #setup_kernel_size_offset]
    str x13, [x11, #start_kernel_size_offset]
    
    // start_preset.start_offset = setup_preset.start_offset
    ldr x13, [x10, #setup_start_offset_offset]
    str x13, [x11, #start_start_offset_offset]
    mov x21, x13  // x21 = start_offset
    
    // start_preset.extra_size = setup_preset.extra_size
    ldr x13, [x10, #setup_extra_size_offset]
    str x13, [x11, #start_extra_size_offset]
    
    // start_preset.kernel_pa = kernel_pa
    str x19, [x11, #start_kernel_pa_offset]
    
    // start_preset.map_offset = setup_preset.map_offset
    ldr x13, [x10, #setup_map_offset_offset]
    str x13, [x11, #start_map_offset_offset]
    mov x20, x13  // x20 = map_offset
    
    // [0.13.2 新增] start_preset.sprintf_offset = setup_preset.sprintf_offset
    ldr x13, [x10, #setup_sprintf_offset_offset]
    str x13, [x11, #start_sprintf_offset_offset]
    
    // [0.13.2 新增] start_preset.symbol_lookup_anchor_offset = setup_preset.symbol_lookup_anchor_offset
    ldr x13, [x10, #setup_symbol_lookup_anchor_offset_offset]
    str x13, [x11, #start_symbol_lookup_anchor_offset_offset]

为什么要复制?

  • setup_preset 在磁盘镜像的 setup 段,只读
  • start_preset 在 kpimg 的运行时数据段,可读写
  • 后续 start() 函数需要访问这些数据

0.13.2 新增的偏移

  • sprintf_offset:内核 sprintf 函数的偏移,供 kpimg 运行时调用内核格式化输出
  • symbol_lookup_anchor_offset:符号查找锚点偏移,用于运行时 kallsyms 符号解析的辅助定位

立即数偏移修正 (0.13.2)

start_prepare 后续的 memcpy 调用中,计算 start_preset 内部字段的地址时,部分偏移值(如 start_superkey_offsetstart_root_superkey_offsetstart_patch_config_offset)超过了 ARM64 ADD 指令的 12 位立即数范围(0~4095)。0.13.2 将这些调用改为两条指令形式:

// kernel/base/setup1.S:108-113
// 旧写法(0.12.0):
//   add x0, x11, #start_superkey_offset      // 偏移超出 12 位立即数范围,编译报错
// 新写法(0.13.2):
mov x3, #start_superkey_offset                 // MOV 可以加载 16 位立即数
add x0, x11, x3                                // 用寄存器加法代替立即数加法
add x1, x10, #setup_superkey_offset
mov x2, #SUPER_KEY_LEN
bl memcpy8

// start_preset.root_superkey、start_preset.patch_config 同理
mov x3, #start_root_superkey_offset
add x0, x11, x3
...

为什么需要修正?

  • start_preset 结构体在 0.13.2 中增加了新字段(sprintf_offset、symbol_lookup_anchor_offset 等),导致后续字段的偏移量增大
  • 当偏移超过 4095 字节时,add x0, x11, #imm 会因立即数溢出而无法编码
  • 使用 mov + add 两条指令绕过此限制

备份 map area (tcp_init_sock)

// kernel/base/setup1.S:130-142
// backup map area
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 代码大小 (~2KB)

// start_preset.map_backup_len = map 大小
str x2, [x11, #start_map_backup_len_offset]

// 复制原始代码到 map_backup
add x0, x11, #start_map_backup_offset  // 目标:start_preset.map_backup
add x1, x19, x20                        // 源:kernel_pa + map_offset (tcp_init_sock)
bl memcpy8                              // 备份!

关键点

  • 源地址:kernel_pa + map_offset(tcp_init_sock 的物理地址)
  • 目标地址:start_preset.map_backup(在 kpimg 数据段)
  • 大小:_map_end - _map_start(约 2KB)

此时的备份

start_preset.map_backup[0:2048] = tcp_init_sock 的原始机器码

复制 kpimg 到 start_offset

// kernel/base/setup1.S:144-163
// 计算 start_img_size
ldr x22, [x10, #setup_kpimg_size_offset]    // kpimg 总大小
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

// map_data.start_img_size = start_img_size
str x22, [x9, #map_start_img_size_offset]

// 复制 kpimg(从 _kp_start 开始)+ extra items
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                                // 反向复制(32 位)

rmemcpy32() - 反向复制

// kernel/base/setup1.S:24-36
rmemcpy32:
    subs x2, x2, #0x4
    b.lt .r32end
.r32loop:
    ldr w3, [x1, x2]       ; 从末尾开始读
    str w3, [x0, x2]       ; 写到目标
    subs x2, x2, #0x4
    b.ge .r32loop
.r32end:
    ret

为什么反向复制?

  • 源和目标可能重叠
  • 反向复制可以避免覆盖未复制的数据
  • 类似 memmove() 的处理

复制后的内存

物理地址 PA + start_offset:
  ├─ _kp_start 开始的所有代码和数据
  ├─ Extra Items
  └─ start_preset (包含 map_backup)

map_prepare() - 劫持 paging_init

填充 map_data

// kernel/base/setup1.S:174-231
map_prepare:
    stp x29, x30, [sp, -16]!
    stp x19, x20, [sp, -16]!
    mov x19, x0  // x19 = kernel_pa
    
    // 获取地址
    adrp x9, map_data
    add x9, x9, :lo12:map_data
    adrp x10, setup_preset
    add x10, x10, :lo12:setup_preset
    
    // map_data.kernel_pa = kernel_pa
    str x19, [x9, #map_kernel_pa_offset]
    
    // map_data.map_offset = setup_preset.map_offset
    ldr x11, [x10, #setup_map_offset_offset]
    str x11, [x9, #map_map_offset_offset]
    mov x14, x11  // x14 = map_offset
    
    // map_data.paging_init_relo = setup_preset.paging_init_offset
    ldr x11, [x10, #setup_paging_init_offset_offset]
    str x11, [x9, #map_paging_init_relo_offset]
    mov x15, x11  // x15 = paging_init_offset
    
    // map_data.start_offset = setup_preset.start_offset
    ldr x11, [x10, #setup_start_offset_offset]
    str x11, [x9, #map_start_offset_offset]
    
    // map_data.start_size = _kp_end - _kp_start
    adrp x11, _kp_end
    add x11, x11, :lo12:_kp_end
    adrp x12, _kp_start
    add x12, x12, :lo12:_kp_start
    sub x11, x11, x12
    str x11, [x9, #map_start_size_offset]
    
    // map_data.extra_size = setup_preset.extra_size
    ldr x11, [x10, #setup_extra_size_offset]
    str x11, [x9, #map_extra_size_offset]
    
    // map_data.alloc_size = HOOK_ALLOC_SIZE + MEMORY_ROX_SIZE + MEMORY_RW_SIZE
    mov x11, #HOOK_ALLOC_SIZE    // 1 MB
    add x11, x11, #MEMORY_ROX_SIZE  // + 4 MB
    add x11, x11, #MEMORY_RW_SIZE   // + 2 MB
    str x11, [x9, #map_alloc_size_offset]
    
    // 复制 map_symbol(memblock 函数地址)
    add x0, x9, #map_map_symbol_offset
    add x1, x10, #setup_map_symbol_offset
    mov x2, #MAP_SYMBOL_SIZE
    bl memcpy8

map_data_t 结构

// kernel/base/setup.h:25-50
typedef struct {
    uint32_t paging_init_backup;    // paging_init 的原始指令
    int64_t map_offset;              // tcp_init_sock 偏移
    int64_t start_offset;            // kpimg 运行时位置
    int64_t start_size;              // _kp_end - _kp_start
    int64_t start_img_size;          // 要复制的 kpimg 大小
    int64_t extra_size;              // Extra Items 大小
    int64_t alloc_size;              // 额外分配大小 (7 MB)
    uint64_t kernel_pa;              // 内核物理地址
    uint64_t paging_init_relo;       // paging_init 虚拟地址
    map_symbol_t map_symbol;         // memblock 函数地址
    // 运行时填充:
    int64_t va1_bits;
    int64_t page_shift;
    uint64_t kimage_voffset;
    uint64_t linear_voffset;
} map_data_t;

备份和劫持 paging_init

// kernel/base/setup1.S:233-295
// backup and hook paging_init

// 1. 计算 paging_init 物理地址
add x13, x15, x19                   // x13 = paging_init_offset + kernel_pa

// 2. 读取第一条指令
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                          // 检查是否是 BTI
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(错误!)

// 3. 备份第一条指令
.backup:
str w12, [x9, #map_paging_init_backup_offset]
dsb ish


// ============ 写入跳转指令 ============

// 4. 计算 _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 = 替换位置

// 5. 生成 B 指令
// B_REL(src, dst) = 0x14000000 | ((dst - src) >> 2)
sub x15, x11, x15                   // dst - src
ubfx w15, w15, #2, #26              // 提取 bit[27:2],得到 imm26
mov w12, #0x14000000                // B 操作码
orr w15, w15, w12                   // 组合
str w15, [x13]                      // ❌ 写入到 paging_init

dsb ish


// ============ 复制 map 代码 ============

// 6. 复制 _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                          // ❌ 覆盖 tcp_init_sock

// 恢复寄存器
ldp x19, x20, [sp], 16
ldp x29, x30, [sp], 16
dsb ish
ret

执行后的物理内存

PA + 0x0:           内核头部(仍是 B setup_entry)
PA + map_offset:    _paging_init 代码(覆盖了 tcp_init_sock)
PA + paging_init:   B (PA + map_offset)(劫持了)
PA + start_offset:  kpimg 代码和数据

恢复内核头部并跳转

// kernel/base/setup1.S:333-364
// memcpy(kernel_pa, setup_preset.header_backup, HDR_BACKUP_SIZE)
mov x0, x19                             // 目标:kernel_pa
add x1, x20, #setup_header_backup_offset  // 源:header_backup
mov x2, #HDR_BACKUP_SIZE                // 8 字节
bl memcpy8                              // 恢复原始头部

// 刷新缓存
dsb ish
ic iallu                                // 刷新整个指令缓存
dsb ish
isb

mov x16, x19                            // x16 = kernel_pa

// ============ 恢复所有寄存器 ============

ldp x27, x28, [sp], 16
ldp x25, x26, [sp], 16
ldp x23, x24, [sp], 16
ldp x21, x22, [sp], 16
ldp x19, x20, [sp], 16
ldp x8, x18, [sp], 16
ldp x6, x7, [sp], 16
ldp x4, x5, [sp], 16
ldp x2, x3, [sp], 16
ldp x0, x1, [sp], 16                    // 恢复 FDT 地址
ldp x29, x30, [sp], 16

// 恢复原始栈指针
ldp x9, x10, [sp], 16
mov sp, x9

// ============ 跳转回内核入口 ============

br x16                                  // 跳到 kernel_pa (_head)

此时的状态

  • 内核头部已恢复(B _stext)
  • 所有寄存器恢复到 Bootloader 传递的状态
  • 栈指针恢复
  • CPU 跳转到内核原始入口 _head

内核完全不知道刚才发生了什么


辅助函数

memcpy8() - 字节复制

// kernel/base/setup1.S:10-22
.text
.align 2
.type memcpy8, %function
memcpy8:
    cmp x2, 0
    ble .l8end
.l8loop:
    ldrb w3, [x1], 1       ; 逐字节读取
    strb w3, [x0], 1       ; 逐字节写入
    subs x2, x2, 1
    cbnz x2, .l8loop
.l8end:
    ret

为什么逐字节?

  • 源地址和目标地址可能未对齐
  • 可能跨越缓存行边界
  • 确保数据完整性

rmemcpy32() - 反向 32 位复制

// kernel/base/setup1.S:24-36
.text
.align 2
.type rmemcpy32, %function
rmemcpy32:
    subs x2, x2, #0x4
    b.lt .r32end
.r32loop:
    ldr w3, [x1, x2]       ; 从末尾读取 32 位
    str w3, [x0, x2]       ; 写到目标
    subs x2, x2, #0x4
    b.ge .r32loop
.r32end:
    ret

为什么 32 位?

  • ARM64 指令是 32 位
  • 确保指令不会被拆分
  • 性能更好(减少内存访问次数)

为什么反向?

  • 处理源和目标重叠的情况
  • 如果 目标 > 源 && 目标 < 源 + 大小,正向复制会覆盖数据
  • 反向复制可以安全处理

时序图

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

T0: Bootloader 完成
    内存:[PA: 内核镜像,包含 kpimg]
    
T1: 跳转到 PA + 0x0
    CPU PC = PA (B setup_entry 指令)
    
T2: setup_entry 执行
    栈:临时栈
    寄存器:全部保存
    
T3: start_prepare() 开始
    复制配置:setup_preset → start_preset ✅
      (含 sprintf_offset, symbol_lookup_anchor_offset)
    备份 tcp_init_sock:map_backup ✅
    
T4: 复制 kpimg
    源:PA + setup_offset + 0x10000 (_kp_start)
    目标:PA + start_offset
    状态:✅ kpimg 已复制到运行时位置
    
T5: map_prepare() 开始
    填充 map_data ✅
    
T6: 备份 paging_init[0]
    map_data.paging_init_backup = *(PA + paging_init_offset) ✅
    
T7: 劫持 paging_init
    *(PA + paging_init_offset) = B (PA + map_offset) ❌
    
T8: 覆盖 tcp_init_sock
    复制 _map_start → PA + map_offset ❌
    
T9: 恢复头部并跳转
    *(PA + 0) = header_backup[0] ✅
    br PA
    
T10: 内核继续启动
    CPU 执行 _head
    寄存器:完全恢复
    栈:原始栈

数据流图

┌────────────────────────────────────────────────────────┐
│ 物理内存(PA)                                          │
├────────────────────────────────────────────────────────┤
│                                                         │
│ PA + 0x0          ┌─ Header (B setup_entry)            │
│                   │  ↓ 在 T9 恢复为 B _stext           │
│ PA + 0x40         ├─ Kernel Code                       │
│                   │                                     │
│ PA + map_offset   ├─ tcp_init_sock (原始代码)           │
│ (0xd73550)        │  ↓ 在 T3 备份到 map_backup          │
│                   │  ↓ 在 T8 覆盖为 _paging_init        │
│                   │  ↓ 在后续 restore_map() 恢复        │
│                   │                                     │
│ PA + paging_init  ├─ paging_init (原始代码)             │
│ (0x1abe5d0)       │  ↓ 在 T6 备份第一条指令             │
│                   │  ↓ 在 T7 改为 B (PA+map_offset)     │
│                   │  ↓ 在 _paging_init 中恢复           │
│                   │                                     │
│ PA + setup_offset ├─ setup_preset (只读配置)            │
│ (0x1ab3000)       ├─ setup_entry (一次性代码)           │
│                   ├─ _map_start (会被复制)              │
│                   │                                     │
│ PA + start_offset ├─ kpimg (复制后的运行时位置)         │
│ (0x1ca6000)       │  ├─ _kp_start                      │
│                   │  │   └─ start_preset ✅             │
│                   │  │       └─ map_backup ✅           │
│                   │  ├─ _kp_end                        │
│                   │  └─ Extra Items                    │
│                   │                                     │
└────────────────────────────────────────────────────────┘

关键设计决策

决策 1:为什么不在 setup 阶段恢复 tcp_init_sock?

原因:
  1. setup 阶段没有虚拟内存,只能访问物理地址
  2. 无法使用页表修改权限
  3. 代码段可能是只读的(物理内存保护)
  
解决:
  延迟到 start() 阶段恢复
  此时虚拟内存已建立,可以修改页表

决策 2:为什么需要两次劫持?

第一次劫持:内核入口 → setup_entry
  目的:在物理内存阶段执行准备工作
  限制:无虚拟内存,无法分配内存
  
第二次劫持:paging_init → _paging_init
  目的:在页表初始化时执行内存映射
  优势:可以使用 memblock 分配内存
        可以创建页表映射

决策 3:为什么临时借用 tcp_init_sock?

需求:
  _paging_init 代码必须在内核虚拟地址空间
  但 kpimg 还未映射到虚拟地址
  
解决:
  找一个启动早期不会被调用的函数
  临时覆盖,完成后恢复
  
选择 tcp_init_sock:
  ✅ 网络栈在 kernel_init 后才初始化
  ✅ 大小足够(~2KB)
  ✅ 位置稳定(所有内核版本都有)

潜在问题分析

问题 1:BTI 处理 bug

当前代码的问题

ldr w12, [x13]              // w12 = BTI C (0xD503211F)
// ...
mov w12, w3                 // w12 = NOP (0xD503201F) ❌
str w12, [x9, #map_paging_init_backup_offset]

后果

  • 备份的是 NOP 而不是 BTI C
  • 恢复时 paging_init 缺少 BTI 指令
  • 可能导致 BTI 验证失败

修复(见文档 15):

ldr w12, [x13]              // 读取原始指令
str w12, [x9, #map_paging_init_backup_offset]  // ✅ 直接备份
// 删除所有 BTI 检测和修改代码

问题 2:缓存一致性

// 必须的缓存刷新
dsb ish                     // 数据同步屏障
ic iallu                    // 刷新整个指令缓存
dsb ish
isb                         // 指令同步屏障

如果缺少

  • CPU 可能执行旧的指令(缓存中的)
  • 跳转到错误的地址
  • 数据不一致

问题 3:栈对齐

// ARM64 要求栈 16 字节对齐
adrp x11, stack
add x11, x11, :lo12:stack
add x11, x11, STACK_SIZE
mov sp, x11                 // sp 指向 stack + 2KB

// stack 定义时已对齐
struct { ... } stack __section(.setup.data) __aligned(16);

总结

setup1.S 的核心任务

  1. 计算 kernel_pa:通过自举算法获取物理地址
  2. 准备数据:填充 start_preset 和 map_data
  3. 备份关键区域:tcp_init_sock、paging_init[0]
  4. 复制 kpimg:到运行时位置(start_offset)
  5. 劫持 paging_init:写入跳转指令
  6. 恢复状态:内核头部、寄存器、栈
  7. 无缝衔接:跳回 _head,内核继续启动

代码的精妙之处

  • 仅使用汇编和物理地址
  • 无任何外部依赖
  • 完全自举
  • 对内核透明

下一篇07-paging_init阶段.md - 详解内存映射和虚拟地址初始化


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

评论

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

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