ARMv7-A小系统实践(九):用短描述符页表建立MMU与内存保护

本章记录什么

这一章建立 ARMv7-A short-descriptor MMU 的最小可用映射。内容从虚拟地址的拆分开始,解释 16 KiB 一级表、粗页表、Section 与 Small Page 描述符,然后把链接脚本中的代码、只读数据、可写数据、栈、页表和 MMIO 映射成不同权限与内存类型。

在这一章之前做了什么

前面的启动、上下文和中断仍可在 MMU 关闭时运行,但所有地址都只有“数值”,缺少权限与类型约束。打开 Cache 前更不能回避这一层:同一物理区域被标成 Normal Memory 还是 Device Memory,会直接改变访问排序、缓存和推测行为。

本章采用恒等映射降低早期调试难度,但不会把恒等映射误解为“没有地址转换”。文章给出页表构造代码、开启顺序、PAR 验证方式和常见故障定位。

系列目录 · 上一篇:抢占式调度与 RTEMS 对照

本章的阅读顺序

先读基础概念,确认每个术语解决什么问题;再看设计推导,理解实现为什么采用这种保存集合、初始化顺序或状态机;随后逐段阅读代码和执行轨迹;最后用验证方法与错误案例检查理解。文中保留寄存器名、指令名和标准缩写,叙述部分统一使用简体中文。

从移植步骤理解页表目标

十五、步骤 10:ARMv7 页表、MMU 与内存权限

先画地址图,再写 descriptor:DDR、镜像代码、镜像数据/栈/页表、UART、private
peripheral、SLCR/L2C 和未映射区分别决定 Normal/Device、RO/RW、X/XN、Shareable。

1
2
3
4
5
6
7
预留16KiB对齐L1表与1KiB L2 coarse表
→ 全表初始化为fault
→ 用section建立DDR和MMIO大区
→ 用small page细化镜像代码/数据权限
→ clean页表并invalidate TLB
→ 写TTBCR/TTBR0/DACR
→ DSB/ISB后置SCTLR.M

必须会回答:identity map 为什么仍是 MMU;MMIO 为什么 Device/XN;代码为什么 RO/X、
数据为什么 RW/XN;页表写完后为何需要 clean 与 TLB invalidate。

验证 SCTLR.M、TTBR0、image VA 的 PAR 成功、0x70000000 的 PAR fault,以及开 MMU
后 UART/GIC/Timer 仍工作。只有控制位、地址翻译和既有外设三类证据同时成立,才能认为
最小 MMU 闭环完成。

当前映射方案和准确边界

4. MMU 与 Cache 设计

当前采用 identity mapping,虚拟地址等于物理地址;这不代表“没有 MMU”,而是便于
早期移植调试,同时已落实权限和内存类型。

区域 属性
DDR 大区 Normal、WBWA、Shareable
镜像 .vectors/.text/.rodata 只读、可执行
.data/.bss/.stacks/.translation_tables 可读写、XN
UART/SLCR/SCU/GIC/Timer/L2C-310 Device、Shareable、XN
未显式映射区域 Fault descriptor

初始化顺序不能随意调换:

1
2
3
4
5
6
7
建立并 clean 页表
→ TLB invalidate
→ TTBR0/TTBCR/DACR
→ SCTLR.M
→ SCU + ACTLR.SMP
→ L1 invalidate 后开 C/I/Z
→ L2C-310 invalidate 后 enable

DMA 尚未接入。真板增加 DMA 后,必须根据缓冲区是否 cacheable 设计
clean/invalidate、屏障和所有权交接,不能只因为 CPU 读写正常就认为一致性完成。

最终审计中的 MMU 验收项

B. MMU 与内存属性(必做)

新增 16 KiB 对齐的一级转换表,并按 ARMv7-A short-descriptor 规则配置:

  1. DDR:Normal、Write-Back/Write-Allocate、Shareable;
  2. 代码/只读区:只读、可执行;
  3. data/bss/heap/stack:可读写、XN;
  4. UART、SLCR、SCU/GIC/Timer、L2C-310:Device、Shareable、XN;
  5. 未映射区域保持 Fault descriptor;
  6. 设置 TTBCR、TTBR0、DACR,失效 TLB 后使能 SCTLR.M
  7. 增加 VA→PA/PAR 查询验收,证明正常地址可翻译、未映射地址返回 fault。

