第4章 L1页表配置实战:基地址、域访问控制与共享属性
好,咱们直接进入正题。L1页表,说白了就是MMU的第一道关卡。你配置对了,CPU就能顺畅地访问内存;配错了,系统直接挂掉,连个报错都来不及给你。我在几个项目里都吃过这个亏,所以这一章咱们把L1页表的三个核心参数——基地址、域访问控制、共享属性——彻底讲透。
4.1 L1页表基地址:系统内存的“根”
L1页表的基地址,就是页表本身在物理内存中的起始位置。ARM架构要求这个地址必须16KB对齐。为什么?因为L1页表有4096个条目,每个条目4字节,总共16KB。硬件会直接拿这个基地址去索引,不对齐的话,索引会错位。
关键约束:L1页表基地址必须16KB对齐(低14位为0)。
我在一个项目中遇到过这样的问题:同事把基地址写成了0x80001000,结果系统启动时MMU一使能就跳飞。查了半天,发现是地址没对齐。嗯,从那以后我每次配页表都会用宏来检查对齐。
配置基地址的寄存器是TTBR0(Translation Table Base Register 0)。代码很简单,但要注意顺序:
// 假设L1页表放在0x80000000
#define L1_PAGE_TABLE_BASE 0x80000000
// 确保对齐
static_assert((L1_PAGE_TABLE_BASE & 0x3FFF) == 0,
"L1页表基地址必须16KB对齐");
// 写入TTBR0
__asm volatile("MCR p15, 0, %0, c2, c0, 0"
: : "r" (L1_PAGE_TABLE_BASE));
你想想看,这一步其实就是在告诉CPU:“你的内存地图在这里,去查吧。” 基地址一旦写死,整个系统的虚拟地址到物理地址的映射就全靠它了。
4.2 域访问控制:内存区域的“门禁系统”
ARM的域(Domain)机制,说白了就是给内存区域贴标签。L1页表的每个条目里有一个3位的域字段(Domain),可以指定0-15共16个域。然后CP15的域访问控制寄存器(DACR)决定每个域怎么被访问。
| DACR每2位编码 | 访问权限 | 说明 |
|---|---|---|
| 00 | 无访问 | 任何访问都产生域异常 |
| 01 | 客户模式 | 由页表条目中的AP位决定 |
| 10 | 保留 | 不要使用 |
| 11 | 管理者模式 | 不检查AP位,完全放行 |
我个人习惯把关键的系统内存(比如中断向量表、页表本身)放在域0,并设为管理者模式。普通应用程序的内存放在域1,设为客户模式,靠AP位做细粒度控制。
实战建议:域0通常用作“安全域”,所有系统级内存都放这里。DACR中域0配成0b11(管理者),其他域按需配置。这样即使应用程序的AP位配错了,系统关键内存也不会被误写。
配置DACR的代码也很直接:
// 域0: 管理者(11), 域1: 客户(01), 其他域: 无访问(00)
#define DACR_VALUE (0b11 << 0) | (0b01 << 2)
__asm volatile("MCR p15, 0, %0, c3, c0, 0"
: : "r" (DACR_VALUE));
我曾经在一个项目里把DACR配成了全0,结果系统一跑用户程序就崩。你猜怎么着?所有域都是“无访问”,连内核自己都进不了内存。嗯,这个坑我替你们踩过了。
4.3 共享属性:多核系统的“缓存一致性”命脉
共享属性(Shareable)是L1页表条目里的一个位。它告诉硬件:这块内存会不会被多个核或者DMA访问。如果设成非共享(Non-Shareable),CPU可以自由地缓存数据,不用通知别人。如果设成共享(Shareable),硬件必须保证缓存一致性。
为什么这个位重要?我举个例子。你在一个双核系统里,核0往地址0x1000写了一个值,核1去读同一个地址。如果0x1000被设成了Non-Shareable,核0可能只写到了自己的L1缓存里,核1读到的就是旧数据。这种bug极难复现,我调试了整整两周才找到原因。
注意:共享属性只对Normal内存有意义。对于Device内存(比如MMIO寄存器),必须设为Non-Shareable,因为外设寄存器不存在缓存一致性问题。
在L1页表条目中,共享属性位在bit 1(对于段映射)或bit 9(对于页映射)。配置时要注意:
// L1段描述符格式 (32位)
// bit[1]: 共享属性 (0=Non-Shareable, 1=Shareable)
// bit[18:16]: 域编号
// bit[31:20]: 段基地址
// 配置一个共享的段描述符
#define SECTION_SHAREABLE (1 << 1)
#define DOMAIN0 (0 << 16)
#define SECTION_BASE_ADDR 0x80000000
uint32_t l1_entry = SECTION_BASE_ADDR |
SECTION_SHAREABLE |
DOMAIN0 |
0x400; // AP=完全访问
// 写入L1页表
L1_PAGE_TABLE_BASE[0] = l1_entry; // 虚拟地址0-1MB映射到物理0x80000000
4.4 实战:完整配置一个L1页表条目
好了,咱们把三个参数串起来,写一个完整的L1页表配置函数。这个函数我在好几个项目里复用,基本没出过问题。
// L1页表条目配置结构体
typedef struct {
uint32_t phys_base; // 物理基地址 (1MB对齐)
uint8_t domain; // 域编号 (0-15)
uint8_t shareable; // 0=Non-Shareable, 1=Shareable
uint8_t ap; // 访问权限 (0-3)
uint8_t tex; // 内存类型指示
uint8_t cacheable; // 0=Non-Cacheable, 1=Cacheable
uint8_t bufferable; // 0=Non-Bufferable, 1=Bufferable
} l1_section_cfg_t;
uint32_t l1_section_create(l1_section_cfg_t *cfg) {
uint32_t desc = 0;
// 检查物理地址对齐
if (cfg->phys_base & 0xFFFFF) {
return 0; // 错误:不是1MB对齐
}
// 构建描述符
desc = cfg->phys_base; // bit[31:20]
desc |= (cfg->domain & 0xF) << 16; // bit[18:16]
desc |= (cfg->shareable & 1) << 1; // bit[1]
desc |= (cfg->ap & 3) << 10; // bit[11:10]
desc |= (cfg->tex & 7) << 12; // bit[14:12]
desc |= (cfg->cacheable & 1) << 3; // bit[3]
desc |= (cfg->bufferable & 1) << 2; // bit[2]
desc |= 0x400; // 段描述符标识
return desc;
}
// 使用示例:映射DDR为共享、可缓存
l1_section_cfg_t ddr_cfg = {
.phys_base = 0x80000000,
.domain = 0,
.shareable = 1,
.ap = 3, // 完全访问
.tex = 1, // Normal内存
.cacheable = 1,
.bufferable = 1
};
uint32_t entry = l1_section_create(&ddr_cfg);
L1_PAGE_TABLE_BASE[0] = entry; // 虚拟地址0-1MB
核心要点:L1页表配置就是三个参数的组合游戏——基地址决定页表在哪,域决定谁有权访问,共享属性决定缓存一致性策略。三者缺一不可,顺序也不能错。
4.5 知识结构总览
下面这张图把L1页表配置的完整流程和三个核心参数的关系画出来了。你可以把它当作配置时的检查清单。
这张图里我把三个核心参数放在同一层,它们之间没有先后依赖,但都必须在MMU使能之前配好。我个人习惯先配基地址,再填页表条目,最后配DACR和共享属性。这个顺序能避免中间状态导致异常。
调试技巧:如果MMU使能后系统异常,先检查基地址对齐,再检查DACR是否把关键域设成了“无访问”。这两个问题占了L1页表配置错误的80%以上。