paging_init 阶段详解
2026-06-29 · 阅读 0
概述
_paging_init 是 KernelPatch 启动流程中最关键的阶段。它在内核页表初始化时介入,完成以下任务:
- ✅ 分配新的物理内存(避免与内核冲突)
- ✅ 创建页表映射(将 kpimg 映射到虚拟地址)
- ✅ 复制 kpimg 到新位置
- ✅ 恢复 paging_init 原始代码
- ✅ 调用原始 paging_init 函数
- ✅ 跳转到 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_nidMAP_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 能工作?
- ✅ 位于内核地址空间:通过覆盖 tcp_init_sock 实现
- ✅ 可以访问 map_data:数据段紧随代码段(0.13.2 中拷贝到栈缓冲区)
- ✅ 可以调用 memblock API:函数地址提前填充在 map_symbol,通过
map_phys_alloc()统一分配 - ✅ 可以操作页表:通过 Linear Mapping 访问物理页表
- ✅ 执行完立即失效:tcp_init_sock 被恢复,_paging_init 代码消失
设计的精妙之处
- ✅ 自举计算:从当前位置推算 kernel_pa
- ✅ 最小侵入:只临时借用 2KB 空间
- ✅ 完全恢复:执行完后内核完全正常
- ✅ 内存分离:运行时 kpimg 不与内核冲突
0.13.2 架构改进
- ✅ 栈缓冲区隔离:
mem_proc()将全局 map_data 拷贝到栈,避免后续 paging_init 调用后 tcp_init_sock 区域被内核回收导致数据失效 - ✅ 分配器抽象:
map_phys_alloc()屏蔽不同内核版本的 memblock API 差异 - ✅ 选择性重定位:逐个字段检查非零后再加 kernel_va,比盲目遍历更安全(避免意外修改填充字段或类型字段)
- ✅ 防御性编程:
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 的核心任务:
- ✅ 桥接物理和虚拟:建立 kpimg 的虚拟地址映射
- ✅ 内存隔离:分配独立的物理内存
- ✅ 恢复内核:恢复 paging_init,使内核继续正常启动
- ✅ 传递控制:跳转到 start(),开始真正的 patch 工作
技术亮点:
- 在极限环境下(只有 2KB 空间)完成复杂操作
- 直接操作页表和 memblock
- 完美处理缓存一致性
- 无缝切换到虚拟内存环境
下一篇:08-start与patch阶段.md - 详解核心初始化和 hook 安装
文档版本:2.0
最后更新:2026-06-26
评论
- 还没有评论,来说点什么吧。