x64 的 512 GiB 高半区不能机械复制到 32 位 ARM。最终保持现有物理地址的 identity map,
但实现同样的核心功能:代码/数据权限、内存类型、MMIO 隔离和可控翻译表。

一级表、粗页表与 MMU 开启实现

下面给出文章对应的完整实现片段。即使没有配套仓库,也可以沿着注释、寄存器访问和调用关系逐行阅读。代码之后的分析只讨论本章主题,不要求预先掌握其他目录结构。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#include <stddef.h>
#include <stdint.h>

#include "mmu.h"
#include "platform.h"

extern uint32_t __translation_table[];
extern uint32_t __image_page_table[];
extern uint8_t __code_start[];
extern uint8_t __code_end[];

enum {
L1_ENTRY_COUNT = 4096u,
L2_ENTRY_COUNT = 256u,
SECTION_SIZE = 1u << 20,
PAGE_SIZE = 1u << 12,
IMAGE_SECTION_BASE = 0x00100000u,

L1_FAULT = 0u,
L1_COARSE = 1u,
L1_SECTION = 2u,
L1_DOMAIN_SHIFT = 5u,
L1_AP_PRIVILEGED_RW = 1u << 10,
L1_APX = 1u << 15,
L1_SHAREABLE = 1u << 16,
L1_XN = 1u << 4,
L1_TEX_NORMAL = 1u << 12,
L1_CACHEABLE = 1u << 3,
L1_BUFFERABLE = 1u << 2,

L2_SMALL_PAGE = 2u,
L2_XN = 1u,
L2_AP_PRIVILEGED_RW = 1u << 4,
L2_APX = 1u << 9,
L2_SHAREABLE = 1u << 10,
L2_TEX_NORMAL = 1u << 6,
L2_CACHEABLE = 1u << 3,
L2_BUFFERABLE = 1u << 2,

SCTLR_M = 1u << 0,

TTBR0_INNER_WBWA = 1u << 6,
TTBR0_OUTER_WBWA = 1u << 3,
TTBR0_SHAREABLE = 1u << 1
};

static uint32_t section_normal_rw_xn(uint32_t address)
{
return (address & 0xfff00000u) | L1_SECTION | L1_AP_PRIVILEGED_RW |
L1_SHAREABLE | L1_XN | L1_TEX_NORMAL | L1_CACHEABLE | L1_BUFFERABLE;
}

static uint32_t section_device(uint32_t address)
{
return (address & 0xfff00000u) | L1_SECTION | L1_AP_PRIVILEGED_RW |
L1_SHAREABLE | L1_XN | L1_BUFFERABLE;
}

static uint32_t small_page_normal(uint32_t address, uint32_t executable)
{
uint32_t descriptor = (address & 0xfffff000u) | L2_SMALL_PAGE |
L2_AP_PRIVILEGED_RW | L2_SHAREABLE | L2_TEX_NORMAL |
L2_CACHEABLE | L2_BUFFERABLE;

if (executable != 0u) {
descriptor |= L2_APX;
} else {
descriptor |= L2_XN;
}
return descriptor;
}

static void map_device_sections(uint32_t begin, uint32_t end)
{
for (uint32_t address = begin; address < end; address += SECTION_SIZE) {
__translation_table[address >> 20] = section_device(address);
}
}

