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

paging_init 阶段详解

2026-06-29 · 阅读 0

概述

_paging_init 是 KernelPatch 启动流程中最关键的阶段。它在内核页表初始化时介入,完成以下任务:

  1. ✅ 分配新的物理内存(避免与内核冲突)
  2. ✅ 创建页表映射(将 kpimg 映射到虚拟地址)
  3. ✅ 复制 kpimg 到新位置
  4. ✅ 恢复 paging_init 原始代码
  5. ✅ 调用原始 paging_init 函数
  6. ✅ 跳转到 start() 继续初始化

核心文件kernel/base/map.c(264 行)

为什么选择 paging_init?

  • 此时页表刚刚建立,可以访问虚拟内存
  • memblock 分配器可用,可以申请物理内存
  • 可以创建新的页表项
  • 还在早期阶段,大部分内核功能未初始化

执行入口

内核如何调用 _paging_init

内核启动流程:
  _head
    -> __primary_switched
      -> __primary_switch
        -> __enable_mmu
          -> __turn_mmu_on
            -> start_kernel()
              -> setup_arch()
                -> paging_init()     ← 这里被劫持了!

paging_init 的原始代码(已被修改):

paging_init:
  0x000: 14xxxxxx    B _paging_init      ; ❌ 被 map_prepare 改写
  0x004: A9BF7BFD    STP X29, X30, [SP, #-16]!
  ...

跳转到 _paging_init

_paging_init:  (位于 PA + map_offset,即 tcp_init_sock 位置)
  0x000: ...  // map.c 编译出的机器码

函数入口:mem_proc()

// kernel/base/map.c:79-126
// [0.13.2 变更] 签名从 "static map_data_t *mem_proc()" 改为传出参数形式
static __noinline void mem_proc(map_data_t *data)
{
    // [0.13.2 变更] 将 get_data() 返回的全局数据拷贝到调用者提供的栈缓冲区
    *data = *get_data();
    uint64_t kernel_va = get_kva();
    
    
    // ============ 步骤 1:地址重定位 ============
    
    data->kimage_voffset = kernel_va - data->kernel_pa;
    data->paging_init_relo += kernel_va;
    
    // [0.13.2 变更] 选择性重定位 map_symbol 中的函数地址
    // 不再用循环盲目遍历所有 8 字节槽位,改为逐个检查非零再加 kernel_va
    if (data->map_symbol.memblock_reserve_relo)
        data->map_symbol.memblock_reserve_relo += kernel_va;
    if (data->map_symbol.memblock_free_relo)
        data->map_symbol.memblock_free_relo += kernel_va;
    if (data->map_symbol.memblock_phys_alloc_relo)
        data->map_symbol.memblock_phys_alloc_relo += kernel_va;
    if (data->map_symbol.memblock_virt_alloc_relo)
        data->map_symbol.memblock_virt_alloc_relo += kernel_va;
    if (data->map_symbol.memblock_mark_nomap_relo)
        data->map_symbol.memblock_mark_nomap_relo += kernel_va;
    
    
    // ============ 步骤 2:页表参数计算 ============
    
    uint64_t tcr_el1;
    asm volatile("mrs %0, tcr_el1" : "=r"(tcr_el1));
    
    uint64_t t1sz = tcr_el1 << 42 >> 58;  // bits[21:16]
    uint64_t va1_bits = 64 - t1sz;
    data->va1_bits = va1_bits;
    
    uint64_t tg1 = tcr_el1 << 32 >> 62;   // bits[31:30]
    uint64_t page_shift = 12;
    if (tg1 == 1) {
        page_shift = 14;  // 16KB 页面
    } else if (tg1 == 3) {
        page_shift = 16;  // 64KB 页面
    }
    data->page_shift = page_shift;
    
    
    // ============ 步骤 3:探测 linear_voffset ============
    
    // [0.13.2 变更] 增加 memblock_virt_alloc_relo 有效性检查
    if (data->map_symbol.memblock_virt_alloc_relo) {
        // 分配一小块物理内存
        uint64_t detect_phys = 
            ((memblock_phys_alloc_try_nid_f)data->map_symbol.memblock_phys_alloc_relo)
            (0, 0x10, NUMA_NO_NODE);
        
        // 分配对应的虚拟内存
        uint64_t detect_virt = 
            (uint64_t)((memblock_virt_alloc_try_nid_f)data->map_symbol.memblock_virt_alloc_relo)
            (0, 0x10, detect_phys, detect_phys, NUMA_NO_NODE);
        
        // 计算 linear mapping 的偏移
        data->linear_voffset = detect_virt - detect_phys;
    } else {
        // 无法探测 linear_voffset,直接 trap(不可恢复错误)
        __builtin_trap();
    }
}

get_data() - 获取 map_data 地址

// kernel/base/map.c:36-40
map_data_t *get_data()
{
    uint64_t va = get_myva() - sizeof(map_data_t);
    return (map_data_t *)(va & ~((uint64_t)MAP_ALIGN - 1));
}

// kernel/base/map.c:29-34
uint64_t get_myva()
{
    uint64_t this_va;
    asm volatile("adr %0, ." : "=r"(this_va));
    return this_va & ~((uint64_t)MAP_ALIGN - 1);
}

原理

_paging_init 的布局:
  ┌─────────────────┐
  │  map_data_t     │ ← 数据段,16 字节对齐
  ├─────────────────┤
  │  get_myva()     │ ← 当前执行的代码
  │  get_data()     │
  │  mem_proc()     │
  │  ...            │
  └─────────────────┘

通过 ADR 获取当前 PC,
减去 map_data_t 的大小,
就能找到 map_data 的地址。

map_phys_alloc() - 物理内存分配抽象 (0.13.2 新增)

// kernel/base/map.c:50-58
// [0.13.2 新增] 统一的物理内存分配入口
static uint64_t map_phys_alloc(map_data_t *data, uint64_t size, uint64_t align)
{
    if (data->map_symbol.memblock_phys_alloc_type == MAP_SYM_MEMBLOCK_PHYS_ALLOC_TRY_NID ||
        data->map_symbol.memblock_phys_alloc_type == MAP_SYM_MEMBLOCK_ALLOC_TRY_NID) {
        return ((memblock_phys_alloc_try_nid_f)data->map_symbol.memblock_phys_alloc_relo)
            (size, align, NUMA_NO_NODE);
    }
    return 0;
}

为什么需要这个抽象?

  • 不同内核版本使用不同的 memblock 物理分配函数
    • MAP_SYM_MEMBLOCK_PHYS_ALLOC_TRY_NID:较新内核的 memblock_phys_alloc_try_nid
    • MAP_SYM_MEMBLOCK_ALLOC_TRY_NID:较旧内核的 memblock_alloc_try_nid
  • 通过 memblock_phys_alloc_type 字段(在 patching 时由用户态工具填充)区分内核版本
  • 统一调用接口,避免在 get_or_create_pte()_paging_init() 中重复判断
  • 如果类型不匹配,返回 0(分配失败),调用者可据此处理

页表项创建

get_or_create_pte() - 核心函数

// kernel/base/map.c:129-184
// [0.13.2 变更] 不再局部声明 memblock_phys_alloc_try_nid 函数指针,
//               改用 map_phys_alloc(data, ...) 统一分配
static uint64_t get_or_create_pte(map_data_t *data, uint64_t va, uint64_t pa, uint64_t attr_indx)
{
    uint64_t page_shift = data->page_shift;
    uint64_t va_bits = data->va1_bits;
    uint64_t page_level = (va_bits - 4) / (page_shift - 3);
    uint64_t pxd_bits = page_shift - 3;
    uint64_t pxd_ptrs = 1u << pxd_bits;
    
    // 读取 TTBR1_EL1(页全局目录)
    uint64_t ttbr1_el1;
    asm volatile("mrs %0, ttbr1_el1" : "=r"(ttbr1_el1));
    uint64_t baddr = ttbr1_el1 & 0xFFFFFFFFFFFE;
    uint64_t page_size = 1 << page_shift;
    uint64_t page_size_mask = ~(page_size - 1);
    uint64_t attr_prot = 0x40000000000703 | attr_indx;
    
    uint64_t pxd_pa = baddr & page_size_mask;  // PGD 物理地址
    uint64_t pxd_va = phys_to_lm(data, pxd_pa); // PGD 虚拟地址
    uint64_t pxd_entry_va = 0;
    uint64_t block_lv = 0;
    
    // 遍历页表层级
    for (uint64_t lv = 4 - page_level; lv < 4; lv++) {
        uint64_t pxd_shift = (page_shift - 3) * (4 - lv) + 3;
        uint64_t pxd_index = (va >> pxd_shift) & (pxd_ptrs - 1);
        
        pxd_entry_va = pxd_va + pxd_index * 8;
        if (!pxd_entry_va) return 0;
        
        uint64_t pxd_desc = *((uint64_t *)pxd_entry_va);
        
        if ((pxd_desc & 0b11) == 0b11) {
            // Table descriptor
            pxd_pa = pxd_desc & (((1ul << (48 - page_shift)) - 1) << page_shift);
        } else if ((pxd_desc & 0b11) == 0b01) {
            // Block descriptor
            uint64_t block_bits = (3 - lv) * pxd_bits + page_shift;
            pxd_pa = pxd_desc & (((1ul << (48 - block_bits)) - 1) << block_bits);
            block_lv = lv;
        } else {
            // Invalid - 需要分配新页表
            if (lv != 3) {
                pxd_pa = map_phys_alloc(data, page_size, page_size);  // [0.13.2] 使用统一分配
                alloc_flag = 1;
            } else {
                pxd_pa = pa;  // 最后一级,直接使用目标 pa
            }
            pxd_desc = (pxd_pa) | attr_prot;
            *((uint64_t *)pxd_entry_va) = pxd_desc;
        }
        
        pxd_va = phys_to_lm(data, pxd_pa);
        
        // 如果是新分配的页表,清零
        if (alloc_flag) {
            for (uint64_t i = pxd_va; i < pxd_va + page_size; i += 8) {
                *(uint64_t *)i = 0;
            }
        }
        
        if (block_lv) break;
    }
    
    return pxd_entry_va;  // 返回 PTE 地址
}

ARM64 页表层级(4KB 页面,48 位 VA):

Level 0 (PGD): bits[47:39]  - 512 GB 每项
Level 1 (PUD): bits[38:30]  - 1 GB 每项
Level 2 (PMD): bits[29:21]  - 2 MB 每项(可以是 block)
Level 3 (PTE): bits[20:12]  - 4 KB 每项

_paging_init() 主函数

完整流程

// kernel/base/map.c:187-264
void _paging_init()
{
    // [0.13.2 变更] data 从全局指针改为栈变量
    // mem_proc() 不再返回指针,而是将 get_data() 的内容拷贝到栈缓冲区
    map_data_t buf;
    map_data_t *data = &buf;
    mem_proc(data);
    
    
    // ============ 阶段 1:计算大小 ============
    
    uint64_t page_size = 1 << data->page_shift;
    uint64_t old_start_pa = data->start_offset + data->kernel_pa;
    uint64_t reserve_size = data->start_img_size + data->extra_size;
    uint64_t align_extra_size = (data->extra_size + page_size - 1) & ~(page_size - 1);
    uint64_t all_size = data->start_size + align_extra_size + data->alloc_size;
    
    // all_size 包括:
    //   - _kp_start 到 _kp_end (代码 + 数据)
    //   - extra items(对齐)
    //   - HOOK_ALLOC_SIZE (1 MB)
    //   - MEMORY_RW_SIZE (2 MB)
    //   - MEMORY_ROX_SIZE (4 MB)
    // 总共约 2 + 1 + 2 + 4 = 9 MB
    
    
    // ============ 阶段 2:保留旧位置 ============
    
    // 防止 memblock 分配时占用这块内存
    ((memblock_reserve_f)data->map_symbol.memblock_reserve_relo)
        (old_start_pa, reserve_size);
    
    
    // ============ 阶段 3:分配新内存 ============
    
    // [0.13.2] 使用 map_phys_alloc() 统一分配
    uint64_t start_pa = map_phys_alloc(data, all_size, page_size);
    
    // 标记为 nomap(不自动映射到 linear region)
    if (data->map_symbol.memblock_mark_nomap_relo)
        ((memblock_mark_nomap_f)(data->map_symbol.memblock_mark_nomap_relo))
        (start_pa, all_size);
    
    
    // ============ 阶段 4:恢复 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))();
    
    // ⚠️ 注意:原始 map_data 位于 tcp_init_sock 区域,paging_init 调用后该区域可能被内核使用
    // 但 0.13.2 中 data 已拷贝到栈缓冲区,此后仍可安全读取 data
    
    
    // ============ 阶段 5:创建页表映射 ============
    
    // 获取内核代码段的属性(用于 kpimg)
    uint64_t ktext_pte = get_or_create_pte(data, data->paging_init_relo, 0, 0);
    uint64_t attrs = *(uint64_t *)ktext_pte;
    uint64_t attr_indx = attrs & 0b11100;  // Memory Attribute Index
    
    // 清除 WXN (Write eXecute Never)
    uint64_t sctlr_el1 = 0;
    asm volatile("mrs %[reg], sctlr_el1" : [reg] "+r"(sctlr_el1));
    sctlr_el1 &= 0xFFFFFFFFFFF7FFFF;  // 清除 bit 19
    asm volatile("msr sctlr_el1, %[reg]" : : [reg] "r"(sctlr_el1));
    
    // 计算虚拟地址
    uint64_t old_start_va = phys_to_lm(data, old_start_pa);
    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);
        
        // 设置权限:RWX (临时,后续 prot_myself 会细分)
        *(uint64_t *)entry = (*(uint64_t *)entry | 0x8000000000000) & 0xFFDFFFFFFFFFFF7F;
    }
    flush_tlb_all();
    
    
    // ============ 阶段 6:复制 kpimg 到新位置 ============
    
    // 清零新内存
    for (uint64_t i = start_va; i < start_va + all_size; i += 8) {
        *(uint64_t *)i = 0;
    }
    
    // 复制 kpimg 代码和数据
    for (uint64_t i = 0; i < data->start_img_size; i += 8) {
        *(uint64_t *)(start_va + i) = *(uint64_t *)(old_start_va + i);
    }
    
    // 复制 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();
    
    
    // ============ 阶段 7:释放旧内存 ============
    
    ((memblock_free_f)data->map_symbol.memblock_free_relo)
        (old_start_pa, reserve_size);
    
    
    // ============ 阶段 8:跳转到 start() ============
    
    ((start_f)start_va)(data->kimage_voffset, data->linear_voffset);
}

