Minos 虚拟化: 启动页表的构建详解

本文是内存虚拟化一文的补充,详细介绍 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 清零

四个要点

  1. 返回地址bl 跳进来时 LR(x30)被覆盖,先挪到 x26 保存,函数末尾 ret x26 返回。
  2. pagetable_base 来源minos_stack_top 存的是 bootmem 区的顶部地址(boot.S:264-268 里 = __minos_end 4K 对齐 + 所有核栈空间)。adr 取符号地址、ldr 解引用拿到值——这是汇编里”读取全局变量”的标准两连操作。
  3. 页表根为什么清零__stage1_page_table(实为 PUD 表)在链接脚本里只占了 4KB 空间(minos.lds.S:89-93),内容是垃圾,必须清零后才能往上填。
  4. 为什么用物理地址: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 物理地址 | 0b11PUD[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. 关键知识点

  1. MMU 使能前手动建表:此时只能用物理地址,asm_vtop/PTOV=0 决定符号是否需转换。
  2. 分两档映射策略:内核镜像按 4K 页(属性细分),RAM 主体按 2M block(省页表 + 省 TLB 条目)。
  3. 三个分配来源pagetable_base 是唯一”分配器”,从 bootmem 区连续取页当页表页。
  4. 线性定位 trick:镜像区间内 (vaddr − minos_start)>>12 直接定位全局 PTE,是最大优化点。
  5. 属性在启动期就隔离:代码 RX、数据 RW、RO 段 RO、Device 内存不可缓存——安全设计从第一张页表开始。
  6. 返回值传递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_ATTRS1_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 等是链接期常量)。