void zynq_mmu_initialize(void)
{
for (uint32_t index = 0u; index < L1_ENTRY_COUNT; ++index) {
__translation_table[index] = L1_FAULT;
}

/* Zynq-7020 最多 1 GiB DDR;当前镜像和预留 heap 都采用 identity map。 */
for (uint32_t address = 0u; address < 0x40000000u; address += SECTION_SIZE) {
__translation_table[address >> 20] = section_normal_rw_xn(address);
}

const uint32_t code_begin = (uint32_t)(uintptr_t)__code_start & ~(PAGE_SIZE - 1u);
const uint32_t code_end =
((uint32_t)(uintptr_t)__code_end + PAGE_SIZE - 1u) & ~(PAGE_SIZE - 1u);

for (uint32_t index = 0u; index < L2_ENTRY_COUNT; ++index) {
const uint32_t address = IMAGE_SECTION_BASE + index * PAGE_SIZE;
const uint32_t executable = address >= code_begin && address < code_end;
__image_page_table[index] = small_page_normal(address, executable);
}

__translation_table[IMAGE_SECTION_BASE >> 20] =
((uint32_t)(uintptr_t)__image_page_table & 0xfffffc00u) |
(0u << L1_DOMAIN_SHIFT) | L1_COARSE;

map_device_sections(0xe0000000u, 0xe0200000u);
map_device_sections(0xf8000000u, 0xf9000000u);
__translation_table[0xfffu] = section_device(0xfff00000u);

arm_data_sync_barrier();

const uint32_t ttbr0 =
((uint32_t)(uintptr_t)__translation_table & 0xffffc000u) |
TTBR0_INNER_WBWA | TTBR0_OUTER_WBWA | TTBR0_SHAREABLE;
__asm__ volatile("mcr p15, 0, %0, c2, c0, 2" :: "r"(0u) : "memory");
__asm__ volatile("mcr p15, 0, %0, c2, c0, 0" :: "r"(ttbr0) : "memory");
__asm__ volatile("mcr p15, 0, %0, c3, c0, 0" :: "r"(1u) : "memory");
__asm__ volatile("mcr p15, 0, %0, c8, c7, 0" :: "r"(0u) : "memory");
arm_data_sync_barrier();
arm_instruction_sync_barrier();

uint32_t control = arm_read_sctlr();
control |= SCTLR_M;
__asm__ volatile("mcr p15, 0, %0, c1, c0, 0" :: "r"(control) : "memory");
arm_instruction_sync_barrier();
}

void zynq_mmu_initialize_secondary(void)
{
const uint32_t ttbr0 =
((uint32_t)(uintptr_t)__translation_table & 0xffffc000u) |
TTBR0_INNER_WBWA | TTBR0_OUTER_WBWA | TTBR0_SHAREABLE;
__asm__ volatile("mcr p15, 0, %0, c2, c0, 2" :: "r"(0u) : "memory");
__asm__ volatile("mcr p15, 0, %0, c2, c0, 0" :: "r"(ttbr0) : "memory");
__asm__ volatile("mcr p15, 0, %0, c3, c0, 0" :: "r"(1u) : "memory");
__asm__ volatile("mcr p15, 0, %0, c8, c7, 0" :: "r"(0u) : "memory");
arm_data_sync_barrier();
arm_instruction_sync_barrier();
uint32_t control = arm_read_sctlr() | SCTLR_M;
__asm__ volatile("mcr p15, 0, %0, c1, c0, 0" :: "r"(control) : "memory");
arm_instruction_sync_barrier();
}

uint32_t zynq_mmu_is_enabled(void)
{
return arm_read_sctlr() & SCTLR_M;
}

uint32_t zynq_mmu_translation_table(void)
{
uint32_t value;
__asm__ volatile("mrc p15, 0, %0, c2, c0, 0" : "=r"(value));
return value & 0xffffc000u;
}

uint32_t zynq_mmu_translate(uint32_t virtual_address, uint32_t *physical_address)
{
uint32_t par;
__asm__ volatile("mcr p15, 0, %0, c7, c8, 0" :: "r"(virtual_address) : "memory");
arm_instruction_sync_barrier();
__asm__ volatile("mrc p15, 0, %0, c7, c4, 0" : "=r"(par));

if ((par & 1u) != 0u) {
return 0u;
}

if (physical_address != NULL) {
*physical_address = (par & 0xfffff000u) | (virtual_address & 0xfffu);
}
return 1u;
}

从规则走到实际实现

下面回到本章对应的小系统实现。这里不再假定读者已经打开源码,而是把关键地址、数据结构、指令序列和返回路径直接放在文章中解释。

CPU 怎样从地址走到内存

1
2
3
4
5
6
7
8
9
指令产生虚拟地址 VA