地址计算详解

kimage_voffset - 内核虚拟偏移

// kernel/base/map.c:85
data->kimage_voffset = kernel_va - data->kernel_pa;

// 示例:
// kernel_pa = 0x0000000080000000 (物理地址)
// kernel_va = 0xFFFFFFC008000000 (虚拟地址)
// kimage_voffset = 0xFFFFFFC008000000 - 0x80000000
//                = 0xFFFFFFFBF8000000

用途

// 物理地址 → 虚拟地址
uint64_t phys_to_kimg(uint64_t phys)
{
    return phys + kimage_voffset;
}

linear_voffset - Linear Mapping 偏移

// kernel/base/map.c:112-121
// [0.13.2 变更] 增加有效性检查
if (data->map_symbol.memblock_virt_alloc_relo) {
    uint64_t detect_phys = memblock_phys_alloc(...);
    uint64_t detect_virt = memblock_virt_alloc(...);
    data->linear_voffset = detect_virt - detect_phys;
} else {
    __builtin_trap();  // 无法探测,直接 trap
}

// 示例:
// detect_phys = 0x0000000082345000
// detect_virt = 0xFFFFFF8002345000
// linear_voffset = 0xFFFFFF8002345000 - 0x82345000
//                = 0xFFFFFF7F80000000

