本文是内存虚拟化一文的补充,详细介绍 map_boot_mem 启动页表的构建细节。
1. 函数定位与启动链路
minos 内核在 EL2 运行,启用 MMU 后所有代码/数据都必须有 VA→PA 映射。map_boot_mem 就是在 MMU 使能之前,用汇编手工把内核自己的 stage1 页表搭起来的函数。
启动时序(boot.S):
1 2 3 4 5 6 7 8 9 10
| _start → minos_os_start │ ├─ ① 计算并保存 minos_start(入口地址 2M 对齐) ├─ ② 配置 VBAR/DAIF,清 cache / TLB ├─ ③ 写 TTBR0/TTBR1 = __stage1_page_table(此时还是空表) ├─ ④ 配 MAIR / TCR ├─ ⑤ 计算 minos_bootmem_base / minos_stack_top(bootmem 区起点) ├─ ⑥ 清 bss / percpu ├─ ⑦ ★ bl map_boot_mem(本函数:把页表真正填满) ├─ ⑧ 开 MMU(SCTLR.M/C/I)→ br mmu_on → arch_main
|
关键点:函数执行时 MMU 还没开,CPU 直接访问物理地址。所以页表页、内核符号都要当物理地址用(qemu 配置 CONFIG_PTOV_MASK=0,VA==PA,asm_vtop 是空操作,符号天然就是物理地址)。
2. 寄存器别名
文件顶部用 .req 给 x 寄存器起了语义化名字,读懂别名才能看懂函数体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| ; ★ 重要:mem_map.S 的命名整体错位一级(历史遗留)! ; page_table 实为 PUD 表、ttb0_pud 实为 PMD 表、ttb0_pmd 实为 PTE 表 ; 真实结构(SL0 使能,从 Level 1 开始,无 PGD 级): ; TTBR → [PUD 表] → [PMD 表] → [PTE 表] → 物理地址 page_table .req x24 // 实为 PUD 表基址(每项 1G,只填前 4 项覆盖 4GB) ttb0_pud .req x21 // 实为 PMD 表基址(每项 2M,4 张覆盖 4GB) ttb0_pmd .req x22 // 实为 PTE 表基址(每项 4K,按需分配)
vaddr .req x5 // 要映射的虚拟地址 paddr .req x6 // 对应的物理地址 size .req x7 // 映射字节数 pte_attr .req x8 // 页表项属性
tmp_const .req x9 // 临时常量(2M block 起始地址) pud_tmp .req x10 pmd_tmp .req x12 // 当前操作的页表指针(build_normal/io_pte_table 里指向 PTE 表) tmp .req x13 pte_value .req x14 // 组装好的 descriptor 值 pte_index .req x15 // 页表项序号 entry_size .req x16 entry_align .req x17 entry_mask .req x18 pagetable_base .req x23 // ★ bootmem 空闲页分配指针
|
两个辅助宏:
asm_get_boot_page(mem_map.S:57-60):从 pagetable_base 取一页并 +4096。
asm_vtop(asm_marco.S:20-21):符号 VA→物理地址。qemu 下 PTOV_MASK=0 时为空操作。
3. 主函数结构
map_boot_mem 的逻辑分 5 步:
1 2 3 4 5
| ① 初始化:保存 LR、确定空闲页起点、清零页表根(PUD 表) ② 建骨架:build_page_table(PUD→PMD→PTE,RAM 主体 2M block) ③ 精确映射:内核镜像各段按 4K 页填 PTE(build_normal_pte_table × 6) ④ 设备映射:UART 用 Device 属性(build_io_pte_table) ⑤ 收尾:记录 bootmem 分配位置到 minos_end
|
下面逐段展开。
4. 初始化
1 2 3 4 5 6 7 8 9 10 11 12
| map_boot_mem: mov x26, x30 // ① 保存返回地址到 x26
adr pagetable_base, minos_stack_top // ② 取符号地址 ldr pagetable_base, [pagetable_base] // ② 解引用 → 真正的值(bootmem 起点)
mov x1, #0 // ③ memset: value = 0 mov x2, #4096 // ③ memset: length = 4096 ldr page_table, = __stage1_page_table // ③ 页表根符号(实为 PUD 表) asm_vtop page_table // ③ VA → 物理地址 mov x0, page_table // ③ memset: dest bl memset // ③ 页表根 4KB 清零
|
四个要点:
- 返回地址:
bl 跳进来时 LR(x30)被覆盖,先挪到 x26 保存,函数末尾 ret x26 返回。
- pagetable_base 来源:
minos_stack_top 存的是 bootmem 区的顶部地址(boot.S:264-268 里 = __minos_end 4K 对齐 + 所有核栈空间)。adr 取符号地址、ldr 解引用拿到值——这是汇编里”读取全局变量”的标准两连操作。
- 页表根为什么清零:
__stage1_page_table(实为 PUD 表)在链接脚本里只占了 4KB 空间(minos.lds.S:89-93),内容是垃圾,必须清零后才能往上填。
- 为什么用物理地址:MMU 未开,指令访问的是物理地址空间。qemu 下 VA==PA 所以无感,但代码仍保留
asm_vtop 以便在 PTOV!=0 的平台工作。
5. 建立页表骨架
⚠️ mem_map.S 的变量命名整体错位一级。真实层级是 PUD→PMD→PTE 三级(SL0 使能,从 Level 1 开始,无 PGD 级),但代码把:
__stage1_page_table(page_table,x24)叫”PGD”,实为 PUD 表(Level 1,每项 1G)
ttb0_pud(x21)叫”PUD”,实为 PMD 表(Level 2,每项 2M)
ttb0_pmd(x22)叫”PMD”,实为 PTE 表(Level 3,每项 4K)
验证依据:索引粒度。loop_pud 用 0..3 索引(每项 1G → PUD);loop_pmd 用 >>21(每项 2M → PMD);build_normal_pte_table 用 >>12(每项 4K → PTE)。
5.1 分配 4 张 PMD 表
1 2 3 4 5
| mov x3, #4 // 需要 4 张 PMD 表 mov ttb0_pud, pagetable_base // ttb0_pud 实为 PMD 表基址 = bootmem 分配起点 ldr x2, =(4096 << 2) // 4 页 × 4KB = 16KB add pagetable_base, pagetable_base, x2 // 分配指针前进 16KB bl memset // 4 张 PMD 表清零
|
为什么 4 张 PMD 表? 一张 PMD 表 512 项,每项覆盖 2M → 一张覆盖 1G。启动要映射 4GB,所以 4 张 PMD 表正好覆盖 [0, 4G)。
5.2 建立 PUD 到 PMD 的映射
1 2 3 4 5 6 7 8 9 10
| mov entry_size, #4 // 要填 4 个 PUD 项 mov tmp, ttb0_pud // tmp = 第一张 PMD 表 ldr x1, =HOST_TABLE_DES // = S1_DES_TABLE (0b11):指向下一级表的标记 loop_pud: orr pte_value, tmp, x1 // PMD 表物理地址 | TABLE 位 str pte_value, [page_table, pte_index, lsl #3] // 写进 PUD 表项(每项 8 字节) add pte_index, pte_index, #1 add tmp, tmp, #0x1000 // 下一张 PMD 表 sub entry_size, entry_size, #1 cbnz entry_size, loop_pud
|
结果:PUD[0] = PMD0 物理地址 | 0b11,PUD[1] = PMD1 | 0b11,… PUD 前 4 项指向 4 张 PMD 表,覆盖 4GB 虚拟空间。page_table(即 __stage1_page_table)就是这张 PUD 表,TTBR 直接指向它。
5.3 计算 PTE 表数量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| // 计算"需要按 4K 页精确映射的字节数" adr vaddr, minos_bootmem_base ldr vaddr, [vaddr] // bootmem 起点(__minos_end 4K 对齐) mov tmp, vaddr ldr entry_mask, =0x1fffff // 2M-1 add tmp, tmp, entry_mask // 向上 2M 对齐 mvn entry_align, entry_mask and tmp, tmp, entry_align // tmp = bootmem 起点向上 2M 对齐 adr pte_value, minos_start ldr pte_value, [pte_value] // minos_start(内核入口) mov tmp_const, tmp // 保存 2M 对齐的 bootmem 起点 sub size, tmp, pte_value // size = [minos_start, bootmem_2M) 字节数
lsr x3, size, #21 // → 需要几张 PTE 表(每张覆盖 2M) lsl x2, x3, #12 // ×4KB mov ttb0_pmd, pagetable_base add pagetable_base, pagetable_base, x2 // 分配 PTE 表空间 bl memset // PTE 表清零
|
这段涉及三个量的区分,理清如下:
| 量 |
含义 |
minos_start |
内核镜像入口(链接地址),经 boot.S:156-165 保存 |
minos_bootmem_base |
bootmem 分配区起点(内核镜像之后,__minos_end 4K 对齐) |
[minos_start, bootmem_2M对齐) |
内核镜像本体区间,需要按 4K 页精确映射(代码/数据/RO 属性不同) |
[bootmem_2M对齐, CONFIG_MINOS_RAM_SIZE) |
minos RAM 主体,统一用 2M block 粗粒度映射 |
**x3 = size >> 21 算的是”镜像需要几张 PTE 表”**:
- 一张 PTE 表 = 512 项 × 每项 4K = 覆盖 2M
size >> 21 = 镜像字节数 ÷ 2M = 需要几张这样的 PTE 表
- 例:镜像 2MB → x3 = 1 → 分配 4KB,正好一张 PTE 表装下 512 个 4K 页
⚠️ ttb0_pmd(x22)实为 PTE 表基址(每项 4K,Level 3),”PMD”是错位的名字。
5.4 建立 PMD 到 PTE 的映射
1 2 3 4 5 6 7 8 9 10 11 12
| adr vaddr, minos_start ldr vaddr, [vaddr] // minos_start 实际地址 lsr pte_index, vaddr, #21 // → 从第几个 2M 块开始挂表(PMD 项序号) mov tmp, ttb0_pmd ldr x1, =HOST_TABLE_DES loop_pmd: orr pte_value, tmp, x1 // PTE 表地址 | TABLE str pte_value, [ttb0_pud, pte_index, lsl #3] // 写 PMD 表项 add pte_index, pte_index, #1 add tmp, tmp, #4096 // 下一张 PTE 表 sub x3, x3, #1 cbnz x3, loop_pmd
|
即 PMD[minos_start/2M + i] = 第 i 张 PTE 表 | 0b11:把镜像区间对应的 PMD 项指向 5.3 分配的 PTE 表。
5.5 RAM 主体的 2M block 映射
1 2 3 4 5 6 7 8 9 10 11 12
| ldr x2, =CONFIG_MINOS_RAM_SIZE // 0x4000000 = 64MB sub size, x2, size // 总 RAM − 已按 4K 映射部分 lsr size, size, #21 // → 2M 块数量 mov paddr, tmp_const // 从 2M 对齐的 bootmem 起点开始 ldr pte_attr, =BOOTMEM_DATA_BLK_ATTR // 2M block 属性 loop_pmd_blk: orr pte_value, paddr, pte_attr // 物理地址 | block 属性 str pte_value, [ttb0_pud, pte_index, lsl #3] // 直接写 PMD 项 add pte_index, pte_index, #1 add paddr, paddr, #0x200000 // 物理地址 +2M sub size, size, #1 cbnz size, loop_pmd_blk
|
注意:这里 block descriptor(S1_DES_BLOCK)是直接填在 PMD 表项上的(2M block 正确出现在 Level 2 / PMD),不经过 PTE 表。这样 RAM 主体 walk 到 PMD 就命中 block,省一张 PTE 表 + 省 512 个 PTE,启动性能更好。
骨架建完后的完整映射(以地址为主轴,数值来自 minos.elf 实际链接结果):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| Address Mapping Description ───────────────────────────────────────────────────────────────── 0x0000_0000 ──────────────── PMD entry = 0 Not mapped ~ 0x4000_0000 ───────────────────────────────────────────────────────────────── 0x4000_0000 minos_start ──┐ │ reserved (RW) │ ├─ PTE table ── 4K pages 0x4000_0000 __code_start ─┤ │ │ code (RX) │ │ 0x4002_9000 __code_end ───┤ │ ← this 2MB built by 5.3/5.4/6: │ __init_start │ │ PMD[512] → PTE table → per-section 4K pages │ init (RWX) │ │ 0x4002_b000 __data_start ─┤ │ │ data (RW) │ │ 0x4003_9000 __data_end ───┤ │ │ rodata (RO) │ │ 0x4004_e3b9 __minos_end ──┘ │ 0x4004_f000 bootmem_4K ─────┘ 0x4020_0000 bootmem_2M ────┐ │ RAM body (RW) │ ← this 62MB built by 5.5: │ 2M blocks │ PMD[513]..PMD[543] filled directly as 2M blocks │ ... │ (phys 0x4020_0000, 0x4040_0000, ...) 0x4400_0000 RAM end ───────┘ (64MB total) ─────────────────────────────────────────────────────────────────
|
页表在物理内存中的位置 + PMD 表项内容(基址 = 0x4000_0000 + 偏移):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| PA: 0x4000_0000 0x4002_b000 0x4004_f000 0x4005_7000 0x4005_b000 ┌──────────────────┬──────────────────────┬─────────────────────┬────────────────┬───────────┐ │ kernel image │ __stage1_page_table │ stacks (4x8K) │ PMD tables │ PTE table │ │ (text/data/ro) │ = PUD table (4KB) │ [bootmem+32K) │ 4x4KB = 16KB │ 1x4KB │ │ │ = TTBR base │ minos_stack_top │ (ttb0_pud) │ (ttb0_pmd)│ └──────────────────┴──────────────────────┴─────────────────────┴────────────────┴───────────┘ PUD[0..3] stacks grow PMD tables PTE[0..511] points to 4 DOWN from cover 0~4G fill the PMD tables 0x40057000 4 entries used image 2MB
4 个被使用的 PMD 表项分别存什么(关键点——一张表,两种用法):
PMD 表项 覆盖范围 内容 映射到 ────────────────────────────────────────────────────────────────────── [512] 0x4000_0000~ PTE 表地址 | 0b11 ───► 镜像区 2MB,经 0x4020_0000 4K 页映射(5.3/5.4/6 节) [513] 0x4020_0000~ 物理 0x4020_0000 | 0b01 ───► RAM 块 1 \ [514] 0x4040_0000~ 物理 0x4040_0000 | 0b01 ───► RAM 块 2 \ ... ... ... RAM 主体 62MB } 5.5 节 [543] 0x43e0_0000~ 物理 0x43e0_0000 | 0b01 ───► RAM 块 31 / 直接 2M 0x4400_0000 (无 PTE 表) block
- PUD 表 = 0x4002_b000 ← 内核镜像内(链接脚本预留),TTBR 指向这里 - bootmem = 0x4004_f000 ← __minos_end,4K 对齐 - 栈区 = 0x4004_f000 ~ 0x4005_7000 (4 核 × 8K,sp 向下增长) - PMD 表 = 0x4005_7000 ← pagetable_base 起点(读取 minos_stack_top 的值!) - PTE 表 = 0x4005_b000 ← PMD 表之后(只有 1 张,给镜像用) - 表项地址 = 表基址 + 索引×8 例如 PUD[2] @ 0x4002b000 + 2×8 = 0x4002b010
关键点:RAM 主体没有 PTE 表。PMD[513..543] 直接填 `物理地址 | S1_DES_BLOCK(0b01)` ——硬件走到 PMD 就停,不查 PTE。 镜像区的 PMD[512] 填 `PTE 表地址 | 0b11`——会再走深一层。
|
对应的 PMD 表项(每个 PMD 表项覆盖 2M,索引 = 地址 >> 21):
| PMD 表项 |
覆盖范围 |
内容 |
区域 |
| 0 ~ 511 |
0x0 ~ 0x4000_0000 |
0(无效) |
未映射 |
| 512 |
0x4000_0000 ~ 0x4020_0000 |
PTE 表地址 | 0b11 |
镜像区(2MB) |
| 513 ~ 543 |
0x4020_0000 ~ 0x4400_0000 |
物理地址 | 0b01(block) |
RAM 主体(62MB,31 块) |
对照代码各步骤:
| 代码 |
做了什么 |
结果 |
| 5.1 |
分配 4 张 PMD 表(16KB) |
ttb0_pud = 4 张 PMD 表基址,覆盖 0~4G |
| 5.2 |
PUD[0..3] = 4 张 PMD 表地址 |
PUD 前 4 项挂上 PMD 表 |
| 5.3 |
分配 PTE 表(镜像 2MB ÷ 2M = 1 张) |
ttb0_pmd = PTE 表基址 |
| 5.4 |
PMD[512] = PTE 表地址 |
镜像区 PMD 项指向 PTE 表 |
| 5.5 |
PMD[513..543] 直接填 2M block |
RAM 主体 31 个 2M 块,跳过 PTE |
关键理解:**PMD 表就是”分诊台”**——镜像区那项指向 PTE 表(精细 4K 页),RAM 主体那些项直接是 block(粗粒度 2M),一表两用。
6. 内核镜像各段的精确映射
回到主函数,对 6 个段逐段调用 build_normal_pte_table。以代码段为例(96-103 行):
1 2 3 4 5 6 7 8
| __map_code_section: ldr vaddr, =__code_start ldr tmp, =__code_end sub size, tmp, vaddr // 代码段字节数 asm_vtop vaddr // VA → 物理地址 mov paddr, vaddr ldr pte_attr, =BOOTMEM_CODE_ATTR // RX 属性 bl build_normal_pte_table
|
6.1 各段清单与属性
| 段 |
范围(链接符号) |
属性宏 |
权限 |
| 保留内存 |
minos_start → __code_start |
BOOTMEM_DATA_ATTR |
RW + XN |
| 代码段 |
__code_start → __code_end |
BOOTMEM_CODE_ATTR |
RX |
| init 段 |
__init_start → __init_end |
BOOTMEM_INIT_ATTR |
RWX(启动后释放) |
| 数据段 |
__data_start → __data_end |
BOOTMEM_DATA_ATTR |
RW + XN |
| RO 数据 |
__rodata_start → minos_bootmem_base |
BOOTMEM_DATA_RO_ATTR |
RO + XN |
| 剩余 4K 页 |
bootmem_base → 2M 边界 |
BOOTMEM_DATA_ATTR |
RW + XN |
属性宏的实际位(stage1.h:70-75):
1 2 3 4 5 6
| BOOTMEM_CODE_ATTR = S1_ATTR_IDX(MT_NORMAL) | S1_DES_PAGE | S1_NS | S1_AP_RO | S1_SH_INNER | S1_AF BOOTMEM_DATA_ATTR = S1_ATTR_IDX(MT_NORMAL) | S1_DES_PAGE | S1_NS | S1_AP_RW | S1_SH_INNER | S1_AF | S1_XN BOOTMEM_DATA_RO_ATTR = S1_ATTR_IDX(MT_NORMAL) | S1_DES_PAGE | S1_NS | S1_AP_RO | S1_SH_INNER | S1_AF | S1_XN BOOTMEM_INIT_ATTR = S1_ATTR_IDX(MT_NORMAL) | S1_DES_PAGE | S1_NS | S1_AP_RW | S1_SH_INNER | S1_AF BOOTMEM_IO_ATTR = S1_ATTR_IDX(MT_DEVICE_nGnRnE) | S1_DES_PAGE | S1_NS | S1_AP_RW | S1_SH_INNER | S1_AF | S1_XN BOOTMEM_DATA_BLK_ATTR = S1_ATTR_IDX(MT_NORMAL) | S1_DES_BLOCK | S1_NS | S1_AP_RW | S1_SH_INNER | S1_AF | S1_XN | S1_PXN
|
读属性时注意:代码段 RX(可执行、不可写)、数据段 RW+X(不可执行)、RO 段 RO+X、init 段 RWX。硬件强制隔离在此阶段就已埋下。
6.2 按 4K 页填充 PTE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| build_normal_pte_table: and vaddr, vaddr, entry_align // 页对齐(清低 12 位) and paddr, paddr, entry_align and size, size, entry_align
// ★ 核心 trick:直接用"相对 minos_start 的页偏移"定位 PTE adr x0, minos_start ldr x0, [x0] sub x0, vaddr, x0 // 偏移 = vaddr - minos_start ubfx pte_index, x0, #12, #20 // >>12 = 第几个 4K 页(低 20 位) mov pmd_tmp, ttb0_pmd add pmd_tmp, pmd_tmp, pte_index, lsl #3 // PTE 表基址 + idx×8 = 目标 PTE
bic paddr, paddr, entry_mask // 物理地址页对齐 bic paddr, paddr, #0xffff000000000000 // 清高位(符号位,转物理) orr paddr, paddr, pte_attr // 拼上属性位
loop_normal_pte: cbz size, exit_loop str paddr, [pmd_tmp] // 写一个 PTE sub size, size, entry_size // −4K add pmd_tmp, pmd_tmp, #8 // 下一个 PTE(8 字节/项) add paddr, paddr, entry_size // 物理地址 +4K b loop_normal_pte exit_loop: ret
|
为什么能这样定位 PTE? 因为:
ttb0_pmd(此名有误导,实为)所有 PTE 表的总基址(5.3 一次性分配的连续空间)。
minos_start 是第一个按 4K 映射的地址(bootmem 起点之前)。
- 那么
(vaddr − minos_start) >> 12 就是从 ttb0_pmd 起第几个 PTE——因为是线性累加的,ttb0_pmd + idx*8 直接命中目标 PTE,不用逐级算 PUD/PMD index。
这是整个函数最精巧的优化:把”按需逐级定位”简化成”连续线性寻址”。
7. UART 设备映射
1 2 3 4 5
| ldr vaddr, =CONFIG_UART_BASE mov paddr, vaddr ldr size, =CONFIG_UART_IO_SIZE ldr pte_attr, =BOOTMEM_IO_ATTR // Device 属性(nGnRnE + XN) bl build_io_pte_table
|
7.1 设备页表的填充
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| build_io_pte_table: mov x25, x30 // 保存返回地址
// 分配一张独立的 4KB 表(PTE 表) mov x0, pagetable_base add pagetable_base, pagetable_base, #4096 bl memset
// 把 UART 范围向上 2M 对齐 add x1, vaddr, size add x1, x1, entry_mask and x1, x1, entry_align and vaddr, vaddr, entry_align and paddr, paddr, entry_align sub size, x1, vaddr
// PMD 项 → 指向这张 PTE 表(注意 UART 不在镜像区间,index 单独算) ubfx x0, vaddr, #21, #11 // vaddr>>21 = 2M 块号(PMD 项序号) ldr x1, =HOST_TABLE_DES orr x1, pmd_tmp, x1 str x1, [ttb0_pud, x0, lsl #3] // 写 PMD 表项
// 逐 4K 填 PTE ldr entry_align, =0x1fffff and x2, vaddr, entry_align // 2M 块内偏移 lsr x2, x2, #12 // → PTE 序号 orr paddr, paddr, pte_attr // 拼 Device 属性 loop_io_pte: str paddr, [pmd_tmp, x2, lsl #3] sub size, size, entry_size add paddr, paddr, entry_size add x2, x2, #1 cbnz size, loop_io_pte ret x25
|
与 build_normal_pte_table 的区别:
- UART 地址(
CONFIG_UART_BASE)不在 [minos_start, bootmem) 区间,所以不能复用”相对 minos_start 定位”的 trick,必须用 vaddr >> 21 单独算 PMD 项的 2M 块号。
- 分配独立的一张 4KB PTE 表(按
>>12 填 4K 项),专用于 UART 的 4K 页。
- 属性用 Device(
MT_DEVICE_nGnRnE),硬件禁止对 Device 内存乱缓存。
8. 收尾
1 2 3
| adr x0, minos_end str pagetable_base, [x0] // 把 bootmem 分配器当前位置记下来 ret x26 // 返回 boot.S
|
minos_end 记录”bootmem 分配器用到了哪”,后续阶段(如 C 的 mm 子系统)从这里继续分配。ret x26 回到 boot.S:292 继续(dsb ishst; isb 等 MMU 开启动作)。
9. 最终内存布局
映射完成后,stage1 页表覆盖 4GB 地址空间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 0x0000_0000 ┌────────────────────────────┐ │ [unmapped, PMD=0] │ ├────────────────────────────┤ minos_start │ ┌──────────────────────┐ │ │ │ reserved (RW) 4K pg │ │ __code_start│ ├──────────────────────┤ │ │ │ code (RX) 4K pg │ │ __code_end │ ├──────────────────────┤ │ │ │ init (RWX) 4K pg │ │ __init_end │ ├──────────────────────┤ │ │ │ data (RW) 4K pg │ │ __data_end │ ├──────────────────────┤ │ │ │ rodata (RO) 4K pg │ │ bootmem_base│ └──────────────────────┘ │ ← 4K page mapping area ├────────────────────────────┤ │ RAM body (2M block) │ ← BOOTMEM_DATA_BLK_ATTR │ ... up to 64MB │ ├────────────────────────────┤ │ UART (Device) PTE table │ ← CONFIG_UART_BASE └────────────────────────────┘
|
10. 关键知识点
- MMU 使能前手动建表:此时只能用物理地址,
asm_vtop/PTOV=0 决定符号是否需转换。
- 分两档映射策略:内核镜像按 4K 页(属性细分),RAM 主体按 2M block(省页表 + 省 TLB 条目)。
- 三个分配来源:
pagetable_base 是唯一”分配器”,从 bootmem 区连续取页当页表页。
- 线性定位 trick:镜像区间内
(vaddr − minos_start)>>12 直接定位全局 PTE,是最大优化点。
- 属性在启动期就隔离:代码 RX、数据 RW、RO 段 RO、Device 内存不可缓存——安全设计从第一张页表开始。
- 返回值传递:
x26 保存 LR,ret x26 返回;minos_end 是给后续的内存系统”接力棒”。
11. 常见疑问
Q1:为什么分配 4 张 PMD 表就能覆盖 4GB?
一张 PMD 表 512 项 × 每项 2M = 1G。4 张 → 覆盖 4G。这正是 map first 4GB 的注释含义。
Q2:build_page_table 里 RAM 的 2M block 为什么不经过 PTE 表?
block descriptor 可以出现在 level-1(PUD)或 level-2(PMD)。这里 PMD 项直接填 物理地址 | BOOTMEM_DATA_BLK_ATTR(S1_DES_BLOCK),硬件 walk 到 PMD 就命中 block,不再下钻 PTE。这是合法的 huge page 用法。
Q3:镜像区间的 PTE 为什么不按 PUD/PMD 逐级定位?
因为 5.3 分配的 PTE 表是连续分配的(ttb0_pmd 实为其基址),minos_start 是镜像起点,所以 (vaddr−minos_start)>>12 是从 ttb0_pmd 起的连续 PTE 序号。省去逐级 index 计算,代码更短更快。
Q4:这段代码执行完,MMU 开了吗?
没有。map_boot_mem 只负责”填表”,开 MMU 是返回后 boot.S:302-315 的事(写 SCTLR.M/C/I + br mmu_on)。
Q5:为什么函数里有 ldr xN, =符号 和 adr xN, 符号 两种取地址?
adr:取符号的相对地址(PC 相对),用于取 bootmem 变量本身的值(minos_start 等是运行时写的全局变量,存的是值)。
ldr xN, =符号:取符号的链接地址(绝对地址),用于内核镜像段边界(__code_start 等是链接期常量)。