TLB 查询:是否已有匹配的翻译与属性
├─ 命中 → 检查权限、执行属性
└─ 未命中 → 按页表遍历规则读取描述符

得到 PA 与内存属性

Cache / 互连 / 内存或设备

一个有效映射至少回答三件事:物理地址是什么、当前权限能不能访问、这块内存应该怎样被访问。普通 RAM 与 MMIO 不能只因为都是一个 32 位地址就使用同一组属性。

当前源码使用 ARMv7 的 short-descriptor 格式,TTBCR.N=0,让整个地址范围按 TTBR0 的一级表组织。这里没有启用 LPAE 长描述符,也没有实现按进程切换页表。

一张 16 KiB 一级表怎样覆盖 4 GiB

一级表有 4096 个 32 位表项,每项索引由 VA[31:20] 给出:

1
2
3
4096 × 4 字节 = 16 KiB
每个一级索引覆盖 1 MiB VA 区间
4096 × 1 MiB = 4 GiB

普通 section 描述符可以直接映射 1 MiB:

1
2
L1 index = VA >> 20
PA = section_base | (VA & 0x000fffff)

一级表项也可以指向一个 coarse 二级表。二级表有 256 个 32 位项,占 1 KiB,每项映射 4 KiB small page:

1
2
3
4
L1 index = VA[31:20]
L2 index = VA[19:12]
offset = VA[11:0]
PA = small_page_base | offset

这个 L2 是“二级页表”,与 L2 Cache 不是一回事。两个 L2 同时出现在日志里时,应带上 table/cache 名称。

用一个地址手算

VA=0x00101234

1
2
3
一级索引 = 0x001
二级索引 = 0x01
页内偏移 = 0x234

基础版把 VA 的 0x00100000–0x001fffff 区间改为 coarse 表映射。如果二级第 1 项指向物理页 0x00101000,最后 PA 仍为 0x00101234

地址没有变化,但二级表项可以让这一页只读可执行,另一个页可写不可执行。开启 MMU 前相同的整数地址,不会自动带有这种权限区别。

为什么只把镜像区细分为 4 KiB

基础版 src/mmu.c 的整体策略:

  1. 所有一级表项先置 Fault;
  2. 将低 1 GiB 区间按 Normal、WBWA、Shareable、RW/XN 的 section 映射;
  3. 0x00100000 所在的 1 MiB 窗口建立 256 项 small page 表;
  4. 代码覆盖的页设为特权只读、可执行;
  5. 其他 small page 设为特权可写、XN;
  6. UART、private peripherals 等 MMIO 区间映射为 Device、Shareable、XN。

大量 RAM 用 section 可以缩小页表与初始化成本;镜像区需要细粒度权限,所以单独拆页。这个混合策略比一开始为整个 4 GiB 建满 small page 表简单得多。

这里的“映射低 1 GiB”是页表策略,不证明运行环境真的装有 1 GiB DDR。访问已映射但没有物理内存响应的位置,仍然可能导致总线错误;真板应收紧到实际内存图。

描述符中的地址和属性怎样组合

基础版的 Normal section 构造可整理成:

1
2
3
4
5
6
7
8
descriptor = (address & 0xfff00000u)
| L1_SECTION
| L1_AP_PRIVILEGED_RW
| L1_SHAREABLE
| L1_XN
| L1_TEX_NORMAL
| L1_CACHEABLE
| L1_BUFFERABLE;

地址字段要求按映射粒度对齐;低位放类型与属性。不能把未对齐地址直接 OR 进去,因为它会污染属性字段。

本工程的 small page 关键位:

字段 源码使用 作用
描述符类型 small page 选择 4 KiB 翻译
AP[1:0] 特权访问 非特权访问被限制
APX 代码页设置 在当前权限解释下收紧为特权只读
XN 数据页设置 禁止取指执行
TEX/C/B Normal WBWA 普通可缓存内存
S Shareable 参与相应共享域规则

这些解释以工程期望的 short-descriptor、Domain Client 和相关 SCTLR 属性控制状态为前提。若启动环境遗留了不同的 AFE/TRE 等控制配置,AP 与 TEX/C/B 不能不加核对地按同一套表解释。本源码未规范化任意上游环境,应在真板交接中明确这些条件。