ARM64 内存布局

虚拟地址空间(48 位 VA):
  0x0000000000000000 - 0x0000FFFFFFFFFFFF  用户空间
  0xFFFF000000000000 - 0xFFFF7FFFFFFFFFFF  (未使用)
  0xFFFF800000000000 - 0xFFFFBFFFFFFFFFFF  Linear Mapping (直接映射物理内存)
  0xFFFFC00000000000 - 0xFFFFFBFFFFFFFFFF  vmalloc / ioremap 区域
  0xFFFFFC0000000000 - 0xFFFFFFFFFFFFFFFF  Kernel Image (模块)

用途

// 物理地址 → Linear Mapping 虚拟地址
uint64_t phys_to_lm(uint64_t phys)
{
    return phys + linear_voffset;
}

内存分配流程

memblock 分配器

// 在页表初始化阶段,只有 memblock 分配器可用
// 后续会切换到 buddy 分配器和 slab 分配器

// memblock_phys_alloc_try_nid: 分配物理内存
phys_addr_t phys = memblock_phys_alloc_try_nid(
    size,        // 大小
    align,       // 对齐
    nid          // NUMA 节点(0 = 任意)
);

// memblock_virt_alloc_try_nid: 分配虚拟内存(并自动映射)
void *virt = memblock_virt_alloc_try_nid(
    size, 
    align, 
    min_addr,    // 最小物理地址
    max_addr,    // 最大物理地址
    nid
);

// memblock_reserve: 保留内存(防止被分配)
memblock_reserve(base, size);

// memblock_free: 释放内存
memblock_free(base, size);

// memblock_mark_nomap: 标记为不自动映射
memblock_mark_nomap(base, size);

分配示例

物理内存布局(假设):
  0x80000000 ────┐
                │ Kernel Image (27 MB)
  0x81b00000 ────┤
                │ (空闲)
  0x90000000 ────┤
                │ Reserved (设备)
                
调用:
  start_pa = memblock_phys_alloc_try_nid(9 MB, 4 KB, 0)
  
结果:
  start_pa = 0x81b00000 (或其他空闲区域)
  
调用:
  memblock_mark_nomap(0x81b00000, 9 MB)
  
效果:
  这块内存不会被自动映射到 Linear Mapping
  我们需要手动创建页表映射

页表映射流程

映射 kpimg

// kernel/base/map.c:240-244
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 = (*(uint64_t *)entry | 0x8000000000000) & 0xFFDFFFFFFFFFFF7F;
}

页表项格式(Level 3,4KB 页面):

63    59 58  55 54  53  52 51  48 47          12 11 10 9  8 7 6 5 4  3 2 1 0
┌──────┬──────┬──┬───┬──┬──────┬──────────────┬────────────────────────┬─┬─┬─┐
│ Ign  │ SW   │UXN│PXN│C │ Ign  │   PA[47:12]  │  Attrs                 │1│1│Valid│
└──────┴──────┴──┴───┴──┴──────┴──────────────┴────────────────────────┴─┴─┴─┘