按当前位定义,代码 small page 的属性低位组合为 0x65e;普通可写 XN small page 为 0x45f。例如 0x00101000 | 0x65e 表示物理页基址加只读可执行属性。这个数可用于核对具体工程,不应当成所有 ARM 页表格式通用的 magic number。

Device 不能用“关 Cache 的 RAM”随便替代

对 MMIO 的读取可能清状态,写入可能触发硬件动作。CPU 若把它当 Normal memory,推测访问、合并和排序规则可能与设备预期不符。

因此代码把外设设为 Device,而不仅仅清掉 C 位。内存类型、共享属性与屏障共同约束访问行为;volatile 只解决编译器层面的访问表达,不能代替页表属性。

当前映射使用较宽的 section 区间,适合最小实验。产品化时还应减少对保留地址和无设备区域的过度映射。

页表权限与链接脚本要对应

链接脚本把 .vectors、.text 和 .rodata 放在 __code_start–__code_end,再把 .data 对齐到下一页。因此当前页表把 .rodata 所在页也视为可执行代码范围。

这保证了代码只读与数据 XN 的基本分离,但不是“所有只读数据都独立 XN”的最细 W^X 策略。若要进一步收紧,需要把 text 与 rodata 分页,并单独设置执行权限。

还有一个实际扩展限制:只有 0x00100000 这一整个 1 MiB 窗口使用 small page 细分。若镜像代码增长越过 0x00200000,后面的默认 section 是 XN,可能立即触发取指 Abort。链接脚本目前仅检查不超过 16 MiB RAM,没有同等强度地检查“可执行代码必须留在这个 1 MiB 窗口”。扩大内核时应同步调整映射范围和链接断言。

开启 MMU 的真实顺序

CPU0 代码先构建页表,再设置这些系统寄存器:

1
2
3
4
5
6
7
TTBCR = 0
TTBR0 = 一级页表基址 | table walk 属性
DACR = 1,Domain 0 为 Client
TLB invalidate
DSB / ISB
SCTLR.M = 1
ISB

TTBR0 既含表基址,也含页表遍历本身的缓存属性。它不直接替代叶子描述符中的目标内存属性。

DACR 中 Domain 0 取 Client,表示仍检查 AP 权限。若误设成 Manager,许多权限检查就不会按预期生效;“能启动”并不能证明代码页已经真正只读。

源码在初次构表时假定数据 Cache 尚未启用,因此构建后做 DSB,并没有调用 clean-range 清表。如果在 Cache 已启用的路径上修改页表,仅做 DSB 不会自动把脏行写到遍历器应看到的位置。

页表与 TLB 是两份不同状态

修改内存中的描述符,不等于处理器立即放弃旧 TLB 项。对一个运行中的系统,通常要设计:

1
2
3
4
5
6
准备新描述符
→ 必要的页表 Cache clean
→ DSB
→ 相应 TLB invalidation
→ DSB
→ ISB

实际更新还涉及 break-before-make、当前是否仍在该映射上执行、其他核是否持有旧翻译等要求。上面是职责顺序,不是所有在线映射修改都能照抄的一段通用代码。

本系统主要在启动期一次性建立共享页表。CPU1 使用相同表内存,但仍必须独立设置本核 TTBR0、TTBCR、DACR、TLB 和 SCTLR。共享页表不代表共享这些系统寄存器。

PAR 能证明什么

zynq_mmu_translate() 发起 privileged read translation 查询,然后读 PAR:

1
2
3
mcr p15, 0, r0, c7, c8, 0
isb
mrc p15, 0, r1, c7, c4, 0

PAR 的 fault 位表示翻译是否失败;成功时结合页内偏移得到结果物理地址。实验对镜像地址查询成功,对未映射 0x70000000 查询失败。

这个测试不会真的读目标数据,可以先验证表的基本结构。但它只覆盖所请求的翻译访问类型:读翻译成功不证明写保护和 XN 都已通过实测,也不证明外部 DDR 总线真实可达。