设置:
  | 0x8000000000000  (bit 51: Contiguous)
  & ~0x0020000000000080 (清除 bit 53: PXN, bit 7: RDONLY)
  
结果:RWX 权限(临时)

映射后

虚拟地址 start_va (例如 0xFFFFFFC00A000000)
  ↓ 页表映射
物理地址 start_pa (例如 0x81b00000)

恢复 paging_init

为什么在这里恢复?

// kernel/base/map.c:215-219
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))();

时序

T1: 进入 _paging_init
    paging_init[0] = B _paging_init ❌

T2: 恢复指令
    paging_init[0] = 原始指令(BTI C 或其他)✅

T3: 调用原始 paging_init
    paging_init() 正常执行 ✅
    完成页表初始化
    
T4: paging_init 返回
    继续 _paging_init

为什么在调用前恢复?

  • paging_init 可能会扫描自己的代码
  • 避免检测到异常(跳转指令)
  • 保持内核的完整性

复制数据

8 字节对齐复制

// kernel/base/map.c:246-254
// 清零
for (uint64_t i = start_va; i < start_va + all_size; i += 8) {
    *(uint64_t *)i = 0;
}

// 复制 kpimg
for (uint64_t i = 0; i < data->start_img_size; i += 8) {
    *(uint64_t *)(start_va + i) = *(uint64_t *)(old_start_va + i);
}

// 复制 extra
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 字节?

  • ARM64 是 64 位架构
  • 8 字节对齐的访问更高效
  • 减少内存访问次数

复制的内容

源(旧位置)                    目标(新位置)
old_start_va                   start_va
  ├─ _kp_start                   ├─ _kp_start
  │   ├─ .text                   │   ├─ .text
  │   ├─ .data                   │   ├─ .data
  │   │   └─ start_preset       │   │   └─ start_preset
  │   │       └─ map_backup     │   │       └─ map_backup ✅
  │   └─ _kp_end                 │   └─ _kp_end
  └─ Extra Items                 └─ Extra Items

缓存和 TLB 刷新

flush_icache_all()

// kernel/base/map.c:64-70
static void flush_icache_all(void)
{
    asm volatile("dsb ish" : : : "memory");    // 数据同步屏障
    asm volatile("ic ialluis");                 // 刷新所有 CPU 的指令缓存
    asm volatile("dsb ish" : : : "memory");
    asm volatile("isb" : : : "memory");        // 指令同步屏障
}

为什么需要?

  • 修改了代码(paging_init 指令)
  • 修改了页表(新的映射)
  • CPU 的指令缓存可能有旧数据
  • 必须刷新才能执行新代码

flush_tlb_all()

// kernel/base/map.c:54-62
static void flush_tlb_all()
{
    asm volatile("dsb ishst" : : : "memory");
    asm volatile("tlbi vmalle1is\n"           // 刷新所有 EL1 的 TLB
                 "dsb ish\n"
                 "tlbi vmalle1is\n");          // 双重刷新(确保生效)
    asm volatile("dsb ish" : : : "memory");
    asm volatile("isb" : : : "memory");
}

为什么需要?

  • 创建了新的页表项
  • TLB (Translation Lookaside Buffer) 缓存旧的映射
  • 不刷新会导致访问错误的物理地址

跳转到 start()

// kernel/base/map.c:262
((start_f)start_va)(data->kimage_voffset, data->linear_voffset);

// start_f 定义:
typedef int (*start_f)(uint64_t kimage_voffset, uint64_t linear_voffset);

// 等价于:
int (*start)(uint64_t, uint64_t) = (void *)start_va;
start(kimage_voffset, linear_voffset);

传递的参数

  • kimage_voffset:用于物理地址 → 内核虚拟地址转换
  • linear_voffset:用于物理地址 → Linear Mapping 转换

start_va 的计算

start_va = start_pa + kimage_voffset
         = 新分配的物理地址 + 偏移
         = kpimg 在内核虚拟地址空间的位置

物理地址转换函数

phys_to_lm() - 物理到 Linear Mapping

// kernel/base/map.c:48-49 (位于 map_phys_alloc 之前)
static inline uint64_t phys_to_lm(map_data_t *data, uint64_t phys)
{
    return phys + data->linear_voffset;
}

使用场景

// 访问页表(PGD/PUD/PMD/PTE)
uint64_t pgd_pa = 0x80000000;
uint64_t pgd_va = phys_to_lm(data, pgd_pa);
uint64_t *pgd = (uint64_t *)pgd_va;  // 可以访问