要验证权限,需要分别安排可控写保护/执行保护实验,并在 Abort handler 中核对状态,不能把一条“MMU enabled”日志解释成所有页权限均已验收。

调试时按这条顺序查

1
2
3
4
5
6
7
SCTLR.M 是否真的为 1
→ TTBCR/TTBR0 是否指向预期表
→ DACR 是否仍进行权限检查
→ VA 对应的 L1/L2 索引是否算对
→ 描述符的物理基址、类型、AP、XN、TEX/C/B/S
→ 旧 TLB 是否还在
→ fault status 与 fault address 是否支持这个判断

把寄存器值与表项解码写在一起,比只记“MMU 一开就死”更容易形成可验证的假设。地址正确、权限正确、内存属性正确是三个独立检查项。

页表格式和系统寄存器以 ARM ARM 的 VMSA、short-descriptor translation table 章节为准;SoC 的实际内存范围再对照 UG585。

下一篇:Cache、屏障与数据可见性

页表设计对应的链接布局

页表不能脱离镜像布局讨论。下面的脚本给出代码、只读数据、可写数据、栈和转换表的真实边界,MMU 初始化正是依据这些符号细分权限。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
OUTPUT_FORMAT("elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)

MEMORY
{
/* QEMU/当前练习从 DDR 0x00100000 加载;真板需按 FSBL/U-Boot 交接复核。 */
RAM (rwx) : ORIGIN = 0x00100000, LENGTH = 16M
}

PHDRS
{
text PT_LOAD FLAGS(5);
data PT_LOAD FLAGS(6);
}

SECTIONS
{
. = ORIGIN(RAM);
__image_start = .;

.vectors ALIGN(32) :
{
__code_start = .;
KEEP(*(.vectors))
} > RAM :text

.text ALIGN(4) :
{
*(.text.startup)
*(.text .text.*)
} > RAM :text

.rodata ALIGN(4) :
{
*(.rodata .rodata.*)
__code_end = .;
} > RAM :text

/* 代码与可写数据分开页,MMU 才能真正设置 RO/X 与 RW/XN。 */
.data ALIGN(4K) :
{
__data_start = .;
*(.data .data.*)
__data_end = .;
} > RAM :data

.bss ALIGN(8) (NOLOAD) :
{
__bss_start = .;
*(.bss .bss.*)
*(COMMON)
__bss_end = .;
} > RAM :data

.stacks ALIGN(8) (NOLOAD) :
{
/* 每核都必须拥有不同的 banked SP,尤其不能共享 IRQ/SVC 栈。 */
__stack_und_cpu0_bottom = .;
. += 1K;
__stack_und_cpu0_top = .;
__stack_und_cpu1_bottom = .;
. += 1K;
__stack_und_cpu1_top = .;

__stack_abt_cpu0_bottom = .;
. += 1K;
__stack_abt_cpu0_top = .;
__stack_abt_cpu1_bottom = .;
. += 1K;
__stack_abt_cpu1_top = .;

__stack_irq_cpu0_bottom = .;
. += 4K;
__stack_irq_cpu0_top = .;
__stack_irq_cpu1_bottom = .;
. += 4K;
__stack_irq_cpu1_top = .;

__stack_fiq_cpu0_bottom = .;
. += 1K;
__stack_fiq_cpu0_top = .;
__stack_fiq_cpu1_bottom = .;
. += 1K;
__stack_fiq_cpu1_top = .;

__stack_svc_cpu0_bottom = .;
. += 16K;
__stack_svc_cpu0_top = .;
__stack_svc_cpu1_bottom = .;
. += 16K;
__stack_svc_cpu1_top = .;

/* 兼容既有 CPU0 监测符号。 */
__stack_irq_bottom = __stack_irq_cpu0_bottom;
__stack_irq_top = __stack_irq_cpu0_top;
} > RAM :data

.translation_tables ALIGN(16K) (NOLOAD) :
{
__translation_table = .;
. += 16K;
__translation_table_end = .;

__image_page_table = .;
. += 1K;
__image_page_table_end = .;
} > RAM :data

__image_end = .;

/DISCARD/ :
{
*(.comment)
*(.note*)
*(.eh_frame*)
}
}

ASSERT(__image_end <= ORIGIN(RAM) + LENGTH(RAM), "Zynq image exceeds RAM region")

平台地址与页表输入

MMU 代码中的物理地址必须来自明确的平台地图。集中列出这些常量,能够避免把外设区域误映射成普通可缓存内存。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#ifndef ZYNQ7020_PLATFORM_H
#define ZYNQ7020_PLATFORM_H

#include <stdint.h>

#define ZYNQ_UART0_BASE 0xE0000000u
#define ZYNQ_A9_PRIVATE_PERIPHERAL_BASE 0xF8F00000u

static inline uint32_t arm_read_cpsr(void)
{
uint32_t value;
__asm__ volatile("mrs %0, cpsr" : "=r"(value));
return value;
}

static inline uint32_t arm_read_mpidr(void)
{
uint32_t value;
__asm__ volatile("mrc p15, 0, %0, c0, c0, 5" : "=r"(value));
return value;
}

static inline uint32_t arm_cpu_index(void)
{
return arm_read_mpidr() & 0x3u;
}

static inline uint32_t arm_read_sctlr(void)
{
uint32_t value;
__asm__ volatile("mrc p15, 0, %0, c1, c0, 0" : "=r"(value));
return value;
}

static inline uint32_t arm_read_dfsr(void)
{
uint32_t value;
__asm__ volatile("mrc p15, 0, %0, c5, c0, 0" : "=r"(value));
return value;
}

static inline uint32_t arm_read_ifsr(void)
{
uint32_t value;
__asm__ volatile("mrc p15, 0, %0, c5, c0, 1" : "=r"(value));
return value;
}

static inline uint32_t arm_read_dfar(void)
{
uint32_t value;
__asm__ volatile("mrc p15, 0, %0, c6, c0, 0" : "=r"(value));
return value;
}

static inline uint32_t arm_read_ifar(void)
{
uint32_t value;
__asm__ volatile("mrc p15, 0, %0, c6, c0, 2" : "=r"(value));
return value;
}

/* 读取当前模式实际可见的 SP;在 IRQ C 处理函数中读到的就是 sp_irq。 */
static inline uintptr_t arm_read_sp(void)
{
uintptr_t value;
__asm__ volatile("mov %0, sp" : "=r"(value));
return value;
}

static inline void arm_wait_for_interrupt(void)
{
__asm__ volatile("wfi" ::: "memory");
}

static inline void arm_wait_for_event(void)
{
__asm__ volatile("wfe" ::: "memory");
}

static inline void arm_send_event(void)
{
__asm__ volatile("sev" ::: "memory");
}

static inline void arm_enable_irq(void)
{
__asm__ volatile("cpsie i" ::: "memory");
}

static inline void arm_disable_irq(void)
{
__asm__ volatile("cpsid i" ::: "memory");
}

static inline void arm_data_sync_barrier(void)
{
__asm__ volatile("dsb" ::: "memory");
}

static inline void arm_instruction_sync_barrier(void)
{
__asm__ volatile("isb" ::: "memory");
}

#endif

重点回看

恒等映射仍然使用 MMU,并能提供内存类型、执行权限和读写权限。页表描述符必须与链接布局一致,MMIO 应为 Device/XN,页表写入后要 clean,切换转换配置要配合 TLB 失效和屏障。PAR 可以作为地址转换的独立证据。

本章总结

恒等映射仍然使用 MMU,并能提供内存类型、执行权限和读写权限。页表描述符必须与链接布局一致,MMIO 应为 Device/XN,页表写入后要 clean,切换转换配置要配合 TLB 失效和屏障。PAR 可以作为地址转换的独立证据。

下一章在正确页表属性上启用 Cache、SCU 和 L2C-310,并讨论维护与一致性边界。

ARMv7-A小系统实践(九):用短描述符页表建立MMU与内存保护

https://goko-son626.github.io/post/armv7a-08-mmu.html

作者

GoKo Mell

发布于

2026-07-04

更新于

2026-07-04

许可协议

评论

:D 一言句子获取中...