内存分配全景

分配前

物理内存:
  0x80000000 ────┐
                │ Kernel Image
  0x81b00000 ────┤
                │
  0x82000000 ────┤
                │ (DMA/设备保留)
  0x90000000 ────┤
                │ (空闲)
  0xA0000000 ────┘

分配调用

// [0.13.2] 通过 map_phys_alloc() 统一入口分配
start_pa = map_phys_alloc(data, all_size, page_size);
// 内部根据 memblock_phys_alloc_type 分发到对应的 memblock API

分配后

物理内存:
  0x80000000 ────┐
                │ Kernel Image
  0x81b00000 ────┤
                │ kpimg (9 MB, mark_nomap) ← start_pa
  0x82400000 ────┤
                │ (其他)
  0x90000000 ────┘

虚拟内存:
  0xFFFFFC0008000000 ────┐
                         │ Kernel Image (内核映射区)
  0xFFFFFC0009b00000 ────┘

  0xFFFFFF8001b00000 ────┐
                         │ Kernel (Linear Mapping)
  0xFFFFFF8002400000 ────┘

  0xFFFFFFC00A000000 ────┐ ← start_va (手动映射)
                         │ kpimg
  0xFFFFFFC00A900000 ────┘

TCR_EL1 寄存器解析

// kernel/base/map.c:98-110
uint64_t tcr_el1;
asm volatile("mrs %0, tcr_el1" : "=r"(tcr_el1));

// TCR_EL1 (Translation Control Register)
// 控制页表参数

uint64_t t1sz = tcr_el1 << 42 >> 58;  // bits[21:16]
// T1SZ: TTBR1_EL1 的地址空间大小
// VA_bits = 64 - T1SZ

uint64_t va1_bits = 64 - t1sz;
// 48 位 VA: T1SZ = 16, va1_bits = 48
// 39 位 VA: T1SZ = 25, va1_bits = 39

uint64_t tg1 = tcr_el1 << 32 >> 62;   // bits[31:30]
// TG1: TTBR1_EL1 的页面粒度
// 01 = 16 KB
// 10 = 4 KB (通常)
// 11 = 64 KB

uint64_t page_shift = 12;  // 默认 4KB
if (tg1 == 1) page_shift = 14;
else if (tg1 == 3) page_shift = 16;

TCR_EL1 字段

63  39 38 37 36 32 31 30 29 16 15 14 13 12 11 10  8 7 6 5 0
┌────┬──┬──┬─────┬────┬──────┬──┬──┬──┬──┬──┬────┬────────┐
│    │TBI│AS│IPS  │TG1 │ T1SZ │  │  │  │  │  │TG0 │ T0SZ   │
└────┴──┴──┴─────┴────┴──────┴──┴──┴──┴──┴──┴────┴────────┘

页表遍历算法

4 级页表

// 4KB 页面,48 位 VA
page_level = (48 - 4) / (12 - 3) = 44 / 9 = 4

Level 0 (PGD): VA[47:39], 512 项
Level 1 (PUD): VA[38:30], 512 项
Level 2 (PMD): VA[29:21], 512 项
Level 3 (PTE): VA[20:12], 512

遍历示例

映射:VA = 0xFFFFFFC00A123000 -> PA = 0x81b00000

Level 0:
  index = VA[47:39] = 0x1F0
  entry = PGD[0x1F0]
  
Level 1:
  index = VA[38:30] = 0x002
  entry = PUD[0x002]
  
Level 2:
  index = VA[29:21] = 0x009
  entry = PMD[0x009]
  
Level 3:
  index = VA[20:12] = 0x123
  PTE[0x123] = 0x81b00000 | 属性

释放旧内存

// kernel/base/map.c:259
memblock_free(old_start_pa, reserve_size);

// old_start_pa = data->start_offset + data->kernel_pa
//              = 0x1ca6000 + 0x80000000
//              = 0x81ca6000 (setup 阶段复制的位置)

// 现在 kpimg 已经复制到新位置,旧的可以释放

内存回收

释放前:
  0x80000000: Kernel
  0x81b00000: kpimg (新位置,保留)
  0x81ca6000: kpimg (旧位置,仍保留)

释放后:
  0x80000000: Kernel
  0x81b00000: kpimg (新位置)
  0x81ca6000: (空闲,可被 memblock 再分配)

关键点总结

为什么 _paging_init 能工作?

  1. 位于内核地址空间:通过覆盖 tcp_init_sock 实现
  2. 可以访问 map_data:数据段紧随代码段(0.13.2 中拷贝到栈缓冲区)
  3. 可以调用 memblock API:函数地址提前填充在 map_symbol,通过 map_phys_alloc() 统一分配
  4. 可以操作页表:通过 Linear Mapping 访问物理页表
  5. 执行完立即失效:tcp_init_sock 被恢复,_paging_init 代码消失

设计的精妙之处

  1. 自举计算:从当前位置推算 kernel_pa
  2. 最小侵入:只临时借用 2KB 空间
  3. 完全恢复:执行完后内核完全正常
  4. 内存分离:运行时 kpimg 不与内核冲突

0.13.2 架构改进

  1. 栈缓冲区隔离mem_proc() 将全局 map_data 拷贝到栈,避免后续 paging_init 调用后 tcp_init_sock 区域被内核回收导致数据失效
  2. 分配器抽象map_phys_alloc() 屏蔽不同内核版本的 memblock API 差异
  3. 选择性重定位:逐个字段检查非零后再加 kernel_va,比盲目遍历更安全(避免意外修改填充字段或类型字段)
  4. 防御性编程memblock_virt_alloc_relo 无效时调用 __builtin_trap(),而非继续执行并产生未定义行为

故障排查

问题 1:memblock_phys_alloc 失败

// [0.13.2] map_phys_alloc() 返回 0 的两种情况:
// 1. memblock_phys_alloc_type 不匹配任何已知类型
// 2. 底层 memblock_phys_alloc_try_nid 返回 0(内存不足)
start_pa = map_phys_alloc(data, all_size, page_size);
if (!start_pa) {
    // 内存分配失败
    // 可能原因:
    //   - 物理内存不足
    //   - all_size 太大
    //   - memblock_phys_alloc_type 未正确设置
    //   - memblock 算法问题
}

调试

  • 减小 HOOK_ALLOC_SIZE / MEMORY_RW_SIZE / MEMORY_ROX_SIZE
  • 检查设备的物理内存大小
  • 检查 memblock_phys_alloc_type 是否被 patching 工具正确填充

问题 2:页表创建失败

uint64_t entry = get_or_create_pte(data, va, pa, attr_indx);
if (!entry) {
    // 页表项创建失败
    // 可能原因:
    //   - memblock_phys_alloc 失败
    //   - 页表结构损坏
}

问题 3:复制后代码无法执行

可能原因:
  1. 指令缓存未刷新
  2. 虚拟地址映射错误
  3. 页表权限不正确(NX 位设置错误)
  4. start_va 计算错误

验证

// 在 start() 开始时打印
log_boot("start_va: %llx, start_pa: %llx\n", start_va, start_pa);
log_boot("First instruction: %08x\n", *(uint32_t *)start_va);

总结

_paging_init 的核心任务

  1. 桥接物理和虚拟:建立 kpimg 的虚拟地址映射
  2. 内存隔离:分配独立的物理内存
  3. 恢复内核:恢复 paging_init,使内核继续正常启动
  4. 传递控制:跳转到 start(),开始真正的 patch 工作

技术亮点

  • 在极限环境下(只有 2KB 空间)完成复杂操作
  • 直接操作页表和 memblock
  • 完美处理缓存一致性
  • 无缝切换到虚拟内存环境

下一篇08-start与patch阶段.md - 详解核心初始化和 hook 安装


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

评论

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

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